diff --git a/.gitea/workflows/docker.yaml b/.gitea/workflows/docker.yaml index 191dbc9d..22dc15c1 100644 --- a/.gitea/workflows/docker.yaml +++ b/.gitea/workflows/docker.yaml @@ -41,10 +41,10 @@ jobs: docker push ${{ secrets.REGISTRY_PATH }}/umbrella:${{ gitea.ref_name }} docker push ${{ secrets.REGISTRY_PATH }}/umbrella:$TAG - - name: Restart vj.srsoftware.de - if: github.ref == 'refs/heads/main' + - name: Restart umbrella.srsoftware.de + if: github.ref == 'refs/heads/dev' run: | - curl -X POST -H "Authorization: Bearer ${{ secrets.MAKE_BEARER }}" -d vj_start https://make.srsoftware.de/launch + curl -X POST -H "Authorization: Bearer ${{ secrets.ELDORADO_MAKE_BEARER }}" -d umbrella_25_start https://make.eldorado.srsoftware.de/launch Clean-Registry: runs-on: ubuntu-latest diff --git a/Dockerfile b/Dockerfile index 40ff6f95..c964b6da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,15 @@ FROM alpine:3.22 AS svelte_build -RUN apk add bash git npm +RUN apk add npm RUN adduser -Dh /home/svelte svelte -ADD . /home/svelte/Umbrella +ADD frontend /home/svelte/Umbrella/frontend RUN chown -R svelte /home/svelte/Umbrella USER svelte WORKDIR /home/svelte/Umbrella/frontend RUN npm install && npm run build -FROM alpine AS java_build -RUN apk add bash git gradle fontconfig font-opensans openjdk21-jre +FROM alpine:3.22 AS java_build +RUN apk add gradle ADD . /Umbrella WORKDIR /Umbrella COPY --from=svelte_build /home/svelte/Umbrella/frontend/dist web/src/main/resources/web @@ -17,13 +17,18 @@ RUN gradle --no-daemon build FROM alpine -RUN apk add bash fontconfig font-opensans graphviz openjdk21-jre weasyprint -RUN adduser -D umbrella -COPY --from=java_build /Umbrella/backend/build/libs/backend.jar /home/umbrella/jar/ -RUN chown -R umbrella /home/umbrella -ADD https://github.com/plantuml/plantuml/releases/download/v1.2025.10/plantuml-1.2025.10.jar /home/umbrella/plantuml.jar -USER umbrella +RUN apk --no-cache add bash fontconfig font-opensans graphviz openjdk21-jre tzdata weasyprint \ + && adduser -D umbrella WORKDIR /home/umbrella -RUN mkdir .config && ln -s /host/config.json .config/Umbrella.json + EXPOSE 80 -CMD java -jar jar/backend.jar +CMD java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:9999 -jar jar/backend.jar + +ADD https://github.com/plantuml/plantuml/releases/download/v1.2025.10/plantuml-1.2025.10.jar /home/umbrella/plantuml.jar +COPY --from=java_build /Umbrella/backend/build/libs/backend.jar /home/umbrella/jar/ +RUN mkdir .config \ + && ln -s /host/config.json .config/Umbrella.json \ + && chmod a+rx plantuml.jar \ + && chown -R umbrella . \ + && ln -s /usr/share/zoneinfo/Europe/Berlin /etc/localtime +USER umbrella diff --git a/Svelte/Dockerfile b/Svelte/Dockerfile index 95abe102..202d8cd7 100644 --- a/Svelte/Dockerfile +++ b/Svelte/Dockerfile @@ -1,5 +1,5 @@ FROM alpine:3.22 -LABEL Maintainer "Stephan Richter " +LABEL Maintainer "Stephan Richter" ARG UID=1000 ARG GID=1000 RUN apk add bash npm @@ -8,4 +8,4 @@ RUN adduser -u $UID -G svelte -Dh /home/svelte svelte ADD script /opt USER svelte WORKDIR /home/svelte -CMD /bin/bash \ No newline at end of file +CMD /bin/bash diff --git a/Svelte/Makefile b/Svelte/Makefile index c5f4656a..8fd486f6 100644 --- a/Svelte/Makefile +++ b/Svelte/Makefile @@ -3,6 +3,7 @@ default: devel build: image podman run --name svelte-build \ --rm \ + --userns=keep-id:uid=$$(id -u),gid=$$(id -g) \ -v ../frontend:/home/svelte/frontend \ -ti svelte /opt/svelte-build image: @@ -11,6 +12,7 @@ image: devel: image -podman rm -f svelte podman run --name svelte \ + --userns=keep-id:uid=$$(id -u),gid=$$(id -g) \ -v ../frontend:/home/svelte/frontend \ -p 5173:5173 \ -ti svelte /opt/svelte-init diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index 36aa6e03..94839ead 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -23,6 +23,7 @@ dependencies{ implementation(project(":markdown")) implementation(project(":messages")) implementation(project(":notes")) + implementation(project(":poll")) implementation(project(":project")) implementation(project(":stock")) implementation(project(":tags")) @@ -58,6 +59,7 @@ tasks.jar { ":markdown:jar", ":messages:jar", ":notes:jar", + ":poll:jar", ":project:jar", ":stock:jar", ":tags:jar", diff --git a/backend/src/main/java/de/srsoftware/umbrella/backend/Application.java b/backend/src/main/java/de/srsoftware/umbrella/backend/Application.java index 9d707490..42e76b2a 100644 --- a/backend/src/main/java/de/srsoftware/umbrella/backend/Application.java +++ b/backend/src/main/java/de/srsoftware/umbrella/backend/Application.java @@ -1,8 +1,8 @@ /* © SRSoftware 2025 */ package de.srsoftware.umbrella.backend; -import static de.srsoftware.umbrella.core.Constants.*; import static de.srsoftware.umbrella.core.Util.mapLogLevel; +import static de.srsoftware.umbrella.core.constants.Constants.UMBRELLA; import static java.lang.System.Logger.Level.ERROR; import static java.lang.System.Logger.Level.INFO; @@ -12,6 +12,7 @@ import de.srsoftware.tools.ColorLogger; import de.srsoftware.umbrella.bookmarks.BookmarkApi; import de.srsoftware.umbrella.company.CompanyModule; import de.srsoftware.umbrella.contact.ContactModule; +import de.srsoftware.umbrella.core.SettingsService; import de.srsoftware.umbrella.core.Util; import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.documents.DocumentApi; @@ -22,6 +23,7 @@ import de.srsoftware.umbrella.markdown.MarkdownApi; import de.srsoftware.umbrella.message.MessageSystem; import de.srsoftware.umbrella.messagebus.MessageApi; import de.srsoftware.umbrella.notes.NoteModule; +import de.srsoftware.umbrella.poll.PollModule; import de.srsoftware.umbrella.project.ProjectModule; import de.srsoftware.umbrella.stock.StockModule; import de.srsoftware.umbrella.tags.TagModule; @@ -64,10 +66,10 @@ public class Application { var server = HttpServer.create(new InetSocketAddress(port), 0); try { - new Translations().bindPath("/api/translations").on(server); + new Translations(config).bindPath("/api/translations").on(server); new JournalModule(config).bindPath("/api/journal").on(server); new MessageApi().bindPath("/api/bus").on(server); - new MessageSystem(config); + new MessageSystem(config).bindPath("/api/message").on(server); new UserModule(config).bindPath("/api/user").on(server); new TagModule(config).bindPath("/api/tags").on(server); new BookmarkApi(config).bindPath("/api/bookmark").on(server); @@ -80,6 +82,7 @@ public class Application { new MarkdownApi().bindPath("/api/markdown").on(server); new NoteModule(config).bindPath("/api/notes").on(server); new StockModule(config).bindPath("/api/stock").on(server); + new PollModule(config).bindPath("/api/poll").on(server); new ProjectModule(config).bindPath("/api/project").on(server); new ProjectLegacy(config).bindPath("/legacy/project").on(server); new TaskModule(config).bindPath("/api/task").on(server); @@ -88,6 +91,7 @@ public class Application { new WebHandler().bindPath("/").on(server); new WikiModule(config).bindPath("/api/wiki").on(server); new FileModule(config).bindPath("/api/files").on(server); + new SettingsService(config).bindPath("/api/settings").on(server); } catch (Exception e) { LOG.log(ERROR,"Startup failed",e); System.exit(-1); diff --git a/bookmark/build.gradle.kts b/bookmark/build.gradle.kts index 4eebb359..6bd54eb8 100644 --- a/bookmark/build.gradle.kts +++ b/bookmark/build.gradle.kts @@ -1,6 +1,7 @@ description = "Umbrella : Bookmarks" dependencies{ + implementation(project(":bus")) implementation(project(":core")) } diff --git a/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkApi.java b/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkApi.java index c1697c93..4341b4a0 100644 --- a/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkApi.java +++ b/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkApi.java @@ -3,12 +3,19 @@ package de.srsoftware.umbrella.bookmarks; import static de.srsoftware.umbrella.bookmarks.Constants.*; import static de.srsoftware.umbrella.core.ConnectionProvider.connect; -import static de.srsoftware.umbrella.core.Constants.*; -import static de.srsoftware.umbrella.core.Paths.LIST; -import static de.srsoftware.umbrella.core.Paths.SEARCH; +import static de.srsoftware.umbrella.core.ModuleRegistry.tagService; import static de.srsoftware.umbrella.core.Util.mapValues; -import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingFieldException; +import static de.srsoftware.umbrella.core.constants.Field.*; +import static de.srsoftware.umbrella.core.constants.Field.TAGS; +import static de.srsoftware.umbrella.core.constants.Module.BOOKMARK; +import static de.srsoftware.umbrella.core.constants.Path.LIST; +import static de.srsoftware.umbrella.core.constants.Path.SEARCH; +import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*; +import static de.srsoftware.umbrella.messagebus.MessageBus.messageBus; +import static de.srsoftware.umbrella.messagebus.events.Event.EventType.CREATE; +import static de.srsoftware.umbrella.messagebus.events.Event.EventType.UPDATE; import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; +import static java.net.HttpURLConnection.HTTP_OK; import com.sun.net.httpserver.HttpExchange; import de.srsoftware.configuration.Configuration; @@ -17,14 +24,13 @@ import de.srsoftware.tools.SessionToken; import de.srsoftware.umbrella.core.BaseHandler; import de.srsoftware.umbrella.core.ModuleRegistry; import de.srsoftware.umbrella.core.api.BookmarkService; +import de.srsoftware.umbrella.core.constants.Text; import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.model.Token; import de.srsoftware.umbrella.core.model.UmbrellaUser; +import de.srsoftware.umbrella.messagebus.events.BookmarkEvent; import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Map; -import java.util.Optional; +import java.util.*; import org.json.JSONArray; public class BookmarkApi extends BaseHandler implements BookmarkService { @@ -32,11 +38,35 @@ public class BookmarkApi extends BaseHandler implements BookmarkService { public BookmarkApi(Configuration config) { super(); - var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingFieldException(CONFIG_DATABASE)); + var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingField(CONFIG_DATABASE)); db = new SqliteDb(connect(dbFile)); ModuleRegistry.add(this); } + private boolean deleteBookmark(UmbrellaUser user, HttpExchange ex, long urlId) throws IOException { + var bookmark = db.load(urlId,user.id()); + db.remove(user, bookmark); + return sendEmptyResponse(HTTP_OK,ex); + } + + @Override + public boolean doDelete(Path path, HttpExchange ex) throws IOException { + addCors(ex); + try { + Optional token = SessionToken.from(ex).map(Token::of); + var user = ModuleRegistry.userService().loadUser(token); + if (user.isEmpty()) return unauthorized(ex); + var head = path.pop(); + if (head == null) throw missingField(ID); + var id = Long.parseLong(head); + return deleteBookmark(user.get(),ex,id); + } catch (NumberFormatException e){ + throw invalidField(ID, Text.NUMBER); + } catch (UmbrellaException e){ + return send(ex,e); + } + } + @Override public boolean doGet(Path path, HttpExchange ex) throws IOException { addCors(ex); @@ -49,21 +79,33 @@ public class BookmarkApi extends BaseHandler implements BookmarkService { case LIST -> getUserBookmarks(user.get(),ex); case null -> super.doPost(path,ex); default -> { - var id = Long.parseLong(head); - yield getBookmark(user.get(),id,ex); + var urlId = Long.parseLong(head); + yield getBookmark(user.get(),urlId,ex); } }; } catch (NumberFormatException e){ - return sendContent(ex,HTTP_BAD_REQUEST,"Invalid project id"); + throw invalidField(ID, Text.NUMBER); } catch (UmbrellaException e){ return send(ex,e); } } - private boolean getBookmark(UmbrellaUser user, long id, HttpExchange ex) throws IOException { - var bookmark = db.load(id,user.id()); - ModuleRegistry.tagService().getTags(BOOKMARK, id, user).forEach(bookmark.tags()::add); - return sendContent(ex,bookmark); + @Override + public boolean doPatch(Path path, HttpExchange ex) throws IOException { + addCors(ex); + try { + Optional token = SessionToken.from(ex).map(Token::of); + var user = ModuleRegistry.userService().loadUser(token); + if (user.isEmpty()) return unauthorized(ex); + var head = path.pop(); + if (head == null) throw missingField(ID); + var id = Long.parseLong(head); + return patchBookmark(user.get(),ex,id); + } catch (NumberFormatException e){ + throw invalidField(ID, Text.NUMBER); + } catch (UmbrellaException e){ + return send(ex,e); + } } @Override @@ -91,6 +133,12 @@ public class BookmarkApi extends BaseHandler implements BookmarkService { return db.findUrls(key); } + private boolean getBookmark(UmbrellaUser user, long urlId, HttpExchange ex) throws IOException { + var bookmark = db.load(urlId,user.id()); + tagService().getTags(BOOKMARK, urlId, user).forEach(bookmark.tags()::add); + return sendContent(ex,bookmark); + } + private boolean getUserBookmarks(UmbrellaUser user, HttpExchange ex) throws IOException { var param = queryParam(ex); long offset = switch (param.get(OFFSET)){ @@ -108,30 +156,48 @@ public class BookmarkApi extends BaseHandler implements BookmarkService { return sendContent(ex,mapValues(bookmarks)); } + private boolean patchBookmark(UmbrellaUser user, HttpExchange ex, long urlId) throws IOException { + var bookmark = db.load(urlId,user.id()); + var tags = tagService().getTags(BOOKMARK,urlId,user); + var json = json(ex); + var comment = bookmark.comment(); + var url = bookmark.url(); + if (json.has(COMMENT) && json.get(COMMENT) instanceof String c) comment = c; + if (json.has(URL) && json.get(URL) instanceof String u) url = u; + var newBookmark = db.save(url,comment, List.of(user.id()),bookmark.timestamp()); + if (newBookmark.urlId() != urlId) { + tagService().save(BOOKMARK,newBookmark.urlId(),List.of(user.id()),tags); + db.remove(user, bookmark); + messageBus().dispatch(new BookmarkEvent(user,newBookmark,CREATE)); + } else messageBus().dispatch(new BookmarkEvent(user,newBookmark,UPDATE)); + return sendContent(ex,newBookmark); + } + private boolean postBookmark(UmbrellaUser user, HttpExchange ex) throws IOException { var json = json(ex); - if (!(json.has(URL) && json.get(URL) instanceof String url)) throw missingFieldException(URL); - if (!(json.has(COMMENT) && json.get(COMMENT) instanceof String comment)) throw missingFieldException(COMMENT); + if (!(json.has(URL) && json.get(URL) instanceof String url)) throw missingField(URL); + if (!(json.has(COMMENT) && json.get(COMMENT) instanceof String comment)) throw missingField(COMMENT); var userList = new ArrayList(); userList.add(user.id()); if (json.has(SHARE) && json.get(SHARE) instanceof JSONArray arr){ for (Object o : arr.toList()) { - if (!(o instanceof Number uid)) throw UmbrellaException.invalidFieldException(SHARE,"Array of ids"); + if (!(o instanceof Number uid)) throw invalidField(SHARE,"Array of ids"); userList.add(uid.longValue()); } } var bookmark = db.save(url,comment, userList); + messageBus().dispatch(new BookmarkEvent(user,bookmark,CREATE)); if (json.has(TAGS) && json.get(TAGS) instanceof JSONArray tagList){ var list = tagList.toList().stream().map(Object::toString).toList(); - ModuleRegistry.tagService().save(BOOKMARK,bookmark.urlId(), userList, list); + tagService().save(BOOKMARK,bookmark.urlId(), userList, list); } return sendContent(ex,bookmark); } private boolean postSearch(UmbrellaUser user, HttpExchange ex) throws IOException { var json = json(ex); - if (!(json.has(KEY) && json.get(KEY) instanceof String key)) throw missingFieldException(KEY); + if (!(json.has(KEY) && json.get(KEY) instanceof String key)) throw missingField(KEY); var keys = Arrays.asList(key.split(" ")); var fulltext = json.has(FULLTEXT) && json.get(FULLTEXT) instanceof Boolean val && val; var bookmarks = db.findUrls(user.id(),keys); diff --git a/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkDb.java b/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkDb.java index 7ecef538..8e94d9cb 100644 --- a/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkDb.java +++ b/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkDb.java @@ -2,6 +2,7 @@ package de.srsoftware.umbrella.bookmarks; import de.srsoftware.umbrella.core.model.Bookmark; +import de.srsoftware.umbrella.core.model.UmbrellaUser; import java.time.LocalDateTime; import java.util.Collection; import java.util.Map; @@ -15,6 +16,8 @@ public interface BookmarkDb { Bookmark load(long id, long userId); + void remove(UmbrellaUser user, Bookmark bookmark); + Bookmark save(String url, String comment, Collection userIds, LocalDateTime datetime); default Bookmark save(String url, String comment, Collection userIds){ diff --git a/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/SqliteDb.java b/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/SqliteDb.java index d6c471c2..5a965e61 100644 --- a/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/SqliteDb.java +++ b/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/SqliteDb.java @@ -6,15 +6,22 @@ import static de.srsoftware.tools.jdbc.Condition.like; import static de.srsoftware.tools.jdbc.Query.*; import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL; import static de.srsoftware.umbrella.bookmarks.Constants.*; -import static de.srsoftware.umbrella.core.Constants.*; import static de.srsoftware.umbrella.core.Errors.*; -import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.databaseException; -import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.notFound; +import static de.srsoftware.umbrella.core.ModuleRegistry.tagService; +import static de.srsoftware.umbrella.core.constants.Field.*; +import static de.srsoftware.umbrella.core.constants.Text.BOOKMARK; +import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*; +import static de.srsoftware.umbrella.messagebus.MessageBus.messageBus; import static java.text.MessageFormat.format; import static java.time.ZoneOffset.UTC; import de.srsoftware.umbrella.core.BaseDb; +import de.srsoftware.umbrella.core.constants.Module; import de.srsoftware.umbrella.core.model.Bookmark; +import de.srsoftware.umbrella.core.model.Translatable; +import de.srsoftware.umbrella.core.model.UmbrellaUser; +import de.srsoftware.umbrella.messagebus.events.BookmarkEvent; +import de.srsoftware.umbrella.messagebus.events.Event; import java.sql.Connection; import java.sql.SQLException; import java.time.LocalDateTime; @@ -48,7 +55,7 @@ CREATE TABLE IF NOT EXISTS {0} ( PRIMARY KEY (`{1}`,`{2}`) )"""; try { - var stmt = db.prepareStatement(format(sql,TABLE_URL_COMMENTS,URL_ID,USER_ID,COMMENT,TIMESTAMP)); + var stmt = db.prepareStatement(format(sql,TABLE_URL_COMMENTS,URL_ID, USER_ID, COMMENT,TIMESTAMP)); stmt.execute(); stmt.close(); } catch (SQLException e) { @@ -63,7 +70,7 @@ CREATE TABLE IF NOT EXISTS {0} ( stmt.execute(); stmt.close(); } catch (SQLException e) { - throw databaseException(FAILED_TO_CREATE_TABLE,TABLE_URLS).causedBy(e); + throw failedToCreateTable(TABLE_URLS).causedBy(e); } } @@ -76,7 +83,7 @@ CREATE TABLE IF NOT EXISTS {0} ( rs.close();; return map; } catch (SQLException e) { - throw databaseException(FAILED_TO_LOAD_ENTITY,"bookmark list").causedBy(e); + throw failedToLoadObject("bookmark list").causedBy(e); } } @@ -84,7 +91,7 @@ CREATE TABLE IF NOT EXISTS {0} ( public Map findUrls(long userId, Collection keys) { try { var map = new HashMap(); - var query = select(ALL).from(TABLE_URL_COMMENTS).leftJoin(URL_ID,TABLE_URLS,ID) + var query = select(ALL).from(TABLE_URL_COMMENTS).leftJoin(URL_ID,TABLE_URLS, ID) .where(USER_ID, equal(userId)); for (var key : keys) query.where(COMMENT,like("%"+key+"%")); var rs = query.sort(format("{0} DESC",TIMESTAMP)).exec(db); @@ -95,7 +102,7 @@ CREATE TABLE IF NOT EXISTS {0} ( rs.close();; return map; } catch (SQLException e) { - throw databaseException(FAILED_TO_DROP_ENTITY,"bookmark list").causedBy(e); + throw failedToDropObject("bookmark list").causedBy(e); } } @@ -103,7 +110,7 @@ CREATE TABLE IF NOT EXISTS {0} ( public Map list(long userId, Long offset, Long limit) { try { var map = new HashMap(); - var rs = select(ALL).from(TABLE_URL_COMMENTS).leftJoin(URL_ID,TABLE_URLS,ID).where(USER_ID, equal(userId)).sort(format("{0} DESC",TIMESTAMP)).skip(offset).limit(limit).exec(db); + var rs = select(ALL).from(TABLE_URL_COMMENTS).leftJoin(URL_ID,TABLE_URLS, ID).where(USER_ID, equal(userId)).sort(format("{0} DESC",TIMESTAMP)).skip(offset).limit(limit).exec(db); while (rs.next()){ var bookmark = Bookmark.of(rs); map.put(bookmark.urlId(),bookmark); @@ -111,21 +118,34 @@ CREATE TABLE IF NOT EXISTS {0} ( rs.close();; return map; } catch (SQLException e) { - throw databaseException(FAILED_TO_LOAD_ENTITY,"bookmark list").causedBy(e); + throw failedToLoadObject("bookmark list").causedBy(e); } } @Override - public Bookmark load(long id, long userId) { + public Bookmark load(long urlId, long userId) { try { Bookmark result = null; - var rs = select(ALL).from(TABLE_URLS).leftJoin(ID,TABLE_URL_COMMENTS,URL_ID).where(ID,equal(id)).where(USER_ID,equal(userId)).exec(db); + var rs = select(ALL).from(TABLE_URLS).leftJoin(ID,TABLE_URL_COMMENTS,URL_ID).where(ID,equal(urlId)).where(USER_ID,equal(userId)).exec(db); if (rs.next()) result = Bookmark.of(rs); rs.close(); if (result != null) return result; - throw notFound(NO_BOOKMARK_FOR_URLID,id); + throw failedToLoadObject(Translatable.t(BOOKMARK),urlId); } catch (SQLException e) { - throw databaseException(FAILED_TO_LOAD_ENTITY,"bookmark").causedBy(e); + throw failedToLoadObject(Translatable.t(BOOKMARK),urlId).causedBy(e); + } + } + + @Override + public void remove(UmbrellaUser user, Bookmark bookmark) { + try { + var urlId = bookmark.urlId(); + var userId = user.id(); + delete().from(TABLE_URL_COMMENTS).where(USER_ID, equal(userId)).where(URL_ID, equal(urlId)).execute(db); + messageBus().dispatch(new BookmarkEvent(user,bookmark, Event.EventType.DELETE)); + tagService().deleteEntity(Module.BOOKMARK,urlId,userId); + } catch (SQLException e){ + throw failedToDropObject(BOOKMARK); } } @@ -143,12 +163,12 @@ CREATE TABLE IF NOT EXISTS {0} ( rs.close(); stmt.close(); } - var query = replaceInto(TABLE_URL_COMMENTS,URL_ID,USER_ID,COMMENT, TIMESTAMP); + var query = replaceInto(TABLE_URL_COMMENTS,URL_ID, USER_ID, COMMENT, TIMESTAMP); for (long userId : userIds) query.values(urlId,userId,comment,timestamp.toEpochSecond(UTC)); query.execute(db).close(); return Bookmark.of(urlId,url,comment,timestamp); } catch (SQLException e) { - throw databaseException(FAILED_TO_STORE_ENTITY,"url").causedBy(e); + throw failedToStoreObject(this).causedBy(e); } } } diff --git a/bus/src/main/java/de/srsoftware/umbrella/messagebus/EventQueue.java b/bus/src/main/java/de/srsoftware/umbrella/messagebus/EventQueue.java index 047a02b7..ce8f83fe 100644 --- a/bus/src/main/java/de/srsoftware/umbrella/messagebus/EventQueue.java +++ b/bus/src/main/java/de/srsoftware/umbrella/messagebus/EventQueue.java @@ -10,9 +10,11 @@ import java.util.LinkedList; public class EventQueue extends LinkedList> implements AutoCloseable, EventListener { private final InetSocketAddress addr; + private final System.Logger log; public EventQueue(InetSocketAddress addr){ this.addr = addr; + log = System.getLogger(addr.toString()); messageBus().register(this); } @@ -30,7 +32,7 @@ public class EventQueue extends LinkedList> implements AutoCloseable, E @Override public void onEvent(Event event) { - System.getLogger(addr.toString()).log(System.Logger.Level.INFO,"adding event to queue of {1}: {0}",event.eventType(),addr); + log.log(System.Logger.Level.INFO,"adding event to queue of {1}: {0}",event.eventType(),addr); add(event); } } diff --git a/bus/src/main/java/de/srsoftware/umbrella/messagebus/MessageApi.java b/bus/src/main/java/de/srsoftware/umbrella/messagebus/MessageApi.java index 4ff7291f..9081fc56 100644 --- a/bus/src/main/java/de/srsoftware/umbrella/messagebus/MessageApi.java +++ b/bus/src/main/java/de/srsoftware/umbrella/messagebus/MessageApi.java @@ -1,8 +1,9 @@ /* © SRSoftware 2025 */ package de.srsoftware.umbrella.messagebus; -import static de.srsoftware.umbrella.core.Constants.*; import static de.srsoftware.umbrella.core.ModuleRegistry.userService; +import static de.srsoftware.umbrella.core.constants.Constants.*; +import static de.srsoftware.umbrella.core.constants.Field.*; import static java.lang.System.Logger.Level.*; import static java.lang.Thread.sleep; import static java.net.HttpURLConnection.HTTP_OK; @@ -35,8 +36,8 @@ public class MessageApi extends BaseHandler{ var addr = ex.getRemoteAddress(); headers.add(CONTENT_TYPE, MimeType.MIME_EVENT_STREAM); - headers.add(CACHE_CONTROL,NO_CACHE); - headers.add(CONNECTION,KEEP_ALIVE); + headers.add(CACHE_CONTROL, NO_CACHE); + headers.add(CONNECTION, KEEP_ALIVE); headers.add(CONTENT_ENCODING,NONE); ex.sendResponseHeaders(HTTP_OK,0); try (var os = ex.getResponseBody(); var stream = new PrintWriter(os); var eventQueue = new EventQueue(addr)){ @@ -48,17 +49,17 @@ public class MessageApi extends BaseHandler{ if (++counter > 300) counter = sendBeacon(addr,stream); } else { var event = eventQueue.removeFirst(); - //if (event.isIntendedFor(user.get())) { + if (event.isIntendedFor(user.get())) { LOG.log(DEBUG, "sending event to {0}", addr); sendEvent(stream, event); counter = 0; - //} + } } } LOG.log(INFO,"{0} disconnected from event stream.",addr); return true; } catch (InterruptedException e) { - throw new UmbrellaException("EventStream broken").causedBy(e); + throw UmbrellaException.serverError("EventStream broken").causedBy(e); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/bus/src/main/java/de/srsoftware/umbrella/messagebus/MessageBus.java b/bus/src/main/java/de/srsoftware/umbrella/messagebus/MessageBus.java index 75aca937..2fd57df6 100644 --- a/bus/src/main/java/de/srsoftware/umbrella/messagebus/MessageBus.java +++ b/bus/src/main/java/de/srsoftware/umbrella/messagebus/MessageBus.java @@ -3,6 +3,7 @@ package de.srsoftware.umbrella.messagebus; import de.srsoftware.umbrella.messagebus.events.Event; import java.util.HashSet; +import java.util.List; import java.util.Set; public class MessageBus { @@ -11,11 +12,11 @@ public class MessageBus { private MessageBus(){} - public void dispatch(Event event){ + public void dispatch(Event event){ new Thread(() -> { // TODO: use thread pool try { Thread.sleep(100); - listeners.parallelStream().forEach(l -> l.onEvent(event)); + List.copyOf(listeners).parallelStream().forEach(l -> l.onEvent(event)); } catch (InterruptedException ignored) { } }).start(); diff --git a/bus/src/main/java/de/srsoftware/umbrella/messagebus/events/BookmarkEvent.java b/bus/src/main/java/de/srsoftware/umbrella/messagebus/events/BookmarkEvent.java new file mode 100644 index 00000000..0447e8e7 --- /dev/null +++ b/bus/src/main/java/de/srsoftware/umbrella/messagebus/events/BookmarkEvent.java @@ -0,0 +1,40 @@ +/* © SRSoftware 2025 */ +package de.srsoftware.umbrella.messagebus.events; + +import static de.srsoftware.umbrella.core.constants.Module.BOOKMARK; +import static de.srsoftware.umbrella.core.model.Translatable.t; + +import de.srsoftware.umbrella.core.constants.Field; +import de.srsoftware.umbrella.core.model.Bookmark; +import de.srsoftware.umbrella.core.model.Translatable; +import de.srsoftware.umbrella.core.model.UmbrellaUser; +import java.util.Collection; +import java.util.List; + +public class BookmarkEvent extends Event { + + public BookmarkEvent(UmbrellaUser initiator, Bookmark bookmark, EventType type){ + super(initiator,BOOKMARK,bookmark,type); + } + @Override + public Collection audience() { + return List.of(initiator()); + } + + @Override + public Translatable describe() { + return switch (eventType()){ + case CREATE -> t("New bookmark created"); + case DELETE -> t("The bookmark '{url}' has been deleted", Field.URL, payload().url()); + case UPDATE -> t("Bookmark updated"); + default -> null; + }; + } + + @Override + public Translatable subject() { + return describe(); + } + + +} diff --git a/bus/src/main/java/de/srsoftware/umbrella/messagebus/events/Event.java b/bus/src/main/java/de/srsoftware/umbrella/messagebus/events/Event.java index d6861778..09b68d86 100644 --- a/bus/src/main/java/de/srsoftware/umbrella/messagebus/events/Event.java +++ b/bus/src/main/java/de/srsoftware/umbrella/messagebus/events/Event.java @@ -1,22 +1,24 @@ /* © SRSoftware 2025 */ package de.srsoftware.umbrella.messagebus.events; -import static de.srsoftware.umbrella.core.Constants.*; +import static de.srsoftware.umbrella.core.constants.Field.*; import static java.util.Optional.*; import de.srsoftware.tools.Diff; import de.srsoftware.tools.Mappable; +import de.srsoftware.umbrella.core.model.Translatable; import de.srsoftware.umbrella.core.model.UmbrellaUser; +import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Optional; import org.json.JSONObject; - public abstract class Event { public enum EventType { CREATE, + MEMBER_ADDED, UPDATE, DELETE; } @@ -37,13 +39,15 @@ public abstract class Event { public Event(UmbrellaUser initiator, String module, Payload payload, Map oldData){ this.initiator = initiator; - this.module = module; - this.payload = payload; + this.module = module; + this.payload = payload; this.eventType = EventType.UPDATE; - this.oldData = oldData; + this.oldData = oldData; } - public abstract String describe(); + public abstract Collection audience(); + + public abstract Translatable describe(); private Map dropMarkdown(Map map) { var result = new HashMap(); @@ -56,15 +60,21 @@ public abstract class Event { } public Optional diff(){ - return oldData == null ? empty() : of(Diff.MapDiff.diff(dropMarkdown(oldData),dropMarkdown(payload.toMap()))); + return oldData == null ? empty() : of(Diff.MapDiff.diff(filter(oldData),filter(payload.toMap()))); } - public String eventType(){ - return eventType.toString(); + public EventType eventType(){ + return eventType; } - public abstract boolean isIntendedFor(UmbrellaUser user); + protected Map filter(Map map){ + return dropMarkdown(map); + } + + public boolean isIntendedFor(UmbrellaUser user){ + return audience().contains(user); + } public UmbrellaUser initiator(){ return initiator; @@ -88,7 +98,13 @@ public abstract class Event { return module; } + protected Map oldData() { + return oldData; + } + public Payload payload(){ return payload; } + + public abstract Translatable subject(); } diff --git a/bus/src/main/java/de/srsoftware/umbrella/messagebus/events/ItemEvent.java b/bus/src/main/java/de/srsoftware/umbrella/messagebus/events/ItemEvent.java new file mode 100644 index 00000000..9f886933 --- /dev/null +++ b/bus/src/main/java/de/srsoftware/umbrella/messagebus/events/ItemEvent.java @@ -0,0 +1,48 @@ +/* © SRSoftware 2025 */ +package de.srsoftware.umbrella.messagebus.events; + +import static de.srsoftware.umbrella.core.constants.Field.*; +import static de.srsoftware.umbrella.core.model.Translatable.t; + +import de.srsoftware.umbrella.core.ModuleRegistry; +import de.srsoftware.umbrella.core.api.Owner; +import de.srsoftware.umbrella.core.model.*; +import java.util.Collection; +import java.util.List; + +public class ItemEvent extends Event{ + public ItemEvent(UmbrellaUser initiator, String module, Item item, EventType type) { + super(initiator, module, item, type); + } + + @Override + public Collection audience() { + Owner owner = payload().location().resolve().owner().resolve(); + if (owner instanceof UmbrellaUser user) return List.of(user); + if (owner instanceof Company company) return ModuleRegistry.companyService().getMembers(company.id()); + return List.of(); + } + + @Override + public Translatable describe() { + return switch (eventType()){ + case CREATE -> describeCreate(); + case null, default -> null; + }; + } + + private Translatable describeCreate() { + var loc = payload().location().resolve().name(); + return t("{user} added \"{item}\" to \"{location}\"", USER,initiator().name(), ITEM, payload().name(), LOCATION, loc); + } + + + @Override + public Translatable subject() { + var loc = payload().location().resolve().name(); + return switch (eventType()){ + case CREATE -> t("A new item has been added to \"{location}\":",LOCATION,loc); + case null, default -> null; + }; + } +} diff --git a/bus/src/main/java/de/srsoftware/umbrella/messagebus/events/ProjectEvent.java b/bus/src/main/java/de/srsoftware/umbrella/messagebus/events/ProjectEvent.java index c5674807..78534b3a 100644 --- a/bus/src/main/java/de/srsoftware/umbrella/messagebus/events/ProjectEvent.java +++ b/bus/src/main/java/de/srsoftware/umbrella/messagebus/events/ProjectEvent.java @@ -1,25 +1,69 @@ /* © SRSoftware 2025 */ package de.srsoftware.umbrella.messagebus.events; -import static de.srsoftware.umbrella.core.Constants.PROJECT; +import static de.srsoftware.umbrella.core.constants.Field.*; +import static de.srsoftware.umbrella.core.constants.Module.PROJECT; +import static de.srsoftware.umbrella.core.model.Translatable.t; +import static de.srsoftware.umbrella.messagebus.events.Event.EventType.MEMBER_ADDED; -import de.srsoftware.umbrella.core.model.Project; -import de.srsoftware.umbrella.core.model.UmbrellaUser; +import de.srsoftware.umbrella.core.constants.Field; +import de.srsoftware.umbrella.core.model.*; +import java.util.Collection; import java.util.Map; public class ProjectEvent extends Event{ + private final UmbrellaUser newMember; + public ProjectEvent(UmbrellaUser initiator, Project project, EventType type){ super(initiator, PROJECT, project, type); + newMember = null; } public ProjectEvent(UmbrellaUser initiator, Project project, Map oldData){ super(initiator, PROJECT, project, oldData); + newMember = null; + } + + public ProjectEvent(UmbrellaUser initiator, Project project, UmbrellaUser newMember){ + super(initiator, PROJECT, project, MEMBER_ADDED); + this.newMember = newMember; } @Override - public String describe() { - return diff().orElse("[TODO: ProjectEvent.describe]"); + public Collection audience() { + return payload().members().values().stream().map(Member::user).toList(); + } + + @Override + public Translatable describe() { + return switch (eventType()){ + case CREATE -> describeCreate(); + case DELETE -> t("The project '{project}' has been deleted by {user}", Field.PROJECT, payload().name(), USER, initiator().name()); + case MEMBER_ADDED -> describeMemberAdded(); + case UPDATE -> describeUpdate(); + }; + } + + private Translatable describeCreate() { + var head = t("You have been added to the new project '{project}', created by {user}:\n\n{body}", Field.PROJECT, payload().name(), BODY, payload().description(), USER, initiator().name()); + return t("{head}\n\n{link}","head",head,"link",link()); + } + + private Translatable describeMemberAdded() { + var head = t("'{name}' has been added to '{object}' by '{user}'.",NAME,newMember.name(),Field.OBJECT,payload().name(),USER,initiator().name()); + return t("{head}\n\n{link}","head",head,"link",link()); + } + + private Translatable describeUpdate() { + var head = t("Changes in project '{project}':\n\n{body}",Field.PROJECT,payload().name(),BODY,diff().orElse("")); + return t("{head}\n\n{link}","head",head,"link",link()); + } + + @Override + protected Map filter(Map map) { + map.remove(MEMBERS); + return super.filter(map); } @Override @@ -29,4 +73,16 @@ public class ProjectEvent extends Event{ } return false; } -} + + private Translatable link() { + return t("You can view/edit this project at {base_url}/project/{id}/view",ID,payload().id()); + } + + @Override + public Translatable subject() { + return switch (eventType()){ + case CREATE -> t("The project '{project}' has been created", Field.PROJECT, payload().name()); + case DELETE -> t("The project '{project}' has been deleted",Field.PROJECT, payload().name()); + case MEMBER_ADDED, UPDATE -> t("Project '{project}' was edited",Field.PROJECT,payload().name()); + }; + }} diff --git a/bus/src/main/java/de/srsoftware/umbrella/messagebus/events/TaskEvent.java b/bus/src/main/java/de/srsoftware/umbrella/messagebus/events/TaskEvent.java index b2b63d1a..b6d5c008 100644 --- a/bus/src/main/java/de/srsoftware/umbrella/messagebus/events/TaskEvent.java +++ b/bus/src/main/java/de/srsoftware/umbrella/messagebus/events/TaskEvent.java @@ -1,25 +1,87 @@ /* © SRSoftware 2025 */ package de.srsoftware.umbrella.messagebus.events; -import static de.srsoftware.umbrella.core.Constants.TASK; -import de.srsoftware.umbrella.core.model.Task; -import de.srsoftware.umbrella.core.model.UmbrellaUser; +import static de.srsoftware.umbrella.core.ModuleRegistry.*; +import static de.srsoftware.umbrella.core.constants.Field.*; +import static de.srsoftware.umbrella.core.constants.Module.TASK; +import static de.srsoftware.umbrella.core.model.Translatable.t; +import static de.srsoftware.umbrella.messagebus.events.Event.EventType.MEMBER_ADDED; + +import de.srsoftware.umbrella.core.constants.Field; +import de.srsoftware.umbrella.core.model.*; +import java.util.Collection; +import java.util.List; import java.util.Map; public class TaskEvent extends Event{ + private final UmbrellaUser newMember; + public TaskEvent(UmbrellaUser initiator, Task task, EventType type){ super(initiator, TASK, task, type); + newMember = null; } public TaskEvent(UmbrellaUser initiator, Task task, Map oldData){ super(initiator, TASK, task, oldData); + newMember = null; + } + + public TaskEvent(UmbrellaUser initiator, Task task, UmbrellaUser newMember) { + super(initiator, TASK, task, MEMBER_ADDED); + this.newMember = newMember; } @Override - public String describe() { - return diff().orElse("[TODO: TaskEvent.describe()]"); + public Collection audience() { + return payload().members().values().stream().map(Member::user).toList(); + } + + @Override + public Translatable describe() { + return switch (eventType()){ + case CREATE -> describeCreate(); + case DELETE -> t("The task '{task}' has been deleted by {user}",Field.TASK, payload().name(), USER, initiator().name()); + case MEMBER_ADDED -> describeMemberAdded(); + case UPDATE -> describeUpdate(); + }; + } + + private Translatable describeCreate() { + String parentName = null; + var pid = payload().parentTaskId(); + if (pid != null) { + var parent = taskService().load(List.of(pid)).get(pid); + if (parent != null) parentName = parent.name(); + } + if (parentName == null){ + var project = projectService().load(payload().projectId()); + if (project != null) parentName = project.name(); + } + if (parentName == null) parentName = "?"; + var head = t("'{task}' has been added to '{object}':\n\n{body}", Field.TASK, payload().name(), OBJECT, parentName, BODY, payload().description()); + return t("{head}\n\n{link}","head",head,"link",link()); + } + + private Translatable describeMemberAdded() { + var head = t("'{name}' has been added to '{object}' by '{user}'.",NAME,newMember.name(), OBJECT,payload().name(),USER,initiator().name()); + return t("{head}\n\n{link}","head",head,"link",link()); + } + + private Translatable describeUpdate() { + var head = t("Changes in task '{task}':\n\n{body}",Field.TASK,payload().name(),BODY,diff().orElse("")); + return t("{head}\n\n{link}","head",head,"link",link()); + } + + @Override + protected Map filter(Map map) { + map.remove(MEMBERS); + var o = map.get(STATUS); + if (o instanceof Number status) try { + map.put(STATUS,Status.of(status.intValue()).name()); + } catch (IllegalArgumentException ignores){} + return super.filter(map); } @Override @@ -29,4 +91,16 @@ public class TaskEvent extends Event{ } return false; } -} + + private Translatable link() { + return t("You can view/edit this task at {base_url}/task/{id}/view",ID,payload().id()); + } + + @Override + public Translatable subject() { + return switch (eventType()){ + case CREATE -> t("The task '{task}' has been created", Field.TASK, payload().name()); + case DELETE -> t("The task '{task}' has been deleted",Field.TASK, payload().name()); + case MEMBER_ADDED, UPDATE -> t("Task '{task}' was edited",Field.TASK,payload().name()); + }; + }} diff --git a/bus/src/main/java/de/srsoftware/umbrella/messagebus/events/WikiEvent.java b/bus/src/main/java/de/srsoftware/umbrella/messagebus/events/WikiEvent.java index 434057de..2a6c5b91 100644 --- a/bus/src/main/java/de/srsoftware/umbrella/messagebus/events/WikiEvent.java +++ b/bus/src/main/java/de/srsoftware/umbrella/messagebus/events/WikiEvent.java @@ -1,32 +1,80 @@ /* © SRSoftware 2025 */ package de.srsoftware.umbrella.messagebus.events; -import static de.srsoftware.umbrella.core.Constants.WIKI; +import static de.srsoftware.umbrella.core.constants.Field.*; +import static de.srsoftware.umbrella.core.constants.Module.WIKI; +import static de.srsoftware.umbrella.core.model.Translatable.t; +import static de.srsoftware.umbrella.messagebus.events.Event.EventType.MEMBER_ADDED; -import de.srsoftware.umbrella.core.model.UmbrellaUser; -import de.srsoftware.umbrella.core.model.WikiPage; +import de.srsoftware.umbrella.core.constants.Field; +import de.srsoftware.umbrella.core.model.*; +import java.util.Collection; import java.util.Map; public class WikiEvent extends Event{ + private final UmbrellaUser newMember; + public WikiEvent(UmbrellaUser initiator, WikiPage page, EventType type){ super(initiator, WIKI, page, type); + newMember = null; } public WikiEvent(UmbrellaUser initiator, WikiPage page, Map oldData){ super(initiator, WIKI, page, oldData); + newMember = null; + } + + public WikiEvent(UmbrellaUser initiator, WikiPage page, UmbrellaUser newMember){ + super(initiator,WIKI,page,MEMBER_ADDED); + this.newMember = newMember; } @Override - public String describe() { - return diff().orElse("[TODO: WikiEvent.describe()]"); + public Collection audience() { + return payload().members().values().stream().map(Member::user).toList(); } @Override - public boolean isIntendedFor(UmbrellaUser user) { - for (var member : payload().members().values()){ - if (member.user().equals(user)) return true; - } - return false; + public Translatable describe() { + return switch (eventType()){ + case CREATE -> describeCreate(); + case DELETE -> describeDelete(); + case MEMBER_ADDED -> describeMemberAdded(); + case UPDATE -> describeUpdate(); + }; + } + + public Translatable describeCreate(){ + var head = t("New wiki page {name} has been created"); + return t("{head}:\n\n{object}\n\n{link}","head",head,OBJECT,payload().content(),"link",link()); + } + + public Translatable describeDelete(){ + return t("The wiki page '{page}' has been deleted by {user}", "page", payload().title(), USER, initiator().name()); + } + + public Translatable describeMemberAdded(){ + var head = t("'{name}' has been added to '{object}' by '{user}'.",NAME,newMember.name(), OBJECT,payload().title(),USER,initiator().name()); + return t("{head}\n\n{link}","head",head,"link",link()); + } + + private Translatable describeUpdate() { + var head = t("Changes in wiki page '{id}':\n\n{body}",Field.ID,payload().title(),BODY,diff().orElse("")); + return t("{head}\n\n{link}","head",head,"link",link()); + } + + private Translatable link() { + return t("You can view/edit this wiki page at {base_url}/wiki/{id}/view",ID,payload().id()); + } + + + @Override + public Translatable subject() { + return switch (eventType()){ + case CREATE -> t("The wiki page '{name}' has been created", Field.NAME, payload().title()); + case DELETE -> t("The wiki page '{name}' has been deleted",Field.NAME, payload().title()); + case MEMBER_ADDED, UPDATE -> t("wiki page '{name}' was edited",Field.NAME,payload().title()); + }; } } diff --git a/company/src/main/java/de/srsoftware/umbrella/company/CompanyModule.java b/company/src/main/java/de/srsoftware/umbrella/company/CompanyModule.java index 17b6c356..a3a200b5 100644 --- a/company/src/main/java/de/srsoftware/umbrella/company/CompanyModule.java +++ b/company/src/main/java/de/srsoftware/umbrella/company/CompanyModule.java @@ -4,12 +4,15 @@ package de.srsoftware.umbrella.company; import static de.srsoftware.umbrella.company.Constants.CONFIG_DATABASE; import static de.srsoftware.umbrella.company.Constants.NEXT_CUSTOMER_NUMBER; import static de.srsoftware.umbrella.core.ConnectionProvider.connect; -import static de.srsoftware.umbrella.core.Constants.*; import static de.srsoftware.umbrella.core.ModuleRegistry.*; -import static de.srsoftware.umbrella.core.Paths.LIST; -import static de.srsoftware.umbrella.core.Paths.SEARCH; import static de.srsoftware.umbrella.core.Util.mapValues; +import static de.srsoftware.umbrella.core.constants.Field.*; +import static de.srsoftware.umbrella.core.constants.Path.LIST; +import static de.srsoftware.umbrella.core.constants.Path.SEARCH; +import static de.srsoftware.umbrella.core.constants.Text.COMPANY_WITH_ID; +import static de.srsoftware.umbrella.core.constants.Text.LONG; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*; +import static de.srsoftware.umbrella.core.model.Translatable.t; import com.sun.net.httpserver.HttpExchange; import de.srsoftware.configuration.Configuration; @@ -30,17 +33,17 @@ public class CompanyModule extends BaseHandler implements CompanyService { public CompanyModule(Configuration config) throws UmbrellaException { super(); - var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingFieldException(CONFIG_DATABASE)); + var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingField(CONFIG_DATABASE)); companyDb = new SqliteDb(connect(dbFile)); ModuleRegistry.add(this); } private boolean deleteCompany(long companyId, UmbrellaUser user, HttpExchange ex) throws IOException { var company = get(companyId); - if (!membership(companyId,user.id())) throw forbidden("You are mot a member of company {0}",company.name()); - if (!documentService().list(companyId).isEmpty()) throw forbidden("There are documents owned by {0}",company.name()); - if (!itemService().redefineMe(companyId).isEmpty()) throw forbidden("There are items owned by {0}",company.name()); - if (!projectService().listCompanyProjects(companyId,true).isEmpty()) throw forbidden("There are projects owned by {0}",company.name()); + if (!membership(companyId,user.id())) throw forbidden("You are mot a member of company {company}", COMPANY,company.name()); + if (!documentService().list(companyId).isEmpty()) throw forbidden("There are documents owned by {company}", COMPANY,company.name()); + if (!itemService().getCompanyItems(companyId).isEmpty()) throw forbidden("There are items owned by {company}", COMPANY,company.name()); + if (!projectService().listCompanyProjects(companyId,true).isEmpty()) throw forbidden("There are projects owned by {company}", COMPANY,company.name()); return sendContent(ex, companyDb.drop(companyId)); } @@ -57,7 +60,7 @@ public class CompanyModule extends BaseHandler implements CompanyService { default -> deleteCompany(Long.parseLong(head), user.get(), ex); }; } catch (NumberFormatException n) { - return send(ex,invalidFieldException(ID,"ID (Long)")); + return send(ex,invalidField(ID,t(LONG))); } catch (UmbrellaException e) { return send(ex,e); } @@ -99,7 +102,7 @@ public class CompanyModule extends BaseHandler implements CompanyService { default -> patchCompany(Long.parseLong(head), user.get(), ex); }; } catch (NumberFormatException n) { - return send(ex,invalidFieldException(ID,"ID (Long)")); + return send(ex,invalidField(ID,t(LONG))); } catch (UmbrellaException e) { return send(ex,e); } @@ -119,7 +122,7 @@ public class CompanyModule extends BaseHandler implements CompanyService { default -> super.doPost(path,ex); }; } catch (NumberFormatException n) { - return send(ex,invalidFieldException(ID,"ID (Long)")); + return send(ex,invalidField(ID,t(LONG))); } catch (UmbrellaException e) { return send(ex,e); } @@ -153,7 +156,7 @@ public class CompanyModule extends BaseHandler implements CompanyService { private boolean getNextCustomerNumber(UmbrellaUser user, long companyId, HttpExchange ex) throws IOException { var company = companyDb.load(companyId); - if (!membership(companyId,user.id())) throw forbidden("You are mot a member of company {0}",company.name()); + if (!membership(companyId,user.id())) throw notAmember(t(COMPANY_WITH_ID,ID,company.name())); var nextCustomerNumber = companyDb.getNextCustomerNumber(companyId); return sendContent(ex,nextCustomerNumber); } @@ -186,7 +189,7 @@ public class CompanyModule extends BaseHandler implements CompanyService { private boolean patchCompany(long companyId, UmbrellaUser user, HttpExchange ex) throws IOException { var company = get(companyId); - if (!membership(companyId,user.id())) throw forbidden("You are mot a member of company {0}",company.name()); + if (!membership(companyId,user.id())) throw notAmember(t(COMPANY_WITH_ID,ID,company.name())); var json = json(ex); company = companyDb.save(company.patch(json)); @@ -221,7 +224,7 @@ public class CompanyModule extends BaseHandler implements CompanyService { private boolean postSearch(UmbrellaUser user, HttpExchange ex) throws IOException { var json = json(ex); - if (!(json.has(KEY) && json.get(KEY) instanceof String key)) throw missingFieldException(KEY); + if (!(json.has(KEY) && json.get(KEY) instanceof String key)) throw missingField(KEY); var keys = Arrays.asList(key.split(" ")); var companies = companyDb.find(user,keys); return sendContent(ex,mapValues(companies)); diff --git a/company/src/main/java/de/srsoftware/umbrella/company/SqliteDb.java b/company/src/main/java/de/srsoftware/umbrella/company/SqliteDb.java index 62351560..55dc7942 100644 --- a/company/src/main/java/de/srsoftware/umbrella/company/SqliteDb.java +++ b/company/src/main/java/de/srsoftware/umbrella/company/SqliteDb.java @@ -7,17 +7,19 @@ import static de.srsoftware.tools.jdbc.Query.*; import static de.srsoftware.tools.jdbc.Query.Dialect.SQLITE; import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL; import static de.srsoftware.umbrella.company.Constants.*; -import static de.srsoftware.umbrella.core.Constants.*; -import static de.srsoftware.umbrella.core.Constants.COMPANY; import static de.srsoftware.umbrella.core.Errors.*; -import static de.srsoftware.umbrella.core.Field.*; -import static de.srsoftware.umbrella.core.Field.COMPANY_ID; +import static de.srsoftware.umbrella.core.constants.Field.*; +import static de.srsoftware.umbrella.core.constants.Field.COMPANY; +import static de.srsoftware.umbrella.core.constants.Field.NUMBER; +import static de.srsoftware.umbrella.core.constants.Field.TYPE; +import static de.srsoftware.umbrella.core.constants.Text.*; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*; +import static de.srsoftware.umbrella.core.model.Translatable.t; import static java.text.MessageFormat.format; import de.srsoftware.umbrella.company.api.CompanyDb; import de.srsoftware.umbrella.core.BaseDb; -import de.srsoftware.umbrella.core.ModuleRegistry; +import de.srsoftware.umbrella.core.constants.Text; import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.model.Company; import de.srsoftware.umbrella.core.model.UmbrellaUser; @@ -67,7 +69,7 @@ CREATE TABLE IF NOT EXISTS "companies" ( stmt.execute(); stmt.close(); } catch (SQLException e) { - throw databaseException(FAILED_TO_CREATE_TABLE,TABLE_COMPANIES).causedBy(e); + throw failedToCreateTable(TABLE_COMPANIES).causedBy(e); } } @@ -78,7 +80,7 @@ CREATE TABLE IF NOT EXISTS "companies" ( stmt.execute(); stmt.close(); } catch (SQLException e) { - throw databaseException(FAILED_TO_CREATE_TABLE,TABLE_COMPANIES).causedBy(e); + throw failedToCreateTable(TABLE_COMPANIES_USERS).causedBy(e); } } @@ -91,7 +93,7 @@ CREATE TABLE IF NOT EXISTS "companies" ( .execute(db) .close(); } catch (SQLException e) { - throw databaseException(FAILED_TO_ASSIGN_USER_TO_COMPANY).causedBy(e); + throw databaseException(FAILED_TO_ASSIGN_A_TO_B,"a", t(USER_WITH_ID, ID,user_id),"b", t(COMPANY_WITH_ID, ID,company_id)).causedBy(e); } } @@ -102,7 +104,7 @@ CREATE TABLE IF NOT EXISTS "companies" ( delete().from(TABLE_COMPANIES).where(ID,equal(companyId)).execute(db); return companyId; } catch (SQLException e) { - throw databaseException(FAILED_TO_DROP_ENTITY,"company",companyId).causedBy(e); + throw failedToDropObject("company "+ companyId).causedBy(e); } } @@ -111,7 +113,7 @@ CREATE TABLE IF NOT EXISTS "companies" ( try { delete().from(TABLE_COMPANIES_USERS).where(COMPANY_ID,equal(companyId)).where(USER_ID,equal(userId)).execute(db); } catch (SQLException e) { - throw databaseException(FAILED_TO_DROP_ENTITY_OF_ENTITY,"user",userId,"company",companyId).causedBy(e); + throw failedToDropObjectFromObject("user",userId,"company",companyId).causedBy(e); } } @@ -124,14 +126,14 @@ CREATE TABLE IF NOT EXISTS "companies" ( rs.close(); return ids; } catch (SQLException e) { - throw databaseException(FAILED_TO_LOAD_ENTITY_MEMBERS,COMPANY,companyId).causedBy(e); + throw failedToLoadMembers(t("company {company}", COMPANY,companyId)).causedBy(e); } } @Override public String getNextCustomerNumber(long companyId) { try { - var rs = select(LAST_CUSTOMER_NUMBER,CUSTOMER_NUMBER_PREFIX).from(TABLE_COMPANIES).where(ID,equal(companyId)).exec(db); + var rs = select(LAST_CUSTOMER_NUMBER, CUSTOMER_NUMBER_PREFIX).from(TABLE_COMPANIES).where(ID,equal(companyId)).exec(db); var last = 0L; String prefix = null; if (rs.next()){ @@ -142,7 +144,7 @@ CREATE TABLE IF NOT EXISTS "companies" ( var next = last+1; return prefix+next; // TODO: currently not taking growing number lengths into account, this should be resolved! } catch (SQLException e) { - throw databaseException(FAILED_TO_LOAD_CUSTOMER_NUM_SETTINGS,companyId).causedBy(e); + throw failedToLoadObject(t("customer number settings for {company_id}",COMPANY_ID,companyId)).causedBy(e); } } @@ -152,7 +154,7 @@ CREATE TABLE IF NOT EXISTS "companies" ( var query = select(DISTINCT).from(TABLE_COMPANIES).leftJoin(ID,TABLE_COMPANIES_USERS,COMPANY_ID) .where(USER_ID,equal(user.id())); for (var key : keys){ - query.where(format("CONCAT({0},\" \",{1},\" \",{2},\" \",{3},\" \",{4})",NAME,ADDRESS,EMAIL,PHONE,BANK_ACCOUNT),like("%"+key+"%")); + query.where(format("CONCAT({0},\" \",{1},\" \",{2},\" \",{3},\" \",{4})", NAME, ADDRESS, EMAIL,PHONE,BANK_ACCOUNT),like("%"+key+"%")); } var rs = query.exec(db); var companies = new HashMap(); @@ -163,7 +165,8 @@ CREATE TABLE IF NOT EXISTS "companies" ( rs.close(); return companies; } catch (SQLException e){ - throw databaseException(FAILED_TO_SEARCH_DB, ModuleRegistry.translator().translate(user.language(),COMPANY)).causedBy(e); + throw failedToSearchDb(t(Text.COMPANY)).causedBy(e); + //throw databaseException(FAILED_TO_SEARCH_DB, ModuleRegistry.translator().translate(user.language(),COMPANY)).causedBy(e); } } @@ -179,7 +182,7 @@ CREATE TABLE IF NOT EXISTS "companies" ( rs.close(); return companies; } catch (SQLException e) { - throw databaseException(FAILED_TO_LOAD_ENTITIES_OF_OWNER,"companies","user "+userId).causedBy(e); + throw databaseException(FAILED_TO_LOAD_ENTITIES_OF_OWNER,TYPE,COMPANIES, OWNER, t(USER_WITH_ID, ID,userId)); } } @@ -190,10 +193,10 @@ CREATE TABLE IF NOT EXISTS "companies" ( Company company = null; if (rs.next()) company = Company.of(rs); rs.close(); - if (company == null) throw notFound(FAILED_TO_LOAD_ENTITY_BY_ID,"company",companyId); + if (company == null) throw notFound(FAILED_TO_LOAD_OBJECT_BY_ID, OBJECT, t(Text.COMPANY), ID,companyId); return company; } catch (SQLException e){ - throw databaseException(FAILED_TO_LOAD_ENTITY_BY_ID,"company",companyId).causedBy(e); + throw failedToLoadObject(t(Text.COMPANY),companyId).causedBy(e); } } @@ -202,7 +205,7 @@ CREATE TABLE IF NOT EXISTS "companies" ( try { if (company.id() == 0){ // new long id = 0; - var rs = insertInto(TABLE_COMPANIES,NAME, ADDRESS, EMAIL, PHONE, BANK_ACCOUNT, COURT, CURRENCY, TAX_NUMBER, DECIMALS, DECIMAL_SEPARATOR, THOUSANDS_SEPARATOR, LAST_CUSTOMER_NUMBER, CUSTOMER_NUMBER_PREFIX) + var rs = insertInto(TABLE_COMPANIES, NAME, ADDRESS, EMAIL, PHONE, BANK_ACCOUNT, COURT, CURRENCY, TAX_NUMBER, DECIMALS, DECIMAL_SEPARATOR, THOUSANDS_SEPARATOR, LAST_CUSTOMER_NUMBER, CUSTOMER_NUMBER_PREFIX) .values(company.name(),company.address(),company.email(),company.phone(),company.bankAccount(),company.court(),company.currency(),company.taxId(),company.decimals(),company.decimalSeparator(),company.thousandsSeparator(),0,company.customerNumberPrefix()) .execute(db) .getGeneratedKeys(); @@ -225,7 +228,7 @@ CREATE TABLE IF NOT EXISTS "companies" ( return company; } } catch (SQLException e){ - throw databaseException(FAILED_TO_STORE_ENTITY,company.name()).causedBy(e); + throw failedToStoreObject(company.name()).causedBy(e); } } @@ -233,13 +236,13 @@ CREATE TABLE IF NOT EXISTS "companies" ( public void saveNewCustomer(long companyId, String id) { var p = Pattern.compile("(?s)(.*?)(\\d+)$"); var m = p.matcher(id); - if (!m.matches()) throw unprocessable("{0} is not a valid customer id: it does not end with a number!"); + if (!m.matches()) throw unprocessable("{number} is not a valid customer id: it does not end with a number!", NUMBER,id); String prefix = m.group(1); // Prefix before last number long number = Long.parseLong(m.group(2)); // The last numeric part try { - update(TABLE_COMPANIES).set(LAST_CUSTOMER_NUMBER,CUSTOMER_NUMBER_PREFIX).where(ID,equal(companyId)).prepare(db).apply(number,prefix).close(); + update(TABLE_COMPANIES).set(LAST_CUSTOMER_NUMBER, CUSTOMER_NUMBER_PREFIX).where(ID,equal(companyId)).prepare(db).apply(number,prefix).close(); } catch (SQLException e) { - throw databaseException(FAILED_TO_UPDATE_ENTITY, LAST_CUSTOMER_NUMBER).causedBy(e); + throw failedToStoreObject(LAST_CUSTOMER_NUMBER).causedBy(e); } } } diff --git a/contact/src/main/java/de/srsoftware/umbrella/contact/ContactModule.java b/contact/src/main/java/de/srsoftware/umbrella/contact/ContactModule.java index 03245eef..087e6980 100644 --- a/contact/src/main/java/de/srsoftware/umbrella/contact/ContactModule.java +++ b/contact/src/main/java/de/srsoftware/umbrella/contact/ContactModule.java @@ -4,9 +4,9 @@ package de.srsoftware.umbrella.contact; import static de.srsoftware.umbrella.contact.Constants.CONFIG_DATABASE; import static de.srsoftware.umbrella.core.ConnectionProvider.connect; import static de.srsoftware.umbrella.core.ModuleRegistry.userService; -import static de.srsoftware.umbrella.core.Paths.LIST; import static de.srsoftware.umbrella.core.Util.mapValues; -import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingFieldException; +import static de.srsoftware.umbrella.core.constants.Path.LIST; +import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingField; import com.sun.net.httpserver.HttpExchange; import de.srsoftware.configuration.Configuration; @@ -28,7 +28,7 @@ public class ContactModule extends BaseHandler implements ContactService { public ContactModule(Configuration config) throws UmbrellaException { super(); - var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingFieldException(CONFIG_DATABASE)); + var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingField(CONFIG_DATABASE)); contactDb = new SqliteDb(connect(dbFile)); ModuleRegistry.add(this); } diff --git a/contact/src/main/java/de/srsoftware/umbrella/contact/SqliteDb.java b/contact/src/main/java/de/srsoftware/umbrella/contact/SqliteDb.java index 9c8a35ba..70d27d0b 100644 --- a/contact/src/main/java/de/srsoftware/umbrella/contact/SqliteDb.java +++ b/contact/src/main/java/de/srsoftware/umbrella/contact/SqliteDb.java @@ -6,10 +6,12 @@ import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL; import static de.srsoftware.tools.jdbc.Query.insertInto; import static de.srsoftware.tools.jdbc.Query.select; import static de.srsoftware.umbrella.contact.Constants.*; -import static de.srsoftware.umbrella.core.Constants.*; import static de.srsoftware.umbrella.core.Errors.*; -import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.databaseException; -import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.notFound; +import static de.srsoftware.umbrella.core.constants.Field.*; +import static de.srsoftware.umbrella.core.constants.Field.TYPE; +import static de.srsoftware.umbrella.core.constants.Text.*; +import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*; +import static de.srsoftware.umbrella.core.model.Translatable.t; import static java.text.MessageFormat.format; import de.srsoftware.tools.jdbc.Query; @@ -45,7 +47,7 @@ public class SqliteDb extends BaseDb implements ContactDb{ try { db.prepareStatement(sql).execute(); } catch (SQLException e) { - throw databaseException(ERROR_FAILED_CREATE_TABLE,TABLE_CONTACTS).causedBy(e); + throw failedToCreateTable(TABLE_CONTACTS).causedBy(e); } } @@ -55,7 +57,7 @@ public class SqliteDb extends BaseDb implements ContactDb{ try { db.prepareStatement(sql).execute(); } catch (SQLException e) { - throw databaseException(ERROR_FAILED_CREATE_TABLE,TABLE_CONTACTS_USERS).causedBy(e); + throw failedToCreateTable(TABLE_CONTACTS_USERS).causedBy(e); } } @@ -67,7 +69,7 @@ public class SqliteDb extends BaseDb implements ContactDb{ Query.delete().from(TABLE_CONTACTS_USERS).where(CONTACT_ID,equal(contact.id())).execute(db); db.setAutoCommit(true); } catch (SQLException e){ - throw databaseException(FAILED_TO_DROP_ENTITY,"contact",contact.id()).causedBy(e); + throw failedToDropObject(t(CONTACT_WITH_ID, ID,contact.id())).causedBy(e); } } @@ -83,7 +85,10 @@ public class SqliteDb extends BaseDb implements ContactDb{ rs.close(); return contacts; } catch (SQLException e) { - throw databaseException(FAILED_TO_LOAD_ENTITIES_OF_OWNER,"contacts",userId).causedBy(e); + throw databaseException(FAILED_TO_LOAD_ENTITIES_OF_OWNER, + TYPE, t(CONTACTS), + OWNER, t(USER_WITH_ID, ID,userId) + ).causedBy(e); } } @@ -95,9 +100,12 @@ public class SqliteDb extends BaseDb implements ContactDb{ if (rs.next()) contact = Contact.of(rs); rs.close(); if (contact != null) return contact; - throw notFound(FAILED_TO_LOAD_ENTITY_BY_ID, "contact", contactId); + throw failedToLoadObject(t(CONTACT), contactId); } catch (SQLException e) { - throw databaseException(FAILED_TO_LOAD_ENTITIES_OF_OWNER,"contacts",userId).causedBy(e); + throw databaseException(FAILED_TO_LOAD_ENTITIES_OF_OWNER, + TYPE, t(CONTACT), + OWNER, t(USER_WITH_ID, ID,userId) + ); } } @@ -105,19 +113,19 @@ public class SqliteDb extends BaseDb implements ContactDb{ public Contact save(Contact contact) { if (contact.id() == 0){ // new contact try { - var rs = insertInto(TABLE_CONTACTS,DATA).values(contact.vcard()).execute(db).getGeneratedKeys(); + var rs = insertInto(TABLE_CONTACTS, DATA).values(contact.vcard()).execute(db).getGeneratedKeys(); Long id = null; if (rs.next()) id = rs.getLong(1); rs.close(); if (id != null) return new Contact(id,contact.vcard()); - throw databaseException(FAILED_TO_STORE_ENTITY,"vcard"); + throw failedToStoreObject(VCARD); } catch (SQLException e) { - throw databaseException(FAILED_TO_STORE_ENTITY,"vcard").causedBy(e); + throw failedToStoreObject(VCARD).causedBy(e); } } else try { // update Query.update(TABLE_CONTACTS).set(DATA).where(ID,equal(contact.id())).prepare(db).apply(contact.vcard()).execute(); } catch (SQLException e) { - throw databaseException(FAILED_TO_UPDATE_ENTITY,"vcard").causedBy(e); + throw failedToStoreObject(VCARD).causedBy(e); } return contact; } @@ -125,9 +133,9 @@ public class SqliteDb extends BaseDb implements ContactDb{ @Override public void setOwner(long userId, Contact contact) { try { - Query.replaceInto(TABLE_CONTACTS_USERS,USER_ID,CONTACT_ID,ASSIGNED).values(userId,contact.id(),false).execute(db).close(); + Query.replaceInto(TABLE_CONTACTS_USERS, USER_ID,CONTACT_ID,ASSIGNED).values(userId,contact.id(),false).execute(db).close(); } catch (SQLException e) { - throw databaseException(FAILED_TO_ASSIGN_CONTACT_TO_USER,contact.id(),userId).causedBy(e); + throw databaseException(FAILED_TO_ASSIGN_A_TO_B,"a", t(CONTACT_WITH_ID, ID,contact.id()),"b", t(USER_WITH_ID, ID,userId)).causedBy(e); } } } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/BaseDb.java b/core/src/main/java/de/srsoftware/umbrella/core/BaseDb.java index cabe0887..89a916d6 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/BaseDb.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/BaseDb.java @@ -4,9 +4,10 @@ package de.srsoftware.umbrella.core; import static de.srsoftware.tools.jdbc.Condition.equal; import static de.srsoftware.tools.jdbc.Query.replaceInto; import static de.srsoftware.tools.jdbc.Query.update; -import static de.srsoftware.umbrella.core.Constants.*; -import static de.srsoftware.umbrella.core.Constants.TABLE_SETTINGS; -import static java.lang.System.Logger.Level.ERROR; +import static de.srsoftware.umbrella.core.constants.Constants.TABLE_SETTINGS; +import static de.srsoftware.umbrella.core.constants.Field.*; +import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.failedToCreateTable; +import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.failedToReadFromTable; import static java.lang.System.Logger.Level.INFO; import static java.text.MessageFormat.format; @@ -37,8 +38,7 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255) stmt.execute(); stmt.close(); } catch (SQLException e) { - LOG.log(ERROR,ERROR_FAILED_CREATE_TABLE,TABLE_SETTINGS,e); - throw new RuntimeException(e); + throw failedToCreateTable(TABLE_SETTINGS).causedBy(e); } var version = 0; @@ -49,8 +49,7 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255) return version; } catch (SQLException e) { - LOG.log(ERROR,ERROR_READ_TABLE,DB_VERSION,TABLE_SETTINGS,e); - throw new RuntimeException(e); + throw failedToReadFromTable(DB_VERSION,TABLE_SETTINGS).causedBy(e); } } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/BaseHandler.java b/core/src/main/java/de/srsoftware/umbrella/core/BaseHandler.java index 3aaaae8d..f1e67371 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/BaseHandler.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/BaseHandler.java @@ -2,10 +2,8 @@ package de.srsoftware.umbrella.core; import static de.srsoftware.tools.Optionals.nullable; -import static de.srsoftware.umbrella.core.ModuleRegistry.translator; import static java.lang.System.Logger.Level.*; import static java.net.HttpURLConnection.*; -import static java.text.MessageFormat.format; import com.sun.net.httpserver.HttpExchange; import de.srsoftware.tools.Path; @@ -74,13 +72,10 @@ public abstract class BaseHandler extends PathHandler { public boolean send(HttpExchange ex, UmbrellaException e) throws IOException { var cause = e.getCause(); - String lang = languages(ex).stream().findFirst().orElse(null); - var translatedMessage = translator().translate(lang,e.getMessage()); - if (cause != null){ - var msg = "en".equals(lang) ? translatedMessage : translator().translate("en",e.getMessage()); - LOG.log(ERROR,format(msg,e.fills()),cause); - } - return sendContent(ex,e.statusCode(),format(translatedMessage,e.fills())); + String lang = languages(ex).stream().findFirst().orElse("en"); + var translatedMessage = e.message().translate(lang); + if (cause != null) LOG.log(ERROR,translatedMessage,cause); + return sendContent(ex,e.statusCode(),translatedMessage); } public boolean unauthorized(HttpExchange ex) throws IOException { diff --git a/core/src/main/java/de/srsoftware/umbrella/core/Constants.java b/core/src/main/java/de/srsoftware/umbrella/core/Constants.java deleted file mode 100644 index 1ba584ed..00000000 --- a/core/src/main/java/de/srsoftware/umbrella/core/Constants.java +++ /dev/null @@ -1,164 +0,0 @@ -/* © SRSoftware 2025 */ -package de.srsoftware.umbrella.core; - - -import static java.nio.charset.StandardCharsets.UTF_8; - -public class Constants { - - private Constants(){} - - public static final String ACTION = "action"; - public static final String ADDRESS = "address"; - public static final String ALLOWED_STATES = "allowed_states"; - public static final String ATTACHMENTS = "attachments"; - public static final String AUTHORIZATION = "Authorization"; - - public static final String BODY = "body"; - public static final String BOOKMARK = "bookmark"; - - public static final String CACHE_CONTROL = "Cache-Control"; - public static final String CODE = "code"; - public static final String COMMENT = "comment"; - public static final String COMPANY = "company"; - public static final String COMPANY_ID = "company_id"; - public static final String CONNECTION = "Connection"; - public static final String CONTENT = "content"; - public static final String CONTENT_DISPOSITION = "Content-Disposition"; - public static final String CONTENT_ENCODING = "Content-Encoding"; - public static final String CONTENT_TYPE = "Content-Type"; - public static final String CUSTOMER_NUMBER_PREFIX = "customer_number_prefix"; - - public static final String DATA = "data"; - public static final String DB_VERSION = "db_version"; - public static final String DATE = "date"; - public static final String DECIMALS = "decimals"; - public static final String DECIMAL_SEPARATOR = "decimal_separator"; - public static final String DELETED = "deleted"; - public static final String DESCRIPTION = "description"; - public static final String DOMAIN = "domain"; - public static final String DROP_MEMBER = "drop_member"; - public static final String DUE_DATE = "due_date"; - public static final String DURATION = "duration"; - - public static final String EMAIL = "email"; - public static final String END_TIME = "end_time"; - public static final String ENTITY_ID = "entity_id"; - public static final String ERROR_FAILED_CREATE_TABLE = "Failed to create \"{0}\" table!"; - public static final String ERROR_INVALID_FIELD = "Expected {0} to be {1}!"; - public static final String ERROR_MISSING_CONFIG = "Config is missing value for {0}!"; - public static final String ERROR_MISSING_FIELD = "Json is missing {0} field!"; - public static final String ERROR_READ_TABLE = "Failed to read {0} from {1} table"; - public static final String EST_TIME = "est_time"; - 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 FROM = "from"; - public static final String FULLTEXT = "fulltext"; - - public static final String GET = "GET"; - public static final String GUEST_ALLOWED = "guest_allowed"; - - public static final String HASH = "hash"; - - public static final String ID = "id"; - - public static final String JSONARRAY = "json array"; - public static final String JSONOBJECT = "json object"; - - public static final String KEEP_ALIVE = "keep-alive"; - public static final String KEY = "key"; - - public static final String LANGUAGE = "language"; - public static final String LAST_CUSTOMER_NUMBER = "last_customer_number"; - public static final String LIMIT = "limit"; - public static final String LOCATION = "location"; - public static final String LOCATION_ID = "location_id"; - public static final String LOGIN = "login"; - - public static final String MEMBERS = "members"; - public static final String MESSAGES = "messages"; - public static final String MODULE = "module"; - - public static final String NAME = "name"; - public static final String NEW_MEMBER = "new_member"; - public static final String MIME = "mime"; - public static final String NO_CACHE = "no-cache"; - public static final String NO_INDEX = "no_index"; - public static final String NONE = "none"; - public static final String NOTE = "note"; - public static final String NUMBER = "number"; - - public static final String OFFSET = "offset"; - public static final String OPTIONAL = "optional"; - public static final String OWNER = "owner"; - public static final String OWNER_NUMBER = "owner_number"; - - public static final String PARENT_LOCATION_ID = "parent_location_id"; - public static final String PARENT_TASK_ID = "parent_task_id"; - public static final String PASS = "pass"; - public static final String PASSWORD = "password"; - public static final String PATH = "path"; - public static final String PERMISSION = "permission"; - public static final String POST = "POST"; - public static final String PRIORITY = "priority"; - public static final String PROJECT = "project"; - public static final String PROJECT_ID = "project_id"; - public static final String PROPERTIES = "properties"; - public static final String PROPERTY = "property"; - - public static final String RECEIVERS = "receivers"; - public static final String REDIRECT = "redirect"; - public static final String RENDERED = "rendered"; - public static final String REQUIRED_TASKS_IDS = "required_tasks_ids"; - - public static final String SENDER = "sender"; - public static final String SETTINGS = "settings"; - public static final String SHOW_CLOSED = "show_closed"; - public static final String SOURCE = "source"; - public static final String START_DATE = "start_date"; - public static final String START_TIME = "start_time"; - public static final String STATE = "state"; - public static final String STATUS = "status"; - public static final String STATUS_CODE = "code"; - public static final String STRING = "string"; - public static final String SUBJECT = "subject"; - - public static final String TABLE_SETTINGS = "settings"; - public static final String TAGS = "tags"; - public static final String TAG_COLORS = "tag_colors"; - public static final String TASK = "task"; - public static final String TASK_IDS = "task_ids"; - public static final String TAX = "tax"; - public static final String TAX_RATE = "tax_rate"; - public static final String TEMPLATE = "template"; - public static final String TEXT = "text"; - public static final String THOUSANDS_SEPARATOR = "thousands_separator"; - public static final String THEME = "theme"; - public static final String TITLE = "title"; - public static final String TIMESTAMP = "timestamp"; - public static final String TO = "to"; - public static final String TOKEN = "token"; - public static final String TOTAL_PRIO = "total_prio"; - public static final String TYPE = "type"; - - public static final String UMBRELLA = "Umbrella"; - public static final String UNIT = "unit"; - public static final String URI = "uri"; - public static final String URL = "url"; - public static final String USER = "user"; - public static final String USERS = "users"; - public static final String USER_ID = "user_id"; - public static final String USER_LIST = "user_list"; - public static final String USES = "uses"; - public static final String UTF8 = UTF_8.displayName(); - - public static final String VALUE = "value"; - public static final String VCARD = "vcard"; - public static final String VERSION = "version"; - public static final String VERSIONS = "versions"; - - public static final String WIKI = "wiki"; -} diff --git a/core/src/main/java/de/srsoftware/umbrella/core/Errors.java b/core/src/main/java/de/srsoftware/umbrella/core/Errors.java index 09b1c594..21edfb67 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/Errors.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/Errors.java @@ -2,38 +2,42 @@ package de.srsoftware.umbrella.core; public class Errors { - public static final String FAILED_TO_ADD_COLUMN = "failed_to_add_column"; - public static final String FAILED_TO_ADD_PROPERTY_TO_ITEM = "failed_to_add_prop_to_item"; - public static final String FAILED_TO_ASSIGN_USER_TO_COMPANY = "failed_to_assign_user_to_company"; - public static final String FAILED_TO_ASSIGN_CONTACT_TO_USER = "failed_to_assign_contact_to_user"; - public static final String FAILED_TO_CHECK_ENTITY_AVAILABLE = "failed_to_check_entity_available"; - public static final String FAILED_TO_CHECK_FILE_PERMISSIONS = "failed_to_check_file_permissions"; - public static final String FAILED_TO_CREATE_STATE = "failed_to_create_state"; - public static final String FAILED_TO_CREATE_TABLE = "failed_to_create_table"; - public static final String FAILED_TO_DROP_ENTITY = "failed_to_drop_entity"; - public static final String FAILED_TO_DROP_ENTITY_OF_ENTITY = "failed_to_drop_entity_from_entity"; - public static final String FAILED_TO_DROP_NOTES = "failed_to_drop_notes"; - public static final String FAILED_TO_GET_FREE_ID = "failed_to_get_free_id"; - public static final String FAILED_TO_LIST_ENTITIES = "failed_to_list_entities"; - public static final String FAILED_TO_LOAD_CHILD_LOCATIONS = "failed_to_load_child_locations"; - public static final String FAILED_TO_LOAD_ENTITY_MEMBERS = "failed_to_load_entity_members"; - public static final String FAILED_TO_LOAD_ENTITIES_OF_OWNER = "failed_to_load_entities_of_owner"; - public static final String FAILED_TO_LOAD_CUSTOMER_NUM_SETTINGS = "failed_to_load_customer_number_settings"; - public static final String FAILED_TO_LOAD_CUSTOMER_PRICE = "failed_to_load_customer_price"; - public static final String FAILED_TO_LOAD_CUSTOMER_SETTINGS = "failed_to_load_customer_settings"; - public static final String FAILED_TO_LOAD_ENTITY = "failed_to_load_entity"; - public static final String FAILED_TO_LOAD_ENTITY_BY_ID = "failed_to_load_entity_by_id"; - public static final String FAILED_TO_LOAD_USER_SETTINGS = "failed_to_load_user_settings"; - public static final String FAILED_TO_MOVE = "failed_to_move"; - public static final String FAILED_TO_READ_LAST_DOCID = "failed_to_read_last_docId"; - public static final String FAILED_TO_SEARCH_DB = "failed_to_search_db"; - public static final String FAILED_TO_STORE_ENTITY = "failed_to_store_entity"; - public static final String FAILED_TO_SWITCH_POSITIONS = "failed_to_switch_positions"; - public static final String FAILED_TO_UPDATE_COLUMN = "failed_to_update_column"; - public static final String FAILED_TO_UPDATE_ENTITY = "failed_to_update_entity"; - public static final String FAILED_TO_UPDATE_TABLE = "failed_to_update_table"; - public static final String MISSING_NEW_ITEM_ID = "missing_new_item_id"; - public static final String NO_BOOKMARK_FOR_URLID = "no_bookmark_for_urlid"; - public static final String UNEXPECTED_ITEM_ID_FORMAT = "unexpected_item_id_format"; - public static final String UNKNOWN_ITEM_LOCATION = "unknown_item_location"; + public static final String ADDRESS_MISSING = "{object} address does not contain street address / post code / city"; + + public static final String FAILED_TO_ADD_COLUMN = "Failed to add {name} column to {table} table"; + public static final String FAILED_TO_ADD_PROPERTY_TO_ITEM = "Failed to add new property to item {object}"; + public static final String FAILED_TO_ASSIGN_A_TO_B = "Failed to assign {a} to {b}."; + public static final String FAILED_TO_CHECK_ENTITY_AVAILABLE = "Failed to check availability of {object}"; + public static final String FAILED_TO_CHECK_FILE_PERMISSIONS = "Failed to check file permissions!"; + public static final String FAILED_TO_CREATE_STATE = "Failed to create custom state!"; + public static final String FAILED_TO_CREATE_TABLE = "Failed to create table `{name}`"; + public static final String FAILED_TO_DROP_OBJECT = "Failed to drop {object}!"; + public static final String FAILED_TO_DROP_ENTITY_OF_ENTITY = "Failed to drop {dropped_type} {object} from {related_type} {related}!"; + public static final String FAILED_TO_DROP_NOTES = "Failed to delete notes of ({module} {id})"; + public static final String FAILED_TO_GET_FREE_ID = "Failed to query free ID"; + public static final String FAILED_TO_LIST_ENTITIES = "Failed to list {type}"; + public static final String FAILED_TO_LOAD_CHILD_LOCATIONS = "Failed to load child locations for {parent}"; + public static final String FAILED_TO_LOAD_OBJECT_MEMBERS = "Failed to load members of {object}"; + public static final String FAILED_TO_LOAD_ENTITIES_OF_OWNER = "Failed to load {type} of {owner}!"; + public static final String FAILED_TO_LOAD_CUSTOMER_PRICE = "Failed to load customer price (company: {company}, customer: {customer}, item: {item})"; + public static final String FAILED_TO_LOAD_CUSTOMER_SETTINGS = "Failed to load customer settings (company: {company}, document type: {type})"; + public static final String FAILED_TO_LOAD_OBJECT = "Failed to load {object}!"; + public static final String FAILED_TO_LOAD_OBJECT_BY_ID = "Failed to load {object} by id (id)!"; + public static final String FAILED_TO_MOVE = "Failed to move {old} to {new}!"; + public static final String FAILED_TO_READ_FROM_TABLE = "Failed to read {field} from {table}!"; + public static final String FAILED_TO_READ_LAST_DOCID = "Failed to read last document id"; + public static final String FAILED_TO_SEARCH_DB = "Failed to search in {module} db"; + public static final String FAILED_TO_STORE_OBJECT = "Failed to store {object}"; + public static final String FAILED_TO_SWITCH_POSITIONS = "Failed to switch positions {a} and {b} of document {document}"; + public static final String FAILED_TO_UPDATE_COLUMN = "Failed to update column {old} → {new} of {table}"; + public static final String FAILED_TO_UPDATE_OBJECT = "Failed to update {object} in database"; + public static final String INVALID_EMAIL = "\"{email}\" is not a valid email address!"; + public static final String INVALID_FIELD = "Expected {field} to be {expected}!"; + public static final String INVALID_URL = "\"{url}\" is not a valid URL!"; + public static final String MISSING_CONFIG = "Config is missing value for {key}!"; + public static final String MISSING_FIELD = "Json is missing {field} field!"; + public static final String MISSING_NEW_ITEM_ID = "Old item id ({id}) has no new counterpart!"; + public static final String NO_MEMBER = "You are not a member of {object}!"; + public static final String UNEXPECTED_ITEM_ID_FORMAT = "Expected old item ID to be of the form ss:dd:dd, encountered {object}!"; + public static final String UNKNOWN_ITEM_LOCATION = "Item {object} of {owner} refers to location {location}, which is unknown!"; } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/Field.java b/core/src/main/java/de/srsoftware/umbrella/core/Field.java deleted file mode 100644 index d7756424..00000000 --- a/core/src/main/java/de/srsoftware/umbrella/core/Field.java +++ /dev/null @@ -1,51 +0,0 @@ -/* © SRSoftware 2025 */ -package de.srsoftware.umbrella.core; - -public class Field{ - public static final String AMOUNT = "amount"; - public static final String BANK_ACCOUNT = "bank_account"; - public static final String COMPANY = "company"; - public static final String COMPANY_ID = "company_id"; - public static final String COURT = "court"; - public static final String CURRENCY = "currency"; - public static final String CUSTOMER = "customer"; - public static final String CUSTOMER_EMAIL = "customer_email"; - public static final String CUSTOMER_NUMBER = "customer_number"; - public static final String CUSTOMER_TAX_NUMBER = "customer_tax_number"; - public static final String DEFAULT_HEADER = "default_header"; - public static final String DEFAULT_FOOTER = "default_footer"; - public static final String DEFAULT_MAIL = "type_mail_text"; - public static final String DELIVERY = "delivery"; - public static final String DELIVERY_DATE = "delivery_date"; - public static final String DOCUMENT = "document"; - public static final String DOCUMENT_ID = "document_id"; - public static final String DOC_TYPE_ID = "document_type_id"; - public static final String END_TIME = "end_time"; - public static final String FOOTER = "footer"; - public static final String GROSS_SUM = "gross_sum"; - public static final String HEAD = "head"; - public static final String ITEM = "item"; - public static final String ITEM_CODE = "item_code"; - public static final String NET_PRICE = "net_price"; - public static final String NET_SUM = "net_sum"; - public static final String NEXT_TYPE = "next_type_id"; - public static final String PHONE = "phone"; - public static final String POS = "pos"; - public static final String POSITIONS = "positions"; - public static final String PRICE = "single_price"; - public static final String PRICE_FORMAT = "price_format"; - public static final String START_TIME = "start_time"; - public static final String TASKS = "tasks"; - public static final String TAX_NUMBER = "tax_number"; - public static final String TAX = "tax"; - public static final String TAX_ID = "tax_id"; - public static final String TEMPLATE_ID = "template_id"; - public static final String TIME_ID = "time_id"; - public static final String TYPE = "type"; - public static final String TYPE_ID = "type_id"; - public static final String TYPE_NUMBER = "type_number"; - public static final String TYPE_PREFIX = "type_prefix"; - public static final String TYPE_SUFFIX = "type_suffix"; - public static final String UNIT = "unit"; - public static final String UNIT_PRICE = "unit_price"; -} \ No newline at end of file diff --git a/core/src/main/java/de/srsoftware/umbrella/core/ModuleRegistry.java b/core/src/main/java/de/srsoftware/umbrella/core/ModuleRegistry.java index ea02c2c0..dff11922 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/ModuleRegistry.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/ModuleRegistry.java @@ -12,6 +12,7 @@ public class ModuleRegistry { private FileService fileService; private MarkdownService markdownService; private NoteService noteService; + private PollService pollService; private PostBox postBox; private ProjectService projectService; private StockService stockService; @@ -33,9 +34,10 @@ public class ModuleRegistry { case ContactService cs: singleton.contactService = cs; break; case DocumentService ds: singleton.documentService = ds; break; case FileService fs: singleton.fileService = fs; break; - case StockService is: singleton.stockService = is; break; + case StockService is: singleton.stockService = is; break; case MarkdownService ms: singleton.markdownService = ms; break; case NoteService ns: singleton.noteService = ns; break; + case PollService ps: singleton.pollService = ps; break; case PostBox pb: singleton.postBox = pb; break; case ProjectService ps: singleton.projectService = ps; break; case TagService ts: singleton.tagService = ts; break; @@ -81,6 +83,10 @@ public class ModuleRegistry { return singleton.noteService; } + public static PollService pollService() { + return singleton.pollService; + } + public static PostBox postBox() { return singleton.postBox; } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/Paths.java b/core/src/main/java/de/srsoftware/umbrella/core/Paths.java deleted file mode 100644 index 14cd80a7..00000000 --- a/core/src/main/java/de/srsoftware/umbrella/core/Paths.java +++ /dev/null @@ -1,26 +0,0 @@ -/* © SRSoftware 2025 */ -package de.srsoftware.umbrella.core; - -public class Paths { - private Paths(){}; - - public static final String ADD = "add"; - public static final String AVAILABLE = "available"; - public static final String CSS = "css"; - public static final String COMMON_TEMPLATES = "common_templates"; - public static final String JSON = "json"; - public static final String LEGACY = "legacy"; - public static final String LIST = "list"; - public static final String LOGOUT = "logout"; - public static final String PAGE = "page"; - public static final String SEARCH = "search"; - public static final String SERVICE = "service"; - public static final String SETTINGS = "settings"; - public static final String STATES = "states"; - public static final String STARTED = "started"; - public static final String STOP = "stop"; - public static final String SUBMIT = "submit"; - public static final String TAGGED = "tagged"; - public static final String TOKEN = "token"; - public static final String VIEW = "view"; -} diff --git a/core/src/main/java/de/srsoftware/umbrella/core/SettingsService.java b/core/src/main/java/de/srsoftware/umbrella/core/SettingsService.java new file mode 100644 index 00000000..924519e9 --- /dev/null +++ b/core/src/main/java/de/srsoftware/umbrella/core/SettingsService.java @@ -0,0 +1,119 @@ +/* © SRSoftware 2025 */ +package de.srsoftware.umbrella.core; + +import static de.srsoftware.umbrella.core.ModuleRegistry.userService; +import static de.srsoftware.umbrella.core.constants.Constants.CLASS; +import static de.srsoftware.umbrella.core.constants.Field.*; +import static de.srsoftware.umbrella.core.constants.Path.MENU; +import static de.srsoftware.umbrella.core.constants.Text.*; +import static de.srsoftware.umbrella.core.constants.Text.USERS; +import static java.text.MessageFormat.format; + +import com.sun.net.httpserver.HttpExchange; +import de.srsoftware.configuration.Configuration; +import de.srsoftware.tools.Mappable; +import de.srsoftware.tools.Path; +import de.srsoftware.tools.SessionToken; +import de.srsoftware.umbrella.core.constants.Module; +import de.srsoftware.umbrella.core.constants.Text; +import de.srsoftware.umbrella.core.exceptions.UmbrellaException; +import de.srsoftware.umbrella.core.model.Token; +import de.srsoftware.umbrella.core.model.UmbrellaUser; +import java.io.IOException; +import java.util.*; +import org.json.JSONObject; + +public class SettingsService extends BaseHandler { + + private final Configuration config; + + public SettingsService(Configuration config) { + this.config = config; + } + + @Override + public boolean doGet(Path path, HttpExchange ex) throws IOException { + addCors(ex); + try { + Optional token = SessionToken.from(ex).map(Token::of); + var user = userService().loadUser(token); + if (user.isEmpty()) return unauthorized(ex); + var head = path.pop(); + return switch (head) { + case MENU -> getMenuSettings(user.get(), ex); + case null, default -> super.doGet(path, ex); + }; + } catch (UmbrellaException e) { + return send(ex, e); + } + } + + private record MenuEntry(int pos, String module, String clazz, String title) implements Mappable { + public static MenuEntry of(int pos, String module){ + return new MenuEntry(pos, module, module, module); + } + + public static MenuEntry of(int pos, String module, String title){ + return new MenuEntry(pos,module,module,title); + } + public static MenuEntry of(int pos, String module, String clazz, String title){ + return new MenuEntry(pos,module,clazz,title); + } + + @Override + public Map toMap() { + return Map.of(MODULE,module,CLASS,clazz,TITLE,title); + } + } + private boolean getMenuSettings(UmbrellaUser user, HttpExchange ex) throws IOException { + Optional modules = config.get("umbrella.modules"); + if (modules.isEmpty()) throw UmbrellaException.missingConfig("umbrella.modules"); + + var entries = new ArrayList(); + entries.add(MenuEntry.of(1, Module.USER, USERS)); + entries.add(MenuEntry.of(2, Module.COMPANY, COMPANIES)); + entries.add(MenuEntry.of(3, Module.PROJECT,Text.PROJECTS)); + entries.add(MenuEntry.of(4,Module.TASK,Text.TASKS)); + entries.add(MenuEntry.of(5,Module.TAGS)); + entries.add(MenuEntry.of(6,Module.DOCUMENT,"doc",Text.DOCUMENTS)); + entries.add(MenuEntry.of(7,Module.BOOKMARK,"mark",BOOKMARKS)); + entries.add(MenuEntry.of(8,Module.NOTES,"note",Text.NOTES)); + entries.add(MenuEntry.of(9,Module.FILES,"file", FILES)); + entries.add(MenuEntry.of(10,Module.TIME, Text.TIMETRACKING)); + entries.add(MenuEntry.of(11,Module.WIKI)); + entries.add(MenuEntry.of(12,Module.CONTACT, CONTACTS)); + entries.add(MenuEntry.of(13,Module.STOCK)); + entries.add(MenuEntry.of(14,Module.MESSAGE, MESSAGES)); + entries.add(MenuEntry.of(15,Module.POLL,Text.POLLS)); + + for (var i=0; i val = config.get(key); + if (val.isEmpty()) { + key = format("umbrella.modules.{0}.menu_index",entry.module); + val = config.get(key); + } + if (val.isPresent()) { + var index = val.get(); + if (index<0) { + entries.remove(i); + i--; + continue; + } else { + entry = MenuEntry.of(index,entry.module,entry.clazz,entry.title); + entries.set(i,entry); + } + } + key = format("umbrella.modules.{0}.baseUrl",entry.module); + Optional baseUrl = config.get(key); + if (baseUrl.isPresent()) { + entry = MenuEntry.of(entry.pos,baseUrl.get(), entry.clazz,entry.title); + entries.set(i,entry); + } + } + var list = entries.stream().sorted((a,b) -> a.pos - b.pos) + .map(MenuEntry::toMap).toList(); + return sendContent(ex,list); + } +} \ No newline at end of file diff --git a/core/src/main/java/de/srsoftware/umbrella/core/Util.java b/core/src/main/java/de/srsoftware/umbrella/core/Util.java index 5f61b836..7f38839d 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/Util.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/Util.java @@ -3,8 +3,13 @@ package de.srsoftware.umbrella.core; import static de.srsoftware.tools.MimeType.MIME_FORM_URL; import static de.srsoftware.tools.MimeType.MIME_JSON; +import static de.srsoftware.tools.PathHandler.GET; +import static de.srsoftware.tools.PathHandler.POST; import static de.srsoftware.tools.Strings.hex; -import static de.srsoftware.umbrella.core.Constants.*; +import static de.srsoftware.umbrella.core.Errors.INVALID_URL; +import static de.srsoftware.umbrella.core.constants.Constants.TIME_FORMATTER; +import static de.srsoftware.umbrella.core.constants.Field.*; +import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.serverError; import static java.lang.System.Logger.Level.*; import static java.lang.System.Logger.Level.WARNING; import static java.nio.charset.StandardCharsets.UTF_8; @@ -14,6 +19,7 @@ import de.srsoftware.tools.Mappable; import de.srsoftware.tools.Query; import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.model.Hash; +import de.srsoftware.umbrella.core.model.Translatable; import java.io.*; import java.net.HttpURLConnection; import java.net.URI; @@ -30,11 +36,51 @@ import org.json.JSONObject; public class Util { public static final System.Logger LOG = System.getLogger("Util"); - private static final Pattern UML_PATTERN = Pattern.compile("@start(\\w+)(.*)@end(\\1)",Pattern.DOTALL); + private static final Pattern UML_PATTERN = Pattern.compile("@start(\\w+)(.*?)@end(\\1)",Pattern.DOTALL); + private static final Pattern SPREADSHEET_PATTERN = Pattern.compile("@startsheet(.*?)@endsheet",Pattern.DOTALL); private static File plantumlJar = null; private static final JParsedown MARKDOWN = new JParsedown(); public static final String SHA1 = "SHA-1"; private static final MessageDigest SHA1_DIGEST; + private static final Map umlCache = new HashMap<>(); + + private static final String SCRIPT = """ + +
+ + """; static { try { @@ -72,13 +118,38 @@ public class Util { public static String markdown(String source){ if (source == null) return source; try { + var matcher = SPREADSHEET_PATTERN.matcher(source); + var count = 0; + while (matcher.find()){ + count++; + var sheetData = matcher.group(0).trim(); + var start = matcher.start(0); + var end = matcher.end(0); + source = source.substring(0, start) + + "
" + + sheetData.substring(11,sheetData.length()-10) + + "
" + + source.substring(end); + matcher = SPREADSHEET_PATTERN.matcher(source); + } if (plantumlJar != null && plantumlJar.exists()) { - var matcher = UML_PATTERN.matcher(source); - if (matcher.find()) { + matcher = UML_PATTERN.matcher(source); + while (matcher.find()) { var uml = matcher.group(0).trim(); var start = matcher.start(0); var end = matcher.end(0); + var umlHash = uml.hashCode(); + LOG.log(DEBUG,"Hash of Plantuml code: {0}",umlHash); + var svg = umlCache.get(umlHash); + if (svg != null){ + LOG.log(DEBUG,"Serving Plantuml generated SVG from cache…"); + source = source.substring(0, start) + svg + source.substring(end); + matcher = UML_PATTERN.matcher(source); + continue; + } + + LOG.log(DEBUG,"Cache miss. Generating SVG from plantuml code…"); ProcessBuilder processBuilder = new ProcessBuilder("java", "-jar", plantumlJar.getAbsolutePath(), "-tsvg", "-pipe"); var ignored = processBuilder.redirectErrorStream(); var process = processBuilder.start(); @@ -89,8 +160,11 @@ public class Util { try (InputStream is = process.getInputStream()) { byte[] out = is.readAllBytes(); - var svg = new String(out, UTF_8); + LOG.log(DEBUG,"Generated SVG. Pushing to cache…"); + svg = new String(out, UTF_8); + umlCache.put(umlHash,svg); source = source.substring(0, start) + svg + source.substring(end); + matcher = UML_PATTERN.matcher(source); } } } @@ -117,7 +191,7 @@ public class Util { url = new URI(location).toURL(); } catch (Exception e) { LOG.log(WARNING,"{0} is not a valid url",location,e); - throw new UmbrellaException(500,"{0} is not a valid url",location).causedBy(e); + throw serverError(INVALID_URL,URL,location).causedBy(e); } return request(url,data,postMime,auth); } @@ -160,13 +234,13 @@ public class Util { var is = conn.getErrorStream(); is.transferTo(bos); is.close(); - throw new UmbrellaException(500, bos.toString(UTF_8)); + throw new UmbrellaException(500, Translatable.t(bos.toString(UTF_8))); } } catch (UmbrellaException e){ throw e; } catch (Exception e) { LOG.log(WARNING,"Request to {0} failed: {1}",target,e.getMessage()); - throw new UmbrellaException(500,"Request to {0} failed!",target).causedBy(e); + throw new UmbrellaException(500, Translatable.t("Request to {url} failed!",URL,target)).causedBy(e); } } @@ -180,7 +254,7 @@ public class Util { plantumlJar = file; } - public static LocalDateTime dateTimeOf(long epocSecs){ - return LocalDateTime.ofInstant(Instant.ofEpochSecond(epocSecs), ZoneId.systemDefault()); + public static String dateTimeOf(long epochMilis){ + return LocalDateTime.ofInstant(Instant.ofEpochMilli(epochMilis), ZoneId.systemDefault()).format(TIME_FORMATTER); } } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/api/Owner.java b/core/src/main/java/de/srsoftware/umbrella/core/api/Owner.java index ee7bce89..c1490565 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/api/Owner.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/api/Owner.java @@ -1,8 +1,6 @@ /* © SRSoftware 2025 */ package de.srsoftware.umbrella.core.api; -import static de.srsoftware.umbrella.core.Constants.*; - import de.srsoftware.tools.Mappable; public interface Owner extends Mappable { diff --git a/core/src/main/java/de/srsoftware/umbrella/core/api/PollService.java b/core/src/main/java/de/srsoftware/umbrella/core/api/PollService.java new file mode 100644 index 00000000..c559ce2e --- /dev/null +++ b/core/src/main/java/de/srsoftware/umbrella/core/api/PollService.java @@ -0,0 +1,5 @@ +/* © SRSoftware 2025 */ +package de.srsoftware.umbrella.core.api; + +public interface PollService { +} diff --git a/core/src/main/java/de/srsoftware/umbrella/core/api/PostBox.java b/core/src/main/java/de/srsoftware/umbrella/core/api/PostBox.java index 74e8b485..678219f4 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/api/PostBox.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/api/PostBox.java @@ -4,5 +4,5 @@ package de.srsoftware.umbrella.core.api; import de.srsoftware.umbrella.core.model.Envelope; public interface PostBox { - public void send(Envelope envelope); + public void send(Envelope envelope); } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/api/StockService.java b/core/src/main/java/de/srsoftware/umbrella/core/api/StockService.java index c43d9544..f96bf9ab 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/api/StockService.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/api/StockService.java @@ -3,16 +3,12 @@ package de.srsoftware.umbrella.core.api; import de.srsoftware.umbrella.core.model.DbLocation; +import de.srsoftware.umbrella.core.model.Item; + import java.util.Collection; public interface StockService { - /** - * Das war mal die methode um zu checken, ob einer Firma noch Items zugewiesen sind. - * TODO: Diese Methode muss neu definiert werden, sobald der Stock-Service neu implementiert ist. - * @param company_id - * @return - */ - Collection redefineMe(long company_id); + Collection getCompanyItems(long companyID); DbLocation loadLocation(long locationId); } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/api/TagService.java b/core/src/main/java/de/srsoftware/umbrella/core/api/TagService.java index 2198ef6f..08a5b130 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/api/TagService.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/api/TagService.java @@ -8,7 +8,9 @@ import java.util.List; import java.util.Map; public interface TagService { - void deleteEntity(String task, long taskId); + void deleteEntity(String module, long taskId); + + void deleteEntity(String module, long urlId, long userId); Map> getTagUses(UmbrellaUser user, String tag); diff --git a/core/src/main/java/de/srsoftware/umbrella/core/api/Translator.java b/core/src/main/java/de/srsoftware/umbrella/core/api/Translator.java index 9979c0ff..8e8dfcbc 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/api/Translator.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/api/Translator.java @@ -16,5 +16,6 @@ public interface Translator { } return translate(language, parts[0], fills); } + public String translate(String language, String text, Map fills); } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/api/UserService.java b/core/src/main/java/de/srsoftware/umbrella/core/api/UserService.java index fda1ef8b..ae5769e4 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/api/UserService.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/api/UserService.java @@ -3,6 +3,7 @@ package de.srsoftware.umbrella.core.api; import com.sun.net.httpserver.HttpExchange; import de.srsoftware.umbrella.core.exceptions.UmbrellaException; +import de.srsoftware.umbrella.core.model.EmailAddress; import de.srsoftware.umbrella.core.model.Session; import de.srsoftware.umbrella.core.model.Token; import de.srsoftware.umbrella.core.model.UmbrellaUser; @@ -16,6 +17,7 @@ public interface UserService { Map list(Integer start, Integer limit, Collection ids) throws UmbrellaException; Session load(Token token) throws UmbrellaException; UmbrellaUser load(Session session) throws UmbrellaException; + Optional load(EmailAddress email) throws UmbrellaException; UmbrellaUser loadUser(long userId) throws UmbrellaException; Optional loadUser(Optional sessionToken) throws UmbrellaException; Optional loadUser(HttpExchange ex) throws UmbrellaException; diff --git a/core/src/main/java/de/srsoftware/umbrella/core/constants/Constants.java b/core/src/main/java/de/srsoftware/umbrella/core/constants/Constants.java new file mode 100644 index 00000000..8977aa1b --- /dev/null +++ b/core/src/main/java/de/srsoftware/umbrella/core/constants/Constants.java @@ -0,0 +1,33 @@ +/* © SRSoftware 2025 */ +package de.srsoftware.umbrella.core.constants; + + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.time.format.DateTimeFormatter; + +public class Constants { + + private Constants(){ + // prevent instantiation + } + + public static final String CLASS = "class"; + public static final String CONFIG_SESSION_DURATION = "umbrella.session.duration"; + public static final String COUNT = "COUNT(*)"; + + public static final String FALLBACK_LANG = "de"; + public static final String HOME = "home"; + public static final String JSONARRAY = "json array"; + public static final String JSONOBJECT = "json object"; + public static final String KEEP_ALIVE = "keep-alive"; + public static final String NO_CACHE = "no-cache"; + public static final String NONE = "none"; + + public static final String TABLE_SETTINGS = "settings"; + public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + public static final String UMBRELLA = "Umbrella"; + public static final String UTF8 = UTF_8.displayName(); + +} diff --git a/core/src/main/java/de/srsoftware/umbrella/core/constants/Field.java b/core/src/main/java/de/srsoftware/umbrella/core/constants/Field.java new file mode 100644 index 00000000..430e524a --- /dev/null +++ b/core/src/main/java/de/srsoftware/umbrella/core/constants/Field.java @@ -0,0 +1,193 @@ +/* © SRSoftware 2025 */ +package de.srsoftware.umbrella.core.constants; + +public class Field { + public static final String ACTION = "action"; + public static final String ADDRESS = "address"; + public static final String ALLOWED_STATES = "allowed_states"; + public static final String AMOUNT = "amount"; + public static final String ATTACHMENTS = "attachments"; + public static final String AUTHORIZATION = "Authorization"; + + public static final String BASE_URL = "umbrella.base_url"; + public static final String BANK_ACCOUNT = "bank_account"; + public static final String BODY = "body"; + + public static final String CACHE_CONTROL = "Cache-Control"; + public static final String CODE = "code"; + public static final String COMMENT = "comment"; + public static final String COMPANY = "company"; + public static final String COMPANY_ID = "company_id"; + public static final String CONNECTION = "Connection"; + public static final String CONTENT = "content"; + public static final String CONTENT_DISPOSITION = "Content-Disposition"; + public static final String CONTENT_ENCODING = "Content-Encoding"; + public static final String CONTENT_TYPE = "Content-Type"; + public static final String COURT = "court"; + public static final String CURRENCY = "currency"; + public static final String CUSTOMER = "customer"; + public static final String CUSTOMER_EMAIL = "customer_email"; + public static final String CUSTOMER_NUMBER = "customer_number"; + public static final String CUSTOMER_NUMBER_PREFIX = "customer_number_prefix"; + public static final String CUSTOMER_TAX_NUMBER = "customer_tax_number"; + + public static final String DATA = "data"; + public static final String DB_VERSION = "db_version"; + public static final String DATE = "date"; + public static final String DECIMALS = "decimals"; + public static final String DECIMAL_SEPARATOR = "decimal_separator"; + public static final String DEFAULT_HEADER = "default_header"; + public static final String DEFAULT_FOOTER = "default_footer"; + public static final String DEFAULT_MAIL = "type_mail_text"; + public static final String DELETED = "deleted"; + public static final String DELIVERY = "delivery"; + public static final String DELIVERY_DATE = "delivery_date"; + public static final String DESCRIPTION = "description"; + public static final String DOCUMENT = "document"; + public static final String DOCUMENT_ID = "document_id"; + public static final String DOC_TYPE_ID = "document_type_id"; + public static final String DOMAIN = "domain"; + public static final String DROP_MEMBER = "drop_member"; + public static final String DUE_DATE = "due_date"; + public static final String DURATION = "duration"; + + public static final String EDITOR = "editor"; + public static final String EMAIL = "email"; + public static final String END_TIME = "end_time"; + public static final String ENTITY_ID = "entity_id"; + public static final String EST_TIME = "est_time"; + public static final String EVALUATION = "evaluation"; + public static final String EXPECTED = "expected"; + public static final String EXPIRATION = "expiration"; + + public static final String FIELD = "field"; + public static final String FOOTER = "footer"; + public static final String FROM = "from"; + public static final String FULLTEXT = "fulltext"; + + public static final String GROSS_SUM = "gross_sum"; + public static final String GUEST_ALLOWED = "guest_allowed"; + + public static final String HASH = "hash"; + public static final String HEAD = "head"; + public static final String HOURS = "hours"; + + public static final String ID = "id"; + public static final String INSTANTLY = "instantly"; + public static final String ITEM = "item"; + public static final String ITEM_CODE = "item_code"; + + public static final String KEY = "key"; + + public static final String LANGUAGE = "language"; + public static final String LAST_CUSTOMER_NUMBER = "last_customer_number"; + public static final String LIMIT = "limit"; + public static final String LOCATION = "location"; + public static final String LOCATION_ID = "location_id"; + public static final String LOCATIONS = "locations"; + public static final String LOGIN = "login"; + + public static final String MEMBERS = "members"; + public static final String MENU_INDEX = "menu_index"; + public static final String MESSAGE_ID = "message_id"; + public static final String MIME = "mime"; + public static final String MODULE = "module"; + + public static final String NAME = "name"; + public static final String NEW_MEMBER = "new_member"; + public static final String NET_PRICE = "net_price"; + public static final String NET_SUM = "net_sum"; + public static final String NEXT_TYPE = "next_type_id"; + public static final String NO_INDEX = "no_index"; + public static final String NOTE = "note"; + public static final String NUMBER = "number"; + + public static final String OBJECT = "object"; + public static final String OFFSET = "offset"; + public static final String OPTIONS = "options"; + public static final String OPTIONAL = "optional"; + public static final String OPTION_ID = "option_id"; + public static final String OWNER = "owner"; + public static final String OWNER_NUMBER = "owner_number"; + + public static final String PARENT_LOCATION_ID = "parent_location_id"; + public static final String PARENT_TASK_ID = "parent_task_id"; + public static final String PASS = "pass"; + public static final String PASSWORD = "password"; + public static final String PATH = "path"; + public static final String PERMISSION = "permission"; + public static final String PHONE = "phone"; + public static final String POLL_ID = "poll_id"; + public static final String POS = "pos"; + public static final String POSITIONS = "positions"; + public static final String PRICE = "single_price"; + public static final String PRICE_FORMAT = "price_format"; + public static final String PRIORITY = "priority"; + public static final String PRIVATE = "private"; + public static final String PROJECT = "project"; + public static final String PROJECT_ID = "project_id"; + public static final String PROPERTIES = "properties"; + + public static final String RECEIVERS = "receivers"; + public static final String REDIRECT = "redirect"; + public static final String RENDERED = "rendered"; + public static final String REQUIRED_TASKS_IDS = "required_tasks_ids"; + + public static final String SELECTION = "selection"; + public static final String SENDER = "sender"; + public static final String SENDER_USER_ID = "sender_user_id"; + public static final String SETTINGS = "settings"; + public static final String SHARES = "shares"; + public static final String SHOW_CLOSED = "show_closed"; + public static final String SILENT = "silent"; + public static final String SOURCE = "source"; + public static final String START_DATE = "start_date"; + public static final String START_TIME = "start_time"; + public static final String STATE = "state"; + public static final String STATS = "stats"; + public static final String STATUS = "status"; + public static final String STATUS_CODE = "code"; + public static final String SUBJECT = "subject"; + + public static final String TABLE = "table"; + public static final String TAG = "tag"; + public static final String TAGS = "tags"; + public static final String TAG_COLORS = "tag_colors"; + public static final String TASK = "task"; + public static final String TASK_IDS = "task_ids"; + public static final String TASKS = "tasks"; + public static final String TAX = "tax"; + public static final String TAX_ID = "tax_id"; + public static final String TAX_NUMBER = "tax_number"; + public static final String TAX_RATE = "tax_rate"; + public static final String TEMPLATE = "template"; + public static final String TEMPLATE_ID = "template_id"; + public static final String TEXT = "text"; + public static final String THOUSANDS_SEPARATOR = "thousands_separator"; + public static final String THEME = "theme"; + public static final String TIME_ID = "time_id"; + public static final String TIMESTAMP = "timestamp"; + public static final String TITLE = "title"; + public static final String TO = "to"; + public static final String TOKEN = "token"; + public static final String TOTAL_PRIO = "total_prio"; + public static final String TYPE = "type"; + public static final String TYPE_ID = "type_id"; + + public static final String UNIT = "unit"; + public static final String UNIT_PRICE = "unit_price"; + public static final String URI = "uri"; + public static final String URL = "url"; + public static final String USER = "user"; + public static final String USERS = "users"; + public static final String USER_ID = "user_id"; + public static final String USER_LIST = "user_list"; + + public static final String VALUE = "value"; + public static final String VCARD = "vcard"; + public static final String VERSION = "version"; + public static final String VERSIONS = "versions"; + + public static final String WEIGHT = "weight"; + public static final String WEIGHTS = "weights"; +} diff --git a/core/src/main/java/de/srsoftware/umbrella/core/constants/Module.java b/core/src/main/java/de/srsoftware/umbrella/core/constants/Module.java new file mode 100644 index 00000000..f3b8512d --- /dev/null +++ b/core/src/main/java/de/srsoftware/umbrella/core/constants/Module.java @@ -0,0 +1,20 @@ +/* © SRSoftware 2025 */ +package de.srsoftware.umbrella.core.constants; + +public class Module { + public static final String BOOKMARK = "bookmark"; + public static final String COMPANY = "company"; + public static final String CONTACT = "contact"; + public static final String DOCUMENT = "document"; + public static final String FILES = "files"; + public static final String MESSAGE = "message"; + public static final String NOTES = "notes"; + public static final String POLL = "poll"; + public static final String PROJECT = "project"; + public static final String STOCK = "stock"; + public static final String TAGS = "tags"; + public static final String TASK = "task"; + public static final String TIME = "time"; + public static final String USER = "user"; + public static final String WIKI = "wiki"; +} diff --git a/core/src/main/java/de/srsoftware/umbrella/core/constants/Path.java b/core/src/main/java/de/srsoftware/umbrella/core/constants/Path.java new file mode 100644 index 00000000..5a0f7bb6 --- /dev/null +++ b/core/src/main/java/de/srsoftware/umbrella/core/constants/Path.java @@ -0,0 +1,59 @@ +/* © SRSoftware 2025 */ +package de.srsoftware.umbrella.core.constants; + +public class Path { + private Path(){}; + + public static final String ADD = "add"; + public static final String AVAILABLE = "available"; + + public static final String CSS = "css"; + public static final String CLONE = "clone"; + public static final String COMMON_TEMPLATES = "common_templates"; + public static final String COMPANY = "company"; + public static final String CONNECTED = "connected"; + + public static final String EVALUATE = "evaluate"; + + public static final String ITEM = "item"; + + public static final String JSON = "json"; + + public static final String LEGACY = "legacy"; + public static final String LIST = "list"; + public static final String LOCATION = "location"; + public static final String LOCATIONS = "locations"; + public static final String LOGIN = "login"; + + public static final String LOGOUT = "logout"; + + public static final String MENU = "menu"; + + public static final String OPTION = "option"; + + public static final String PAGE = "page"; + public static final String PASSWORD = "password"; + public static final String PERMISSIONS = "permissions"; + public static final String PROJECT = "project"; + public static final String PROPERTIES = "properties"; + public static final String PROPERTY = "property"; + + public static final String READ = "read"; + public static final String REDIRECT = "redirect"; + + public static final String SEARCH = "search"; + public static final String SELECT = "select"; + public static final String SETTINGS = "settings"; + public static final String STATES = "states"; + public static final String STARTED = "started"; + public static final String STATE = "state"; + public static final String STOP = "stop"; + + public static final String TAGGED = "tagged"; + public static final String TOKEN = "token"; + + public static final String USER = "user"; + public static final String USES = "uses"; + + public static final String WEIGHT = "weight"; +} diff --git a/core/src/main/java/de/srsoftware/umbrella/core/constants/Text.java b/core/src/main/java/de/srsoftware/umbrella/core/constants/Text.java new file mode 100644 index 00000000..41aab871 --- /dev/null +++ b/core/src/main/java/de/srsoftware/umbrella/core/constants/Text.java @@ -0,0 +1,93 @@ +/* © SRSoftware 2025 */ +package de.srsoftware.umbrella.core.constants; + +/** + * This is a collection of messages that appear throughout the project + */ +public class Text { + public static final String BOOKMARK = "bookmark"; + public static final String BOOKMARKS = "bookmarks"; + public static final String BOOLEAN = "Boolean"; + + public static final String COMPANIES = "companies"; + public static final String COMPANY = "company"; + public static final String COMPANY_WITH_ID = "company ({id})"; + public static final String CONTACT = "Contact"; + public static final String CONTACTS = "contacts"; + public static final String CONTACT_WITH_ID = "contact ({id})"; + public static final String CUSTOMER = "customer"; + public static final String CUSTOMER_SETTINGS = "customer settings"; + + public static final String DOCUMENT = "document"; + public static final String DOCUMENTS = "documents"; + public static final String DOCUMENT_TYPE_ID = "document type id"; + public static final String DOCUMENT_WITH_ID = "document ({id})"; + + public static final String EMAILS_FOR_RECEIVER = "emails for {email}"; + public static final String EVALUATION = "evaluation"; + + public static final String FILES = "files"; + + public static final String INVALID_DB_CODE = "Encountered invalid dbCode: {code}"; + public static final String ITEM = "item"; + public static final String ITEMS = "items"; + + public static final String LOCATION = "location"; + public static final String LOCATIONS = "locations"; + public static final String LOGIN_SERVICE = "login service"; + public static final String LONG = "Long"; + + public static final String MESSAGE = "message"; + public static final String MESSAGES = "messages"; + + public static final String NOT_ALLOWED_TO_EDIT = "You are not allowed to edit {object}!"; + public static final String NOT_ALLOWED_TO_EVALUATE = "You are not allowed to evaluate this {object}!"; + public static final String NOTE = "note"; + public static final String NOTES = "notes"; + public static final String NOTE_WITH_ID = "note ({id})"; + public static final String NUMBER = "number"; + + public static final Object OPTION = "option" + ; + public static final String PATH = "path"; + public static final String PERMISSION = "permission"; + public static final String POLL = "poll"; + public static final String POLLS = "polls"; + public static final String PROJECTS = "projects"; + public static final String PROJECT_WITH_ID = "project ({id})"; + public static final String PROPERTIES = "properties"; + public static final String PROPERTY = "property"; + + public static final String RECEIVER = "receiver"; + public static final String RECEIVERS = "receivers"; + + public static final String SELECTIONS = "selections"; + public static final String SENDER = "sender"; + public static final String SERVICE_WITH_ID = "service ({id})"; + public static final String SESSION = "session"; + public static final String SETTINGS = "settings"; + public static final String STOCK = "stock"; + public static final String STRING = "string"; + + public static final String TABLE_WITH_NAME = "table {name}"; + public static final String TAGS = "tags"; + public static final String TASK = "task"; + public static final String TASKS = "tasks"; + public static final String TIMETRACKING = "timetracking"; + public static final String TIME_WITH_ID = "time ({id})"; + public static final String TYPE = "type"; + + public static final String UNIT = "unit"; + public static final String USER_WITH_ID = "user ({id})"; + + public static final String WEIGHT = "weight"; + public static final String WIKI = "wiki"; + public static final String WIKI_PAGE = "wiki page"; + public static final String WIKI_PAGES = "wiki pages"; + + public static final String UNIT_PRICE = "unit price"; + public static final String UNKNOWN_FIELD = "unknown field: {id}"; + public static final String USER = "user"; + public static final String USERS = "users"; + +} diff --git a/core/src/main/java/de/srsoftware/umbrella/core/exceptions/UmbrellaException.java b/core/src/main/java/de/srsoftware/umbrella/core/exceptions/UmbrellaException.java index 76d5d71c..eeb1da41 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/exceptions/UmbrellaException.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/exceptions/UmbrellaException.java @@ -1,78 +1,125 @@ /* © SRSoftware 2025 */ package de.srsoftware.umbrella.core.exceptions; -import static de.srsoftware.umbrella.core.Constants.*; +import static de.srsoftware.umbrella.core.Errors.*; import static de.srsoftware.umbrella.core.ResponseCode.HTTP_SERVER_ERROR; import static de.srsoftware.umbrella.core.ResponseCode.HTTP_UNPROCESSABLE; -import static java.lang.System.Logger.Level.ERROR; -import static java.lang.System.Logger.Level.WARNING; -import static java.net.HttpURLConnection.HTTP_FORBIDDEN; -import static java.net.HttpURLConnection.HTTP_NOT_FOUND; -import static java.text.MessageFormat.format; +import static de.srsoftware.umbrella.core.constants.Field.*; +import static java.net.HttpURLConnection.*; + +import de.srsoftware.umbrella.core.model.Translatable; + public class UmbrellaException extends RuntimeException{ private final int statusCode; - private Object[] fills; + private final Translatable message; - public UmbrellaException(String message, Object ... fills){ - this(HTTP_SERVER_ERROR,message,fills); - } - - public UmbrellaException(int statusCode, String message, Object ... fills){ - super(message); - this.fills = fills; + public UmbrellaException(int statusCode, Translatable message){ + super(message.toString()); this.statusCode = statusCode; + this.message = message; } + public UmbrellaException(int statusCode, String rawMessage, Object ... args){ + this(statusCode, Translatable.t(rawMessage,args)); + } + + public static final UmbrellaException badRequest(String rawMessage, Object ... args){ + return new UmbrellaException(HTTP_BAD_REQUEST,rawMessage,args); + } public UmbrellaException causedBy(Exception e) { initCause(e); return this; } - public static UmbrellaException databaseException(String message, Object... fills) { - System.getLogger("Configuration").log(WARNING,message,fills); - return new UmbrellaException(message,fills); + public static UmbrellaException databaseException(String rawMessage, Object ... args) { + return new UmbrellaException(HTTP_SERVER_ERROR,rawMessage,args); } - public Object[] fills(){ - return fills; + public static UmbrellaException failedToCreateTable(String tableName){ + return databaseException(FAILED_TO_CREATE_TABLE, NAME,tableName); } - public static UmbrellaException forbidden(String message, Object... fills) { - return new UmbrellaException(HTTP_FORBIDDEN,message,fills); + public static UmbrellaException failedToDropObject(Object object){ + return databaseException(FAILED_TO_DROP_OBJECT, OBJECT,object); } - @Override - public String getMessage() { - return format(super.getMessage(),fills); + public static UmbrellaException failedToDropObjectFromObject(String typeOfDropped, Object dropped, Object typeOfRelated, Object related){ + return databaseException(FAILED_TO_DROP_ENTITY_OF_ENTITY, + "dropped_type",typeOfDropped, + OBJECT,dropped, + "related_type", typeOfRelated, + "related",related); } - public static UmbrellaException invalidFieldException(String field, String expected){ - return new UmbrellaException(HTTP_UNPROCESSABLE, ERROR_INVALID_FIELD, field, expected); + public static UmbrellaException failedToLoadMembers(Object object){ + return databaseException(FAILED_TO_LOAD_OBJECT_MEMBERS, OBJECT,object); + } + + public static UmbrellaException failedToLoadObject(Object object){ + return databaseException(FAILED_TO_LOAD_OBJECT, OBJECT,object); + } + + public static UmbrellaException failedToLoadObject(Object object, Object id){ + return databaseException(FAILED_TO_LOAD_OBJECT_BY_ID, OBJECT,object, ID,id); + } + + public static UmbrellaException failedToStoreObject(Object object){ + return databaseException(FAILED_TO_STORE_OBJECT, OBJECT,object); + } + + public static UmbrellaException failedToReadFromTable(String field, String table){ + return databaseException(FAILED_TO_READ_FROM_TABLE, FIELD,field, TABLE,table); + } + + public static UmbrellaException failedToSearchDb(Translatable module){ + return databaseException(FAILED_TO_SEARCH_DB, MODULE, module); + } + + public static UmbrellaException forbidden(String rawMessage, Object ... args) { + return new UmbrellaException(HTTP_FORBIDDEN,rawMessage,args); + } + + public static UmbrellaException invalidField(String field, Object expected){ + return new UmbrellaException(HTTP_UNPROCESSABLE, INVALID_FIELD, FIELD, field, EXPECTED, expected); + } + + public Translatable message(){ + return message; + } + + public static UmbrellaException missingConfig(String key){ + return new UmbrellaException(HTTP_SERVER_ERROR,MISSING_CONFIG, KEY,key); } - public static UmbrellaException missingConfigException(String field){ - System.getLogger("Configuration").log(ERROR,ERROR_MISSING_CONFIG, field); - return new UmbrellaException(ERROR_MISSING_CONFIG, field); + public static UmbrellaException missingField(String field){ + return new UmbrellaException(HTTP_UNPROCESSABLE, MISSING_FIELD, FIELD,field); } - - public static UmbrellaException missingFieldException(String field){ - return new UmbrellaException(HTTP_UNPROCESSABLE, ERROR_MISSING_FIELD, field); + public static UmbrellaException notAmember(Object object){ + return forbidden(NO_MEMBER, OBJECT,object); } - public static UmbrellaException notFound(String message, Object... fills) { - return new UmbrellaException(HTTP_NOT_FOUND,message,fills); + public static UmbrellaException notFound(String rawMessage, Object ... args) { + return new UmbrellaException(HTTP_NOT_FOUND,rawMessage,args); + } + + public static UmbrellaException serverError(String rawMessage, Object ... args){ + return new UmbrellaException(HTTP_SERVER_ERROR,rawMessage,args); } public int statusCode(){ return statusCode; } - public static UmbrellaException unprocessable(String message, Object... fills) { - return new UmbrellaException(HTTP_UNPROCESSABLE,message,fills); + public static UmbrellaException unauthorized(String rawMessage, Object ... args) { + return new UmbrellaException(HTTP_UNAUTHORIZED,rawMessage,args); + } + + public static UmbrellaException unprocessable(String rawMessage, Object ... args) { + return new UmbrellaException(HTTP_UNPROCESSABLE,rawMessage, args); } } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Attachment.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Attachment.java index dbf8e59c..bfd46e37 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/Attachment.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Attachment.java @@ -1,9 +1,10 @@ /* © SRSoftware 2025 */ 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.core.constants.Field.*; +import static de.srsoftware.umbrella.core.constants.Text.STRING; +import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*; +import static de.srsoftware.umbrella.core.model.Translatable.t; import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import java.util.Arrays; @@ -29,11 +30,11 @@ public record Attachment(String name, String mime, byte[] content) { public static Attachment of(JSONObject json) throws UmbrellaException { for (var key : Set.of(NAME, MIME, DATA)) { - if (!json.has(key)) throw missingFieldException(key); + if (!json.has(key)) throw missingField(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); + if (!(json.get(NAME) instanceof String name)) throw invalidField(NAME,t(STRING)); + if (!(json.get(MIME) instanceof String mime)) throw invalidField(MIME,t(STRING)); + if (!(json.get(DATA) instanceof String data)) throw invalidField(DATA,t(STRING)); return new Attachment(name,mime, BASE64.decode(data)); } } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Bookmark.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Bookmark.java index 9ffaf6d8..d5d63d00 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/Bookmark.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Bookmark.java @@ -1,8 +1,8 @@ /* © SRSoftware 2025 */ package de.srsoftware.umbrella.core.model; -import static de.srsoftware.umbrella.core.Constants.*; -import static de.srsoftware.umbrella.core.Util.markdown; +import static de.srsoftware.umbrella.core.Util.mapMarkdown; +import static de.srsoftware.umbrella.core.constants.Field.*; import static java.time.ZoneOffset.UTC; import de.srsoftware.tools.Mappable; @@ -28,7 +28,7 @@ public record Bookmark(long urlId, String url, String comment, LocalDateTime tim return Map.of( ID, urlId, URL, url, - COMMENT, Map.of(SOURCE,comment,RENDERED,markdown(comment)), + COMMENT, mapMarkdown(comment), TAGS, tags, TIMESTAMP, timestamp.withNano(0) ); diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Company.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Company.java index 8045c999..b84c7529 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/Company.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Company.java @@ -2,10 +2,8 @@ package de.srsoftware.umbrella.core.model; import static de.srsoftware.tools.Optionals.emptyIfNull; -import static de.srsoftware.umbrella.core.Constants.*; -import static de.srsoftware.umbrella.core.Constants.COMPANY; -import static de.srsoftware.umbrella.core.Field.*; -import static de.srsoftware.umbrella.core.Field.TYPE; +import static de.srsoftware.umbrella.core.constants.Field.*; +import static de.srsoftware.umbrella.core.constants.Module.COMPANY; import static java.util.Map.entry; import de.srsoftware.tools.Mappable; @@ -147,19 +145,19 @@ public class Company implements Mappable, Owner { public Company patch(JSONObject json) { for (var key : json.keySet()){ switch (key){ - case NAME: name = json.getString(NAME); break; - case ADDRESS: address = json.getString(ADDRESS); break; - case COURT: court = json.getString(COURT); break; - case TAX_NUMBER: taxId = json.getString(TAX_NUMBER); break; - case PHONE: phone = json.getString(PHONE); break; - case DECIMAL_SEPARATOR: decimalSeparator = json.getString(DECIMAL_SEPARATOR); break; - case THOUSANDS_SEPARATOR: thousandsSeparator = json.getString(THOUSANDS_SEPARATOR); break; - case LAST_CUSTOMER_NUMBER: lastCustomerNumber = json.getLong(LAST_CUSTOMER_NUMBER); break; - case DECIMALS: decimals = json.getInt(DECIMALS); break; - case CUSTOMER_NUMBER_PREFIX: customerNumberPrefix = json.getString(CUSTOMER_NUMBER_PREFIX); break; - case CURRENCY: currency = json.getString(CURRENCY); break; - case EMAIL: email = json.getString(EMAIL); break; - case BANK_ACCOUNT: bankAccount = json.getString(BANK_ACCOUNT); break; + case NAME: name = json.getString(key); break; + case ADDRESS: address = json.getString(key); break; + case COURT: court = json.getString(key); break; + case TAX_NUMBER: taxId = json.getString(key); break; + case PHONE: phone = json.getString(key); break; + case DECIMAL_SEPARATOR: decimalSeparator = json.getString(key); break; + case THOUSANDS_SEPARATOR: thousandsSeparator = json.getString(key); break; + case LAST_CUSTOMER_NUMBER: lastCustomerNumber = json.getLong(key); break; + case DECIMALS: decimals = json.getInt(key); break; + case CUSTOMER_NUMBER_PREFIX: customerNumberPrefix = json.getString(key); break; + case CURRENCY: currency = json.getString(key); break; + case EMAIL: email = json.getString(key); break; + case BANK_ACCOUNT: bankAccount = json.getString(key); break; default: key = null; } if (key != null) dirtyFields.add(key); diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Contact.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Contact.java index 28b3d0a8..564c8991 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/Contact.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Contact.java @@ -1,8 +1,8 @@ /* © SRSoftware 2025 */ package de.srsoftware.umbrella.core.model; -import static de.srsoftware.umbrella.core.Constants.*; -import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingFieldException; +import static de.srsoftware.umbrella.core.constants.Field.*; +import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*; import static java.text.MessageFormat.format; import de.srsoftware.tools.Mappable; @@ -31,8 +31,8 @@ public class Contact implements Mappable{ } public Contact patch(JSONObject json) { - if (!(json.get(FROM) instanceof String from)) throw missingFieldException(FROM); - if (!(json.get(TO) instanceof String to)) throw missingFieldException(TO); + if (!(json.get(FROM) instanceof String from)) throw missingField(FROM); + if (!(json.get(TO) instanceof String to)) throw missingField(TO); return new Contact(id,vcard.replace(from, to)); } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Customer.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Customer.java index b2b1e2b2..9f79eb7c 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/Customer.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Customer.java @@ -2,9 +2,9 @@ package de.srsoftware.umbrella.core.model; import static de.srsoftware.tools.Optionals.emptyIfNull; -import static de.srsoftware.umbrella.core.Constants.*; -import static de.srsoftware.umbrella.core.Field.*; -import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingFieldException; +import static de.srsoftware.umbrella.core.constants.Constants.FALLBACK_LANG; +import static de.srsoftware.umbrella.core.constants.Field.*; +import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*; import de.srsoftware.tools.Mappable; import de.srsoftware.umbrella.core.exceptions.UmbrellaException; @@ -68,9 +68,9 @@ public final class Customer implements Mappable { } public static Customer of(JSONObject json) throws UmbrellaException { - if (!json.has(ID) || !(json.get(ID) instanceof String id)) throw missingFieldException(ID); - 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(ID) || !(json.get(ID) instanceof String id)) throw missingField(ID); + if (!json.has(NAME) || !(json.get(NAME) instanceof String name)) throw missingField(NAME); + if (!json.has(EMAIL) || !(json.get(EMAIL) instanceof String email)) throw missingField(EMAIL); var taxId = json.has(TAX_ID) && json.get(TAX_ID) instanceof String tid ? tid : null; var lang = json.has(LANGUAGE) && json.get(LANGUAGE) instanceof String l ? l : FALLBACK_LANG; return new Customer(id,name,email,taxId,lang); diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/DbLocation.java b/core/src/main/java/de/srsoftware/umbrella/core/model/DbLocation.java index 78e6c548..f046bbdc 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/DbLocation.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/DbLocation.java @@ -1,8 +1,8 @@ /* © SRSoftware 2025 */ package de.srsoftware.umbrella.core.model; -import static de.srsoftware.umbrella.core.Constants.*; -import static de.srsoftware.umbrella.core.Util.markdown; +import static de.srsoftware.umbrella.core.Util.mapMarkdown; +import static de.srsoftware.umbrella.core.constants.Field.*; import de.srsoftware.umbrella.core.api.Owner; import java.sql.ResultSet; @@ -69,10 +69,10 @@ public class DbLocation extends Location { parentLocationId = json.getLong(field); break; case NAME: - name = json.getString(NAME); + name = json.getString(field); break; case DESCRIPTION: - description = json.getString(DESCRIPTION); + description = json.getString(field); break; default: known = false; @@ -93,7 +93,7 @@ public class DbLocation extends Location { if (description == null) description = ""; map.put(OWNER,owner.toMap()); map.put(NAME,name); - map.put(DESCRIPTION,Map.of(SOURCE,description,RENDERED,markdown(description))); + map.put(DESCRIPTION,mapMarkdown(description)); if (parentLocationId != null) map.put(PARENT_LOCATION_ID,parentLocationId); return map; } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Document.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Document.java index a49d2545..d88ca370 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/Document.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Document.java @@ -2,12 +2,9 @@ package de.srsoftware.umbrella.core.model; import static de.srsoftware.tools.Optionals.emptyIfNull; -import static de.srsoftware.umbrella.core.Constants.*; -import static de.srsoftware.umbrella.core.Field.*; -import static de.srsoftware.umbrella.core.Field.COMPANY; -import static de.srsoftware.umbrella.core.Field.TYPE; import static de.srsoftware.umbrella.core.ResponseCode.HTTP_UNPROCESSABLE; import static de.srsoftware.umbrella.core.Util.mapMarkdown; +import static de.srsoftware.umbrella.core.constants.Field.*; import static java.util.Optional.empty; import de.srsoftware.tools.Mappable; @@ -205,9 +202,9 @@ public final class Document implements Mappable { case HEAD: head = json.getString(key); break; case NUMBER: number = json.getString(key); break; case SENDER: if (json.get(key) instanceof JSONObject nested) sender.patch(nested); break; - case STATE: state = State.of(json.getInt(key)).orElseThrow(() -> new UmbrellaException(HTTP_UNPROCESSABLE,"Invalid state")); break; + case STATE: state = State.of(json.getInt(key)).orElseThrow(() -> new UmbrellaException(HTTP_UNPROCESSABLE, Translatable.t("Invalid state"))); break; case POS: if (json.get(key) instanceof JSONObject nested) positions.patch(nested); break; - case TEMPLATE_ID: if (json.get(key) instanceof String templateId) template = templateId; break; + case TEMPLATE: if (json.get(key) instanceof String templateId) template = templateId; break; default: key = null; } if (key != null) dirtyFields.add(key); @@ -279,7 +276,7 @@ public final class Document implements Mappable { ID, id, NUMBER, number, "type", type.name(), - STATE, Map.of(NAME,state.toString(),ID,state.code), + STATE, Map.of(NAME,state.toString(), ID,state.code), DATE, date, CURRENCY, currency, CUSTOMER, customer.toMap(), diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/EmailAddress.java b/core/src/main/java/de/srsoftware/umbrella/core/model/EmailAddress.java index 9d662607..b4fdac47 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/EmailAddress.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/EmailAddress.java @@ -2,7 +2,9 @@ package de.srsoftware.umbrella.core.model; import static de.srsoftware.tools.Optionals.allSet; -import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; +import static de.srsoftware.umbrella.core.Errors.INVALID_EMAIL; +import static de.srsoftware.umbrella.core.constants.Field.EMAIL; +import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.badRequest; import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import java.util.Objects; @@ -12,7 +14,7 @@ public class EmailAddress { public EmailAddress(String addr) throws UmbrellaException { var parts = addr.split("@"); - if (parts.length != 2 || !allSet(parts[0],parts[1])) throw new UmbrellaException(HTTP_BAD_REQUEST,"\"{0}\" is not a valid email address",addr); + if (parts.length != 2 || !allSet(parts[0],parts[1])) throw badRequest(INVALID_EMAIL, EMAIL,addr); email = addr; } @@ -21,6 +23,11 @@ public class EmailAddress { return obj instanceof EmailAddress other && Objects.equals(email,other.email); } + @Override + public int hashCode() { + return email.hashCode(); + } + @Override public String toString() { return email; diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Envelope.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Envelope.java index 8343118b..35d8c279 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/Envelope.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Envelope.java @@ -1,12 +1,16 @@ /* © SRSoftware 2025 */ 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.core.constants.Constants.JSONARRAY; +import static de.srsoftware.umbrella.core.constants.Constants.JSONOBJECT; +import static de.srsoftware.umbrella.core.constants.Field.ID; +import static de.srsoftware.umbrella.core.constants.Field.RECEIVERS; +import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*; +import static de.srsoftware.umbrella.core.model.Translatable.t; import static java.text.MessageFormat.format; import de.srsoftware.umbrella.core.exceptions.UmbrellaException; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -14,17 +18,19 @@ import java.util.stream.Collectors; import org.json.JSONArray; import org.json.JSONObject; -public class Envelope { - private Message message; - private Set receivers; +public class Envelope> { + private final T message; + private final Set receivers; + private final long id; - public Envelope(Message message, User receiver){ - this(message,new HashSet<>(Set.of(receiver))); + public Envelope(long id, T message, User receiver){ + this(id, message,new HashSet<>(Set.of(receiver))); } - public Envelope(Message message, HashSet receivers) { + public Envelope(long id, T message, Collection receivers) { this.message = message; - this.receivers = receivers; + this.receivers = new HashSet<>(receivers); + this.id = id; } /** @@ -33,25 +39,41 @@ public class Envelope { * @return * @throws UmbrellaException */ - public static Envelope from(JSONObject json) throws UmbrellaException { - if (!json.has(RECEIVERS)) throw missingFieldException(RECEIVERS); - var message = Message.from(json); + public static Envelope from(JSONObject json) throws UmbrellaException { + if (!json.has(RECEIVERS)) throw missingField(RECEIVERS); + var id = json.has(ID) && json.get(ID) instanceof Number n ? n.longValue() : 0; + var message = TranslatedMessage.from(json); var obj = json.get(RECEIVERS); if (obj instanceof JSONObject) obj = new JSONArray(List.of(obj)); - if (!(obj instanceof JSONArray receiverList)) throw invalidFieldException(RECEIVERS, JSONARRAY); + if (!(obj instanceof JSONArray receiverList)) throw invalidField(RECEIVERS, t(JSONARRAY)); var receivers = new HashSet(); for (var o : receiverList){ - if (!(o instanceof JSONObject receiverData)) throw invalidFieldException("entries of "+ RECEIVERS, JSONOBJECT); + if (!(o instanceof JSONObject receiverData)) throw invalidField("entries of "+ RECEIVERS, t(JSONOBJECT)); receivers.add(User.of(receiverData)); } - return new Envelope(message,receivers); + return new Envelope<>(id, message, receivers); + } + + @Override + public final boolean equals(Object o) { + if (!(o instanceof Envelope envelope)) return false; + return message.equals(envelope.message) && id == envelope.id; + } + + @Override + public int hashCode() { + return message.hashCode(); + } + + public long id(){ + return id; } public boolean isFor(User receiver) { - return receivers.contains(receiver); + return receivers.stream().anyMatch(r -> r.equals(receiver)); } - public Message message(){ + public T message(){ return message; } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Item.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Item.java index 70cd93e3..e7fbc43e 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/Item.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Item.java @@ -1,11 +1,12 @@ /* © SRSoftware 2025 */ package de.srsoftware.umbrella.core.model; -import static de.srsoftware.umbrella.core.Constants.*; import static de.srsoftware.umbrella.core.Util.mapMarkdown; +import static de.srsoftware.umbrella.core.constants.Field.*; import de.srsoftware.tools.Mappable; import de.srsoftware.umbrella.core.api.Owner; +import de.srsoftware.umbrella.core.constants.Field; import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; @@ -75,7 +76,7 @@ public class Item implements Mappable { var owner = OwnerRef.of(rs); var ownerNumber = rs.getLong(OWNER_NUMBER); var location = Location.of(rs); - var code = rs.getString(CODE); + var code = rs.getString(Field.CODE); var name = rs.getString(NAME); var description = rs.getString(DESCRIPTION); return new Item(id, owner, ownerNumber, location, code, name, description); @@ -97,7 +98,7 @@ public class Item implements Mappable { for (var field : json.keySet()){ var known = true; switch (field) { - case CODE: + case Field.CODE: code = json.getString(field); break; case NAME: @@ -124,7 +125,7 @@ public class Item implements Mappable { map.put(OWNER,owner.toMap()); map.put(ID,id); map.put(LOCATION,location.toMap()); - map.put(CODE,code); + map.put(Field.CODE,code); map.put(NAME,name); map.put(DESCRIPTION,mapMarkdown(description)); map.put(OWNER_NUMBER,ownerNumber); diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Location.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Location.java index 8381da4e..44d31958 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/Location.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Location.java @@ -1,9 +1,9 @@ /* © SRSoftware 2025 */ package de.srsoftware.umbrella.core.model; -import static de.srsoftware.umbrella.core.Constants.ID; -import static de.srsoftware.umbrella.core.Constants.LOCATION_ID; import static de.srsoftware.umbrella.core.ModuleRegistry.stockService; +import static de.srsoftware.umbrella.core.constants.Field.ID; +import static de.srsoftware.umbrella.core.constants.Field.LOCATION_ID; import static java.text.MessageFormat.format; import de.srsoftware.tools.Mappable; diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Member.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Member.java index 896d8018..d46fe82b 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/Member.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Member.java @@ -2,7 +2,8 @@ package de.srsoftware.umbrella.core.model; -import static de.srsoftware.umbrella.core.Constants.*; +import static de.srsoftware.umbrella.core.constants.Field.PERMISSION; +import static de.srsoftware.umbrella.core.constants.Field.USER; import de.srsoftware.tools.Mappable; import java.util.HashMap; diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Message.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Message.java index 2ebf159a..f07b5aba 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/Message.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Message.java @@ -1,47 +1,37 @@ /* © SRSoftware 2025 */ 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 java.text.MessageFormat.format; -import de.srsoftware.umbrella.core.exceptions.UmbrellaException; -import java.util.*; -import org.json.JSONArray; -import org.json.JSONObject; +import java.util.Collection; +import java.util.Objects; -public record Message(UmbrellaUser sender, String subject, String body, Map fills, List 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 abstract class Message { + private final Collection attachments; + private final T body, subject; + private final UmbrellaUser sender; + private long utcTime; + + public Message(UmbrellaUser sender, T subject, T body, Collection attachments){ + this.sender = sender; + this.subject = subject; + this.body = body; + this.attachments = attachments; + this.utcTime = System.currentTimeMillis(); } - public static Message from(JSONObject json) throws UmbrellaException { - for (var key : Set.of(SENDER, SUBJECT, BODY)) { - if (!json.has(key)) throw missingFieldException(key); - } - if (!(json.get(SENDER) instanceof JSONObject senderObject)) throw invalidFieldException(SENDER, JSONOBJECT); - if (!(json.get(SUBJECT) instanceof String subject && isSet(subject))) throw invalidFieldException(SUBJECT,STRING); - if (!(json.get(BODY) instanceof String body && isSet(body))) throw invalidFieldException(BODY,STRING); + public Collection attachments(){ + return attachments; + } - var user = UmbrellaUser.of(senderObject); - if (!(user instanceof UmbrellaUser sender)) throw new UmbrellaException(400,"Sender is not an umbrella user!"); - var attachments = new ArrayList(); - if (json.has(ATTACHMENTS)){ - var jsonAttachments = json.get(ATTACHMENTS); - if (jsonAttachments instanceof JSONObject obj) jsonAttachments = new JSONArray(List.of(obj)); - if (jsonAttachments instanceof JSONArray arr){ - for (var att : arr){ - if (!(att instanceof JSONObject o)) throw new UmbrellaException(400,"Attachments contains entry that is not an object: {}",att); - var attachment = Attachment.of(o); - attachments.add(attachment); - } - } - } - return new Message(sender,subject,body,null, attachments); + public T body(){ + return body; + } + + @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 @@ -49,8 +39,25 @@ public record Message(UmbrellaUser sender, String subject, String body, Map utcTime(long newValue){ + utcTime = newValue; + return this; + } } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Note.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Note.java index adf28ab3..26410b12 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/Note.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Note.java @@ -1,8 +1,8 @@ /* © SRSoftware 2025 */ package de.srsoftware.umbrella.core.model; -import static de.srsoftware.umbrella.core.Constants.*; -import static de.srsoftware.umbrella.core.Util.markdown; +import static de.srsoftware.umbrella.core.Util.mapMarkdown; +import static de.srsoftware.umbrella.core.constants.Field.*; import static java.time.ZoneOffset.UTC; import de.srsoftware.tools.Mappable; @@ -72,7 +72,7 @@ public class Note implements Mappable { MODULE,module, ENTITY_ID,entityId, USER_ID,authorId, - TEXT,Map.of(RENDERED,markdown(text),SOURCE,text), + TEXT,mapMarkdown(text), TIMESTAMP,timestamp.withNano(0) ); } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/OwnerRef.java b/core/src/main/java/de/srsoftware/umbrella/core/model/OwnerRef.java index 1a64888f..962ff163 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/OwnerRef.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/OwnerRef.java @@ -1,13 +1,16 @@ /* © SRSoftware 2025 */ package de.srsoftware.umbrella.core.model; -import static de.srsoftware.umbrella.core.Constants.*; -import static de.srsoftware.umbrella.core.Constants.ID; +import static de.srsoftware.umbrella.core.constants.Module.COMPANY; import static de.srsoftware.umbrella.core.ModuleRegistry.companyService; import static de.srsoftware.umbrella.core.ModuleRegistry.userService; -import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.unprocessable; +import static de.srsoftware.umbrella.core.constants.Field.*; +import static de.srsoftware.umbrella.core.constants.Module.USER; +import static de.srsoftware.umbrella.core.constants.Text.INVALID_DB_CODE; +import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*; import de.srsoftware.umbrella.core.api.Owner; +import de.srsoftware.umbrella.core.constants.Field; import java.sql.ResultSet; import java.sql.SQLException; import java.util.HashMap; @@ -20,12 +23,12 @@ public class OwnerRef implements Owner { public OwnerRef(String dbCode){ var parts = dbCode.split(":"); - if (parts.length != 2) throw unprocessable("Encountered invalid dbCode: {0}",dbCode); + if (parts.length != 2) throw unprocessable(INVALID_DB_CODE, Field.CODE,dbCode); type = parts[0]; try { id = Long.parseLong(parts[1]); } catch (NumberFormatException e) { - throw unprocessable("Encountered invalid dbCode: {0}",dbCode); + throw unprocessable(INVALID_DB_CODE, Field.CODE,dbCode); } } @@ -59,7 +62,7 @@ public class OwnerRef implements Owner { return switch (type){ case COMPANY -> companyService().get(id()); case USER -> userService().loadUser(id()); - case null, default -> throw unprocessable("Encountered invalid owner type: {0}",type); + case null, default -> throw unprocessable("Encountered invalid owner type: {type}", TYPE,type); }; } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Permission.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Permission.java index 9e9506bc..aa047de6 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/Permission.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Permission.java @@ -1,11 +1,11 @@ /* © SRSoftware 2025 */ package de.srsoftware.umbrella.core.model; -import static de.srsoftware.umbrella.core.Constants.CODE; -import static de.srsoftware.umbrella.core.Constants.NAME; +import static de.srsoftware.umbrella.core.constants.Field.NAME; import static java.text.MessageFormat.format; import de.srsoftware.tools.Mappable; +import de.srsoftware.umbrella.core.constants.Field; import java.security.InvalidParameterException; import java.util.Map; @@ -31,12 +31,12 @@ public enum Permission implements Mappable { for (var p : Permission.values()){ if (p.code == code) return p; } - throw new InvalidParameterException(format("{0} is not a valid permission code")); + throw new InvalidParameterException(format("{0} is not a valid permission code",code)); } @Override public Map toMap() { - return Map.of(NAME,name(),CODE,code); + return Map.of(NAME,name(), Field.CODE,code); } public boolean mayWrite() { diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Poll.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Poll.java new file mode 100644 index 00000000..8b92e731 --- /dev/null +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Poll.java @@ -0,0 +1,309 @@ +/* © SRSoftware 2025 */ +package de.srsoftware.umbrella.core.model; + +import static de.srsoftware.umbrella.core.constants.Field.*; +import static java.text.MessageFormat.format; + +import de.srsoftware.tools.Mappable; +import de.srsoftware.umbrella.core.ModuleRegistry; +import de.srsoftware.umbrella.core.Util; +import de.srsoftware.umbrella.core.api.Owner; +import de.srsoftware.umbrella.core.constants.Field; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; +import java.util.stream.Collectors; + +public class Poll implements Mappable { + public static class Option implements Mappable{ + + private int id; + + Integer status; + private String description; + private String name; + private final Set dirtyFields = new HashSet<>(); + public Option(int id, String name, String description, Integer status) { + this.id = id; + this.name = name; + this.description = description; + this.status = status; + } + public String description(){ + return description; + } + + public Option description(String newVal){ + description = newVal; + dirtyFields.add(DESCRIPTION); + return this; + } + + public int id(){ + return id; + } + + public Option id(Integer newVal){ + this.id = newVal; + dirtyFields.add(ID); + return this; + } + + public boolean isDirty(){ + return !dirtyFields.isEmpty(); + } + + public boolean isNew(){ + return dirtyFields.contains(ID); + } + + public String name(){ + return name; + } + + public Option name(String newVal){ + name = newVal; + dirtyFields.add(NAME); + return this; + } + + public static Option of(ResultSet rs) throws SQLException { + var id = rs.getInt(ID); + var name = rs.getString(NAME); + var description = rs.getString(DESCRIPTION); + var status = rs.getInt(STATUS); + + return new Option(id,name,description,status); + } + + public Integer status(){ + return status; + } + + public Option status(int newValue){ + this.status = newValue; + dirtyFields.add(STATUS); + return this; + } + + @Override + public Map toMap() { + return Map.of( + ID,id, + NAME,name, + DESCRIPTION, Map.of( + SOURCE,description, + RENDERED,Util.markdown(description) + ), + STATUS,status + ); + } + + @Override + public String toString() { + return format("Option \"{0}\"",name); + } + + + } + public static class Evaluation { + private static class Histogram extends HashMap{ + public Double average() { + var sum = 0; + var count = 0; + for (var entry : entrySet()){ + var weight = entry.getKey(); + var votes = entry.getValue(); + count += votes; + sum += weight * votes; + } + if (count < 1) return null; + return sum/(double) count; + } + } + private static class OptionStats extends HashMap{} + private static class Stats extends HashMap{ + public Stats(OptionStats optionStats) { + for (var entry : optionStats.entrySet()){ + var optionId = entry.getKey(); + var histo = entry.getValue(); + var average = histo.average(); + var os = get(average); + if (os == null) put(average,os = new OptionStats()); + os.put(optionId,histo); + } + } + } + /* + + { + options : { + 1: option 1 + 2: option 2 + }, + stat : { + 0.571 : { // average + 1 : { // option + -2 : 0, // weight → selections + -1 : 1, + 0 : 2 + 1 : 3 + 2 : 1 + } + 2 : + } + } + } + + */ + + private final Map options = new HashMap<>(); + private final OptionStats optionStats = new OptionStats(); + + public Evaluation(Poll poll) { + for (var option : poll.options){ + options.put(option.id,option); + var histo = new Histogram(); + for (var w : poll.weights.keySet()) histo.put(w,0); + optionStats.put(option.id,histo); + } + + } + + public void count(ResultSet rs) throws SQLException { + var optionId = rs.getInt(OPTION_ID); + //var userId = rs.getObject(USER); + var weight = rs.getInt(WEIGHT); + var histogram = optionStats.get(optionId); + histogram.put(weight,histogram.get(weight)+1); + } + + public Map toMap() { + return new Stats(optionStats); + } + + } + private Owner owner; + private String id, name, description; + + private boolean isPrivate; + private List