From a4fffbe91b1d94bef73dc39d0f8747bb7088fdb7 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Mon, 22 Dec 2025 15:12:25 +0100 Subject: [PATCH 1/5] preparing journal module --- backend/build.gradle.kts | 2 ++ .../umbrella/backend/Application.java | 2 ++ .../umbrella/bookmarks/BookmarkApi.java | 2 +- journal/build.gradle.kts | 7 +++++ .../umbrella/journal/Constants.java | 6 ++++ .../umbrella/journal/JournalDb.java | 4 +++ .../umbrella/journal/JournalModule.java | 29 +++++++++++++++++++ .../srsoftware/umbrella/journal/SqliteDb.java | 21 ++++++++++++++ settings.gradle.kts | 2 ++ 9 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 journal/build.gradle.kts create mode 100644 journal/src/main/java/de/srsoftware/umbrella/journal/Constants.java create mode 100644 journal/src/main/java/de/srsoftware/umbrella/journal/JournalDb.java create mode 100644 journal/src/main/java/de/srsoftware/umbrella/journal/JournalModule.java create mode 100644 journal/src/main/java/de/srsoftware/umbrella/journal/SqliteDb.java diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index 1bd54ba..36aa6e0 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -18,6 +18,7 @@ dependencies{ implementation(project(":core")) implementation(project(":documents")) implementation(project(":files")) + implementation(project(":journal")) implementation(project(":legacy")) implementation(project(":markdown")) implementation(project(":messages")) @@ -52,6 +53,7 @@ tasks.jar { ":core:jar", ":documents:jar", ":files:jar", + ":journal:jar", ":legacy:jar", ":markdown:jar", ":messages: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 a6379dd..060a0bf 100644 --- a/backend/src/main/java/de/srsoftware/umbrella/backend/Application.java +++ b/backend/src/main/java/de/srsoftware/umbrella/backend/Application.java @@ -16,6 +16,7 @@ import de.srsoftware.umbrella.core.Util; import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.documents.DocumentApi; import de.srsoftware.umbrella.files.FileModule; +import de.srsoftware.umbrella.journal.JournalModule; import de.srsoftware.umbrella.legacy.*; import de.srsoftware.umbrella.markdown.MarkdownApi; import de.srsoftware.umbrella.message.MessageSystem; @@ -64,6 +65,7 @@ public class Application { var server = HttpServer.create(new InetSocketAddress(port), 0); try { new Translations().bindPath("/api/translations").on(server); + new JournalModule().bindPath("/api/journal").on(server); new MessageApi().bindPath("/api/bus").on(server); new MessageSystem(config); new UserModule(config).bindPath("/api/user").on(server); 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 eea9fab..c1697c9 100644 --- a/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkApi.java +++ b/bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkApi.java @@ -34,7 +34,7 @@ public class BookmarkApi extends BaseHandler implements BookmarkService { super(); var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingFieldException(CONFIG_DATABASE)); db = new SqliteDb(connect(dbFile)); - ModuleRegistry.add(this); + ModuleRegistry.add(this); } @Override diff --git a/journal/build.gradle.kts b/journal/build.gradle.kts new file mode 100644 index 0000000..53d1e4b --- /dev/null +++ b/journal/build.gradle.kts @@ -0,0 +1,7 @@ +description = "Umbrella : Journal" + +dependencies{ + implementation(project(":bus")) + implementation(project(":core")) +} + diff --git a/journal/src/main/java/de/srsoftware/umbrella/journal/Constants.java b/journal/src/main/java/de/srsoftware/umbrella/journal/Constants.java new file mode 100644 index 0000000..2a8e6d5 --- /dev/null +++ b/journal/src/main/java/de/srsoftware/umbrella/journal/Constants.java @@ -0,0 +1,6 @@ +/* © SRSoftware 2025 */ +package de.srsoftware.umbrella.journal; + +public class Constants { + public static final String CONFIG_DATABASE = "umbrella.modules.journal.database"; +} diff --git a/journal/src/main/java/de/srsoftware/umbrella/journal/JournalDb.java b/journal/src/main/java/de/srsoftware/umbrella/journal/JournalDb.java new file mode 100644 index 0000000..b7f7040 --- /dev/null +++ b/journal/src/main/java/de/srsoftware/umbrella/journal/JournalDb.java @@ -0,0 +1,4 @@ +package de.srsoftware.umbrella.journal; + +public interface JournalDb { +} diff --git a/journal/src/main/java/de/srsoftware/umbrella/journal/JournalModule.java b/journal/src/main/java/de/srsoftware/umbrella/journal/JournalModule.java new file mode 100644 index 0000000..1de6ec3 --- /dev/null +++ b/journal/src/main/java/de/srsoftware/umbrella/journal/JournalModule.java @@ -0,0 +1,29 @@ +package de.srsoftware.umbrella.journal; + +import de.srsoftware.configuration.Configuration; +import de.srsoftware.umbrella.core.BaseHandler; +import de.srsoftware.umbrella.core.ModuleRegistry; +import de.srsoftware.umbrella.messagebus.EventListener; +import de.srsoftware.umbrella.messagebus.events.Event; + +import static de.srsoftware.umbrella.core.ConnectionProvider.connect; +import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingFieldException; +import static de.srsoftware.umbrella.journal.Constants.CONFIG_DATABASE; + + +public class JournalModule extends BaseHandler implements EventListener { + + private final SqliteDb journalDb; + + public JournalModule(Configuration config){ + super(); + var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingFieldException(CONFIG_DATABASE)) + journalDb = new SqliteDb(connect(dbFile)); + ModuleRegistry.add(this); + } + + @Override + public void onEvent(Event event) { + + } +} diff --git a/journal/src/main/java/de/srsoftware/umbrella/journal/SqliteDb.java b/journal/src/main/java/de/srsoftware/umbrella/journal/SqliteDb.java new file mode 100644 index 0000000..1aca72f --- /dev/null +++ b/journal/src/main/java/de/srsoftware/umbrella/journal/SqliteDb.java @@ -0,0 +1,21 @@ +package de.srsoftware.umbrella.journal; + +import de.srsoftware.umbrella.core.BaseDb; + +import java.sql.Connection; + +public class SqliteDb extends BaseDb implements JournalDb{ + public SqliteDb(Connection connection) { + super(connection); + } + + protected int createTables() { + int currentVersion = createSettingsTable(); + switch (currentVersion){ + case 0: + createJournalTable(); + } + + return setCurrentVersion(1); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index aaf9341..c5f2d7c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,3 +21,5 @@ include("translations") include("user") include("web") include("wiki") + +include("journal") \ No newline at end of file From 81dc30359d73d917b62ab1cf047d577d24813f17 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Fri, 9 Jan 2026 08:47:20 +0100 Subject: [PATCH 2/5] working on journal module Signed-off-by: Stephan Richter --- .../umbrella/backend/Application.java | 2 +- .../umbrella/messagebus/events/Event.java | 16 ++++++-- .../messagebus/events/ProjectEvent.java | 5 +++ .../umbrella/messagebus/events/TaskEvent.java | 5 +++ .../de/srsoftware/umbrella/core/BaseDb.java | 1 - .../srsoftware/umbrella/core/Constants.java | 5 +-- .../umbrella/journal/Constants.java | 3 ++ .../umbrella/journal/JournalDb.java | 3 ++ .../umbrella/journal/JournalModule.java | 7 ++-- .../srsoftware/umbrella/journal/SqliteDb.java | 40 +++++++++++++++++++ 10 files changed, 75 insertions(+), 12 deletions(-) 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 060a0bf..9d70749 100644 --- a/backend/src/main/java/de/srsoftware/umbrella/backend/Application.java +++ b/backend/src/main/java/de/srsoftware/umbrella/backend/Application.java @@ -65,7 +65,7 @@ public class Application { var server = HttpServer.create(new InetSocketAddress(port), 0); try { new Translations().bindPath("/api/translations").on(server); - new JournalModule().bindPath("/api/journal").on(server); + new JournalModule(config).bindPath("/api/journal").on(server); new MessageApi().bindPath("/api/bus").on(server); new MessageSystem(config); new UserModule(config).bindPath("/api/user").on(server); 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 c7f7029..9bf1697 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 @@ -18,22 +18,28 @@ public abstract class Event { } private UmbrellaUser initiator; - private String realm; + private String module; private Payload payload; private EventType eventType; - public Event(UmbrellaUser initiator, String realm, Payload payload, EventType type){ + public Event(UmbrellaUser initiator, String module, Payload payload, EventType type){ this.initiator = initiator; - this.realm = realm; + this.module = module; this.payload = payload; this.eventType = type; } + public abstract String describe(); + public String eventType(){ return eventType.toString(); } public abstract boolean isIntendedFor(UmbrellaUser user); + public UmbrellaUser initiator(){ + return initiator; + } + public String json(){ Class clazz = payload.getClass(); { // get the highest superclass that is not object @@ -48,6 +54,10 @@ public abstract class Event { return new JSONObject(map).toString(); } + public String module(){ + return module; + } + public Payload payload(){ return payload; } 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 6ef424b..a768a52 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 @@ -12,6 +12,11 @@ public class ProjectEvent extends Event{ super(initiator, PROJECT, project, type); } + @Override + public String describe() { + return "[TODO: ProjectEvent.describe]"; + } + @Override public boolean isIntendedFor(UmbrellaUser user) { for (var member : payload().members().values()){ 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 8f5bf24..5a2090d 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 @@ -12,6 +12,11 @@ public class TaskEvent extends Event{ super(initiator, TASK, task, type); } + @Override + public String describe() { + return "[TODO: TaskEvent.describe()]"; + } + @Override public boolean isIntendedFor(UmbrellaUser user) { for (var member : payload().members().values()){ 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 60d43e7..cabe088 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/BaseDb.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/BaseDb.java @@ -75,5 +75,4 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255) throw new RuntimeException(e); } } - } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/Constants.java b/core/src/main/java/de/srsoftware/umbrella/core/Constants.java index 8c0ff1e..6804d29 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/Constants.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/Constants.java @@ -8,10 +8,7 @@ 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"; diff --git a/journal/src/main/java/de/srsoftware/umbrella/journal/Constants.java b/journal/src/main/java/de/srsoftware/umbrella/journal/Constants.java index 2a8e6d5..e22de9b 100644 --- a/journal/src/main/java/de/srsoftware/umbrella/journal/Constants.java +++ b/journal/src/main/java/de/srsoftware/umbrella/journal/Constants.java @@ -3,4 +3,7 @@ package de.srsoftware.umbrella.journal; public class Constants { public static final String CONFIG_DATABASE = "umbrella.modules.journal.database"; + + public static final String ERROR_WRITE_EVENT = "Failed to write {0} event of {1} to journal!"; + public static final String TABLE_JOURNAL = "journal"; } diff --git a/journal/src/main/java/de/srsoftware/umbrella/journal/JournalDb.java b/journal/src/main/java/de/srsoftware/umbrella/journal/JournalDb.java index b7f7040..7d2445c 100644 --- a/journal/src/main/java/de/srsoftware/umbrella/journal/JournalDb.java +++ b/journal/src/main/java/de/srsoftware/umbrella/journal/JournalDb.java @@ -1,4 +1,7 @@ package de.srsoftware.umbrella.journal; +import de.srsoftware.umbrella.messagebus.events.Event; + public interface JournalDb { + void logEvent(Event event); } diff --git a/journal/src/main/java/de/srsoftware/umbrella/journal/JournalModule.java b/journal/src/main/java/de/srsoftware/umbrella/journal/JournalModule.java index 1de6ec3..c66124c 100644 --- a/journal/src/main/java/de/srsoftware/umbrella/journal/JournalModule.java +++ b/journal/src/main/java/de/srsoftware/umbrella/journal/JournalModule.java @@ -13,17 +13,18 @@ import static de.srsoftware.umbrella.journal.Constants.CONFIG_DATABASE; public class JournalModule extends BaseHandler implements EventListener { - private final SqliteDb journalDb; + private final JournalDb journalDb; public JournalModule(Configuration config){ super(); - var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingFieldException(CONFIG_DATABASE)) + var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingFieldException(CONFIG_DATABASE)); journalDb = new SqliteDb(connect(dbFile)); ModuleRegistry.add(this); } @Override public void onEvent(Event event) { - + LOG.log(System.Logger.Level.DEBUG,"{0} @ {1} ({2})",event.eventType(),event.module(),event.initiator()); + journalDb.logEvent(event); } } diff --git a/journal/src/main/java/de/srsoftware/umbrella/journal/SqliteDb.java b/journal/src/main/java/de/srsoftware/umbrella/journal/SqliteDb.java index 1aca72f..f8405c9 100644 --- a/journal/src/main/java/de/srsoftware/umbrella/journal/SqliteDb.java +++ b/journal/src/main/java/de/srsoftware/umbrella/journal/SqliteDb.java @@ -1,8 +1,19 @@ package de.srsoftware.umbrella.journal; +import de.srsoftware.tools.jdbc.Query; import de.srsoftware.umbrella.core.BaseDb; +import de.srsoftware.umbrella.core.exceptions.UmbrellaException; +import de.srsoftware.umbrella.messagebus.events.Event; import java.sql.Connection; +import java.sql.SQLException; + +import static de.srsoftware.tools.jdbc.Query.insertInto; +import static de.srsoftware.umbrella.core.Constants.*; +import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.databaseException; +import static de.srsoftware.umbrella.journal.Constants.ERROR_WRITE_EVENT; +import static de.srsoftware.umbrella.journal.Constants.TABLE_JOURNAL; +import static java.text.MessageFormat.format; public class SqliteDb extends BaseDb implements JournalDb{ public SqliteDb(Connection connection) { @@ -18,4 +29,33 @@ public class SqliteDb extends BaseDb implements JournalDb{ return setCurrentVersion(1); } + + private void createJournalTable() { + var sql = """ + CREATE TABLE {0} ( + {1} INT PRIMARY KEY, + {2} INT, + {3} VARCHAR(255) NOT NULL, + {4} VARCHAR(16) NUT NULL, + {5} TEXT + ); + """; + sql = format(sql,TABLE_JOURNAL,ID,USER_ID,MODULE,ACTION,DESCRIPTION); + try { + db.prepareStatement(sql).execute(); + } catch (SQLException e) { + throw databaseException(ERROR_FAILED_CREATE_TABLE,TABLE_JOURNAL); + } + } + + @Override + public void logEvent(Event event) { + try { + insertInto(TABLE_JOURNAL,USER_ID,MODULE,ACTION,DESCRIPTION) + .values(event.initiator().id(), event.module(), event.eventType(), event.describe()) + .execute(db).close(); + } catch (SQLException e) { + throw databaseException(ERROR_WRITE_EVENT,event.eventType(),event.initiator().name()); + } + } } From 0dd640de3044a7160a928acf1a19f54e690a71b9 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Fri, 9 Jan 2026 13:02:53 +0100 Subject: [PATCH 3/5] working on preparing journal loggin Signed-off-by: Stephan Richter --- .../de/srsoftware/umbrella/messagebus/EventListener.java | 2 +- .../de/srsoftware/umbrella/messagebus/EventQueue.java | 4 ++-- .../de/srsoftware/umbrella/messagebus/MessageBus.java | 4 ++-- .../umbrella/core/exceptions/UmbrellaException.java | 8 +++++++- .../java/de/srsoftware/umbrella/journal/JournalDb.java | 2 +- .../de/srsoftware/umbrella/journal/JournalModule.java | 9 +++++++-- .../java/de/srsoftware/umbrella/journal/SqliteDb.java | 6 +++--- .../java/de/srsoftware/umbrella/task/TaskModule.java | 4 +++- 8 files changed, 26 insertions(+), 13 deletions(-) diff --git a/bus/src/main/java/de/srsoftware/umbrella/messagebus/EventListener.java b/bus/src/main/java/de/srsoftware/umbrella/messagebus/EventListener.java index 02b9e07..54664e8 100644 --- a/bus/src/main/java/de/srsoftware/umbrella/messagebus/EventListener.java +++ b/bus/src/main/java/de/srsoftware/umbrella/messagebus/EventListener.java @@ -4,5 +4,5 @@ package de.srsoftware.umbrella.messagebus; import de.srsoftware.umbrella.messagebus.events.Event; public interface EventListener { - void onEvent(Event event); + void onEvent(Event event); } 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 a999455..047a02b 100644 --- a/bus/src/main/java/de/srsoftware/umbrella/messagebus/EventQueue.java +++ b/bus/src/main/java/de/srsoftware/umbrella/messagebus/EventQueue.java @@ -7,7 +7,7 @@ import de.srsoftware.umbrella.messagebus.events.Event; import java.net.InetSocketAddress; import java.util.LinkedList; -public class EventQueue extends LinkedList implements AutoCloseable, EventListener { +public class EventQueue extends LinkedList> implements AutoCloseable, EventListener { private final InetSocketAddress addr; @@ -29,7 +29,7 @@ public class EventQueue extends LinkedList implements AutoCloseable, Even } @Override - public void onEvent(Event event) { + public void onEvent(Event event) { System.getLogger(addr.toString()).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/MessageBus.java b/bus/src/main/java/de/srsoftware/umbrella/messagebus/MessageBus.java index 148aab9..75aca93 100644 --- a/bus/src/main/java/de/srsoftware/umbrella/messagebus/MessageBus.java +++ b/bus/src/main/java/de/srsoftware/umbrella/messagebus/MessageBus.java @@ -6,8 +6,8 @@ import java.util.HashSet; import java.util.Set; public class MessageBus { - private static MessageBus SINGLETON = new MessageBus(); - private Set listeners = new HashSet<>(); + private static final MessageBus SINGLETON = new MessageBus(); + private final Set listeners = new HashSet<>(); private MessageBus(){} 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 a8bd997..76d5d71 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 @@ -8,6 +8,7 @@ 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; public class UmbrellaException extends RuntimeException{ @@ -43,7 +44,12 @@ public class UmbrellaException extends RuntimeException{ return new UmbrellaException(HTTP_FORBIDDEN,message,fills); } - public static UmbrellaException invalidFieldException(String field,String expected){ + @Override + public String getMessage() { + return format(super.getMessage(),fills); + } + + public static UmbrellaException invalidFieldException(String field, String expected){ return new UmbrellaException(HTTP_UNPROCESSABLE, ERROR_INVALID_FIELD, field, expected); } diff --git a/journal/src/main/java/de/srsoftware/umbrella/journal/JournalDb.java b/journal/src/main/java/de/srsoftware/umbrella/journal/JournalDb.java index 7d2445c..14c443c 100644 --- a/journal/src/main/java/de/srsoftware/umbrella/journal/JournalDb.java +++ b/journal/src/main/java/de/srsoftware/umbrella/journal/JournalDb.java @@ -3,5 +3,5 @@ package de.srsoftware.umbrella.journal; import de.srsoftware.umbrella.messagebus.events.Event; public interface JournalDb { - void logEvent(Event event); + void logEvent(Event event); } diff --git a/journal/src/main/java/de/srsoftware/umbrella/journal/JournalModule.java b/journal/src/main/java/de/srsoftware/umbrella/journal/JournalModule.java index c66124c..066312f 100644 --- a/journal/src/main/java/de/srsoftware/umbrella/journal/JournalModule.java +++ b/journal/src/main/java/de/srsoftware/umbrella/journal/JournalModule.java @@ -3,12 +3,16 @@ package de.srsoftware.umbrella.journal; import de.srsoftware.configuration.Configuration; import de.srsoftware.umbrella.core.BaseHandler; import de.srsoftware.umbrella.core.ModuleRegistry; +import de.srsoftware.umbrella.core.model.Message; import de.srsoftware.umbrella.messagebus.EventListener; +import de.srsoftware.umbrella.messagebus.MessageBus; import de.srsoftware.umbrella.messagebus.events.Event; import static de.srsoftware.umbrella.core.ConnectionProvider.connect; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingFieldException; import static de.srsoftware.umbrella.journal.Constants.CONFIG_DATABASE; +import static de.srsoftware.umbrella.messagebus.MessageBus.messageBus; +import static java.lang.System.Logger.Level.INFO; public class JournalModule extends BaseHandler implements EventListener { @@ -20,11 +24,12 @@ public class JournalModule extends BaseHandler implements EventListener { var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingFieldException(CONFIG_DATABASE)); journalDb = new SqliteDb(connect(dbFile)); ModuleRegistry.add(this); + messageBus().register(this); } @Override - public void onEvent(Event event) { - LOG.log(System.Logger.Level.DEBUG,"{0} @ {1} ({2})",event.eventType(),event.module(),event.initiator()); + public void onEvent(Event event) { + LOG.log(DEBUG,"{0} @ {1} (by {2})",event.eventType(),event.module(),event.initiator().name()); journalDb.logEvent(event); } } diff --git a/journal/src/main/java/de/srsoftware/umbrella/journal/SqliteDb.java b/journal/src/main/java/de/srsoftware/umbrella/journal/SqliteDb.java index f8405c9..1ca82ae 100644 --- a/journal/src/main/java/de/srsoftware/umbrella/journal/SqliteDb.java +++ b/journal/src/main/java/de/srsoftware/umbrella/journal/SqliteDb.java @@ -32,11 +32,11 @@ public class SqliteDb extends BaseDb implements JournalDb{ private void createJournalTable() { var sql = """ - CREATE TABLE {0} ( + CREATE TABLE IF NOT EXISTS {0} ( {1} INT PRIMARY KEY, {2} INT, {3} VARCHAR(255) NOT NULL, - {4} VARCHAR(16) NUT NULL, + {4} VARCHAR(16) NOT NULL, {5} TEXT ); """; @@ -49,7 +49,7 @@ public class SqliteDb extends BaseDb implements JournalDb{ } @Override - public void logEvent(Event event) { + public void logEvent(Event event) { try { insertInto(TABLE_JOURNAL,USER_ID,MODULE,ACTION,DESCRIPTION) .values(event.initiator().id(), event.module(), event.eventType(), event.describe()) diff --git a/task/src/main/java/de/srsoftware/umbrella/task/TaskModule.java b/task/src/main/java/de/srsoftware/umbrella/task/TaskModule.java index de70bee..37d4994 100644 --- a/task/src/main/java/de/srsoftware/umbrella/task/TaskModule.java +++ b/task/src/main/java/de/srsoftware/umbrella/task/TaskModule.java @@ -335,6 +335,7 @@ public class TaskModule extends BaseHandler implements TaskService { private boolean patchTask(HttpExchange ex, long taskId, UmbrellaUser user) throws IOException { var task = loadMembers(taskDb.load(taskId)); + var old = task.toMap(); var member = task.members().get(user.id()); if (member == null || member.permission() == READ_ONLY) throw forbidden("You are not a allowed to edit {0}!", task.name()); var json = json(ex); @@ -342,7 +343,8 @@ public class TaskModule extends BaseHandler implements TaskService { if (json.has(MEMBERS) && json.get(MEMBERS) instanceof JSONObject memberJson) patchMembers(task, memberJson); if (json.has(NEW_MEMBER) && json.get(NEW_MEMBER) instanceof Number num) addMember(task, num.longValue()); if (json.has(PARENT_TASK_ID) && json.get(PARENT_TASK_ID) instanceof Number ptid && newParentIsSubtask(task, ptid.longValue())) throw forbidden("Task must not be sub-task of itself."); - taskDb.save(task.patch(json)); + var curr = taskDb.save(task.patch(json)).toMap(); + var diff = diff(old,curr); var tagList = tagService().getTags(TASK, taskId, user); messageBus().dispatch(new TaskEvent(user,new TaggedTask(task,tagList), UPDATE)); return sendContent(ex, task); From 10ea200a2ef74b1fe486005309bf77c0835952f1 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Sat, 10 Jan 2026 01:10:24 +0100 Subject: [PATCH 4/5] refactoring Events for better journal Signed-off-by: Stephan Richter --- .../umbrella/messagebus/events/Event.java | 47 ++++++++++++++++--- .../messagebus/events/ProjectEvent.java | 7 ++- .../umbrella/messagebus/events/TaskEvent.java | 7 ++- .../srsoftware/umbrella/core/model/Task.java | 14 +++++- .../umbrella/journal/JournalDb.java | 1 + .../umbrella/journal/JournalModule.java | 17 ++++--- .../srsoftware/umbrella/journal/SqliteDb.java | 18 ++++--- .../umbrella/project/ProjectModule.java | 4 +- .../srsoftware/umbrella/task/TaskModule.java | 29 ++---------- 9 files changed, 89 insertions(+), 55 deletions(-) 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 9bf1697..4abdc2a 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,35 +1,70 @@ /* © SRSoftware 2025 */ package de.srsoftware.umbrella.messagebus.events; -import static de.srsoftware.umbrella.core.Constants.USER; +import static de.srsoftware.umbrella.core.Constants.*; +import static java.text.MessageFormat.format; +import static java.util.Optional.*; import de.srsoftware.tools.Mappable; import de.srsoftware.umbrella.core.model.UmbrellaUser; +import java.util.HashMap; import java.util.Map; +import java.util.Optional; import org.json.JSONObject; + public abstract class Event { public enum EventType { CREATE, UPDATE, DELETE; - } - private UmbrellaUser initiator; - private String module; - private Payload payload; - private EventType eventType; + private final UmbrellaUser initiator; + private final String module; + private final Payload payload; + private final EventType eventType; + private final Map oldData; + public Event(UmbrellaUser initiator, String module, Payload payload, EventType type){ this.initiator = initiator; this.module = module; this.payload = payload; this.eventType = type; + this.oldData = null; + } + + public Event(UmbrellaUser initiator, String module, Payload payload, Map oldData){ + this.initiator = initiator; + this.module = module; + this.payload = payload; + this.eventType = EventType.UPDATE; + this.oldData = oldData; } public abstract String describe(); + private String diff(Map a, Map b){ + // TODO: replace by better implementation + return format("{0}\n→\n{1}",dropMarkdown(a),dropMarkdown(b)); + } + + private Map dropMarkdown(Map map) { + var result = new HashMap(); + for (var entry : map.entrySet()){ + var v = entry.getValue(); + if (v instanceof Map m && m.containsKey(RENDERED) && m.get(SOURCE) instanceof String s) v=s; + result.put(entry.getKey(),v); + } + return result; + } + + public Optional diff(){ + return oldData == null ? empty() : of(diff(oldData,payload.toMap())); + } + + public String eventType(){ return eventType.toString(); } 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 a768a52..c567480 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 @@ -5,6 +5,7 @@ import static de.srsoftware.umbrella.core.Constants.PROJECT; import de.srsoftware.umbrella.core.model.Project; import de.srsoftware.umbrella.core.model.UmbrellaUser; +import java.util.Map; public class ProjectEvent extends Event{ @@ -12,9 +13,13 @@ public class ProjectEvent extends Event{ super(initiator, PROJECT, project, type); } + public ProjectEvent(UmbrellaUser initiator, Project project, Map oldData){ + super(initiator, PROJECT, project, oldData); + } + @Override public String describe() { - return "[TODO: ProjectEvent.describe]"; + return diff().orElse("[TODO: ProjectEvent.describe]"); } @Override 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 5a2090d..b2b63d1 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 @@ -5,6 +5,7 @@ import static de.srsoftware.umbrella.core.Constants.TASK; import de.srsoftware.umbrella.core.model.Task; import de.srsoftware.umbrella.core.model.UmbrellaUser; +import java.util.Map; public class TaskEvent extends Event{ @@ -12,9 +13,13 @@ public class TaskEvent extends Event{ super(initiator, TASK, task, type); } + public TaskEvent(UmbrellaUser initiator, Task task, Map oldData){ + super(initiator, TASK, task, oldData); + } + @Override public String describe() { - return "[TODO: TaskEvent.describe()]"; + return diff().orElse("[TODO: TaskEvent.describe()]"); } @Override diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Task.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Task.java index 0e77071..7fa25ee 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/Task.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Task.java @@ -27,7 +27,7 @@ public class Task implements Mappable { private boolean noIndex, showClosed; private final Map members; private final Set dirtyFields = new HashSet<>(); - + private final Set tags = new HashSet<>(); public Task (long id, long projectId, Long parentTaskId, String name, String description, int status, Double estimatedTime, LocalDate start, LocalDate dueDate, boolean showClosed, boolean noIndex, Map members, int priority){ this.id = id; @@ -218,6 +218,16 @@ public class Task implements Mappable { return status; } + public Task tags(Collection newValue){ + tags.clear(); + tags.addAll(newValue); + return this; + } + + public Set tags(){ + return tags; + } + @Override public Map toMap() { var map = new HashMap(); @@ -240,7 +250,7 @@ public class Task implements Mappable { map.put(REQUIRED_TASKS_IDS,requiredTasksIds); map.put(SHOW_CLOSED,showClosed); map.put(TOTAL_PRIO,totalPrio()); - + map.put(TAGS,tags); return map; } diff --git a/journal/src/main/java/de/srsoftware/umbrella/journal/JournalDb.java b/journal/src/main/java/de/srsoftware/umbrella/journal/JournalDb.java index 14c443c..69a08f0 100644 --- a/journal/src/main/java/de/srsoftware/umbrella/journal/JournalDb.java +++ b/journal/src/main/java/de/srsoftware/umbrella/journal/JournalDb.java @@ -1,3 +1,4 @@ +/* © SRSoftware 2025 */ package de.srsoftware.umbrella.journal; import de.srsoftware.umbrella.messagebus.events.Event; diff --git a/journal/src/main/java/de/srsoftware/umbrella/journal/JournalModule.java b/journal/src/main/java/de/srsoftware/umbrella/journal/JournalModule.java index 066312f..39ab069 100644 --- a/journal/src/main/java/de/srsoftware/umbrella/journal/JournalModule.java +++ b/journal/src/main/java/de/srsoftware/umbrella/journal/JournalModule.java @@ -1,18 +1,17 @@ +/* © SRSoftware 2025 */ package de.srsoftware.umbrella.journal; -import de.srsoftware.configuration.Configuration; -import de.srsoftware.umbrella.core.BaseHandler; -import de.srsoftware.umbrella.core.ModuleRegistry; -import de.srsoftware.umbrella.core.model.Message; -import de.srsoftware.umbrella.messagebus.EventListener; -import de.srsoftware.umbrella.messagebus.MessageBus; -import de.srsoftware.umbrella.messagebus.events.Event; - import static de.srsoftware.umbrella.core.ConnectionProvider.connect; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingFieldException; import static de.srsoftware.umbrella.journal.Constants.CONFIG_DATABASE; import static de.srsoftware.umbrella.messagebus.MessageBus.messageBus; -import static java.lang.System.Logger.Level.INFO; +import static java.lang.System.Logger.Level.DEBUG; + +import de.srsoftware.configuration.Configuration; +import de.srsoftware.umbrella.core.BaseHandler; +import de.srsoftware.umbrella.core.ModuleRegistry; +import de.srsoftware.umbrella.messagebus.EventListener; +import de.srsoftware.umbrella.messagebus.events.Event; public class JournalModule extends BaseHandler implements EventListener { diff --git a/journal/src/main/java/de/srsoftware/umbrella/journal/SqliteDb.java b/journal/src/main/java/de/srsoftware/umbrella/journal/SqliteDb.java index 1ca82ae..7c27596 100644 --- a/journal/src/main/java/de/srsoftware/umbrella/journal/SqliteDb.java +++ b/journal/src/main/java/de/srsoftware/umbrella/journal/SqliteDb.java @@ -1,13 +1,6 @@ +/* © SRSoftware 2025 */ package de.srsoftware.umbrella.journal; -import de.srsoftware.tools.jdbc.Query; -import de.srsoftware.umbrella.core.BaseDb; -import de.srsoftware.umbrella.core.exceptions.UmbrellaException; -import de.srsoftware.umbrella.messagebus.events.Event; - -import java.sql.Connection; -import java.sql.SQLException; - import static de.srsoftware.tools.jdbc.Query.insertInto; import static de.srsoftware.umbrella.core.Constants.*; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.databaseException; @@ -15,6 +8,11 @@ import static de.srsoftware.umbrella.journal.Constants.ERROR_WRITE_EVENT; import static de.srsoftware.umbrella.journal.Constants.TABLE_JOURNAL; import static java.text.MessageFormat.format; +import de.srsoftware.umbrella.core.BaseDb; +import de.srsoftware.umbrella.messagebus.events.Event; +import java.sql.Connection; +import java.sql.SQLException; + public class SqliteDb extends BaseDb implements JournalDb{ public SqliteDb(Connection connection) { super(connection); @@ -33,8 +31,8 @@ public class SqliteDb extends BaseDb implements JournalDb{ private void createJournalTable() { var sql = """ CREATE TABLE IF NOT EXISTS {0} ( - {1} INT PRIMARY KEY, - {2} INT, + {1} INTEGER PRIMARY KEY, + {2} INTEGER, {3} VARCHAR(255) NOT NULL, {4} VARCHAR(16) NOT NULL, {5} TEXT diff --git a/project/src/main/java/de/srsoftware/umbrella/project/ProjectModule.java b/project/src/main/java/de/srsoftware/umbrella/project/ProjectModule.java index 31de459..43b7e5e 100644 --- a/project/src/main/java/de/srsoftware/umbrella/project/ProjectModule.java +++ b/project/src/main/java/de/srsoftware/umbrella/project/ProjectModule.java @@ -14,7 +14,6 @@ import static de.srsoftware.umbrella.core.model.Status.OPEN; import static de.srsoftware.umbrella.core.model.Status.PREDEFINED; 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 de.srsoftware.umbrella.project.Constants.CONFIG_DATABASE; import static java.lang.Boolean.TRUE; import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; @@ -210,13 +209,14 @@ public class ProjectModule extends BaseHandler implements ProjectService { private boolean patchProject(HttpExchange ex, long projectId, UmbrellaUser user) throws IOException, UmbrellaException { var project = loadMembers(projectDb.load(projectId)); if (!project.hasMember(user)) throw forbidden("You are not a member of {0}",project.name()); + var old = project.toMap(); var json = json(ex); if (json.has(DROP_MEMBER) && json.get(DROP_MEMBER) instanceof Number id) dropMember(project,id.longValue()); if (json.has(MEMBERS) && json.get(MEMBERS) instanceof JSONObject memberJson) patchMembers(project,memberJson); if (json.has(NEW_MEMBER) && json.get(NEW_MEMBER) instanceof Number num) addMember(project,num.longValue()); project = projectDb.save(project.patch(json), user); - messageBus().dispatch(new ProjectEvent(user,project, UPDATE)); + messageBus().dispatch(new ProjectEvent(user,project, old)); return sendContent(ex,project.toMap()); } diff --git a/task/src/main/java/de/srsoftware/umbrella/task/TaskModule.java b/task/src/main/java/de/srsoftware/umbrella/task/TaskModule.java index 37d4994..39f4a8d 100644 --- a/task/src/main/java/de/srsoftware/umbrella/task/TaskModule.java +++ b/task/src/main/java/de/srsoftware/umbrella/task/TaskModule.java @@ -13,7 +13,6 @@ import static de.srsoftware.umbrella.core.model.Permission.*; import static de.srsoftware.umbrella.core.model.Permission.OWNER; 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 de.srsoftware.umbrella.project.Constants.PERMISSIONS; import static de.srsoftware.umbrella.task.Constants.*; import static java.lang.System.Logger.Level.WARNING; @@ -42,22 +41,6 @@ import org.json.JSONObject; public class TaskModule extends BaseHandler implements TaskService { - private static class TaggedTask extends Task{ - private final Collection tags; - - public TaggedTask(Task task, Collection tags) { - super(task.id(), task.projectId(), task.parentTaskId(), task.name(), task.description(), task.status(), task.estimatedTime(), task.start(), task.dueDate(), task.showClosed(), task.noIndex(), task.members(), task.priority()); - this.tags = tags; - } - - @Override - public Map toMap() { - var map = super.toMap(); - map.put(TAGS,tags); - return map; - } - } - private final TaskDb taskDb; public TaskModule(Configuration config) throws UmbrellaException { @@ -343,10 +326,8 @@ public class TaskModule extends BaseHandler implements TaskService { if (json.has(MEMBERS) && json.get(MEMBERS) instanceof JSONObject memberJson) patchMembers(task, memberJson); if (json.has(NEW_MEMBER) && json.get(NEW_MEMBER) instanceof Number num) addMember(task, num.longValue()); if (json.has(PARENT_TASK_ID) && json.get(PARENT_TASK_ID) instanceof Number ptid && newParentIsSubtask(task, ptid.longValue())) throw forbidden("Task must not be sub-task of itself."); - var curr = taskDb.save(task.patch(json)).toMap(); - var diff = diff(old,curr); - var tagList = tagService().getTags(TASK, taskId, user); - messageBus().dispatch(new TaskEvent(user,new TaggedTask(task,tagList), UPDATE)); + task = taskDb.save(task.patch(json)).tags(tagService().getTags(TASK, taskId, user)); + messageBus().dispatch(new TaskEvent(user, task, old)); return sendContent(ex, task); } @@ -407,8 +388,8 @@ public class TaskModule extends BaseHandler implements TaskService { if ((tagList == null || tagList.isEmpty())) tagList = tagService().getTags(PROJECT, projectId, user); if (tagList != null && !tagList.isEmpty()) tagService().save(TASK, task.id(), null, tagList); task = loadMembers(task); - - messageBus().dispatch(new TaskEvent(user,new TaggedTask(task,tagList), CREATE)); + task.tags(tagList); + messageBus().dispatch(new TaskEvent(user, task, CREATE)); return sendContent(ex, task); } @@ -449,6 +430,6 @@ public class TaskModule extends BaseHandler implements TaskService { } private Map addTags(Map taskList, Map> tags) { - return taskList.values().stream().map(task -> new TaggedTask(task, tags.get(task.id()))).collect(Collectors.toMap(Task::id, t -> t)); + return taskList.values().stream().map(task -> task.tags(tags.get(task.id()))).collect(Collectors.toMap(Task::id, t -> t)); } } From 9d35952949277e3ff514ba871448218763d21b1c Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Sat, 10 Jan 2026 22:19:29 +0100 Subject: [PATCH 5/5] implemented event-based page-refresh for wiki pages --- .../umbrella/messagebus/events/WikiEvent.java | 35 +++++++++++++++++++ .../srsoftware/umbrella/core/Constants.java | 1 + frontend/src/routes/wiki/View.svelte | 27 ++++++++++---- wiki/build.gradle.kts | 1 + .../srsoftware/umbrella/wiki/WikiModule.java | 8 ++++- 5 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 bus/src/main/java/de/srsoftware/umbrella/messagebus/events/WikiEvent.java 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 new file mode 100644 index 0000000..20d7adb --- /dev/null +++ b/bus/src/main/java/de/srsoftware/umbrella/messagebus/events/WikiEvent.java @@ -0,0 +1,35 @@ +/* © SRSoftware 2025 */ +package de.srsoftware.umbrella.messagebus.events; + +import de.srsoftware.umbrella.core.model.Task; +import de.srsoftware.umbrella.core.model.UmbrellaUser; +import de.srsoftware.umbrella.core.model.WikiPage; + +import java.util.Map; + +import static de.srsoftware.umbrella.core.Constants.TASK; +import static de.srsoftware.umbrella.core.Constants.WIKI; + + +public class WikiEvent extends Event{ + public WikiEvent(UmbrellaUser initiator, WikiPage page, EventType type){ + super(initiator, WIKI, page, type); + } + + public WikiEvent(UmbrellaUser initiator, WikiPage page, Map oldData){ + super(initiator, WIKI, page, oldData); + } + + @Override + public String describe() { + return diff().orElse("[TODO: WikiEvent.describe()]"); + } + + @Override + public boolean isIntendedFor(UmbrellaUser user) { + for (var member : payload().members().values()){ + if (member.user().equals(user)) return true; + } + return false; + } +} diff --git a/core/src/main/java/de/srsoftware/umbrella/core/Constants.java b/core/src/main/java/de/srsoftware/umbrella/core/Constants.java index 6804d29..1ba584e 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/Constants.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/Constants.java @@ -160,4 +160,5 @@ public class Constants { public static final String VERSION = "version"; public static final String VERSIONS = "versions"; + public static final String WIKI = "wiki"; } diff --git a/frontend/src/routes/wiki/View.svelte b/frontend/src/routes/wiki/View.svelte index 56dbeee..09c51fe 100644 --- a/frontend/src/routes/wiki/View.svelte +++ b/frontend/src/routes/wiki/View.svelte @@ -1,7 +1,7 @@ {#if page && page.versions}
diff --git a/wiki/build.gradle.kts b/wiki/build.gradle.kts index bc92fdf..fccd08a 100644 --- a/wiki/build.gradle.kts +++ b/wiki/build.gradle.kts @@ -1,5 +1,6 @@ description = "Umbrella : Wiki" dependencies{ + implementation(project(":bus")) implementation(project(":core")) } \ No newline at end of file diff --git a/wiki/src/main/java/de/srsoftware/umbrella/wiki/WikiModule.java b/wiki/src/main/java/de/srsoftware/umbrella/wiki/WikiModule.java index b0a6f5b..17217a1 100644 --- a/wiki/src/main/java/de/srsoftware/umbrella/wiki/WikiModule.java +++ b/wiki/src/main/java/de/srsoftware/umbrella/wiki/WikiModule.java @@ -11,6 +11,7 @@ import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*; import static de.srsoftware.umbrella.core.model.Permission.EDIT; import static de.srsoftware.umbrella.core.model.Permission.READ_ONLY; import static de.srsoftware.umbrella.wiki.Constants.*; +import static de.srsoftware.umbrella.messagebus.MessageBus.messageBus; import com.sun.net.httpserver.HttpExchange; import de.srsoftware.configuration.Configuration; @@ -20,6 +21,8 @@ import de.srsoftware.umbrella.core.BaseHandler; import de.srsoftware.umbrella.core.api.WikiService; import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.model.*; +import de.srsoftware.umbrella.messagebus.events.WikiEvent; + import java.io.IOException; import java.util.Arrays; import java.util.Optional; @@ -190,10 +193,13 @@ public class WikiModule extends BaseHandler implements WikiService { private boolean patchPage(Path path, UmbrellaUser user, HttpExchange ex) throws IOException { var id = path.pop(); var page = loadPage(id, null); + var old = page.toMap(); var member = page.members().get(user.id()); if (member == null || member.permission() != EDIT) throw forbidden("You are not allowed to edit {0}!",id); var json = json(ex); - return sendContent(ex,wikiDb.save(page.patch(json, userService()))); + page = wikiDb.save(page.patch(json, userService())); + messageBus().dispatch(new WikiEvent(user,page,old)); + return sendContent(ex,page); } private boolean postNewPage(String title, UmbrellaUser user, HttpExchange ex) throws IOException {