preparing for sending documents

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
2025-07-17 20:15:28 +02:00
parent 93449e4bad
commit 6a58087ace
19 changed files with 146 additions and 110 deletions

View File

@@ -33,9 +33,12 @@ public class Constants {
public static final String ESTIMATED_TIME = "estimated_time";
public static final String EXPIRATION = "expiration";
public static final String FALLBACK_LANG = "de";
public static final String GET = "GET";
public static final String ID = "id";
public static final String JSONARRAY = "json array";
public static final String JSONOBJECT = "json object";
public static final String KEY = "key";
public static final String LANGUAGE = "language";
public static final String LOGIN = "login";

View File

@@ -1,5 +1,7 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.message.model;
package de.srsoftware.umbrella.core.api;
import de.srsoftware.umbrella.core.model.Envelope;
public interface PostBox {
public void send(Envelope envelope);

View File

@@ -1,7 +1,6 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.core.api;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

View File

@@ -1,5 +1,5 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.message.model;
package de.srsoftware.umbrella.core.model;
import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.invalidFieldException;
@@ -16,16 +16,6 @@ public record Attachment(String name, String mime, byte[] content) {
private static final Base64.Decoder BASE64 = Base64.getDecoder();
public static <T> Attachment of(JSONObject json) throws UmbrellaException {
for (var key : Set.of(NAME, MIME, DATA)) {
if (!json.has(key)) throw missingFieldException(key);
}
if (!(json.get(NAME) instanceof String name)) throw invalidFieldException(NAME,STRING);
if (!(json.get(MIME) instanceof String mime)) throw invalidFieldException(MIME,STRING);
if (!(json.get(DATA) instanceof String data)) throw invalidFieldException(DATA,STRING);
return new Attachment(name,mime, BASE64.decode(data));
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Attachment that)) return false;
@@ -36,4 +26,14 @@ public record Attachment(String name, String mime, byte[] content) {
public int hashCode() {
return Objects.hash(name, mime, Arrays.hashCode(content));
}
public static <T> Attachment of(JSONObject json) throws UmbrellaException {
for (var key : Set.of(NAME, MIME, DATA)) {
if (!json.has(key)) throw missingFieldException(key);
}
if (!(json.get(NAME) instanceof String name)) throw invalidFieldException(NAME,STRING);
if (!(json.get(MIME) instanceof String mime)) throw invalidFieldException(MIME,STRING);
if (!(json.get(DATA) instanceof String data)) throw invalidFieldException(DATA,STRING);
return new Attachment(name,mime, BASE64.decode(data));
}
}

View File

@@ -1,14 +1,12 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.message.model;
package de.srsoftware.umbrella.core.model;
import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.invalidFieldException;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingFieldException;
import static de.srsoftware.umbrella.message.Constants.*;
import static java.text.MessageFormat.format;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.EmailAddress;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -18,13 +16,13 @@ import org.json.JSONObject;
public class Envelope {
private Message message;
private Set<UmbrellaUser> receivers;
private Set<User> receivers;
public Envelope(Message message, UmbrellaUser receiver){
public Envelope(Message message, User receiver){
this(message,new HashSet<>(Set.of(receiver)));
}
public Envelope(Message message, HashSet<UmbrellaUser> receivers) {
public Envelope(Message message, HashSet<User> receivers) {
this.message = message;
this.receivers = receivers;
}
@@ -41,15 +39,15 @@ public class Envelope {
var obj = json.get(RECEIVERS);
if (obj instanceof JSONObject) obj = new JSONArray(List.of(obj));
if (!(obj instanceof JSONArray receiverList)) throw invalidFieldException(RECEIVERS, JSONARRAY);
var receivers = new HashSet<UmbrellaUser>();
var receivers = new HashSet<User>();
for (var o : receiverList){
if (!(o instanceof JSONObject receiverData)) throw invalidFieldException("entries of "+ RECEIVERS, JSONOBJECT);
receivers.add(UmbrellaUser.of(receiverData));
receivers.add(User.of(receiverData));
}
return new Envelope(message,receivers);
}
public boolean isFor(UmbrellaUser receiver) {
public boolean isFor(User receiver) {
return receivers.contains(receiver);
}
@@ -57,12 +55,12 @@ public class Envelope {
return message;
}
public Set<UmbrellaUser> receivers(){
public Set<User> receivers(){
return receivers;
}
@Override
public String toString() {
return format("{0} (to: {1}), subject: {2}",getClass().getSimpleName(),receivers.stream().map(UmbrellaUser::email).map(EmailAddress::toString).collect(Collectors.joining(", ")),message.subject());
return format("{0} (to: {1}), subject: {2}",getClass().getSimpleName(),receivers.stream().map(User::email).map(EmailAddress::toString).collect(Collectors.joining(", ")),message.subject());
}
}

View File

@@ -1,20 +1,24 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.message.model;
package de.srsoftware.umbrella.core.model;
import static de.srsoftware.tools.Optionals.isSet;
import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.invalidFieldException;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingFieldException;
import static de.srsoftware.umbrella.message.Constants.*;
import static java.text.MessageFormat.format;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.util.*;
import org.json.JSONArray;
import org.json.JSONObject;
public record Message(UmbrellaUser sender, String subject, String body, Map<String,String> fills, List<Attachment> attachments) {
@Override
public boolean equals(Object o) {
if (!(o instanceof Message message)) return false;
return Objects.equals(sender, message.sender) && Objects.equals(subject, message.subject) && Objects.equals(body, message.body) && Objects.equals(attachments, message.attachments);
}
public static Message from(JSONObject json) throws UmbrellaException {
for (var key : Set.of(SENDER, SUBJECT, BODY)) {
if (!json.has(key)) throw missingFieldException(key);
@@ -40,12 +44,6 @@ public record Message(UmbrellaUser sender, String subject, String body, Map<Stri
return new Message(sender,subject,body,null, attachments);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Message message)) return false;
return Objects.equals(sender, message.sender) && Objects.equals(subject, message.subject) && Objects.equals(body, message.body) && Objects.equals(attachments, message.attachments);
}
@Override
public int hashCode() {
return Objects.hash(subject, body, attachments);

View File

@@ -3,72 +3,43 @@ package de.srsoftware.umbrella.core.model;
import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingFieldException;
import de.srsoftware.tools.Mappable;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import org.json.JSONObject;
/* © SRSoftware 2025 */
public class UmbrellaUser implements Mappable {
public class UmbrellaUser extends User implements Mappable {
private final long id;
private final String theme, name, lang;
private final EmailAddress email;
private final String theme;
public UmbrellaUser(long id, String name, EmailAddress email, String theme, String languageCode) {
this.email = email;
this.name = name;
super(name, email, languageCode);
this.id = id;
this.theme = theme;
this.lang = languageCode;
}
public EmailAddress email(){
return email;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof UmbrellaUser user)) return false;
return Objects.equals(email, user.email)
&& Objects.equals(name, user.name)
return Objects.equals(email(), user.email())
&& Objects.equals(name(), user.name())
&& Objects.equals(id, user.id)
&& Objects.equals(lang, user.lang);
&& Objects.equals(language(), user.language());
}
@Override
public int hashCode() {
return Objects.hash(email, id, lang, name);
return Objects.hash(email(), id, language(), name());
}
public long id(){
return id;
}
public String language(){
return lang;
}
public String name(){
return name;
}
public static UmbrellaUser of(JSONObject json) throws UmbrellaException {
if (json.has(USER)) json = json.getJSONObject(USER);
if (!json.has(ID)) throw missingFieldException(ID);
var id = json.getLong(ID);
var name = json.has(NAME) ? json.getString(NAME) : null;
var email = json.has(EMAIL) ? new EmailAddress(json.getString(EMAIL)) : null;
var theme = json.has(THEME) ? json.getString(THEME) : null;
var lang = json.has(LANGUAGE) ? json.getString(LANGUAGE) : null;
return new UmbrellaUser(id, name, email, theme, lang);
}
public String theme(){
return theme;
}
@@ -81,7 +52,7 @@ public class UmbrellaUser implements Mappable {
map.put(NAME, name());
map.put(EMAIL,email() instanceof EmailAddress ea ? ea.toString() : null);
map.put(THEME,theme);
map.put(LANGUAGE,lang);
map.put(LANGUAGE,language());
return map;
}
}

View File

@@ -0,0 +1,52 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.core.model;
import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.Constants.EMAIL;
import static de.srsoftware.umbrella.core.Constants.LANGUAGE;
import static de.srsoftware.umbrella.core.Constants.NAME;
import static de.srsoftware.umbrella.core.Constants.THEME;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingFieldException;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import org.json.JSONObject;
public class User {
private String lang, name;
private EmailAddress email;
public User(String name, EmailAddress email, String languageCode){
lang = languageCode;
this.name = name;
this.email = email;
}
public EmailAddress email(){
return email;
}
public String language(){
return lang;
}
public String name(){
return name;
}
public static User of(JSONObject json) throws UmbrellaException {
if (json.has(USER)) json = json.getJSONObject(USER);
if (!(json.has(NAME) && json.get(NAME) instanceof String name)) throw missingFieldException(NAME);
if (!(json.has(EMAIL) && json.get(EMAIL) instanceof String email)) throw missingFieldException(EMAIL);
if (!(json.has(LANGUAGE) && json.get(LANGUAGE) instanceof String lang)) throw missingFieldException(LANGUAGE);
var addr = new EmailAddress(email);
if (json.has(ID) && json.get(ID) instanceof Number id) {
var theme = json.has(THEME) ? json.getString(THEME) : null;
return new UmbrellaUser(id.longValue(), name, addr, theme, lang);
}
return new User(name,addr,lang);
}
}

View File

@@ -1,12 +1,12 @@
import de.srsoftware.umbrella.core.api.Translator;
import org.json.JSONObject;
import org.junit.jupiter.api.Test;
import java.util.Map;
/* © SRSoftware 2025 */
import static java.text.MessageFormat.format;
import static org.junit.jupiter.api.Assertions.assertEquals;
import de.srsoftware.umbrella.core.api.Translator;
import java.util.Map;
import org.json.JSONObject;
import org.junit.jupiter.api.Test;
public class TranslatorTest {
private static final Translator translator = new Translator() {

View File

@@ -5,10 +5,10 @@ dependencies{
implementation(project(":core"))
implementation("de.srsoftware:configuration.json:1.0.3")
implementation("de.srsoftware:document.api:1.0.1")
implementation("de.srsoftware:document.file:1.0.0")
implementation("de.srsoftware:document.processor:1.0.2")
implementation("de.srsoftware:document.zugferd:1.0.3")
implementation("de.srsoftware:document.api:2.0.0")
implementation("de.srsoftware:document.file:1.0.1")
implementation("de.srsoftware:document.processor:1.0.3")
implementation("de.srsoftware:document.zugferd:1.0.4")
implementation("de.srsoftware:tools.mime:1.1.2")
}

View File

@@ -29,7 +29,6 @@ import de.srsoftware.configuration.Configuration;
import de.srsoftware.document.api.Content;
import de.srsoftware.document.api.DocumentRegistry;
import de.srsoftware.document.api.RenderError;
import de.srsoftware.document.api.RenderResult;
import de.srsoftware.document.files.DocumentDirectory;
import de.srsoftware.document.processor.latex.LatexFactory;
import de.srsoftware.document.processor.weasyprint.WeasyFactory;
@@ -42,12 +41,11 @@ import de.srsoftware.tools.SessionToken;
import de.srsoftware.tools.Tuple;
import de.srsoftware.umbrella.core.BaseHandler;
import de.srsoftware.umbrella.core.api.CompanyService;
import de.srsoftware.umbrella.core.api.PostBox;
import de.srsoftware.umbrella.core.api.Translator;
import de.srsoftware.umbrella.core.api.UserService;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.Company;
import de.srsoftware.umbrella.core.model.Token;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import de.srsoftware.umbrella.core.model.*;
import de.srsoftware.umbrella.documents.model.*;
import de.srsoftware.umbrella.documents.model.Customer;
import java.io.File;
@@ -227,7 +225,11 @@ public class DocumentApi extends BaseHandler {
if (!(json.has(SUBJECT) && json.get(SUBJECT) instanceof String subject)) throw missingFieldException(SUBJECT);
if (!(json.has(CONTENT) && json.get(CONTENT) instanceof String content)) throw missingFieldException(CONTENT);
postBox.send()
var attachment = new Attachment(doc.number()+".pdf",rendered.mimeType(),rendered.bytes());
var message = new Message(user,subject,content,null,List.of(attachment));
var envelope = new Envelope(message,new User(doc.customer().name(),new EmailAddress(email),doc.customer().language()));
messages.send(envelope);
// TODO postBox.send(…)
return sendEmptyResponse(HTTP_NOT_IMPLEMENTED,ex);
}

View File

@@ -560,7 +560,7 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255)
var customerId = rs.getString(FIELD_CUSTOMER_NUMBER);
var customerTaxNumber = rs.getString(FIELD_CUSTOMER_TAX_NUMBER);
var customerEmail = rs.getString(FIELD_CUSTOMER_EMAIL);
var customer = new Customer(customerId, customerName, customerEmail, customerTaxNumber);
var customer = new Customer(customerId, customerName, customerEmail, customerTaxNumber,FALLBACK_LANG);
var sender = new Sender(senderName,bankAccount,taxNumber,court);
var template = toTemplate(rs);
return new Document(id,company,number,type,date, Document.State.of(state).orElse(State.ERROR),template,delivery,head,footer,currency, DEFAULT_THOUSANDS_SEPARATOR,sender,customer,new PositionList());

View File

@@ -88,7 +88,7 @@ public abstract class TemplateDoc implements Document {
if (MIME_HTML.equals(mimeType())) value = value.replace("\n","<span class=\"break\"></span>\n");
source = source.replace("<? "+token+" ?>",value);
}
return new StringContent(source);
return new StringContent(source,content.mimeType());
}
return precursor;
}

View File

@@ -18,13 +18,15 @@ public final class Customer implements Mappable {
private String name;
private String email;
private String taxNumber;
private String language;
private final Set<String> dirtyFields = new HashSet<>();
public Customer(String id, String name, String email, String taxNumber) {
public Customer(String id, String name, String email, String taxNumber, String language) {
this.id = id;
this.name = name;
this.email = email;
this.taxNumber = taxNumber;
this.language = language;
}
public void clean() {
@@ -59,6 +61,10 @@ public final class Customer implements Mappable {
return !dirtyFields.isEmpty();
}
public String language(){
return language;
}
public String name() {
return name;
}
@@ -68,7 +74,8 @@ public final class Customer implements Mappable {
if (!json.has(NAME) || !(json.get(NAME) instanceof String name)) throw missingFieldException(NAME);
if (!json.has(EMAIL) || !(json.get(EMAIL) instanceof String email)) throw missingFieldException(EMAIL);
if (!json.has(FIELD_TAX_ID) || !(json.get(FIELD_TAX_ID) instanceof String taxId)) throw missingFieldException(FIELD_TAX_ID);
return new Customer(id,name,email,taxId);
var lang = json.has(LANGUAGE) && json.get(LANGUAGE) instanceof String l ? l : FALLBACK_LANG;
return new Customer(id,name,email,taxId,lang);
}
public void patch(JSONObject json) {

View File

@@ -2,10 +2,9 @@
package de.srsoftware.umbrella.documents.model;
import de.srsoftware.tools.Mappable;
import static de.srsoftware.umbrella.documents.Constants.*;
import de.srsoftware.tools.Mappable;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;

View File

@@ -16,10 +16,7 @@ public class Constants {
public static final String FIELD_HOST = "host";
public static final String FIELD_PORT = "port";
public static final String HOST = "mail.smtp.host";
public static final String JSONARRAY = "json array";
public static final String JSONOBJECT = "json object";
public static final String PORT = "mail.smtp.port";
public static final String RECEIVERS = "receivers";
public static final String SSL = "mail.smtp.ssl.enable";
public static final String SUBMISSION = "submission";

View File

@@ -9,12 +9,13 @@ import static de.srsoftware.umbrella.message.Constants.*;
import static java.lang.System.Logger.Level.*;
import de.srsoftware.configuration.Configuration;
import de.srsoftware.umbrella.core.api.PostBox;
import de.srsoftware.umbrella.core.api.Translator;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.Envelope;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import de.srsoftware.umbrella.core.model.User;
import de.srsoftware.umbrella.message.model.CombinedMessage;
import de.srsoftware.umbrella.message.model.Envelope;
import de.srsoftware.umbrella.message.model.PostBox;
import jakarta.activation.DataHandler;
import jakarta.mail.Message;
import jakarta.mail.MessagingException;
@@ -31,7 +32,7 @@ import java.util.function.BiFunction;
public class MessageSystem implements PostBox {
public static final System.Logger LOG = System.getLogger(MessageSystem.class.getSimpleName());
private final Timer timer = new Timer();
private record Receiver(UmbrellaUser user, de.srsoftware.umbrella.message.model.Message message){}
private record Receiver(User user, de.srsoftware.umbrella.core.model.Message message){}
private class SubmissionTask extends TimerTask{
@@ -99,11 +100,11 @@ public class MessageSystem implements PostBox {
private synchronized void processMessages(Integer scheduledHour) {
LOG.log(INFO,"Running {0}…",scheduledHour == null ? "instantly" : "scheduled at "+scheduledHour);
var queue = new ArrayList<>(this.queue);
var dueRecipients = new ArrayList<UmbrellaUser>();
List<UmbrellaUser> recipients = queue.stream().map(Envelope::receivers).flatMap(Set::stream).filter(Objects::nonNull).distinct().toList();
var dueRecipients = new ArrayList<User>();
List<User> recipients = queue.stream().map(Envelope::receivers).flatMap(Set::stream).filter(Objects::nonNull).distinct().toList();
{ // for known users: get notification preferences, fallback to _immediately_ for unknown users
for (UmbrellaUser recv : recipients) {
for (User recv : recipients) {
if (recv instanceof UmbrellaUser uu) {
try {
if (!db.getSettings(uu).sendAt(scheduledHour)) continue;
@@ -157,7 +158,7 @@ public class MessageSystem implements PostBox {
}
private void send(CombinedMessage message, UmbrellaUser receiver, Date date) throws MessagingException {
private void send(CombinedMessage message, User receiver, Date date) throws MessagingException {
LOG.log(TRACE,"Sending combined message to {0}…",receiver);
session = session();
MimeMessage msg = new MimeMessage(session);

View File

@@ -5,6 +5,8 @@ import static java.lang.System.Logger.Level.DEBUG;
import static java.lang.System.Logger.Level.TRACE;
import static java.text.MessageFormat.format;
import de.srsoftware.umbrella.core.model.Attachment;
import de.srsoftware.umbrella.core.model.Message;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.util.*;
import java.util.function.BiFunction;

View File

@@ -30,14 +30,14 @@ import de.srsoftware.configuration.Configuration;
import de.srsoftware.tools.Path;
import de.srsoftware.tools.SessionToken;
import de.srsoftware.umbrella.core.BaseHandler;
import de.srsoftware.umbrella.core.api.PostBox;
import de.srsoftware.umbrella.core.api.UserService;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.EmailAddress;
import de.srsoftware.umbrella.core.model.Envelope;
import de.srsoftware.umbrella.core.model.Message;
import de.srsoftware.umbrella.core.model.Token;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import de.srsoftware.umbrella.message.MessageSystem;
import de.srsoftware.umbrella.message.model.Envelope;
import de.srsoftware.umbrella.message.model.Message;
import de.srsoftware.umbrella.user.api.LoginServiceDb;
import de.srsoftware.umbrella.user.api.UserDb;
import de.srsoftware.umbrella.user.model.*;
@@ -74,7 +74,7 @@ public class UserModule extends BaseHandler implements UserService {
private final LoginServiceDb logins;
private final HashMap<String, State> stateMap = new HashMap<>(); // map from state to OIDC provider name
private final HashMap<String,String> tokenMap = new HashMap<>();
private final MessageSystem messages;
private final PostBox messages;
static {
try {
@@ -84,7 +84,7 @@ public class UserModule extends BaseHandler implements UserService {
}
}
public UserModule(Configuration config, MessageSystem messageSystem) throws UmbrellaException {
public UserModule(Configuration config, PostBox messageSystem) throws UmbrellaException {
var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingConfigException(CONFIG_DATABASE));
// may be splitted in separate db files later
logins = new SqliteDB(connect(dbFile));
@@ -443,6 +443,11 @@ public class UserModule extends BaseHandler implements UserService {
}
}
@Override
public PostBox postBox() {
return messages;
}
private boolean postCreate(HttpExchange ex) throws IOException, UmbrellaException {
var optUser = loadUser(ex);
if (!(optUser.isPresent() && optUser.get() instanceof DbUser dbUser)) return unauthorized(ex);