From 6a6b96b112fd5b9f49a8605d5a72a6db3eb34032 Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Sun, 29 Mar 2026 15:24:43 +0200 Subject: [PATCH 01/36] Refactored Modules --- .../de/igslandstuhl/database/Application.java | 2 +- .../de/igslandstuhl/database/Registry.java | 2 +- .../database/{api => }/modules/WebModule.java | 50 ++++-------- .../config}/BoolSetting.java | 2 +- .../database/modules/config/ModuleConfig.java | 78 +++++++++++++++++++ .../config}/ModuleSetting.java | 4 +- .../handlers/PostRequestHandler.java | 2 +- 7 files changed, 99 insertions(+), 41 deletions(-) rename src/main/java/de/igslandstuhl/database/{api => }/modules/WebModule.java (72%) rename src/main/java/de/igslandstuhl/database/{api/modules => modules/config}/BoolSetting.java (94%) create mode 100644 src/main/java/de/igslandstuhl/database/modules/config/ModuleConfig.java rename src/main/java/de/igslandstuhl/database/{api/modules => modules/config}/ModuleSetting.java (89%) diff --git a/src/main/java/de/igslandstuhl/database/Application.java b/src/main/java/de/igslandstuhl/database/Application.java index 58a25c8..35a04d7 100644 --- a/src/main/java/de/igslandstuhl/database/Application.java +++ b/src/main/java/de/igslandstuhl/database/Application.java @@ -7,8 +7,8 @@ import de.igslandstuhl.database.api.SerializationException; import de.igslandstuhl.database.api.Subject; import de.igslandstuhl.database.api.Topic; -import de.igslandstuhl.database.api.modules.WebModule; import de.igslandstuhl.database.holidays.Holiday; +import de.igslandstuhl.database.modules.WebModule; import de.igslandstuhl.database.server.Server; import de.igslandstuhl.database.server.commands.Command; import de.igslandstuhl.database.server.webserver.WebPath; diff --git a/src/main/java/de/igslandstuhl/database/Registry.java b/src/main/java/de/igslandstuhl/database/Registry.java index de0e8f9..642fc3b 100644 --- a/src/main/java/de/igslandstuhl/database/Registry.java +++ b/src/main/java/de/igslandstuhl/database/Registry.java @@ -5,7 +5,7 @@ import java.util.Map; import java.util.stream.Stream; -import de.igslandstuhl.database.api.modules.WebModule; +import de.igslandstuhl.database.modules.WebModule; import de.igslandstuhl.database.server.commands.Command; import de.igslandstuhl.database.server.commands.CommandDescription; import de.igslandstuhl.database.server.webserver.WebPath; diff --git a/src/main/java/de/igslandstuhl/database/api/modules/WebModule.java b/src/main/java/de/igslandstuhl/database/modules/WebModule.java similarity index 72% rename from src/main/java/de/igslandstuhl/database/api/modules/WebModule.java rename to src/main/java/de/igslandstuhl/database/modules/WebModule.java index acf5492..621cff5 100644 --- a/src/main/java/de/igslandstuhl/database/api/modules/WebModule.java +++ b/src/main/java/de/igslandstuhl/database/modules/WebModule.java @@ -1,9 +1,11 @@ -package de.igslandstuhl.database.api.modules; +package de.igslandstuhl.database.modules; -import java.util.LinkedList; import java.util.List; import de.igslandstuhl.database.Registry; +import de.igslandstuhl.database.modules.config.BoolSetting; +import de.igslandstuhl.database.modules.config.ModuleConfig; +import de.igslandstuhl.database.modules.config.ModuleSetting; public abstract class WebModule { private String id; @@ -12,8 +14,6 @@ public abstract class WebModule { private boolean enabled; - private List> settings = new LinkedList<>(); - public WebModule(String id, String name, String description) { this.id = id; this.name = name; @@ -41,36 +41,12 @@ public String toJSON() { sb.append("\"name\":\"").append(name).append("\","); sb.append("\"description\":\"").append(description).append("\","); sb.append("\"enabled\":").append(enabled).append(","); - sb.append("\"settings\":{"); - for (int i = 0; i < settings.size(); i++) { - sb.append("\"").append(settings.get(i).getKey()).append("\":").append(settings.get(i).toJSON()); - if (i < settings.size() - 1) { - sb.append(","); - } - } - sb.append("}"); + sb.append("\"config\":").append(getConfig().toJSON()).append(","); sb.append("}"); return sb.toString(); } - public List> getSettings() { - return settings; - } - public ModuleSetting getSettingByKey(String key) { - for (ModuleSetting setting : settings) { - if (setting.getKey().equals(key)) { - return setting; - } - } - return null; - } - public BoolSetting getBoolSetting(String key) { - ModuleSetting setting = getSettingByKey(key); - if (setting instanceof BoolSetting) { - return (BoolSetting) setting; - } - return null; - } + public abstract ModuleConfig getConfig(); protected abstract void onEnable(); protected abstract void onDisable(); @@ -97,14 +73,18 @@ public void load() { onLoad(); } - public void toggleSetting(String key) { - getBoolSetting(key).toggle(); - } - private static class DummyModule extends WebModule { + private final ModuleConfig config; public DummyModule(String id, String name, String description, List> settings) { super(id, name, description); - this.getSettings().addAll(settings); + config = new ModuleConfig(this, settings) { + + }; + } + + @Override + public ModuleConfig getConfig() { + return config; } @Override diff --git a/src/main/java/de/igslandstuhl/database/api/modules/BoolSetting.java b/src/main/java/de/igslandstuhl/database/modules/config/BoolSetting.java similarity index 94% rename from src/main/java/de/igslandstuhl/database/api/modules/BoolSetting.java rename to src/main/java/de/igslandstuhl/database/modules/config/BoolSetting.java index 556ec3e..1b32004 100644 --- a/src/main/java/de/igslandstuhl/database/api/modules/BoolSetting.java +++ b/src/main/java/de/igslandstuhl/database/modules/config/BoolSetting.java @@ -1,4 +1,4 @@ -package de.igslandstuhl.database.api.modules; +package de.igslandstuhl.database.modules.config; public class BoolSetting extends ModuleSetting { public BoolSetting(String key, String name, String description, boolean defaultValue) { diff --git a/src/main/java/de/igslandstuhl/database/modules/config/ModuleConfig.java b/src/main/java/de/igslandstuhl/database/modules/config/ModuleConfig.java new file mode 100644 index 0000000..f152e2c --- /dev/null +++ b/src/main/java/de/igslandstuhl/database/modules/config/ModuleConfig.java @@ -0,0 +1,78 @@ +package de.igslandstuhl.database.modules.config; + +import java.util.Arrays; +import java.util.List; + +import de.igslandstuhl.database.modules.WebModule; + +public abstract class ModuleConfig { + private final T module; + + private final BoolSetting[] boolSettings; + + public ModuleConfig(T module, ModuleSetting[] moduleSettings) { + this.module = module; + List boolSettings = Arrays.stream(moduleSettings).filter((s) -> s instanceof BoolSetting).map((s) -> (BoolSetting) s).toList(); + this.boolSettings = boolSettings.toArray(new BoolSetting[boolSettings.size()]); + } + + public ModuleConfig(T module, List> moduleSettings) { + this.module = module; + List boolSettings = moduleSettings.stream().filter((s) -> s instanceof BoolSetting).map((s) -> (BoolSetting) s).toList(); + this.boolSettings = boolSettings.toArray(new BoolSetting[boolSettings.size()]); + } + + public T getModule() { + return module; + } + + private BoolSetting findBoolSetting(String key) { + return Arrays.stream(boolSettings) + .filter((s) -> s.getKey().equals(key)) + .findAny().orElseThrow(); + } + + public boolean getBool(String key) { + return findBoolSetting(key).getValue(); + } + + public void setBool(String key, boolean value) { + findBoolSetting(key).setValue(value); + } + public void enableSetting(String key) { + findBoolSetting(key).enable(); + } + public void disableSetting(String key) { + findBoolSetting(key).disable(); + } + public void toggleSetting(String key) { + findBoolSetting(key).toggle(); + } + + public String toJSON() { + StringBuilder builder = new StringBuilder("{"); + builder.append("'settings': {") + .append("'bools': ["); + for (int i = 0; i < boolSettings.length; i++) { + builder.append(boolSettings[i].toJSON()); + if (i < boolSettings.length - 1) { + builder.append(", "); + } + } + builder + .append("]}, ") + .append("'values': {"); + for (int i = 0; i < boolSettings.length; i++) { + builder + .append("'") + .append(boolSettings[i].getKey()) + .append("': ") + .append(boolSettings[i].getValue()); + if (i < boolSettings.length - 1) { + builder.append(", "); + } + } + builder.append("}}"); + return builder.toString(); + } +} diff --git a/src/main/java/de/igslandstuhl/database/api/modules/ModuleSetting.java b/src/main/java/de/igslandstuhl/database/modules/config/ModuleSetting.java similarity index 89% rename from src/main/java/de/igslandstuhl/database/api/modules/ModuleSetting.java rename to src/main/java/de/igslandstuhl/database/modules/config/ModuleSetting.java index 59df0d9..0fffab5 100644 --- a/src/main/java/de/igslandstuhl/database/api/modules/ModuleSetting.java +++ b/src/main/java/de/igslandstuhl/database/modules/config/ModuleSetting.java @@ -1,4 +1,4 @@ -package de.igslandstuhl.database.api.modules; +package de.igslandstuhl.database.modules.config; public class ModuleSetting { private final String key; @@ -7,7 +7,7 @@ public class ModuleSetting { private final T defaultValue; private T value; - public ModuleSetting(String key, String name, String description, T defaultValue) { + ModuleSetting(String key, String name, String description, T defaultValue) { this.key = key; this.name = name; this.description = description; diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/handlers/PostRequestHandler.java b/src/main/java/de/igslandstuhl/database/server/webserver/handlers/PostRequestHandler.java index 47d5d65..d145ffe 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/handlers/PostRequestHandler.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/handlers/PostRequestHandler.java @@ -417,7 +417,7 @@ public static void registerHandlers() { }); HttpHandler.registerPostRequestHandler("/toggle-module-setting", AccessLevel.ADMIN, (rq) -> { String[] key = rq.getString("key").split(":"); - Registry.moduleRegistry().get(key[0]).toggleSetting(key[1]); + Registry.moduleRegistry().get(key[0]).getConfig().toggleSetting(key[1]); return PostResponse.ok("Module setting toggled", ContentType.TEXT_PLAIN, rq); }); From 8d5d01577b49d2e0c51e5a100d3b095d7b5c49d1 Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Mon, 30 Mar 2026 01:59:46 +0200 Subject: [PATCH 02/36] Now using new config format in frontend --- src/main/resources/js/site/student-database.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/resources/js/site/student-database.js b/src/main/resources/js/site/student-database.js index 2087fc6..a989539 100644 --- a/src/main/resources/js/site/student-database.js +++ b/src/main/resources/js/site/student-database.js @@ -92,8 +92,8 @@ async function fetchTopicList(subjectId, grade) { async function fetchTasks(taskIds, studentId) { return await getJsonWithPost('/tasks', { ids: taskIds, studentId }); } -async function fetchModuleSettings(moduleKey) { - return (await getJsonWithPost('/get-module', { key: moduleKey })).settings; +async function fetchModuleConfig(moduleKey) { + return (await getJsonWithPost('/get-module', { key: moduleKey })).config; } async function getStudents(classId) { @@ -587,10 +587,10 @@ function createBarChart(subject, subjectName, studentData, settings) { const predicted = studentData.predictedProgress && studentData.predictedProgress[subjectName] ? studentData.predictedProgress[subjectName].predictedProgress : 0; - const grade = settings.show_current_grade.value ? getGrade(progress) : 0; + const grade = settings.show_current_grade ? getGrade(progress) : 0; const predictedGrade = getGrade(predicted); - if (!settings || settings.show_current_progress.value) { + if (!settings || settings.show_current_progress) { const gradeInfo = document.createElement('div'); gradeInfo.className = 'grade-current'; gradeInfo.innerHTML = `Aktueller Fortschritt: ${getGradeLabel(grade)} (${Math.round(progress * 100)}%)`; @@ -801,7 +801,7 @@ function loadStudentDashboard(studentData, subjects, teacherPerms) { // Show stu }); } async function loadStudentResultView(studentData) { - const settings = await fetchModuleSettings('result_view'); + const config = await fetchModuleConfig('result_view'); document.getElementById('student-name').textContent = `${studentData.firstName} ${studentData.lastName}`; @@ -813,7 +813,7 @@ async function loadStudentResultView(studentData) { const charts = document.getElementById('charts'); subjects.forEach(subject => { - charts.appendChild(createBarChart(subject, subject.name, studentData, settings)); + charts.appendChild(createBarChart(subject, subject.name, studentData, config.values)); }); } const graduationLevels = ["Neustarter", "Starter", "Durchstarter", "Lernprofi"]; From 90ca8aa58874b52413a00cf2626c7405493253f2 Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Mon, 30 Mar 2026 12:51:37 +0200 Subject: [PATCH 03/36] New AccessManager approach: using WebPath registry for access managing --- .../server/webserver/AccessManager.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/AccessManager.java b/src/main/java/de/igslandstuhl/database/server/webserver/AccessManager.java index cd92e5a..f287017 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/AccessManager.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/AccessManager.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.Map; +import de.igslandstuhl.database.Registry; import de.igslandstuhl.database.api.User; import de.igslandstuhl.database.server.Server; import de.igslandstuhl.database.server.resources.ResourceLocation; @@ -107,10 +108,21 @@ private AccessManager() { * @param user the username of the user, or null if not authenticated * @param resource the ResourceLocation representing the resource to check access for * @return true if the user has access to the resource, false otherwise + * @deprecated Use hasAccess(String user, String path) */ + @Deprecated public boolean hasAccess(String user, ResourceLocation resource) { return hasAccess(User.getUser(user), resource); } + /** + * Checks if a user has access to a specific resource. + * + * @param user the user, or null if not authenticated + * @param resource the ResourceLocation representing the resource to check access for + * @return true if the user has access to the resource, false otherwise + * @deprecated Use hasAccess(User user, String path) + */ + @Deprecated public boolean hasAccess(User user, ResourceLocation resource) { if (Arrays.asList(PUBLIC_SPACES).contains(resource.namespace()) || Arrays.asList(PUBLIC_LOCATIONS).contains(resource.resource())) { return true; @@ -128,4 +140,48 @@ public boolean hasAccess(User user, ResourceLocation resource) { return false; } } + /** + * Checks if a user has access to a specific access level + * @param user the user, can be null to indicate no user logged in + * @param accessLevel the access level + * @return true, if the user has access, otherwise false + */ + public boolean hasAccess(User user, AccessLevel accessLevel) { + if (accessLevel == AccessLevel.PUBLIC) { + return true; + } else if (user == null || user == User.ANONYMOUS) { + return false; + } else if (accessLevel == AccessLevel.NONE) { + return false; + } else if (accessLevel == AccessLevel.USER) { + return true; + } else if (accessLevel == AccessLevel.STUDENT) { + return user.isStudent(); + } else if (user.isStudent()) { + return false; + } else if (accessLevel == AccessLevel.TEACHER) { + return true; + } else { + return user.isAdmin(); // Must be AccessLevel.ADMIN + } + } + /** + * Checks if a user has access to a specific web path + * @param user the username of the user, can be null to indicate no user logged in + * @param path the web path + * @return true, if the user has access, otherwise false + */ + public boolean hasAccess(String user, String path) { + return hasAccess(User.getUser(user), path); + } + /** + * Checks if a user has access to a specific web path + * @param user user, can be null to indicate no user logged in + * @param path the web path + * @return true, if the user has access, otherwise false + */ + public boolean hasAccess(User user, String path) { + AccessLevel accessLevel = Registry.webPathRegistry().get(path).accessLevel(); + return hasAccess(user, accessLevel); + } } From 681e32ca552160dcb68dd570c646942319e7aa12 Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Mon, 30 Mar 2026 13:05:56 +0200 Subject: [PATCH 04/36] Added ModuleRequestHandler new type of virtual resource: module resource handled by ModuleRequestHandler GetResponse using sql resources and module resources new get_path: /module-list GetResponse now using new AccessManager approach --- .../webserver/handlers/GetRequestHandler.java | 17 +++++++++--- .../handlers/get/ModuleRequestHandler.java | 27 +++++++++++++++++++ .../webserver/responses/GetResponse.java | 11 +++++--- src/main/resources/meta/paths/get_paths.json | 7 +++++ 4 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 src/main/java/de/igslandstuhl/database/server/webserver/handlers/get/ModuleRequestHandler.java diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/handlers/GetRequestHandler.java b/src/main/java/de/igslandstuhl/database/server/webserver/handlers/GetRequestHandler.java index f85f759..5f06802 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/handlers/GetRequestHandler.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/handlers/GetRequestHandler.java @@ -3,8 +3,10 @@ import java.util.List; import de.igslandstuhl.database.Registry; +import de.igslandstuhl.database.api.User; import de.igslandstuhl.database.server.Server; import de.igslandstuhl.database.server.webserver.WebPath; +import de.igslandstuhl.database.server.webserver.handlers.get.ModuleRequestHandler; import de.igslandstuhl.database.server.webserver.requests.GetRequest; import de.igslandstuhl.database.server.webserver.requests.RequestType; import de.igslandstuhl.database.server.webserver.responses.GetResponse; @@ -35,19 +37,27 @@ public final HttpResponse handleRequest(GetRequest request) { HttpHandler handler = Registry.getRequestHandlerRegistry().get(path); return handler.handleHttpRequest(request); } + + private static User getUser(GetRequest request) { + return Server.getInstance().getWebServer().getSessionManager().getSessionUser(request); + } public static GetResponse handleFileRequest(GetRequest request) { - String user = Server.getInstance().getWebServer().getSessionManager().getSessionUser(request).getUsername(); + String user = getUser(request).getUsername(); return GetResponse.getResource(request, request.toResourceLocation(user), user, false); } public static GetResponse handleTemplatingFileRequest(GetRequest request) { - String user = Server.getInstance().getWebServer().getSessionManager().getSessionUser(request).getUsername(); + String user = getUser(request).getUsername(); return GetResponse.getResource(request, request.toResourceLocation(user), user, true); } public static GetResponse handleSQLRequest(GetRequest request) { - String user = Server.getInstance().getWebServer().getSessionManager().getSessionUser(request).getUsername(); + String user = getUser(request).getUsername(); return GetResponse.getResource(request, request.toResourceLocation(user), user, false); } + public static GetResponse handleModuleRequest(GetRequest request) { + User user = getUser(request); + return ModuleRequestHandler.handleRequest(user, request); + } public final void registerHandlers() { if (Registry.getRequestHandlerRegistry().stream().count() > 0) return; // already registered @@ -58,6 +68,7 @@ public final void registerHandlers() { case "FileRequestHandler" -> GetRequestHandler::handleFileRequest; case "TemplatingFileRequestHandler" -> GetRequestHandler::handleTemplatingFileRequest; case "SQLRequestHandler" -> GetRequestHandler::handleSQLRequest; + case "ModuleRequestHandler" -> GetRequestHandler::handleModuleRequest; default -> throw new IllegalArgumentException("Unknown handler type: " + webPath.handlerType()); }; HttpHandler.registerGetRequestHandler(path, webPath.accessLevel(), handlerFunction); diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/handlers/get/ModuleRequestHandler.java b/src/main/java/de/igslandstuhl/database/server/webserver/handlers/get/ModuleRequestHandler.java new file mode 100644 index 0000000..046ac42 --- /dev/null +++ b/src/main/java/de/igslandstuhl/database/server/webserver/handlers/get/ModuleRequestHandler.java @@ -0,0 +1,27 @@ +package de.igslandstuhl.database.server.webserver.handlers.get; + +import de.igslandstuhl.database.Registry; +import de.igslandstuhl.database.api.User; +import de.igslandstuhl.database.server.resources.ResourceLocation; +import de.igslandstuhl.database.server.webserver.requests.GetRequest; +import de.igslandstuhl.database.server.webserver.responses.GetResponse; + +public class ModuleRequestHandler { + public static GetResponse handleRequest(User user, GetRequest request) { + if (user == null || !user.isAdmin()) return GetResponse.unauthorized(request); + if (!request.getPath().equals("/module-list")) return GetResponse.notFound(request); + + return GetResponse.getResource(request, new ResourceLocation("virtual", "module", "list"), user.getUsername(), false); + } + public static String getModuleResource(String resource) { + if (resource.equals("list")) { + return "[" + + Registry.moduleRegistry().keyStream() + .reduce("", (s1, s2) -> s1 + ", '" + s2 + "'") + .substring(2) + + "]"; + } else { + throw new NullPointerException("Module Resource not found: " + resource); + } + } +} diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/responses/GetResponse.java b/src/main/java/de/igslandstuhl/database/server/webserver/responses/GetResponse.java index 1262092..29cf9cd 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/responses/GetResponse.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/responses/GetResponse.java @@ -10,6 +10,7 @@ import de.igslandstuhl.database.server.webserver.ContentType; import de.igslandstuhl.database.server.webserver.NoWebResourceException; import de.igslandstuhl.database.server.webserver.Status; +import de.igslandstuhl.database.server.webserver.handlers.get.ModuleRequestHandler; import de.igslandstuhl.database.server.webserver.requests.HttpRequest; /** @@ -139,7 +140,7 @@ public GetResponse(HttpRequest request, Status status, ResourceLocation resource */ public static GetResponse getResource(HttpRequest request, ResourceLocation resourceLocation, String user, boolean isTemplating) { try { - if (AccessManager.getInstance().hasAccess(user, resourceLocation)) { + if (AccessManager.getInstance().hasAccess(user, request.getPath())) { return new GetResponse(request, Status.OK, resourceLocation, ContentType.ofResourceLocation(resourceLocation), user, isTemplating); } else { return unauthorized(request); @@ -172,8 +173,12 @@ public void respond(PrintStream out) { if (!resourceLocation.isVirtual()) { resource = Server.getInstance().getResourceManager().readResourceCompletely(resourceLocation); } else { - resource = Server.getInstance().getResourceManager().readVirtualResource(user, resourceLocation); - if (resource == null) throw new NullPointerException(); + if (resourceLocation.namespace().equals("module")) { + resource = ModuleRequestHandler.getModuleResource(resourceLocation.resource()); + } else { + resource = Server.getInstance().getResourceManager().readVirtualResource(user, resourceLocation); + } + if (resource == null) throw new NullPointerException(); } } if (isTemplating) { diff --git a/src/main/resources/meta/paths/get_paths.json b/src/main/resources/meta/paths/get_paths.json index fe64bda..877b697 100644 --- a/src/main/resources/meta/paths/get_paths.json +++ b/src/main/resources/meta/paths/get_paths.json @@ -330,5 +330,12 @@ "namespaces": ["icons"], "context": "imgs", "access_level": "public" + }, + "/module-list": { + "type": "GET", + "handler_type": "ModuleRequestHandler", + "namespaces": ["module"], + "context": "virtual", + "access_level": "admin" } } \ No newline at end of file From 32d30405b6f4490e2bb7756ff03ea8557ba8ca96 Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Mon, 30 Mar 2026 14:49:48 +0200 Subject: [PATCH 05/36] Fixed a few bugs --- .../de/igslandstuhl/database/modules/WebModule.java | 2 +- .../database/modules/config/ModuleConfig.java | 12 ++++++------ .../webserver/handlers/PostRequestHandler.java | 4 ++-- .../webserver/handlers/get/ModuleRequestHandler.java | 2 +- .../server/webserver/responses/GetResponse.java | 5 ++++- .../server/webserver/responses/PostResponse.java | 6 +++--- 6 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/main/java/de/igslandstuhl/database/modules/WebModule.java b/src/main/java/de/igslandstuhl/database/modules/WebModule.java index 621cff5..7e55b3a 100644 --- a/src/main/java/de/igslandstuhl/database/modules/WebModule.java +++ b/src/main/java/de/igslandstuhl/database/modules/WebModule.java @@ -41,7 +41,7 @@ public String toJSON() { sb.append("\"name\":\"").append(name).append("\","); sb.append("\"description\":\"").append(description).append("\","); sb.append("\"enabled\":").append(enabled).append(","); - sb.append("\"config\":").append(getConfig().toJSON()).append(","); + sb.append("\"config\":").append(getConfig().toJSON()); sb.append("}"); return sb.toString(); } diff --git a/src/main/java/de/igslandstuhl/database/modules/config/ModuleConfig.java b/src/main/java/de/igslandstuhl/database/modules/config/ModuleConfig.java index f152e2c..40cb198 100644 --- a/src/main/java/de/igslandstuhl/database/modules/config/ModuleConfig.java +++ b/src/main/java/de/igslandstuhl/database/modules/config/ModuleConfig.java @@ -51,8 +51,8 @@ public void toggleSetting(String key) { public String toJSON() { StringBuilder builder = new StringBuilder("{"); - builder.append("'settings': {") - .append("'bools': ["); + builder.append("\"settings\": {") + .append("\"bools\": ["); for (int i = 0; i < boolSettings.length; i++) { builder.append(boolSettings[i].toJSON()); if (i < boolSettings.length - 1) { @@ -61,12 +61,12 @@ public String toJSON() { } builder .append("]}, ") - .append("'values': {"); + .append("\"values\": {"); for (int i = 0; i < boolSettings.length; i++) { builder - .append("'") + .append("\"") .append(boolSettings[i].getKey()) - .append("': ") + .append("\": ") .append(boolSettings[i].getValue()); if (i < boolSettings.length - 1) { builder.append(", "); @@ -75,4 +75,4 @@ public String toJSON() { builder.append("}}"); return builder.toString(); } -} +} \ No newline at end of file diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/handlers/PostRequestHandler.java b/src/main/java/de/igslandstuhl/database/server/webserver/handlers/PostRequestHandler.java index d145ffe..5216f0f 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/handlers/PostRequestHandler.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/handlers/PostRequestHandler.java @@ -110,13 +110,13 @@ private static PostResponse handleStudentGetData(APIPostRequest request) { String path = request.getPath().replace("student-", "my"); Student student = request.getCurrentStudent(); String email = student.getEmail(); // Email is the username for the student - return PostResponse.getResource(WebResourceHandler.locationFromPath(path, student), email, request); + return PostResponse.getResource(WebResourceHandler.locationFromPath(path, student), email, request, path); } private static PostResponse handleTeacherGetData(APIPostRequest request) { String path = request.getPath().replace("teacher-", "my"); Teacher teacher = request.getCurrentTeacher(); String email = teacher.getEmail(); // Email is the username for the teacher - return PostResponse.getResource(WebResourceHandler.locationFromPath(path, User.getUser(email)), email, request); + return PostResponse.getResource(WebResourceHandler.locationFromPath(path, User.getUser(email)), email, request, path); } private static PostResponse handleTaskChange(APIPostRequest request, int newStatus) throws IOException, SQLException { Student student = request.getCurrentStudent(); diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/handlers/get/ModuleRequestHandler.java b/src/main/java/de/igslandstuhl/database/server/webserver/handlers/get/ModuleRequestHandler.java index 046ac42..a44feaa 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/handlers/get/ModuleRequestHandler.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/handlers/get/ModuleRequestHandler.java @@ -17,7 +17,7 @@ public static String getModuleResource(String resource) { if (resource.equals("list")) { return "[" + Registry.moduleRegistry().keyStream() - .reduce("", (s1, s2) -> s1 + ", '" + s2 + "'") + .reduce("", (s1, s2) -> s1 + ", \"" + s2 + "\"") .substring(2) + "]"; } else { diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/responses/GetResponse.java b/src/main/java/de/igslandstuhl/database/server/webserver/responses/GetResponse.java index 29cf9cd..6afd12f 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/responses/GetResponse.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/responses/GetResponse.java @@ -139,8 +139,11 @@ public GetResponse(HttpRequest request, Status status, ResourceLocation resource * @return the GetResponse object */ public static GetResponse getResource(HttpRequest request, ResourceLocation resourceLocation, String user, boolean isTemplating) { + return getResource(request, resourceLocation, user, isTemplating, request.getPath()); + } + public static GetResponse getResource(HttpRequest request, ResourceLocation resourceLocation, String user, boolean isTemplating, String path) { try { - if (AccessManager.getInstance().hasAccess(user, request.getPath())) { + if (AccessManager.getInstance().hasAccess(user, path)) { return new GetResponse(request, Status.OK, resourceLocation, ContentType.ofResourceLocation(resourceLocation), user, isTemplating); } else { return unauthorized(request); diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/responses/PostResponse.java b/src/main/java/de/igslandstuhl/database/server/webserver/responses/PostResponse.java index b572963..344de23 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/responses/PostResponse.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/responses/PostResponse.java @@ -153,10 +153,10 @@ public static PostResponse ok(String body, ContentType contentType, PostRequest * @param user the user who made the request * @return the PostResponse object */ - public static PostResponse getResource(ResourceLocation resourceLocation, String user, PostRequest request) { + public static PostResponse getResource(ResourceLocation resourceLocation, String user, PostRequest request, String path) { try { - if (AccessManager.getInstance().hasAccess(user, resourceLocation)) { - return new PostResponse(Status.OK, GetResponse.getResource(request, resourceLocation, user, false).getResponseBody(), ContentType.ofResourceLocation(resourceLocation), request); + if (AccessManager.getInstance().hasAccess(user, path)) { + return new PostResponse(Status.OK, GetResponse.getResource(request, resourceLocation, user, false, path).getResponseBody(), ContentType.ofResourceLocation(resourceLocation), request); } else { return unauthorized("You have to be logged in to access this resource.", request); } From fe8c056f8c404387fc376b921e1df90ac2bae768 Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Mon, 30 Mar 2026 14:50:15 +0200 Subject: [PATCH 06/36] Added some functionality to modules page --- src/main/resources/html/admin/modules.html | 4 ++ .../resources/js/site/student-database.js | 41 ++++++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/main/resources/html/admin/modules.html b/src/main/resources/html/admin/modules.html index 01f6d23..2b40437 100644 --- a/src/main/resources/html/admin/modules.html +++ b/src/main/resources/html/admin/modules.html @@ -4,4 +4,8 @@

Module

Willkommen in der Modulübersicht!

+
+ \ No newline at end of file diff --git a/src/main/resources/js/site/student-database.js b/src/main/resources/js/site/student-database.js index a989539..eb669b4 100644 --- a/src/main/resources/js/site/student-database.js +++ b/src/main/resources/js/site/student-database.js @@ -92,8 +92,14 @@ async function fetchTopicList(subjectId, grade) { async function fetchTasks(taskIds, studentId) { return await getJsonWithPost('/tasks', { ids: taskIds, studentId }); } +async function fetchModuleKeys() { + return await getJson('/module-list'); +} +async function fetchModule(moduleKey) { + return await getJsonWithPost('/get-module', { key: moduleKey }) +} async function fetchModuleConfig(moduleKey) { - return (await getJsonWithPost('/get-module', { key: moduleKey })).config; + return (await fetchModule(moduleKey)).config; } async function getStudents(classId) { @@ -156,6 +162,9 @@ async function beginTask(studentId, taskId) { async function updateRoom(studentId, room) { return await post('/update-room', { studentId, room }); } +async function toggleModule(moduleKey) { + return await post('/toggle-module', { key: moduleKey }) +} async function deleteClass(classId) { return await post('/delete-class', { id: classId }); @@ -816,6 +825,36 @@ async function loadStudentResultView(studentData) { charts.appendChild(createBarChart(subject, subject.name, studentData, config.values)); }); } +let module_panels = {} +function loadModuleSection(moduleKey) { + return createPanel(moduleKey, document.createElement("div"), async (header, body) => { + const module = await fetchModule(moduleKey); + header.textContent = module.name; + body.innerHTML = ` +

${module.description.replace("\n", "

")}

+ + + + + + + + + + +
KeyValue +
ID${module.id}
Name${module.name}
Enabled${module.enabled}
+ `; + }) +} +async function loadModulesView(moduleContainer) { + const modules = fetchModuleKeys(); + (await modules).forEach(async key => { + const moduleSection = loadModuleSection(key); + module_panels[key] = moduleSection; + moduleContainer.appendChild(moduleSection); + }); +} const graduationLevels = ["Neustarter", "Starter", "Durchstarter", "Lernprofi"]; let currentClass = JSON.parse(sessionStorage.getItem('currentClass')); document.addEventListener('DOMContentLoaded', async () => { From 2368a71cdaf4b2c96990b7418afc6c51102c299d Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Mon, 30 Mar 2026 16:13:39 +0200 Subject: [PATCH 07/36] New module loading system modules now get loaded from jars inside the "modules" folder by the ClassLoader, and then registered by ModuleLoader --- build.gradle.kts | 1 + .../de/igslandstuhl/database/Application.java | 4 +- .../database/modules/ModuleLoader.java | 104 ++++++++++++++++++ .../database/modules/WebModule.java | 36 +++--- 4 files changed, 125 insertions(+), 20 deletions(-) create mode 100644 src/main/java/de/igslandstuhl/database/modules/ModuleLoader.java diff --git a/build.gradle.kts b/build.gradle.kts index c1fb7ec..4d18abf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { implementation("commons-codec:commons-codec:1.19.0") implementation("com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20260101.1") implementation("org.jline:jline:3.30.6") // for better console input handling + implementation("org.yaml:snakeyaml:2.2") // plugin imports testImplementation("org.junit.jupiter:junit-jupiter:5.13.4") // using JUnit 5 (latest) testRuntimeOnly("org.junit.platform:junit-platform-launcher") diff --git a/src/main/java/de/igslandstuhl/database/Application.java b/src/main/java/de/igslandstuhl/database/Application.java index 35a04d7..10c7fbf 100644 --- a/src/main/java/de/igslandstuhl/database/Application.java +++ b/src/main/java/de/igslandstuhl/database/Application.java @@ -8,7 +8,7 @@ import de.igslandstuhl.database.api.Subject; import de.igslandstuhl.database.api.Topic; import de.igslandstuhl.database.holidays.Holiday; -import de.igslandstuhl.database.modules.WebModule; +import de.igslandstuhl.database.modules.ModuleLoader; import de.igslandstuhl.database.server.Server; import de.igslandstuhl.database.server.commands.Command; import de.igslandstuhl.database.server.webserver.WebPath; @@ -105,7 +105,7 @@ public static void main(String[] args) throws Exception { Holiday.setupCurrentSchoolYear(); PostRequestHandler.registerHandlers(); - WebModule.registerModules(); + ModuleLoader.getInstance().registerModules(); WebPath.registerPaths(); GetRequestHandler.getInstance().registerHandlers(); diff --git a/src/main/java/de/igslandstuhl/database/modules/ModuleLoader.java b/src/main/java/de/igslandstuhl/database/modules/ModuleLoader.java new file mode 100644 index 0000000..aee2ccf --- /dev/null +++ b/src/main/java/de/igslandstuhl/database/modules/ModuleLoader.java @@ -0,0 +1,104 @@ +package de.igslandstuhl.database.modules; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.yaml.snakeyaml.Yaml; + +import de.igslandstuhl.database.Registry; +import de.igslandstuhl.database.modules.config.BoolSetting; + +public class ModuleLoader { + private final List classLoaders = new ArrayList<>(); + private static final ModuleLoader INSTANCE = new ModuleLoader(); + public static ModuleLoader getInstance() { + return INSTANCE; + } + private ModuleLoader() {} + + private Map loadYaml(URLClassLoader classLoader) { + try (InputStream is = classLoader.getResourceAsStream("module.yml")) { + if (is == null) return null; + + Yaml yaml = new Yaml(); + return yaml.load(is); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + public void loadModulesFromJar(File jarFile) { + try { + URLClassLoader classLoader = new URLClassLoader( + new URL[]{jarFile.toURI().toURL()}, + getClass().getClassLoader() + ); + classLoaders.add(classLoader); + + Map yaml = loadYaml(classLoader); + if (yaml == null) { + System.err.println("No module.yml found in " + jarFile.getName()); + return; + } + + String mainClassName = (String) yaml.get("main"); + String id = (String) yaml.get("id"); + String name = (String) yaml.get("name"); + String description = (String) yaml.get("description"); + + Class clazz = classLoader.loadClass(mainClassName); + + if (!WebModule.class.isAssignableFrom(clazz)) { + throw new IllegalStateException("Main class does not extend WebModule"); + } + + WebModule module = (WebModule) clazz.getDeclaredConstructor().newInstance(); + module.init(id, name, description); + module.load(); + registerModule(module); + + } catch (Exception e) { + e.printStackTrace(); + } + } + public void loadAllModules(File folder) { + File[] jars = folder.listFiles((dir, name) -> name.endsWith(".jar")); + if (jars == null) return; + + for (File jar : jars) { + loadModulesFromJar(jar); + } + } + public void unloadModules() { + classLoaders.forEach((l) -> { + try { + l.close(); + } catch (IOException e) { + throw new RuntimeException("Problem while unloading", e); + } + }); + } + + + + private void registerModule(WebModule module) { + if (Registry.moduleRegistry().keyStream().anyMatch(module.getId()::equals)) { + throw new IllegalStateException("Duplicate module id: " + module.getId()); + } + Registry.moduleRegistry().register(module.getId(), module); + } + public void registerModules() { + registerModule(new WebModule.DummyModule("result_view", "Student Results View", "The view displaying the student's current progress and prognoses for the final result", List.of( + new BoolSetting("show_prognosis", "Show Prognosis", "Whether to display the prognosis for the final result", true), + new BoolSetting("show_current_progress", "Show Current", "Whether to display the current progress to the subject (in percent)", true), + new BoolSetting("show_current_grade", "Show Currently Achieved Grade", "Whether to display the grade the student would achieve when they decide to immediately stop working", false) + ))); + loadAllModules(new File("modules")); + } +} diff --git a/src/main/java/de/igslandstuhl/database/modules/WebModule.java b/src/main/java/de/igslandstuhl/database/modules/WebModule.java index 7e55b3a..a3edb50 100644 --- a/src/main/java/de/igslandstuhl/database/modules/WebModule.java +++ b/src/main/java/de/igslandstuhl/database/modules/WebModule.java @@ -1,9 +1,11 @@ package de.igslandstuhl.database.modules; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.util.List; -import de.igslandstuhl.database.Registry; -import de.igslandstuhl.database.modules.config.BoolSetting; import de.igslandstuhl.database.modules.config.ModuleConfig; import de.igslandstuhl.database.modules.config.ModuleSetting; @@ -13,12 +15,18 @@ public abstract class WebModule { private String description; private boolean enabled; + private boolean initialized = false; - public WebModule(String id, String name, String description) { + public WebModule() { + this.enabled = true; + } + + void init(String id, String name, String description) { + if (initialized) throw new IllegalStateException("Plugin already initialized"); this.id = id; this.name = name; this.description = description; - this.enabled = true; + this.initialized = true; } public String getId() { @@ -69,14 +77,14 @@ public void toggle() { enable(); } } - public void load() { + void load() { onLoad(); } - private static class DummyModule extends WebModule { + static class DummyModule extends WebModule { private final ModuleConfig config; public DummyModule(String id, String name, String description, List> settings) { - super(id, name, description); + init(id, name, description); config = new ModuleConfig(this, settings) { }; @@ -102,16 +110,8 @@ protected void onLoad() { // Dummy load logic } } - - private static void registerModule(WebModule module) { - Registry.moduleRegistry().register(module.getId(), module); - } - public static void registerModules() { - registerModule(new DummyModule("result_view", "Student Results View", "The view displaying the student's current progress and prognoses for the final result", List.of( - new BoolSetting("show_prognosis", "Show Prognosis", "Whether to display the prognosis for the final result", true), - new BoolSetting("show_current_progress", "Show Current", "Whether to display the current progress to the subject (in percent)", true), - new BoolSetting("show_current_grade", "Show Currently Achieved Grade", "Whether to display the grade the student would achieve when they decide to immediately stop working", false) - ))); - } + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public static @interface Module {} } From a3dab83e6108ddec98e5a07a56d5d0d745481668 Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Mon, 30 Mar 2026 17:20:59 +0200 Subject: [PATCH 08/36] Now publishing to maven so that plugins can import ignoring the modules folder to make debugging easier --- .gitignore | 3 ++- build.gradle.kts | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ca67cc2..81ca0b4 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ target .idea bin .gradle -build \ No newline at end of file +build +modules \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 4d18abf..51fb147 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,6 +2,7 @@ plugins { java application id("com.github.johnrengelman.shadow") version "8.1.1" + id("maven-publish") } group = "igs-landstuhl" @@ -51,3 +52,27 @@ java { languageVersion.set(JavaLanguageVersion.of(17)) // or another version you prefer } } + +publishing { + publications { + create("mavenJava") { + from(components["java"]) + + groupId = "igs-landstuhl" + artifactId = "student-database" + version = project.version.toString() + } + } + + repositories { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/Learn-Monitor/student-database/") + + credentials { + username = System.getenv("GITHUB_ACTOR") + password = System.getenv("GITHUB_TOKEN") + } + } + } +} \ No newline at end of file From 72df3006d488fbad832b0db62a25907ef1eece24 Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Mon, 30 Mar 2026 18:17:26 +0200 Subject: [PATCH 09/36] Added Dependency, better Module loading system --- .gitignore | 2 +- .../database/modules/ModuleDescription.java | 7 ++ .../database/modules/ModuleLoader.java | 74 ++++++++++++++++--- .../database/modules/ModuleSort.java | 57 ++++++++++++++ .../database/modules/PreLoadedModule.java | 11 +++ .../database/modules/WebModule.java | 3 + 6 files changed, 141 insertions(+), 13 deletions(-) create mode 100644 src/main/java/de/igslandstuhl/database/modules/ModuleDescription.java create mode 100644 src/main/java/de/igslandstuhl/database/modules/ModuleSort.java create mode 100644 src/main/java/de/igslandstuhl/database/modules/PreLoadedModule.java diff --git a/.gitignore b/.gitignore index 81ca0b4..55d80c8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,4 @@ target bin .gradle build -modules \ No newline at end of file +/modules \ No newline at end of file diff --git a/src/main/java/de/igslandstuhl/database/modules/ModuleDescription.java b/src/main/java/de/igslandstuhl/database/modules/ModuleDescription.java new file mode 100644 index 0000000..33c4133 --- /dev/null +++ b/src/main/java/de/igslandstuhl/database/modules/ModuleDescription.java @@ -0,0 +1,7 @@ +package de.igslandstuhl.database.modules; + +import java.util.List; + +public record ModuleDescription(String id, String name, String description, String main, List depends) { + +} diff --git a/src/main/java/de/igslandstuhl/database/modules/ModuleLoader.java b/src/main/java/de/igslandstuhl/database/modules/ModuleLoader.java index aee2ccf..da570c0 100644 --- a/src/main/java/de/igslandstuhl/database/modules/ModuleLoader.java +++ b/src/main/java/de/igslandstuhl/database/modules/ModuleLoader.java @@ -3,11 +3,15 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import org.yaml.snakeyaml.Yaml; @@ -15,7 +19,7 @@ import de.igslandstuhl.database.modules.config.BoolSetting; public class ModuleLoader { - private final List classLoaders = new ArrayList<>(); + private final List moduleInfos = new ArrayList<>(); private static final ModuleLoader INSTANCE = new ModuleLoader(); public static ModuleLoader getInstance() { return INSTANCE; @@ -33,24 +37,32 @@ private Map loadYaml(URLClassLoader classLoader) { return null; } } - public void loadModulesFromJar(File jarFile) { + public PreLoadedModule loadModuleFromJar(File jarFile) { + URLClassLoader classLoader; try { - URLClassLoader classLoader = new URLClassLoader( + classLoader = new URLClassLoader( new URL[]{jarFile.toURI().toURL()}, getClass().getClassLoader() ); - classLoaders.add(classLoader); + } catch (MalformedURLException e) { + e.printStackTrace(); + return null; + } + try { Map yaml = loadYaml(classLoader); if (yaml == null) { System.err.println("No module.yml found in " + jarFile.getName()); - return; + classLoader.close(); + return null; } String mainClassName = (String) yaml.get("main"); String id = (String) yaml.get("id"); String name = (String) yaml.get("name"); - String description = (String) yaml.get("description"); + String description = (String) yaml.getOrDefault("description", ""); + @SuppressWarnings("unchecked") + List depends = (List) yaml.getOrDefault("depends", new ArrayList<>()); Class clazz = classLoader.loadClass(mainClassName); @@ -58,11 +70,26 @@ public void loadModulesFromJar(File jarFile) { throw new IllegalStateException("Main class does not extend WebModule"); } - WebModule module = (WebModule) clazz.getDeclaredConstructor().newInstance(); - module.init(id, name, description); + return new PreLoadedModule(new ModuleDescription(id, name, description, mainClassName, depends), clazz, classLoader); + + } catch (Exception e) { + e.printStackTrace(); + try { + classLoader.close(); + } catch (IOException e1) { + System.out.println("FAILED to close class loader"); + e1.printStackTrace(); + } + return null; + } + } + public void load(PreLoadedModule preload) { + WebModule module; + try { + module = (WebModule) preload.clazz().getDeclaredConstructor().newInstance(); + module.init(preload.description()); module.load(); registerModule(module); - } catch (Exception e) { e.printStackTrace(); } @@ -71,18 +98,41 @@ public void loadAllModules(File folder) { File[] jars = folder.listFiles((dir, name) -> name.endsWith(".jar")); if (jars == null) return; + List modules = new ArrayList<>(); + for (File jar : jars) { - loadModulesFromJar(jar); + PreLoadedModule m = loadModuleFromJar(jar); + if (m != null) { + modules.add(m); + } } + + // Check for duplicate ids + Set ids = new HashSet<>(); + for (PreLoadedModule m : modules) { + if (!ids.add(m.description().id())) { + throw new IllegalStateException("Duplicate module id: " + m.description().id()); + } + } + + ModuleSort.sortModules(modules).forEach((p) -> moduleInfos.add(p)); + moduleInfos.forEach(this::load); } public void unloadModules() { - classLoaders.forEach((l) -> { + Collections.reverse(moduleInfos); + moduleInfos.forEach((m) -> { + WebModule module = Registry.moduleRegistry().get(m.description().id()); + if (module != null && module.isEnabled()) { + module.disable(); + } + try { - l.close(); + m.classLoader().close(); } catch (IOException e) { throw new RuntimeException("Problem while unloading", e); } }); + moduleInfos.clear(); } diff --git a/src/main/java/de/igslandstuhl/database/modules/ModuleSort.java b/src/main/java/de/igslandstuhl/database/modules/ModuleSort.java new file mode 100644 index 0000000..9a4cec3 --- /dev/null +++ b/src/main/java/de/igslandstuhl/database/modules/ModuleSort.java @@ -0,0 +1,57 @@ +package de.igslandstuhl.database.modules; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class ModuleSort { + private ModuleSort() {} + private static void visit( + PreLoadedModule module, + Map map, + List sorted, + Set visited, + Set visiting + ) { + String id = module.description().id(); + + if (visited.contains(id)) return; + + if (visiting.contains(id)) { + throw new IllegalStateException("Circular dependency detected: " + id); + } + + visiting.add(id); + + for (String dep : module.description().depends()) { + PreLoadedModule dependency = map.get(dep); + if (dependency == null) { + throw new IllegalStateException("Missing dependency: " + dep + " for " + id); + } + visit(dependency, map, sorted, visited, visiting); + } + + visiting.remove(id); + visited.add(id); + sorted.add(module); + } + public static List sortModules(List modules) { + Map map = new HashMap<>(); + for (PreLoadedModule m : modules) { + map.put(m.description().id(), m); + } + + List sorted = new ArrayList<>(); + Set visited = new HashSet<>(); + Set visiting = new HashSet<>(); + + for (PreLoadedModule m : modules) { + visit(m, map, sorted, visited, visiting); + } + + return sorted; + } +} diff --git a/src/main/java/de/igslandstuhl/database/modules/PreLoadedModule.java b/src/main/java/de/igslandstuhl/database/modules/PreLoadedModule.java new file mode 100644 index 0000000..126808f --- /dev/null +++ b/src/main/java/de/igslandstuhl/database/modules/PreLoadedModule.java @@ -0,0 +1,11 @@ +package de.igslandstuhl.database.modules; + +import java.net.URLClassLoader; + +record PreLoadedModule ( + ModuleDescription description, + Class clazz, + URLClassLoader classLoader +) { + +} \ No newline at end of file diff --git a/src/main/java/de/igslandstuhl/database/modules/WebModule.java b/src/main/java/de/igslandstuhl/database/modules/WebModule.java index a3edb50..b13d174 100644 --- a/src/main/java/de/igslandstuhl/database/modules/WebModule.java +++ b/src/main/java/de/igslandstuhl/database/modules/WebModule.java @@ -28,6 +28,9 @@ void init(String id, String name, String description) { this.description = description; this.initialized = true; } + void init(ModuleDescription description) { + init(description.id(), description.name(), description.description()); + } public String getId() { return id; From 7a7109eeaceca341454e1a89d41d138bce9e50c5 Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Mon, 30 Mar 2026 18:19:37 +0200 Subject: [PATCH 10/36] Safer yaml handling --- .../database/modules/ModuleLoader.java | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/main/java/de/igslandstuhl/database/modules/ModuleLoader.java b/src/main/java/de/igslandstuhl/database/modules/ModuleLoader.java index da570c0..22201b1 100644 --- a/src/main/java/de/igslandstuhl/database/modules/ModuleLoader.java +++ b/src/main/java/de/igslandstuhl/database/modules/ModuleLoader.java @@ -59,10 +59,22 @@ public PreLoadedModule loadModuleFromJar(File jarFile) { String mainClassName = (String) yaml.get("main"); String id = (String) yaml.get("id"); - String name = (String) yaml.get("name"); + if (id == null || mainClassName == null) { + throw new IllegalStateException("Invalid module.yml in " + jarFile.getName() + ": you must define id and main"); + } + String name = (String) yaml.getOrDefault("name", id); String description = (String) yaml.getOrDefault("description", ""); - @SuppressWarnings("unchecked") - List depends = (List) yaml.getOrDefault("depends", new ArrayList<>()); + + Object dependsObj = yaml.get("depends"); + List depends = new ArrayList<>(); + + if (dependsObj instanceof List) { + for (Object o : (List) dependsObj) { + if (o instanceof String s) { + depends.add(s); + } + } + } Class clazz = classLoader.loadClass(mainClassName); From c7a9776a74fa725986b2576c7783bca808ece10d Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Mon, 30 Mar 2026 18:25:55 +0200 Subject: [PATCH 11/36] Fixed a few more loading bugs --- .../de/igslandstuhl/database/modules/ModuleLoader.java | 7 ++++++- .../java/de/igslandstuhl/database/modules/WebModule.java | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/igslandstuhl/database/modules/ModuleLoader.java b/src/main/java/de/igslandstuhl/database/modules/ModuleLoader.java index 22201b1..6e252da 100644 --- a/src/main/java/de/igslandstuhl/database/modules/ModuleLoader.java +++ b/src/main/java/de/igslandstuhl/database/modules/ModuleLoader.java @@ -100,9 +100,11 @@ public void load(PreLoadedModule preload) { try { module = (WebModule) preload.clazz().getDeclaredConstructor().newInstance(); module.init(preload.description()); - module.load(); registerModule(module); + module.load(); } catch (Exception e) { + System.err.println("Failed to load module: " + preload.description().id()); + moduleInfos.remove(preload); e.printStackTrace(); } } @@ -130,6 +132,9 @@ public void loadAllModules(File folder) { ModuleSort.sortModules(modules).forEach((p) -> moduleInfos.add(p)); moduleInfos.forEach(this::load); } + public void enableModules() { + moduleInfos.forEach((m) -> Registry.moduleRegistry().get(m.description().id()).enable()); + } public void unloadModules() { Collections.reverse(moduleInfos); moduleInfos.forEach((m) -> { diff --git a/src/main/java/de/igslandstuhl/database/modules/WebModule.java b/src/main/java/de/igslandstuhl/database/modules/WebModule.java index b13d174..8a79894 100644 --- a/src/main/java/de/igslandstuhl/database/modules/WebModule.java +++ b/src/main/java/de/igslandstuhl/database/modules/WebModule.java @@ -18,7 +18,7 @@ public abstract class WebModule { private boolean initialized = false; public WebModule() { - this.enabled = true; + this.enabled = false; } void init(String id, String name, String description) { From d430dd7fcc7d19fa0eaaae9bbb977f7858b23539 Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Mon, 30 Mar 2026 19:00:51 +0200 Subject: [PATCH 12/36] New ResourceManager approach Now using providers instead of scanning the classpath Three types of providers (so far): File provider (from the filesystem), plugin provider (from a module), core provider (from the core system, i. e. the fat jar) --- .../database/modules/ModuleLoader.java | 3 + .../modules/PluginResourceProvider.java | 63 ++++++++ .../resources/CoreResourceProvider.java | 127 ++++++++++++++++ .../resources/FileResourceProvider.java | 66 ++++++++ .../server/resources/ResourceManager.java | 141 +++--------------- .../server/resources/ResourceProvider.java | 11 ++ 6 files changed, 293 insertions(+), 118 deletions(-) create mode 100644 src/main/java/de/igslandstuhl/database/modules/PluginResourceProvider.java create mode 100644 src/main/java/de/igslandstuhl/database/server/resources/CoreResourceProvider.java create mode 100644 src/main/java/de/igslandstuhl/database/server/resources/FileResourceProvider.java create mode 100644 src/main/java/de/igslandstuhl/database/server/resources/ResourceProvider.java diff --git a/src/main/java/de/igslandstuhl/database/modules/ModuleLoader.java b/src/main/java/de/igslandstuhl/database/modules/ModuleLoader.java index 6e252da..a9d32d5 100644 --- a/src/main/java/de/igslandstuhl/database/modules/ModuleLoader.java +++ b/src/main/java/de/igslandstuhl/database/modules/ModuleLoader.java @@ -20,6 +20,9 @@ public class ModuleLoader { private final List moduleInfos = new ArrayList<>(); + public List getModuleInfos() { + return moduleInfos; + } private static final ModuleLoader INSTANCE = new ModuleLoader(); public static ModuleLoader getInstance() { return INSTANCE; diff --git a/src/main/java/de/igslandstuhl/database/modules/PluginResourceProvider.java b/src/main/java/de/igslandstuhl/database/modules/PluginResourceProvider.java new file mode 100644 index 0000000..5b69a91 --- /dev/null +++ b/src/main/java/de/igslandstuhl/database/modules/PluginResourceProvider.java @@ -0,0 +1,63 @@ +package de.igslandstuhl.database.modules; + +import java.io.File; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.List; +import java.util.regex.Pattern; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import de.igslandstuhl.database.server.resources.ResourceLocation; +import de.igslandstuhl.database.server.resources.ResourceProvider; + +public class PluginResourceProvider implements ResourceProvider { + + @Override + public InputStream open(ResourceLocation location) { + String path = location.context() + "/" + location.namespace() + "/" + location.resource(); + + for (PreLoadedModule module : ModuleLoader.getInstance().getModuleInfos()) { + ClassLoader cl = module.classLoader(); + InputStream stream = cl.getResourceAsStream(path); + + if (stream != null) { + return stream; + } + } + + return null; + } + + @Override + public Collection list(Pattern pattern) { + List result = new ArrayList<>(); + + for (PreLoadedModule module : ModuleLoader.getInstance().getModuleInfos()) { + try (ZipFile zip = new ZipFile(new File(module.classLoader().getURLs()[0].toURI()))) { + + Enumeration entries = zip.entries(); + + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + + if (entry.isDirectory()) continue; + + String name = entry.getName(); + + if (pattern.matcher(name).matches()) { + ResourceLocation loc = ResourceLocation.fromPath(name); + if (loc != null) result.add(loc); + } + } + + } catch (Exception e) { + e.printStackTrace(); + } + } + + return result; + } +} diff --git a/src/main/java/de/igslandstuhl/database/server/resources/CoreResourceProvider.java b/src/main/java/de/igslandstuhl/database/server/resources/CoreResourceProvider.java new file mode 100644 index 0000000..758efad --- /dev/null +++ b/src/main/java/de/igslandstuhl/database/server/resources/CoreResourceProvider.java @@ -0,0 +1,127 @@ +package de.igslandstuhl.database.server.resources; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.regex.Pattern; +import java.util.zip.ZipEntry; +import java.util.zip.ZipException; +import java.util.zip.ZipFile; + +public class CoreResourceProvider implements ResourceProvider { + /** + * Checks if a zip entry name is safe (to prevent zip slipping). + */ + private boolean isSafeZipEntryName(String entryName, Path rootDir) { + // Resolve entry against a fixed root and normalize + Path resolvedPath = rootDir.resolve(entryName).normalize(); + + // Entry is safe if it stays within the root directory + return resolvedPath.startsWith(rootDir); + } + @Override + public InputStream open(ResourceLocation location) { + String path = "/" + location.context() + "/" + location.namespace() + "/" + location.resource(); + return getClass().getResourceAsStream(path); + } + + @Override + public Collection list(Pattern pattern) { + List result = new ArrayList<>(); + + String classPath = System.getProperty("java.class.path", "."); + String[] elements = classPath.split(System.getProperty("path.separator")); + + for (String element : elements) { + Path path = Path.of(element); + + if (Files.isDirectory(path)) { + result.addAll(getResourcesFromDirectory(path, pattern, path)); + } else { + result.addAll(getResourcesFromJarFile(path, pattern)); + } + } + + return result; + } + + /** + * Get all resources from a jar file or a directory that match the given pattern. + * + * @param jarFilePath the jar file or directory to search in + * @param pattern the pattern to match + * @return the resources in the order they are found + */ + private Collection getResourcesFromJarFile(final Path jarFilePath, final Pattern pattern) { + final ArrayList retval = new ArrayList<>(); + // Virtual root – no real filesystem access needed + final Path virtualRoot = Paths.get("").toAbsolutePath().normalize(); + ZipFile zf; + try { + zf = new ZipFile(jarFilePath.toFile()); + } catch (final ZipException e) { + throw new Error(e); + } catch (final NoSuchFileException e) { + return Collections.emptySet(); + } catch (final IOException e) { + throw new Error(e); + } + final Enumeration e = zf.entries(); + while (e.hasMoreElements()) { + final ZipEntry ze = e.nextElement(); + final String fileName = ze.getName(); + if (!isSafeZipEntryName(fileName, virtualRoot)) { + // Optionally log or throw, here we skip unsafe entries + continue; + } + final boolean accept = pattern.matcher(fileName).matches(); + if (accept) { + ResourceLocation location = ResourceLocation.fromPath(fileName); + if (location != null) retval.add(location); + } + } + try { + zf.close(); + } catch (final IOException e1) { + throw new Error(e1); + } + return retval; + } + + /** + * Get all resources from a directory that match the given pattern. + * + * @param directory the directory to search in + * @param pattern the pattern to match + * @return the resources in the order they are found + */ + private Collection getResourcesFromDirectory(final Path directory, final Pattern pattern, final Path toplevelPath) { + final ArrayList retval = new ArrayList<>(); + try { + Files.list(directory).forEach((path) -> { + if (Files.isDirectory(path)) { + retval.addAll(getResourcesFromDirectory(path, pattern, toplevelPath)); + } else { + final Path relativePath = toplevelPath.relativize(path); + final boolean accept = pattern.matcher(relativePath.toString()).matches(); + if (accept) { + ResourceLocation location = ResourceLocation.fromPath(relativePath); + if (location != null) retval.add(location); + } + } + }); + } catch (IOException e) { + e.printStackTrace(); + return retval; + } + return retval; + } +} diff --git a/src/main/java/de/igslandstuhl/database/server/resources/FileResourceProvider.java b/src/main/java/de/igslandstuhl/database/server/resources/FileResourceProvider.java new file mode 100644 index 0000000..7c2f9c5 --- /dev/null +++ b/src/main/java/de/igslandstuhl/database/server/resources/FileResourceProvider.java @@ -0,0 +1,66 @@ +package de.igslandstuhl.database.server.resources; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.regex.Pattern; + +public class FileResourceProvider implements ResourceProvider { + + private final Path root; + + public FileResourceProvider(Path root) { + this.root = root; + } + + @Override + public InputStream open(ResourceLocation location) { + try { + Path file = root + .resolve(location.context()) + .resolve(location.namespace()) + .resolve(location.resource()) + .normalize(); + + // 🔐 Security: Path Traversal verhindern + if (!file.startsWith(root)) { + return null; + } + + if (!Files.exists(file) || Files.isDirectory(file)) { + return null; + } + + return Files.newInputStream(file); + } catch (IOException e) { + return null; + } + } + + @Override + public Collection list(Pattern pattern) { + List result = new ArrayList<>(); + + try { + Files.walk(root).forEach(path -> { + if (Files.isRegularFile(path)) { + Path relative = root.relativize(path); + String normalized = relative.toString().replace("\\", "/"); + + if (pattern.matcher(normalized).matches()) { + ResourceLocation loc = ResourceLocation.fromPath(normalized); + if (loc != null) result.add(loc); + } + } + }); + } catch (IOException e) { + e.printStackTrace(); + } + + return result; + } +} \ No newline at end of file diff --git a/src/main/java/de/igslandstuhl/database/server/resources/ResourceManager.java b/src/main/java/de/igslandstuhl/database/server/resources/ResourceManager.java index 7b2501e..0876064 100644 --- a/src/main/java/de/igslandstuhl/database/server/resources/ResourceManager.java +++ b/src/main/java/de/igslandstuhl/database/server/resources/ResourceManager.java @@ -6,41 +6,36 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.NoSuchFileException; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; -import java.util.Collections; -import java.util.Enumeration; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import java.util.stream.Stream; -import java.util.zip.ZipEntry; -import java.util.zip.ZipException; -import java.util.zip.ZipFile; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; +import de.igslandstuhl.database.modules.PluginResourceProvider; import de.igslandstuhl.database.server.Server; /** * Manages Resources in the application */ public class ResourceManager { - /** - * Checks if a zip entry name is safe (to prevent zip slipping). - */ - private boolean isSafeZipEntryName(String entryName, Path rootDir) { - // Resolve entry against a fixed root and normalize - Path resolvedPath = rootDir.resolve(entryName).normalize(); - - // Entry is safe if it stays within the root directory - return resolvedPath.startsWith(rootDir); + private final List providers; + public ResourceManager(ResourceProvider... providers) { + this.providers = Arrays.asList(providers); + } + public ResourceManager() { + this( + new FileResourceProvider(Path.of("resources")), // highest priority + new PluginResourceProvider(), + new CoreResourceProvider() + ); } /** @@ -51,104 +46,13 @@ private boolean isSafeZipEntryName(String entryName, Path rootDir) { * @return the resources in the order they are found */ public Collection getResources(final Pattern pattern) { - final ArrayList retval = new ArrayList<>(); - final String classPath = System.getProperty("java.class.path", "."); - final String[] classPathElements = classPath.split(System.getProperty("path.separator")); - for (final String element : classPathElements) { - retval.addAll(getResources(element, pattern)); - } - return retval; - } - - /** - * for a single element of java.class.path get a Collection of resources - * Pattern pattern = Pattern.compile(".*"); gets all resources - * - * @param element the class path element to search in - * @param pattern the pattern to match - * @return the resources in the order they are found - */ - private Collection getResources(final String element, final Pattern pattern) { - final ArrayList retval = new ArrayList<>(); - final Path path = Path.of(element); - if (Files.isDirectory(path)) { - retval.addAll(getResourcesFromDirectory(path, pattern, path)); - } else { - retval.addAll(getResourcesFromJarFile(path, pattern)); - } - return retval; - } + List result = new ArrayList<>(); - /** - * Get all resources from a jar file or a directory that match the given pattern. - * - * @param jarFilePath the jar file or directory to search in - * @param pattern the pattern to match - * @return the resources in the order they are found - */ - private Collection getResourcesFromJarFile(final Path jarFilePath, final Pattern pattern) { - final ArrayList retval = new ArrayList<>(); - // Virtual root – no real filesystem access needed - final Path virtualRoot = Paths.get("").toAbsolutePath().normalize(); - ZipFile zf; - try { - zf = new ZipFile(jarFilePath.toFile()); - } catch (final ZipException e) { - throw new Error(e); - } catch (final NoSuchFileException e) { - return Collections.emptySet(); - } catch (final IOException e) { - throw new Error(e); - } - final Enumeration e = zf.entries(); - while (e.hasMoreElements()) { - final ZipEntry ze = e.nextElement(); - final String fileName = ze.getName(); - if (!isSafeZipEntryName(fileName, virtualRoot)) { - // Optionally log or throw, here we skip unsafe entries - continue; - } - final boolean accept = pattern.matcher(fileName).matches(); - if (accept) { - ResourceLocation location = ResourceLocation.fromPath(fileName); - if (location != null) retval.add(location); - } - } - try { - zf.close(); - } catch (final IOException e1) { - throw new Error(e1); + for (ResourceProvider provider : providers) { + result.addAll(provider.list(pattern)); } - return retval; - } - /** - * Get all resources from a directory that match the given pattern. - * - * @param directory the directory to search in - * @param pattern the pattern to match - * @return the resources in the order they are found - */ - private Collection getResourcesFromDirectory(final Path directory, final Pattern pattern, final Path toplevelPath) { - final ArrayList retval = new ArrayList<>(); - try { - Files.list(directory).forEach((path) -> { - if (Files.isDirectory(path)) { - retval.addAll(getResourcesFromDirectory(path, pattern, toplevelPath)); - } else { - final Path relativePath = toplevelPath.relativize(path); - final boolean accept = pattern.matcher(relativePath.toString()).matches(); - if (accept) { - ResourceLocation location = ResourceLocation.fromPath(relativePath); - if (location != null) retval.add(location); - } - } - }); - } catch (IOException e) { - e.printStackTrace(); - return retval; - } - return retval; + return result; } /** @@ -162,7 +66,7 @@ public BufferedReader[] openResourcesAsReader(Pattern pattern) { List readers = new ArrayList<>(); for (ResourceLocation resource : getResources(pattern)) { try { - readers.add(new BufferedReader(new InputStreamReader(openResourceAsStream(resource)))); + readers.add(new BufferedReader(new InputStreamReader(openResourceAsStream(resource), StandardCharsets.UTF_8))); } catch (IOException e) { throw new IllegalStateException(e); } @@ -179,12 +83,13 @@ public BufferedReader[] openResourcesAsReader(Pattern pattern) { * @throws FileNotFoundException if the resource is not found */ public InputStream openResourceAsStream(ResourceLocation location) throws FileNotFoundException { - String url = "/" + location.context() + "/" + location.namespace() + "/" + location.resource(); - InputStream stream = ResourceManager.class.getResourceAsStream(url); - if (stream == null) { - throw new FileNotFoundException(url + " not found in classpath or resources."); + for (ResourceProvider provider : providers) { + InputStream stream = provider.open(location); + if (stream != null) { + return stream; + } } - return stream; + throw new FileNotFoundException(location.toString()); } /** diff --git a/src/main/java/de/igslandstuhl/database/server/resources/ResourceProvider.java b/src/main/java/de/igslandstuhl/database/server/resources/ResourceProvider.java new file mode 100644 index 0000000..0817690 --- /dev/null +++ b/src/main/java/de/igslandstuhl/database/server/resources/ResourceProvider.java @@ -0,0 +1,11 @@ +package de.igslandstuhl.database.server.resources; + +import java.io.InputStream; +import java.util.Collection; +import java.util.regex.Pattern; + +public interface ResourceProvider { + InputStream open(ResourceLocation location); + + Collection list(Pattern pattern); +} From deb3ad053dafc1b2b5369b71f320c805dd944641 Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Mon, 30 Mar 2026 19:21:07 +0200 Subject: [PATCH 13/36] Renamed Modules to Plugins for the concept to match the label better --- .../de/igslandstuhl/database/Application.java | 4 +- .../de/igslandstuhl/database/Registry.java | 8 +- .../database/modules/ModuleDescription.java | 7 -- .../database/modules/PreLoadedModule.java | 11 --- .../WebModule.java => plugins/Plugin.java} | 31 +++--- .../database/plugins/PluginDescription.java | 7 ++ .../PluginLoader.java} | 98 +++++++++---------- .../PluginResourceProvider.java | 6 +- .../PluginSort.java} | 30 +++--- .../database/plugins/PreLoadedPlugin.java | 11 +++ .../config/BoolSetting.java | 4 +- .../config/PluginConfig.java} | 20 ++-- .../config/PluginSetting.java} | 6 +- .../server/resources/ResourceManager.java | 2 +- .../webserver/handlers/GetRequestHandler.java | 9 +- .../handlers/PostRequestHandler.java | 16 +-- ...Handler.java => PluginRequestHandler.java} | 12 +-- .../webserver/responses/GetResponse.java | 6 +- src/main/resources/html/admin/dashboard.html | 2 +- .../html/admin/{modules.html => plugins.html} | 4 +- src/main/resources/js/admin/build_modules.js | 0 .../resources/js/site/student-database.js | 48 ++++----- src/main/resources/meta/paths/get_paths.json | 16 +-- 23 files changed, 172 insertions(+), 186 deletions(-) delete mode 100644 src/main/java/de/igslandstuhl/database/modules/ModuleDescription.java delete mode 100644 src/main/java/de/igslandstuhl/database/modules/PreLoadedModule.java rename src/main/java/de/igslandstuhl/database/{modules/WebModule.java => plugins/Plugin.java} (72%) create mode 100644 src/main/java/de/igslandstuhl/database/plugins/PluginDescription.java rename src/main/java/de/igslandstuhl/database/{modules/ModuleLoader.java => plugins/PluginLoader.java} (60%) rename src/main/java/de/igslandstuhl/database/{modules => plugins}/PluginResourceProvider.java (89%) rename src/main/java/de/igslandstuhl/database/{modules/ModuleSort.java => plugins/PluginSort.java} (58%) create mode 100644 src/main/java/de/igslandstuhl/database/plugins/PreLoadedPlugin.java rename src/main/java/de/igslandstuhl/database/{modules => plugins}/config/BoolSetting.java (87%) rename src/main/java/de/igslandstuhl/database/{modules/config/ModuleConfig.java => plugins/config/PluginConfig.java} (83%) rename src/main/java/de/igslandstuhl/database/{modules/config/ModuleSetting.java => plugins/config/PluginSetting.java} (88%) rename src/main/java/de/igslandstuhl/database/server/webserver/handlers/get/{ModuleRequestHandler.java => PluginRequestHandler.java} (71%) rename src/main/resources/html/admin/{modules.html => plugins.html} (67%) delete mode 100644 src/main/resources/js/admin/build_modules.js diff --git a/src/main/java/de/igslandstuhl/database/Application.java b/src/main/java/de/igslandstuhl/database/Application.java index 10c7fbf..3caa61d 100644 --- a/src/main/java/de/igslandstuhl/database/Application.java +++ b/src/main/java/de/igslandstuhl/database/Application.java @@ -8,7 +8,7 @@ import de.igslandstuhl.database.api.Subject; import de.igslandstuhl.database.api.Topic; import de.igslandstuhl.database.holidays.Holiday; -import de.igslandstuhl.database.modules.ModuleLoader; +import de.igslandstuhl.database.plugins.PluginLoader; import de.igslandstuhl.database.server.Server; import de.igslandstuhl.database.server.commands.Command; import de.igslandstuhl.database.server.webserver.WebPath; @@ -105,7 +105,7 @@ public static void main(String[] args) throws Exception { Holiday.setupCurrentSchoolYear(); PostRequestHandler.registerHandlers(); - ModuleLoader.getInstance().registerModules(); + PluginLoader.getInstance().registerPlugins(); WebPath.registerPaths(); GetRequestHandler.getInstance().registerHandlers(); diff --git a/src/main/java/de/igslandstuhl/database/Registry.java b/src/main/java/de/igslandstuhl/database/Registry.java index 642fc3b..3fc0c20 100644 --- a/src/main/java/de/igslandstuhl/database/Registry.java +++ b/src/main/java/de/igslandstuhl/database/Registry.java @@ -5,7 +5,7 @@ import java.util.Map; import java.util.stream.Stream; -import de.igslandstuhl.database.modules.WebModule; +import de.igslandstuhl.database.plugins.Plugin; import de.igslandstuhl.database.server.commands.Command; import de.igslandstuhl.database.server.commands.CommandDescription; import de.igslandstuhl.database.server.webserver.WebPath; @@ -18,7 +18,7 @@ public class Registry implements Closeable { private static final Registry COMMAND_DESCRIPTION_REGISTRY = new Registry<>(); private static final Registry> POST_HANDLER_REGISTRY = new Registry<>(); private static final Registry> GET_HANDLER_REGISTRY = new Registry<>(); - private static final Registry MODULE_REGISTRY = new Registry<>(); + private static final Registry PLUGIN_REGISTRY = new Registry<>(); private static final Registry WEB_PATH_REGISTRY = new Registry<>(); public static Registry commandRegistry() { return COMMAND_REGISTRY; @@ -29,8 +29,8 @@ public static Registry> postRequestHandlerRe public static Registry> getRequestHandlerRegistry() { return GET_HANDLER_REGISTRY; } - public static Registry moduleRegistry() { - return MODULE_REGISTRY; + public static Registry pluginRegistry() { + return PLUGIN_REGISTRY; } public static Registry commandDescriptionRegistry() { return COMMAND_DESCRIPTION_REGISTRY; diff --git a/src/main/java/de/igslandstuhl/database/modules/ModuleDescription.java b/src/main/java/de/igslandstuhl/database/modules/ModuleDescription.java deleted file mode 100644 index 33c4133..0000000 --- a/src/main/java/de/igslandstuhl/database/modules/ModuleDescription.java +++ /dev/null @@ -1,7 +0,0 @@ -package de.igslandstuhl.database.modules; - -import java.util.List; - -public record ModuleDescription(String id, String name, String description, String main, List depends) { - -} diff --git a/src/main/java/de/igslandstuhl/database/modules/PreLoadedModule.java b/src/main/java/de/igslandstuhl/database/modules/PreLoadedModule.java deleted file mode 100644 index 126808f..0000000 --- a/src/main/java/de/igslandstuhl/database/modules/PreLoadedModule.java +++ /dev/null @@ -1,11 +0,0 @@ -package de.igslandstuhl.database.modules; - -import java.net.URLClassLoader; - -record PreLoadedModule ( - ModuleDescription description, - Class clazz, - URLClassLoader classLoader -) { - -} \ No newline at end of file diff --git a/src/main/java/de/igslandstuhl/database/modules/WebModule.java b/src/main/java/de/igslandstuhl/database/plugins/Plugin.java similarity index 72% rename from src/main/java/de/igslandstuhl/database/modules/WebModule.java rename to src/main/java/de/igslandstuhl/database/plugins/Plugin.java index 8a79894..f8f2218 100644 --- a/src/main/java/de/igslandstuhl/database/modules/WebModule.java +++ b/src/main/java/de/igslandstuhl/database/plugins/Plugin.java @@ -1,15 +1,11 @@ -package de.igslandstuhl.database.modules; +package de.igslandstuhl.database.plugins; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; import java.util.List; -import de.igslandstuhl.database.modules.config.ModuleConfig; -import de.igslandstuhl.database.modules.config.ModuleSetting; +import de.igslandstuhl.database.plugins.config.PluginConfig; +import de.igslandstuhl.database.plugins.config.PluginSetting; -public abstract class WebModule { +public abstract class Plugin { private String id; private String name; private String description; @@ -17,7 +13,7 @@ public abstract class WebModule { private boolean enabled; private boolean initialized = false; - public WebModule() { + public Plugin() { this.enabled = false; } @@ -28,7 +24,7 @@ void init(String id, String name, String description) { this.description = description; this.initialized = true; } - void init(ModuleDescription description) { + void init(PluginDescription description) { init(description.id(), description.name(), description.description()); } @@ -57,7 +53,7 @@ public String toJSON() { return sb.toString(); } - public abstract ModuleConfig getConfig(); + public abstract PluginConfig getConfig(); protected abstract void onEnable(); protected abstract void onDisable(); @@ -84,17 +80,17 @@ void load() { onLoad(); } - static class DummyModule extends WebModule { - private final ModuleConfig config; - public DummyModule(String id, String name, String description, List> settings) { + static class DummyModule extends Plugin { + private final PluginConfig config; + public DummyModule(String id, String name, String description, List> settings) { init(id, name, description); - config = new ModuleConfig(this, settings) { + config = new PluginConfig(this, settings) { }; } @Override - public ModuleConfig getConfig() { + public PluginConfig getConfig() { return config; } @@ -113,8 +109,5 @@ protected void onLoad() { // Dummy load logic } } - @Retention(RetentionPolicy.RUNTIME) - @Target(ElementType.TYPE) - public static @interface Module {} } diff --git a/src/main/java/de/igslandstuhl/database/plugins/PluginDescription.java b/src/main/java/de/igslandstuhl/database/plugins/PluginDescription.java new file mode 100644 index 0000000..1a2d7eb --- /dev/null +++ b/src/main/java/de/igslandstuhl/database/plugins/PluginDescription.java @@ -0,0 +1,7 @@ +package de.igslandstuhl.database.plugins; + +import java.util.List; + +public record PluginDescription(String id, String name, String description, String main, List depends) { + +} diff --git a/src/main/java/de/igslandstuhl/database/modules/ModuleLoader.java b/src/main/java/de/igslandstuhl/database/plugins/PluginLoader.java similarity index 60% rename from src/main/java/de/igslandstuhl/database/modules/ModuleLoader.java rename to src/main/java/de/igslandstuhl/database/plugins/PluginLoader.java index a9d32d5..346c331 100644 --- a/src/main/java/de/igslandstuhl/database/modules/ModuleLoader.java +++ b/src/main/java/de/igslandstuhl/database/plugins/PluginLoader.java @@ -1,4 +1,4 @@ -package de.igslandstuhl.database.modules; +package de.igslandstuhl.database.plugins; import java.io.File; import java.io.IOException; @@ -16,21 +16,21 @@ import org.yaml.snakeyaml.Yaml; import de.igslandstuhl.database.Registry; -import de.igslandstuhl.database.modules.config.BoolSetting; +import de.igslandstuhl.database.plugins.config.BoolSetting; -public class ModuleLoader { - private final List moduleInfos = new ArrayList<>(); - public List getModuleInfos() { - return moduleInfos; +public class PluginLoader { + private final List pluginInfos = new ArrayList<>(); + public List getPluginInfos() { + return pluginInfos; } - private static final ModuleLoader INSTANCE = new ModuleLoader(); - public static ModuleLoader getInstance() { + private static final PluginLoader INSTANCE = new PluginLoader(); + public static PluginLoader getInstance() { return INSTANCE; } - private ModuleLoader() {} + private PluginLoader() {} private Map loadYaml(URLClassLoader classLoader) { - try (InputStream is = classLoader.getResourceAsStream("module.yml")) { + try (InputStream is = classLoader.getResourceAsStream("plugin.yml")) { if (is == null) return null; Yaml yaml = new Yaml(); @@ -40,7 +40,7 @@ private Map loadYaml(URLClassLoader classLoader) { return null; } } - public PreLoadedModule loadModuleFromJar(File jarFile) { + public PreLoadedPlugin loadPluginFromJar(File jarFile) { URLClassLoader classLoader; try { classLoader = new URLClassLoader( @@ -55,7 +55,7 @@ public PreLoadedModule loadModuleFromJar(File jarFile) { Map yaml = loadYaml(classLoader); if (yaml == null) { - System.err.println("No module.yml found in " + jarFile.getName()); + System.err.println("No plugin.yml found in " + jarFile.getName()); classLoader.close(); return null; } @@ -63,7 +63,7 @@ public PreLoadedModule loadModuleFromJar(File jarFile) { String mainClassName = (String) yaml.get("main"); String id = (String) yaml.get("id"); if (id == null || mainClassName == null) { - throw new IllegalStateException("Invalid module.yml in " + jarFile.getName() + ": you must define id and main"); + throw new IllegalStateException("Invalid plugin.yml in " + jarFile.getName() + ": you must define id and main"); } String name = (String) yaml.getOrDefault("name", id); String description = (String) yaml.getOrDefault("description", ""); @@ -81,11 +81,11 @@ public PreLoadedModule loadModuleFromJar(File jarFile) { Class clazz = classLoader.loadClass(mainClassName); - if (!WebModule.class.isAssignableFrom(clazz)) { - throw new IllegalStateException("Main class does not extend WebModule"); + if (!Plugin.class.isAssignableFrom(clazz)) { + throw new IllegalStateException("Main class does not extend Plugin"); } - return new PreLoadedModule(new ModuleDescription(id, name, description, mainClassName, depends), clazz, classLoader); + return new PreLoadedPlugin(new PluginDescription(id, name, description, mainClassName, depends), clazz, classLoader); } catch (Exception e) { e.printStackTrace(); @@ -98,77 +98,77 @@ public PreLoadedModule loadModuleFromJar(File jarFile) { return null; } } - public void load(PreLoadedModule preload) { - WebModule module; + public void load(PreLoadedPlugin preload) { + Plugin plugin; try { - module = (WebModule) preload.clazz().getDeclaredConstructor().newInstance(); - module.init(preload.description()); - registerModule(module); - module.load(); + plugin = (Plugin) preload.clazz().getDeclaredConstructor().newInstance(); + plugin.init(preload.description()); + registerPlugin(plugin); + plugin.load(); } catch (Exception e) { System.err.println("Failed to load module: " + preload.description().id()); - moduleInfos.remove(preload); + pluginInfos.remove(preload); e.printStackTrace(); } } - public void loadAllModules(File folder) { + public void loadAllPlugins(File folder) { File[] jars = folder.listFiles((dir, name) -> name.endsWith(".jar")); if (jars == null) return; - List modules = new ArrayList<>(); + List plugins = new ArrayList<>(); for (File jar : jars) { - PreLoadedModule m = loadModuleFromJar(jar); + PreLoadedPlugin m = loadPluginFromJar(jar); if (m != null) { - modules.add(m); + plugins.add(m); } } // Check for duplicate ids Set ids = new HashSet<>(); - for (PreLoadedModule m : modules) { - if (!ids.add(m.description().id())) { - throw new IllegalStateException("Duplicate module id: " + m.description().id()); + for (PreLoadedPlugin p : plugins) { + if (!ids.add(p.description().id())) { + throw new IllegalStateException("Duplicate module id: " + p.description().id()); } } - ModuleSort.sortModules(modules).forEach((p) -> moduleInfos.add(p)); - moduleInfos.forEach(this::load); + PluginSort.sortPlugins(plugins).forEach((p) -> pluginInfos.add(p)); + pluginInfos.forEach(this::load); } - public void enableModules() { - moduleInfos.forEach((m) -> Registry.moduleRegistry().get(m.description().id()).enable()); + public void enablePlugins() { + pluginInfos.forEach((p) -> Registry.pluginRegistry().get(p.description().id()).enable()); } - public void unloadModules() { - Collections.reverse(moduleInfos); - moduleInfos.forEach((m) -> { - WebModule module = Registry.moduleRegistry().get(m.description().id()); - if (module != null && module.isEnabled()) { - module.disable(); + public void unloadPlugins() { + Collections.reverse(pluginInfos); + pluginInfos.forEach((p) -> { + Plugin plugin = Registry.pluginRegistry().get(p.description().id()); + if (plugin != null && plugin.isEnabled()) { + plugin.disable(); } try { - m.classLoader().close(); + p.classLoader().close(); } catch (IOException e) { throw new RuntimeException("Problem while unloading", e); } }); - moduleInfos.clear(); + pluginInfos.clear(); } - private void registerModule(WebModule module) { - if (Registry.moduleRegistry().keyStream().anyMatch(module.getId()::equals)) { - throw new IllegalStateException("Duplicate module id: " + module.getId()); + private void registerPlugin(Plugin plugin) { + if (Registry.pluginRegistry().keyStream().anyMatch(plugin.getId()::equals)) { + throw new IllegalStateException("Duplicate module id: " + plugin.getId()); } - Registry.moduleRegistry().register(module.getId(), module); + Registry.pluginRegistry().register(plugin.getId(), plugin); } - public void registerModules() { - registerModule(new WebModule.DummyModule("result_view", "Student Results View", "The view displaying the student's current progress and prognoses for the final result", List.of( + public void registerPlugins() { + registerPlugin(new Plugin.DummyModule("result_view", "Student Results View", "The view displaying the student's current progress and prognoses for the final result", List.of( new BoolSetting("show_prognosis", "Show Prognosis", "Whether to display the prognosis for the final result", true), new BoolSetting("show_current_progress", "Show Current", "Whether to display the current progress to the subject (in percent)", true), new BoolSetting("show_current_grade", "Show Currently Achieved Grade", "Whether to display the grade the student would achieve when they decide to immediately stop working", false) ))); - loadAllModules(new File("modules")); + loadAllPlugins(new File("modules")); } } diff --git a/src/main/java/de/igslandstuhl/database/modules/PluginResourceProvider.java b/src/main/java/de/igslandstuhl/database/plugins/PluginResourceProvider.java similarity index 89% rename from src/main/java/de/igslandstuhl/database/modules/PluginResourceProvider.java rename to src/main/java/de/igslandstuhl/database/plugins/PluginResourceProvider.java index 5b69a91..30ca068 100644 --- a/src/main/java/de/igslandstuhl/database/modules/PluginResourceProvider.java +++ b/src/main/java/de/igslandstuhl/database/plugins/PluginResourceProvider.java @@ -1,4 +1,4 @@ -package de.igslandstuhl.database.modules; +package de.igslandstuhl.database.plugins; import java.io.File; import java.io.InputStream; @@ -19,7 +19,7 @@ public class PluginResourceProvider implements ResourceProvider { public InputStream open(ResourceLocation location) { String path = location.context() + "/" + location.namespace() + "/" + location.resource(); - for (PreLoadedModule module : ModuleLoader.getInstance().getModuleInfos()) { + for (PreLoadedPlugin module : PluginLoader.getInstance().getPluginInfos()) { ClassLoader cl = module.classLoader(); InputStream stream = cl.getResourceAsStream(path); @@ -35,7 +35,7 @@ public InputStream open(ResourceLocation location) { public Collection list(Pattern pattern) { List result = new ArrayList<>(); - for (PreLoadedModule module : ModuleLoader.getInstance().getModuleInfos()) { + for (PreLoadedPlugin module : PluginLoader.getInstance().getPluginInfos()) { try (ZipFile zip = new ZipFile(new File(module.classLoader().getURLs()[0].toURI()))) { Enumeration entries = zip.entries(); diff --git a/src/main/java/de/igslandstuhl/database/modules/ModuleSort.java b/src/main/java/de/igslandstuhl/database/plugins/PluginSort.java similarity index 58% rename from src/main/java/de/igslandstuhl/database/modules/ModuleSort.java rename to src/main/java/de/igslandstuhl/database/plugins/PluginSort.java index 9a4cec3..05f59d4 100644 --- a/src/main/java/de/igslandstuhl/database/modules/ModuleSort.java +++ b/src/main/java/de/igslandstuhl/database/plugins/PluginSort.java @@ -1,4 +1,4 @@ -package de.igslandstuhl.database.modules; +package de.igslandstuhl.database.plugins; import java.util.ArrayList; import java.util.HashMap; @@ -7,16 +7,16 @@ import java.util.Map; import java.util.Set; -public class ModuleSort { - private ModuleSort() {} +public class PluginSort { + private PluginSort() {} private static void visit( - PreLoadedModule module, - Map map, - List sorted, + PreLoadedPlugin plugin, + Map map, + List sorted, Set visited, Set visiting ) { - String id = module.description().id(); + String id = plugin.description().id(); if (visited.contains(id)) return; @@ -26,8 +26,8 @@ private static void visit( visiting.add(id); - for (String dep : module.description().depends()) { - PreLoadedModule dependency = map.get(dep); + for (String dep : plugin.description().depends()) { + PreLoadedPlugin dependency = map.get(dep); if (dependency == null) { throw new IllegalStateException("Missing dependency: " + dep + " for " + id); } @@ -36,19 +36,19 @@ private static void visit( visiting.remove(id); visited.add(id); - sorted.add(module); + sorted.add(plugin); } - public static List sortModules(List modules) { - Map map = new HashMap<>(); - for (PreLoadedModule m : modules) { + public static List sortPlugins(List plugins) { + Map map = new HashMap<>(); + for (PreLoadedPlugin m : plugins) { map.put(m.description().id(), m); } - List sorted = new ArrayList<>(); + List sorted = new ArrayList<>(); Set visited = new HashSet<>(); Set visiting = new HashSet<>(); - for (PreLoadedModule m : modules) { + for (PreLoadedPlugin m : plugins) { visit(m, map, sorted, visited, visiting); } diff --git a/src/main/java/de/igslandstuhl/database/plugins/PreLoadedPlugin.java b/src/main/java/de/igslandstuhl/database/plugins/PreLoadedPlugin.java new file mode 100644 index 0000000..0f794a0 --- /dev/null +++ b/src/main/java/de/igslandstuhl/database/plugins/PreLoadedPlugin.java @@ -0,0 +1,11 @@ +package de.igslandstuhl.database.plugins; + +import java.net.URLClassLoader; + +record PreLoadedPlugin ( + PluginDescription description, + Class clazz, + URLClassLoader classLoader +) { + +} \ No newline at end of file diff --git a/src/main/java/de/igslandstuhl/database/modules/config/BoolSetting.java b/src/main/java/de/igslandstuhl/database/plugins/config/BoolSetting.java similarity index 87% rename from src/main/java/de/igslandstuhl/database/modules/config/BoolSetting.java rename to src/main/java/de/igslandstuhl/database/plugins/config/BoolSetting.java index 1b32004..1e368ff 100644 --- a/src/main/java/de/igslandstuhl/database/modules/config/BoolSetting.java +++ b/src/main/java/de/igslandstuhl/database/plugins/config/BoolSetting.java @@ -1,6 +1,6 @@ -package de.igslandstuhl.database.modules.config; +package de.igslandstuhl.database.plugins.config; -public class BoolSetting extends ModuleSetting { +public class BoolSetting extends PluginSetting { public BoolSetting(String key, String name, String description, boolean defaultValue) { super(key, name, description, defaultValue); } diff --git a/src/main/java/de/igslandstuhl/database/modules/config/ModuleConfig.java b/src/main/java/de/igslandstuhl/database/plugins/config/PluginConfig.java similarity index 83% rename from src/main/java/de/igslandstuhl/database/modules/config/ModuleConfig.java rename to src/main/java/de/igslandstuhl/database/plugins/config/PluginConfig.java index 40cb198..50145c5 100644 --- a/src/main/java/de/igslandstuhl/database/modules/config/ModuleConfig.java +++ b/src/main/java/de/igslandstuhl/database/plugins/config/PluginConfig.java @@ -1,29 +1,29 @@ -package de.igslandstuhl.database.modules.config; +package de.igslandstuhl.database.plugins.config; import java.util.Arrays; import java.util.List; -import de.igslandstuhl.database.modules.WebModule; +import de.igslandstuhl.database.plugins.Plugin; -public abstract class ModuleConfig { - private final T module; +public abstract class PluginConfig { + private final T plugin; private final BoolSetting[] boolSettings; - public ModuleConfig(T module, ModuleSetting[] moduleSettings) { - this.module = module; + public PluginConfig(T plugin, PluginSetting[] moduleSettings) { + this.plugin = plugin; List boolSettings = Arrays.stream(moduleSettings).filter((s) -> s instanceof BoolSetting).map((s) -> (BoolSetting) s).toList(); this.boolSettings = boolSettings.toArray(new BoolSetting[boolSettings.size()]); } - public ModuleConfig(T module, List> moduleSettings) { - this.module = module; + public PluginConfig(T plugin, List> moduleSettings) { + this.plugin = plugin; List boolSettings = moduleSettings.stream().filter((s) -> s instanceof BoolSetting).map((s) -> (BoolSetting) s).toList(); this.boolSettings = boolSettings.toArray(new BoolSetting[boolSettings.size()]); } - public T getModule() { - return module; + public T getPlugin() { + return plugin; } private BoolSetting findBoolSetting(String key) { diff --git a/src/main/java/de/igslandstuhl/database/modules/config/ModuleSetting.java b/src/main/java/de/igslandstuhl/database/plugins/config/PluginSetting.java similarity index 88% rename from src/main/java/de/igslandstuhl/database/modules/config/ModuleSetting.java rename to src/main/java/de/igslandstuhl/database/plugins/config/PluginSetting.java index 0fffab5..0d632fa 100644 --- a/src/main/java/de/igslandstuhl/database/modules/config/ModuleSetting.java +++ b/src/main/java/de/igslandstuhl/database/plugins/config/PluginSetting.java @@ -1,13 +1,13 @@ -package de.igslandstuhl.database.modules.config; +package de.igslandstuhl.database.plugins.config; -public class ModuleSetting { +public class PluginSetting { private final String key; private final String name; private final String description; private final T defaultValue; private T value; - ModuleSetting(String key, String name, String description, T defaultValue) { + PluginSetting(String key, String name, String description, T defaultValue) { this.key = key; this.name = name; this.description = description; diff --git a/src/main/java/de/igslandstuhl/database/server/resources/ResourceManager.java b/src/main/java/de/igslandstuhl/database/server/resources/ResourceManager.java index 0876064..af56e94 100644 --- a/src/main/java/de/igslandstuhl/database/server/resources/ResourceManager.java +++ b/src/main/java/de/igslandstuhl/database/server/resources/ResourceManager.java @@ -19,7 +19,7 @@ import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; -import de.igslandstuhl.database.modules.PluginResourceProvider; +import de.igslandstuhl.database.plugins.PluginResourceProvider; import de.igslandstuhl.database.server.Server; /** diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/handlers/GetRequestHandler.java b/src/main/java/de/igslandstuhl/database/server/webserver/handlers/GetRequestHandler.java index 5f06802..fc35cab 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/handlers/GetRequestHandler.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/handlers/GetRequestHandler.java @@ -6,7 +6,7 @@ import de.igslandstuhl.database.api.User; import de.igslandstuhl.database.server.Server; import de.igslandstuhl.database.server.webserver.WebPath; -import de.igslandstuhl.database.server.webserver.handlers.get.ModuleRequestHandler; +import de.igslandstuhl.database.server.webserver.handlers.get.PluginRequestHandler; import de.igslandstuhl.database.server.webserver.requests.GetRequest; import de.igslandstuhl.database.server.webserver.requests.RequestType; import de.igslandstuhl.database.server.webserver.responses.GetResponse; @@ -35,6 +35,7 @@ public final HttpResponse handleRequest(GetRequest request) { String path = request.getPath(); HttpHandler handler = Registry.getRequestHandlerRegistry().get(path); + if (handler == null) return GetResponse.notFound(request); return handler.handleHttpRequest(request); } @@ -54,9 +55,9 @@ public static GetResponse handleSQLRequest(GetRequest request) { String user = getUser(request).getUsername(); return GetResponse.getResource(request, request.toResourceLocation(user), user, false); } - public static GetResponse handleModuleRequest(GetRequest request) { + public static GetResponse handlePluginRequest(GetRequest request) { User user = getUser(request); - return ModuleRequestHandler.handleRequest(user, request); + return PluginRequestHandler.handleRequest(user, request); } public final void registerHandlers() { @@ -68,7 +69,7 @@ public final void registerHandlers() { case "FileRequestHandler" -> GetRequestHandler::handleFileRequest; case "TemplatingFileRequestHandler" -> GetRequestHandler::handleTemplatingFileRequest; case "SQLRequestHandler" -> GetRequestHandler::handleSQLRequest; - case "ModuleRequestHandler" -> GetRequestHandler::handleModuleRequest; + case "PluginRequestHandler" -> GetRequestHandler::handlePluginRequest; default -> throw new IllegalArgumentException("Unknown handler type: " + webPath.handlerType()); }; HttpHandler.registerGetRequestHandler(path, webPath.accessLevel(), handlerFunction); diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/handlers/PostRequestHandler.java b/src/main/java/de/igslandstuhl/database/server/webserver/handlers/PostRequestHandler.java index 5216f0f..6b8d4e3 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/handlers/PostRequestHandler.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/handlers/PostRequestHandler.java @@ -408,17 +408,17 @@ public static void registerHandlers() { handleObjectAction(rq, new TypeToken() {}, PostResponse.ok("Successfully changed graduation level", ContentType.TEXT_PLAIN, rq), (student) -> student.changeGraduationLevel(rq.getInt("graduationLevel"))) ); - HttpHandler.registerPostRequestHandler("/get-module", AccessLevel.USER, (rq) -> { - return PostResponse.ok(Registry.moduleRegistry().get(rq.getString("key")).toJSON(), ContentType.JSON, rq); + HttpHandler.registerPostRequestHandler("/get-plugin", AccessLevel.USER, (rq) -> { + return PostResponse.ok(Registry.pluginRegistry().get(rq.getString("key")).toJSON(), ContentType.JSON, rq); }); - HttpHandler.registerPostRequestHandler("/toggle-module", AccessLevel.ADMIN, (rq) -> { - Registry.moduleRegistry().get(rq.getString("key")).toggle(); - return PostResponse.ok("Module toggled", ContentType.TEXT_PLAIN, rq); + HttpHandler.registerPostRequestHandler("/toggle-plugin", AccessLevel.ADMIN, (rq) -> { + Registry.pluginRegistry().get(rq.getString("key")).toggle(); + return PostResponse.ok("Plugin toggled", ContentType.TEXT_PLAIN, rq); }); - HttpHandler.registerPostRequestHandler("/toggle-module-setting", AccessLevel.ADMIN, (rq) -> { + HttpHandler.registerPostRequestHandler("/toggle-plugin-setting", AccessLevel.ADMIN, (rq) -> { String[] key = rq.getString("key").split(":"); - Registry.moduleRegistry().get(key[0]).getConfig().toggleSetting(key[1]); - return PostResponse.ok("Module setting toggled", ContentType.TEXT_PLAIN, rq); + Registry.pluginRegistry().get(key[0]).getConfig().toggleSetting(key[1]); + return PostResponse.ok("Plugin setting toggled", ContentType.TEXT_PLAIN, rq); }); HttpHandler.registerPostRequestHandler("/student-results-csv", AccessLevel.TEACHER, (rq) -> { diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/handlers/get/ModuleRequestHandler.java b/src/main/java/de/igslandstuhl/database/server/webserver/handlers/get/PluginRequestHandler.java similarity index 71% rename from src/main/java/de/igslandstuhl/database/server/webserver/handlers/get/ModuleRequestHandler.java rename to src/main/java/de/igslandstuhl/database/server/webserver/handlers/get/PluginRequestHandler.java index a44feaa..c56842c 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/handlers/get/ModuleRequestHandler.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/handlers/get/PluginRequestHandler.java @@ -6,22 +6,22 @@ import de.igslandstuhl.database.server.webserver.requests.GetRequest; import de.igslandstuhl.database.server.webserver.responses.GetResponse; -public class ModuleRequestHandler { +public class PluginRequestHandler { public static GetResponse handleRequest(User user, GetRequest request) { if (user == null || !user.isAdmin()) return GetResponse.unauthorized(request); - if (!request.getPath().equals("/module-list")) return GetResponse.notFound(request); + if (!request.getPath().equals("/plugin-list")) return GetResponse.notFound(request); - return GetResponse.getResource(request, new ResourceLocation("virtual", "module", "list"), user.getUsername(), false); + return GetResponse.getResource(request, new ResourceLocation("virtual", "plugin", "list"), user.getUsername(), false); } - public static String getModuleResource(String resource) { + public static String getPluginResource(String resource) { if (resource.equals("list")) { return "[" + - Registry.moduleRegistry().keyStream() + Registry.pluginRegistry().keyStream() .reduce("", (s1, s2) -> s1 + ", \"" + s2 + "\"") .substring(2) + "]"; } else { - throw new NullPointerException("Module Resource not found: " + resource); + throw new NullPointerException("Plugin Resource not found: " + resource); } } } diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/responses/GetResponse.java b/src/main/java/de/igslandstuhl/database/server/webserver/responses/GetResponse.java index 6afd12f..6ed9b25 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/responses/GetResponse.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/responses/GetResponse.java @@ -10,7 +10,7 @@ import de.igslandstuhl.database.server.webserver.ContentType; import de.igslandstuhl.database.server.webserver.NoWebResourceException; import de.igslandstuhl.database.server.webserver.Status; -import de.igslandstuhl.database.server.webserver.handlers.get.ModuleRequestHandler; +import de.igslandstuhl.database.server.webserver.handlers.get.PluginRequestHandler; import de.igslandstuhl.database.server.webserver.requests.HttpRequest; /** @@ -176,8 +176,8 @@ public void respond(PrintStream out) { if (!resourceLocation.isVirtual()) { resource = Server.getInstance().getResourceManager().readResourceCompletely(resourceLocation); } else { - if (resourceLocation.namespace().equals("module")) { - resource = ModuleRequestHandler.getModuleResource(resourceLocation.resource()); + if (resourceLocation.namespace().equals("plugin")) { + resource = PluginRequestHandler.getPluginResource(resourceLocation.resource()); } else { resource = Server.getInstance().getResourceManager().readVirtualResource(user, resourceLocation); } diff --git a/src/main/resources/html/admin/dashboard.html b/src/main/resources/html/admin/dashboard.html index 48253fc..40e882f 100644 --- a/src/main/resources/html/admin/dashboard.html +++ b/src/main/resources/html/admin/dashboard.html @@ -12,7 +12,7 @@

Admin Optionen

  • Klassen verwalten
  • Fächer verwalten
  • Räume verwalten
  • -
  • Module verwalten
  • +
  • Module verwalten
  • HTML Editor
  • diff --git a/src/main/resources/html/admin/modules.html b/src/main/resources/html/admin/plugins.html similarity index 67% rename from src/main/resources/html/admin/modules.html rename to src/main/resources/html/admin/plugins.html index 2b40437..0550c4e 100644 --- a/src/main/resources/html/admin/modules.html +++ b/src/main/resources/html/admin/plugins.html @@ -4,8 +4,8 @@

    Module

    Willkommen in der Modulübersicht!

    -
    +
    \ No newline at end of file diff --git a/src/main/resources/js/admin/build_modules.js b/src/main/resources/js/admin/build_modules.js deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/resources/js/site/student-database.js b/src/main/resources/js/site/student-database.js index eb669b4..6836562 100644 --- a/src/main/resources/js/site/student-database.js +++ b/src/main/resources/js/site/student-database.js @@ -92,14 +92,14 @@ async function fetchTopicList(subjectId, grade) { async function fetchTasks(taskIds, studentId) { return await getJsonWithPost('/tasks', { ids: taskIds, studentId }); } -async function fetchModuleKeys() { - return await getJson('/module-list'); +async function fetchPluginKeys() { + return await getJson('/plugin-list'); } -async function fetchModule(moduleKey) { - return await getJsonWithPost('/get-module', { key: moduleKey }) +async function fetchPlugin(pluginKey) { + return await getJsonWithPost('/get-plugin', { key: pluginKey }) } -async function fetchModuleConfig(moduleKey) { - return (await fetchModule(moduleKey)).config; +async function fetchPluginConfig(pluginKey) { + return (await fetchPlugin(pluginKey)).config; } async function getStudents(classId) { @@ -162,8 +162,8 @@ async function beginTask(studentId, taskId) { async function updateRoom(studentId, room) { return await post('/update-room', { studentId, room }); } -async function toggleModule(moduleKey) { - return await post('/toggle-module', { key: moduleKey }) +async function togglePlugin(pluginKey) { + return await post('/toggle-plugin', { key: pluginKey }) } async function deleteClass(classId) { @@ -810,7 +810,7 @@ function loadStudentDashboard(studentData, subjects, teacherPerms) { // Show stu }); } async function loadStudentResultView(studentData) { - const config = await fetchModuleConfig('result_view'); + const config = await fetchPluginConfig('result_view'); document.getElementById('student-name').textContent = `${studentData.firstName} ${studentData.lastName}`; @@ -825,13 +825,13 @@ async function loadStudentResultView(studentData) { charts.appendChild(createBarChart(subject, subject.name, studentData, config.values)); }); } -let module_panels = {} -function loadModuleSection(moduleKey) { - return createPanel(moduleKey, document.createElement("div"), async (header, body) => { - const module = await fetchModule(moduleKey); - header.textContent = module.name; +let plugin_panels = {} +function loadPluginSection(pluginKey) { + return createPanel(pluginKey, document.createElement("div"), async (header, body) => { + const plugin = await fetchPlugin(pluginKey); + header.textContent = plugin.name; body.innerHTML = ` -

    ${module.description.replace("\n", "

    ")}

    +

    ${plugin.description.replace("\n", "

    ")}

    @@ -839,20 +839,20 @@ function loadModuleSection(moduleKey) { - - - + + +
    Key
    ID${module.id}
    Name${module.name}
    Enabled${module.enabled}
    ID${plugin.id}
    Name${plugin.name}
    Enabled${plugin.enabled}
    `; }) } -async function loadModulesView(moduleContainer) { - const modules = fetchModuleKeys(); - (await modules).forEach(async key => { - const moduleSection = loadModuleSection(key); - module_panels[key] = moduleSection; - moduleContainer.appendChild(moduleSection); +async function loadPluginsView(pluginContainer) { + const plugins = fetchPluginKeys(); + (await plugins).forEach(async key => { + const pluginSection = loadPluginSection(key); + plugin_panels[key] = pluginSection; + pluginContainer.appendChild(pluginSection); }); } const graduationLevels = ["Neustarter", "Starter", "Durchstarter", "Lernprofi"]; diff --git a/src/main/resources/meta/paths/get_paths.json b/src/main/resources/meta/paths/get_paths.json index 877b697..ff8c2a5 100644 --- a/src/main/resources/meta/paths/get_paths.json +++ b/src/main/resources/meta/paths/get_paths.json @@ -115,7 +115,7 @@ "access_level": "teacher" }, - "/modules": { + "/plugins": { "type": "GET", "handler_type": "TemplatingFileRequestHandler", "namespaces": ["admin"], @@ -266,14 +266,6 @@ "context": "js", "access_level": "teacher" }, - - "/build_modules.js": { - "type": "GET", - "handler_type": "FileRequestHandler", - "namespaces": ["admin"], - "context": "js", - "access_level": "admin" - }, "/build_teacher.js": { "type": "GET", "handler_type": "FileRequestHandler", @@ -331,10 +323,10 @@ "context": "imgs", "access_level": "public" }, - "/module-list": { + "/plugin-list": { "type": "GET", - "handler_type": "ModuleRequestHandler", - "namespaces": ["module"], + "handler_type": "PluginRequestHandler", + "namespaces": ["plugin"], "context": "virtual", "access_level": "admin" } From 77d4e6ab5de4172027b6964622721a0a76433fc4 Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Mon, 30 Mar 2026 19:37:42 +0200 Subject: [PATCH 14/36] Set version to 1.0.2 --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 51fb147..f5f3620 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ plugins { group = "igs-landstuhl" -version = "v1.0.1-PATCH-2" +version = "v1.0.2" application { mainClass.set("de.igslandstuhl.database.Application") From e83b6da501587b4715808674aa5c0f651d4a62dd Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Mon, 30 Mar 2026 19:40:44 +0200 Subject: [PATCH 15/36] Plugins are now enabled on start --- src/main/java/de/igslandstuhl/database/Application.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/de/igslandstuhl/database/Application.java b/src/main/java/de/igslandstuhl/database/Application.java index 3caa61d..f536f9a 100644 --- a/src/main/java/de/igslandstuhl/database/Application.java +++ b/src/main/java/de/igslandstuhl/database/Application.java @@ -114,6 +114,8 @@ public static void main(String[] args) throws Exception { Server.getInstance().getWebServer().start(); } + PluginLoader.getInstance().enablePlugins(); + while (true) { if (!getInstance().suppressCmd()) { CommandLineUtils.waitForCommandAndExec(); From d24a979dd6a5523e95428619a3d1d410af9fcb57 Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Mon, 30 Mar 2026 22:31:46 +0200 Subject: [PATCH 16/36] Adding config persistence --- .gitignore | 3 +- .../de/igslandstuhl/database/Application.java | 16 +++- .../de/igslandstuhl/database/Registry.java | 3 + .../igslandstuhl/database/plugins/Plugin.java | 8 +- .../database/plugins/PluginLoader.java | 17 +++- .../database/plugins/config/PluginConfig.java | 82 ++++++++++++++++--- 6 files changed, 107 insertions(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index 55d80c8..e6d18f3 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ target bin .gradle build -/modules \ No newline at end of file +/modules +/plugins \ No newline at end of file diff --git a/src/main/java/de/igslandstuhl/database/Application.java b/src/main/java/de/igslandstuhl/database/Application.java index f536f9a..a6d1bd0 100644 --- a/src/main/java/de/igslandstuhl/database/Application.java +++ b/src/main/java/de/igslandstuhl/database/Application.java @@ -4,6 +4,8 @@ import java.util.ArrayList; import java.util.List; +import org.jline.reader.UserInterruptException; + import de.igslandstuhl.database.api.SerializationException; import de.igslandstuhl.database.api.Subject; import de.igslandstuhl.database.api.Topic; @@ -116,10 +118,16 @@ public static void main(String[] args) throws Exception { PluginLoader.getInstance().enablePlugins(); - while (true) { - if (!getInstance().suppressCmd()) { - CommandLineUtils.waitForCommandAndExec(); + Runtime.getRuntime().addShutdownHook(new Thread(() -> PluginLoader.getInstance().unloadPlugins(),"Plugin cleanup thread")); + + try { + while (true) { + if (!getInstance().suppressCmd()) { + CommandLineUtils.waitForCommandAndExec(); + } } - } + } catch (UserInterruptException e) { + System.exit(0); + } // Program exit using Ctrl+C } } diff --git a/src/main/java/de/igslandstuhl/database/Registry.java b/src/main/java/de/igslandstuhl/database/Registry.java index 3fc0c20..6a6eafb 100644 --- a/src/main/java/de/igslandstuhl/database/Registry.java +++ b/src/main/java/de/igslandstuhl/database/Registry.java @@ -53,6 +53,9 @@ public synchronized Stream keyStream() { public synchronized V get(K key) { return objects.get(key); } + public synchronized void unregister(K key) { + objects.remove(key); + } @Override public void close() { diff --git a/src/main/java/de/igslandstuhl/database/plugins/Plugin.java b/src/main/java/de/igslandstuhl/database/plugins/Plugin.java index f8f2218..08c8317 100644 --- a/src/main/java/de/igslandstuhl/database/plugins/Plugin.java +++ b/src/main/java/de/igslandstuhl/database/plugins/Plugin.java @@ -53,7 +53,12 @@ public String toJSON() { return sb.toString(); } - public abstract PluginConfig getConfig(); + /** + * Returns the config of this plugin. + * This should never be null, as it will break the plugin lifecycle otherwise. + * @return the config + */ + public abstract PluginConfig getConfig(); protected abstract void onEnable(); protected abstract void onDisable(); @@ -75,6 +80,7 @@ public void toggle() { } else { enable(); } + getConfig().save(); } void load() { onLoad(); diff --git a/src/main/java/de/igslandstuhl/database/plugins/PluginLoader.java b/src/main/java/de/igslandstuhl/database/plugins/PluginLoader.java index 346c331..a87e3e0 100644 --- a/src/main/java/de/igslandstuhl/database/plugins/PluginLoader.java +++ b/src/main/java/de/igslandstuhl/database/plugins/PluginLoader.java @@ -105,9 +105,13 @@ public void load(PreLoadedPlugin preload) { plugin.init(preload.description()); registerPlugin(plugin); plugin.load(); + if (plugin.getConfig() == null) { + throw new NullPointerException("Plugin must have a config"); + } } catch (Exception e) { - System.err.println("Failed to load module: " + preload.description().id()); + System.err.println("Failed to load plugin: " + preload.description().id()); pluginInfos.remove(preload); + if (Registry.pluginRegistry().get(preload.description().id()) != null) Registry.pluginRegistry().unregister(preload.description().id()); e.printStackTrace(); } } @@ -136,15 +140,22 @@ public void loadAllPlugins(File folder) { pluginInfos.forEach(this::load); } public void enablePlugins() { - pluginInfos.forEach((p) -> Registry.pluginRegistry().get(p.description().id()).enable()); + pluginInfos.forEach((p) -> { + Plugin plugin = Registry.pluginRegistry().get(p.description().id()); + if (plugin.getConfig().isEnabledOnStart()) { + plugin.enable(); + } + }); } public void unloadPlugins() { Collections.reverse(pluginInfos); pluginInfos.forEach((p) -> { Plugin plugin = Registry.pluginRegistry().get(p.description().id()); + plugin.getConfig().save(); if (plugin != null && plugin.isEnabled()) { plugin.disable(); } + Registry.pluginRegistry().unregister(plugin.getId()); try { p.classLoader().close(); @@ -169,6 +180,6 @@ public void registerPlugins() { new BoolSetting("show_current_progress", "Show Current", "Whether to display the current progress to the subject (in percent)", true), new BoolSetting("show_current_grade", "Show Currently Achieved Grade", "Whether to display the grade the student would achieve when they decide to immediately stop working", false) ))); - loadAllPlugins(new File("modules")); + loadAllPlugins(new File("plugins")); } } diff --git a/src/main/java/de/igslandstuhl/database/plugins/config/PluginConfig.java b/src/main/java/de/igslandstuhl/database/plugins/config/PluginConfig.java index 50145c5..61c88c3 100644 --- a/src/main/java/de/igslandstuhl/database/plugins/config/PluginConfig.java +++ b/src/main/java/de/igslandstuhl/database/plugins/config/PluginConfig.java @@ -1,30 +1,47 @@ package de.igslandstuhl.database.plugins.config; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; import java.util.Arrays; import java.util.List; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; + import de.igslandstuhl.database.plugins.Plugin; public abstract class PluginConfig { private final T plugin; - private final BoolSetting[] boolSettings; - public PluginConfig(T plugin, PluginSetting[] moduleSettings) { + private final File configFile; + private boolean enabledOnStart; + + public PluginConfig(T plugin, PluginSetting... moduleSettings) { this.plugin = plugin; List boolSettings = Arrays.stream(moduleSettings).filter((s) -> s instanceof BoolSetting).map((s) -> (BoolSetting) s).toList(); this.boolSettings = boolSettings.toArray(new BoolSetting[boolSettings.size()]); + this.configFile = new File("plugins/config", plugin.getId() + ".json"); + load(); } public PluginConfig(T plugin, List> moduleSettings) { this.plugin = plugin; List boolSettings = moduleSettings.stream().filter((s) -> s instanceof BoolSetting).map((s) -> (BoolSetting) s).toList(); this.boolSettings = boolSettings.toArray(new BoolSetting[boolSettings.size()]); + this.configFile = new File("plugins/config", plugin.getId() + ".json"); + load(); } public T getPlugin() { return plugin; } + public boolean isEnabledOnStart() { + return enabledOnStart; + } private BoolSetting findBoolSetting(String key) { return Arrays.stream(boolSettings) @@ -38,17 +55,28 @@ public boolean getBool(String key) { public void setBool(String key, boolean value) { findBoolSetting(key).setValue(value); + save(); } public void enableSetting(String key) { findBoolSetting(key).enable(); + save(); } public void disableSetting(String key) { findBoolSetting(key).disable(); + save(); } public void toggleSetting(String key) { findBoolSetting(key).toggle(); + save(); } + private JsonObject valuesJSON() { + JsonObject values = new JsonObject(); + for (BoolSetting s : boolSettings) { + values.addProperty(s.getKey(), s.getValue()); + } + return values; + } public String toJSON() { StringBuilder builder = new StringBuilder("{"); builder.append("\"settings\": {") @@ -61,18 +89,46 @@ public String toJSON() { } builder .append("]}, ") - .append("\"values\": {"); - for (int i = 0; i < boolSettings.length; i++) { - builder - .append("\"") - .append(boolSettings[i].getKey()) - .append("\": ") - .append(boolSettings[i].getValue()); - if (i < boolSettings.length - 1) { - builder.append(", "); + .append("\"values\": ") + .append((new Gson()).toJson(valuesJSON())) + .append("}"); + return builder.toString(); + } + + // Persistence + public void save() { + if (configFile.getParentFile() != null && !configFile.getParentFile().exists()) { + configFile.getParentFile().mkdirs(); + } + try (FileWriter writer = new FileWriter(configFile)) { + JsonObject root = new JsonObject(); + root.add("values", valuesJSON()); + root.addProperty("enabled", plugin.isEnabled()); + + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + gson.toJson(root, writer); + } catch (IOException e) { + e.printStackTrace(); + } + } + public void load() { + if (!configFile.exists()) return; + try (FileReader reader = new FileReader(configFile)) { + Gson gson = new Gson(); + JsonObject root = gson.fromJson(reader, JsonObject.class); + + JsonObject values = root.getAsJsonObject("values"); + if (values != null) { + for (BoolSetting s : boolSettings) { + if (values.has(s.getKey())) { + s.setValue(values.get(s.getKey()).getAsBoolean()); + } + } } + enabledOnStart = root.get("enabled").getAsBoolean(); + + } catch (IOException e) { + e.printStackTrace(); } - builder.append("}}"); - return builder.toString(); } } \ No newline at end of file From eaf6c477779e671f814a9c3f795fd726d518a43a Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Mon, 30 Mar 2026 22:45:25 +0200 Subject: [PATCH 17/36] Added BoolSettings to module config page Fixed plugin page title --- src/main/resources/html/admin/plugins.html | 2 +- src/main/resources/js/site/student-database.js | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/main/resources/html/admin/plugins.html b/src/main/resources/html/admin/plugins.html index 0550c4e..f78d759 100644 --- a/src/main/resources/html/admin/plugins.html +++ b/src/main/resources/html/admin/plugins.html @@ -1,4 +1,4 @@ -%[site;title=Lehrer verwalten;content=!FOLLOWS] +%[site;title=Module verwalten;content=!FOLLOWS]

    Module

    diff --git a/src/main/resources/js/site/student-database.js b/src/main/resources/js/site/student-database.js index 6836562..4efa83a 100644 --- a/src/main/resources/js/site/student-database.js +++ b/src/main/resources/js/site/student-database.js @@ -163,7 +163,10 @@ async function updateRoom(studentId, room) { return await post('/update-room', { studentId, room }); } async function togglePlugin(pluginKey) { - return await post('/toggle-plugin', { key: pluginKey }) + return await post('/toggle-plugin', { key: pluginKey }); +} +async function togglePluginSetting(pluginKey, setting) { + return await post('/toggle-plugin-setting', { key: pluginKey + ":" + setting }); } async function deleteClass(classId) { @@ -845,6 +848,13 @@ function loadPluginSection(pluginKey) { `; + const tbody = body.getElementsByTagName("tbody")[0] + const settings = plugin.config.settings; + settings.bools.forEach((b) => { + const tr = document.createElement("tr"); + tr.innerHTML = `${b.name}${b.value}`; + tbody.appendChild(tr); + }); }) } async function loadPluginsView(pluginContainer) { From f1a8dbe517f2572dd722f97a2330bdc66a27cbed Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Mon, 30 Mar 2026 23:21:57 +0200 Subject: [PATCH 18/36] Now merged resources from different ResourceProviders are possible Currently supported: JSON object (shallow merge), JSON list (appending), js and css code (concatenation) --- .../plugins/PluginResourceProvider.java | 17 ++++ .../server/resources/MergeHelper.java | 82 +++++++++++++++++++ .../server/resources/ResourceManager.java | 42 ++++++++++ .../server/resources/ResourceProvider.java | 7 ++ .../database/server/webserver/WebPath.java | 2 +- 5 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 src/main/java/de/igslandstuhl/database/server/resources/MergeHelper.java diff --git a/src/main/java/de/igslandstuhl/database/plugins/PluginResourceProvider.java b/src/main/java/de/igslandstuhl/database/plugins/PluginResourceProvider.java index 30ca068..419dc8f 100644 --- a/src/main/java/de/igslandstuhl/database/plugins/PluginResourceProvider.java +++ b/src/main/java/de/igslandstuhl/database/plugins/PluginResourceProvider.java @@ -30,6 +30,23 @@ public InputStream open(ResourceLocation location) { return null; } + + @Override + public List openAll(ResourceLocation location) { + String path = location.context() + "/" + location.namespace() + "/" + location.resource(); + List inputStreams = new ArrayList<>(); + + for (PreLoadedPlugin module : PluginLoader.getInstance().getPluginInfos()) { + ClassLoader cl = module.classLoader(); + InputStream stream = cl.getResourceAsStream(path); + + if (stream != null) { + inputStreams.add(stream); + } + } + + return inputStreams; + } @Override public Collection list(Pattern pattern) { diff --git a/src/main/java/de/igslandstuhl/database/server/resources/MergeHelper.java b/src/main/java/de/igslandstuhl/database/server/resources/MergeHelper.java new file mode 100644 index 0000000..70ddfbc --- /dev/null +++ b/src/main/java/de/igslandstuhl/database/server/resources/MergeHelper.java @@ -0,0 +1,82 @@ +package de.igslandstuhl.database.server.resources; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +public class MergeHelper { + public static Map readJsonObjectMerged(ResourceManager manager, ResourceLocation location) { + Gson gson = new Gson(); + + return manager.mergeResources(location, + // merger + (a, b) -> { + a.putAll(b); + return a; + }, + //start supplier + HashMap::new, + // parser + (is) -> { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + return gson.fromJson(reader, new TypeToken>(){}.getType()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + ); + } + public static List readJsonListMerged(ResourceManager manager, ResourceLocation location) { + Gson gson = new Gson(); + + return manager.mergeResources( + location, + + // merger + (a, b) -> { + a.addAll(b); + return a; + }, + + // supplier + ArrayList::new, + + // parser + (is) -> { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(is))) { + return gson.fromJson(reader, new TypeToken>(){}.getType()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + ); + } + public static String readMergedCode(ResourceManager manager, ResourceLocation location) { + return manager.mergeResources( + location, + + // merger + (a, b) -> b + "\n" + a, // concat backwards + + // supplier + () -> "", + + // parser + (is) -> { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + return reader.lines().reduce("", (acc, line) -> acc + line + "\n"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + ); + } +} diff --git a/src/main/java/de/igslandstuhl/database/server/resources/ResourceManager.java b/src/main/java/de/igslandstuhl/database/server/resources/ResourceManager.java index af56e94..3707ac8 100644 --- a/src/main/java/de/igslandstuhl/database/server/resources/ResourceManager.java +++ b/src/main/java/de/igslandstuhl/database/server/resources/ResourceManager.java @@ -13,6 +13,9 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; import java.util.regex.Pattern; import java.util.stream.Stream; @@ -92,6 +95,21 @@ public InputStream openResourceAsStream(ResourceLocation location) throws FileNo throw new FileNotFoundException(location.toString()); } + /** + * Opens a resource as input stream in all sources that are found + * @param location the ResourceLocation object representing the resource + * @return an list of InputStreams for the resource + */ + public List openAll(ResourceLocation location) { + List streams = new ArrayList<>(); + + for (ResourceProvider provider : providers) { + streams.addAll(provider.openAll(location)); + } + + return streams; + } + /** * Reads the content of a resource completely as a String. * The resource is identified by its location, which includes context, namespace, and resource name. @@ -172,4 +190,28 @@ public Map readJsonResourceAsMap(ResourceLocation location) throws IOE return json; } } + public Map readJsonResourceMerged(ResourceLocation location) { + return MergeHelper.readJsonObjectMerged(this, location); + } + public String readCodeMerged(ResourceLocation location) { + return MergeHelper.readMergedCode(this, location); + } + + public T mergeResources(ResourceLocation location, BiFunction merger, Supplier start, Function parser) { + T result = start.get(); + + for (InputStream is : openAll(location)) { + try (InputStream stream = is) { + T part = parser.apply(stream); + + if (part != null) { + result = merger.apply(result, part); + } + } catch (IOException e) { + throw new RuntimeException("Failed to merge resource: " + location, e); + } + } + + return result; + } } \ No newline at end of file diff --git a/src/main/java/de/igslandstuhl/database/server/resources/ResourceProvider.java b/src/main/java/de/igslandstuhl/database/server/resources/ResourceProvider.java index 0817690..2983d0b 100644 --- a/src/main/java/de/igslandstuhl/database/server/resources/ResourceProvider.java +++ b/src/main/java/de/igslandstuhl/database/server/resources/ResourceProvider.java @@ -2,10 +2,17 @@ import java.io.InputStream; import java.util.Collection; +import java.util.List; import java.util.regex.Pattern; public interface ResourceProvider { InputStream open(ResourceLocation location); + + default List openAll(ResourceLocation location) { + InputStream stream = open(location); + if (stream == null) return List.of(); + else return List.of(stream); + } Collection list(Pattern pattern); } diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/WebPath.java b/src/main/java/de/igslandstuhl/database/server/webserver/WebPath.java index 7e13e61..3c11767 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/WebPath.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/WebPath.java @@ -16,7 +16,7 @@ public static void registerPath(String path, RequestType type, String handlerTyp public static void registerPaths() throws IOException { if (Registry.webPathRegistry().stream().count() > 0) return; // already registered ResourceLocation metaLocation = new ResourceLocation("meta", "paths", "get_paths.json"); - Map pathData = Server.getInstance().getResourceManager().readJsonResourceAsMap(metaLocation); + Map pathData = Server.getInstance().getResourceManager().readJsonResourceMerged(metaLocation); pathData.keySet().forEach((path) -> { @SuppressWarnings("unchecked") Map pathInfo = (Map) pathData.get(path); From e182a4cb29cc5392fcea3df3bc50b2b95bd1f920 Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Tue, 31 Mar 2026 00:41:30 +0200 Subject: [PATCH 19/36] Fixed a little bug with MergeHelper --- .../igslandstuhl/database/server/resources/MergeHelper.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/de/igslandstuhl/database/server/resources/MergeHelper.java b/src/main/java/de/igslandstuhl/database/server/resources/MergeHelper.java index 70ddfbc..bd5d603 100644 --- a/src/main/java/de/igslandstuhl/database/server/resources/MergeHelper.java +++ b/src/main/java/de/igslandstuhl/database/server/resources/MergeHelper.java @@ -5,7 +5,7 @@ import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -16,14 +16,14 @@ public class MergeHelper { public static Map readJsonObjectMerged(ResourceManager manager, ResourceLocation location) { Gson gson = new Gson(); - return manager.mergeResources(location, + return manager.mergeResources(location, // merger (a, b) -> { a.putAll(b); return a; }, //start supplier - HashMap::new, + () -> (Map)new LinkedHashMap(), // parser (is) -> { try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { From 69176f286d001a6fb4e46e683ef9b5b1bea4c08e Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Tue, 31 Mar 2026 00:41:41 +0200 Subject: [PATCH 20/36] New templating system --- .../de/igslandstuhl/database/Application.java | 2 + .../de/igslandstuhl/database/Registry.java | 37 ++++++++++++++ .../database/client/HTMLFileTemplate.java | 47 +++++++++++++++++ .../database/client/HTMLTemplate.java | 42 ++++++++++++++++ .../TemplatingPreprocessor.java | 50 +++---------------- .../navigation/HTMLNavigationTemplate.java | 14 ++++++ .../navigation/NavigationAppearance.java | 37 ++++++++++++++ .../client/navigation/NavigationElement.java | 4 ++ .../client/navigation/NavigationType.java | 5 ++ .../webserver/responses/GetResponse.java | 1 + .../resources/meta/templates/templates.json | 40 +++++++++++++++ 11 files changed, 237 insertions(+), 42 deletions(-) create mode 100644 src/main/java/de/igslandstuhl/database/client/HTMLFileTemplate.java create mode 100644 src/main/java/de/igslandstuhl/database/client/HTMLTemplate.java rename src/main/java/de/igslandstuhl/database/{server/webserver/responses => client}/TemplatingPreprocessor.java (65%) create mode 100644 src/main/java/de/igslandstuhl/database/client/navigation/HTMLNavigationTemplate.java create mode 100644 src/main/java/de/igslandstuhl/database/client/navigation/NavigationAppearance.java create mode 100644 src/main/java/de/igslandstuhl/database/client/navigation/NavigationElement.java create mode 100644 src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java create mode 100644 src/main/resources/meta/templates/templates.json diff --git a/src/main/java/de/igslandstuhl/database/Application.java b/src/main/java/de/igslandstuhl/database/Application.java index a6d1bd0..b91688f 100644 --- a/src/main/java/de/igslandstuhl/database/Application.java +++ b/src/main/java/de/igslandstuhl/database/Application.java @@ -9,6 +9,7 @@ import de.igslandstuhl.database.api.SerializationException; import de.igslandstuhl.database.api.Subject; import de.igslandstuhl.database.api.Topic; +import de.igslandstuhl.database.client.HTMLTemplate; import de.igslandstuhl.database.holidays.Holiday; import de.igslandstuhl.database.plugins.PluginLoader; import de.igslandstuhl.database.server.Server; @@ -110,6 +111,7 @@ public static void main(String[] args) throws Exception { PluginLoader.getInstance().registerPlugins(); WebPath.registerPaths(); + HTMLTemplate.registerAll(); GetRequestHandler.getInstance().registerHandlers(); if (getInstance().runsWebServer()) { diff --git a/src/main/java/de/igslandstuhl/database/Registry.java b/src/main/java/de/igslandstuhl/database/Registry.java index 6a6eafb..d539b1b 100644 --- a/src/main/java/de/igslandstuhl/database/Registry.java +++ b/src/main/java/de/igslandstuhl/database/Registry.java @@ -2,9 +2,14 @@ import java.io.Closeable; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import java.util.stream.Stream; +import de.igslandstuhl.database.client.HTMLTemplate; +import de.igslandstuhl.database.client.navigation.NavigationElement; +import de.igslandstuhl.database.client.navigation.NavigationType; import de.igslandstuhl.database.plugins.Plugin; import de.igslandstuhl.database.server.commands.Command; import de.igslandstuhl.database.server.commands.CommandDescription; @@ -20,6 +25,10 @@ public class Registry implements Closeable { private static final Registry> GET_HANDLER_REGISTRY = new Registry<>(); private static final Registry PLUGIN_REGISTRY = new Registry<>(); private static final Registry WEB_PATH_REGISTRY = new Registry<>(); + + private static final EnumRegistry NAVIGATION_REGISTRY = new EnumRegistry<>(NavigationType.class); + private static final Registry TEMPLATE_REGISTRY = new Registry<>(); + public static Registry commandRegistry() { return COMMAND_REGISTRY; } @@ -38,6 +47,12 @@ public static Registry commandDescriptionRegistry() public static Registry webPathRegistry() { return WEB_PATH_REGISTRY; } + public static EnumRegistry navigationRegistry() { + return NAVIGATION_REGISTRY; + } + public static Registry templateRegistry() { + return TEMPLATE_REGISTRY; + } private final Map objects = new HashMap<>(); @@ -57,6 +72,28 @@ public synchronized void unregister(K key) { objects.remove(key); } + public static class EnumRegistry, V> { + private final Map> objects = new HashMap<>(); + + private EnumRegistry(Class clazz) { + for (K k :clazz.getEnumConstants()) { + objects.put(k, new HashSet<>()); + } + } + public synchronized void register(K key, V value) { + objects.get(key).add(value); + } + public synchronized Stream stream(K key) { + return objects.get(key).stream(); + } + public synchronized Stream keyStream() { + return objects.keySet().stream(); + } + public synchronized void unregister(K key) { + objects.get(key).clear(); + } + } + @Override public void close() { objects.clear(); diff --git a/src/main/java/de/igslandstuhl/database/client/HTMLFileTemplate.java b/src/main/java/de/igslandstuhl/database/client/HTMLFileTemplate.java new file mode 100644 index 0000000..1b53f8e --- /dev/null +++ b/src/main/java/de/igslandstuhl/database/client/HTMLFileTemplate.java @@ -0,0 +1,47 @@ +package de.igslandstuhl.database.client; + +import java.io.FileNotFoundException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import de.igslandstuhl.database.server.Server; +import de.igslandstuhl.database.server.resources.ResourceLocation; + +public class HTMLFileTemplate implements HTMLTemplate { + private static String sanitizeTemplateName(String name) { + Path base = Paths.get("templates/html"); + Path resolved = base.resolve(name + ".html").normalize(); + + if (!resolved.startsWith(base)) { + throw new IllegalArgumentException("Invalid template name"); + } + + return resolved.getFileName().toString(); + } + private final String templateString; + public HTMLFileTemplate(ResourceLocation file) throws FileNotFoundException { + templateString = Server.getInstance().getResourceManager().readResourceCompletely(file); + } + public HTMLFileTemplate(String file) throws FileNotFoundException { + this(new ResourceLocation("templates", "html", sanitizeTemplateName(file))); + } + @Override + public String fill(Map args) { + if (templateString == null || templateString.isEmpty()) return ""; + Pattern p = Pattern.compile("%\\{([^}]+)\\}"); + Matcher m = p.matcher(templateString); + StringBuffer sb = new StringBuffer(); + while (m.find()) { + String key = m.group(1); + String val = args.getOrDefault(key, ""); + // escape backslashes and dollars for regex replacement + val = val.replace("\\", "\\\\").replace("$", "\\$"); + m.appendReplacement(sb, val); + } + m.appendTail(sb); + return sb.toString(); + } +} diff --git a/src/main/java/de/igslandstuhl/database/client/HTMLTemplate.java b/src/main/java/de/igslandstuhl/database/client/HTMLTemplate.java new file mode 100644 index 0000000..804dac4 --- /dev/null +++ b/src/main/java/de/igslandstuhl/database/client/HTMLTemplate.java @@ -0,0 +1,42 @@ +package de.igslandstuhl.database.client; + +import java.io.FileNotFoundException; +import java.util.Map; + +import de.igslandstuhl.database.Registry; +import de.igslandstuhl.database.client.navigation.HTMLNavigationTemplate; +import de.igslandstuhl.database.client.navigation.NavigationAppearance; +import de.igslandstuhl.database.client.navigation.NavigationType; +import de.igslandstuhl.database.server.Server; +import de.igslandstuhl.database.server.resources.ResourceLocation; + +public interface HTMLTemplate { + public static final ResourceLocation meta = new ResourceLocation("meta", "templates", "templates.json"); + public String fill(Map args); + private static void register(HTMLTemplate template, String key) { + Registry.templateRegistry().register(key, template); + } + public static void registerAll() { + Map json = Server.getInstance().getResourceManager().readJsonResourceMerged(meta); + json.keySet().forEach((key) -> { + @SuppressWarnings("unchecked") + Map template = (Map) json.get(key); + + String type = (String) template.get("type"); + switch (type) { + case "HTMLFileTemplate": + try { + register(new HTMLFileTemplate((String) template.get("path")), key); + } catch (FileNotFoundException e) { + System.err.println("Failed to load html template " + key); + e.printStackTrace(); + } + break; + case "HTMLNavigationTemplate": + register(new HTMLNavigationTemplate(NavigationAppearance.valueOf((String) template.get("appearance")), NavigationType.valueOf((String) template.get("navigation_type"))), key); + default: + break; + } + }); + } +} diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/responses/TemplatingPreprocessor.java b/src/main/java/de/igslandstuhl/database/client/TemplatingPreprocessor.java similarity index 65% rename from src/main/java/de/igslandstuhl/database/server/webserver/responses/TemplatingPreprocessor.java rename to src/main/java/de/igslandstuhl/database/client/TemplatingPreprocessor.java index 2abe8f8..6ef5fe1 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/responses/TemplatingPreprocessor.java +++ b/src/main/java/de/igslandstuhl/database/client/TemplatingPreprocessor.java @@ -1,16 +1,11 @@ -package de.igslandstuhl.database.server.webserver.responses; +package de.igslandstuhl.database.client; import java.io.FileNotFoundException; import java.io.IOException; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import de.igslandstuhl.database.server.Server; -import de.igslandstuhl.database.server.resources.ResourceLocation; +import de.igslandstuhl.database.Registry; public class TemplatingPreprocessor { private static final TemplatingPreprocessor instance = new TemplatingPreprocessor(); @@ -20,20 +15,8 @@ public static TemplatingPreprocessor getInstance() { private TemplatingPreprocessor() {} - private static String sanitizeTemplateName(String name) { - Path base = Paths.get("templates/html"); - Path resolved = base.resolve(name + ".html").normalize(); - - if (!resolved.startsWith(base)) { - throw new IllegalArgumentException("Invalid template name"); - } - - return resolved.getFileName().toString(); - } - - private String getTemplate(String name) throws FileNotFoundException { - ResourceLocation templateLocation = new ResourceLocation("templates", "html", sanitizeTemplateName(name)); - return Server.getInstance().getResourceManager().readResourceCompletely(templateLocation); + private HTMLTemplate getTemplate(String name) throws FileNotFoundException { + return Registry.templateRegistry().get(name); } public String executeTemplating(String content) throws IOException { @@ -92,8 +75,8 @@ public String executeTemplating(String content) throws IOException { String expandedRest = executeTemplating(rest); args.put(followsKey, expandedRemapNull(expandedRest)); // build template and finish (rest is consumed by this template) - String template = getTemplate(templateName); - String filled = fillTemplate(template, args); + HTMLTemplate template = getTemplate(templateName); + String filled = template.fill(args); // expand any templates produced by the filled template String finalFilled = executeTemplating(filled); out.append(finalFilled); @@ -102,8 +85,8 @@ public String executeTemplating(String content) throws IOException { break; } else { // normal case: build template now, then continue after marker - String template = getTemplate(templateName); - String filled = fillTemplate(template, args); + HTMLTemplate template = getTemplate(templateName); + String filled = template.fill(args); // expand templates that might be present inside the filled template String finalFilled = executeTemplating(filled); out.append(finalFilled); @@ -114,23 +97,6 @@ public String executeTemplating(String content) throws IOException { return out.toString(); } - // replace %{key} with args.getOrDefault(key, "") - private static String fillTemplate(String template, Map args) { - if (template == null || template.isEmpty()) return ""; - Pattern p = Pattern.compile("%\\{([^}]+)\\}"); - Matcher m = p.matcher(template); - StringBuffer sb = new StringBuffer(); - while (m.find()) { - String key = m.group(1); - String val = args.getOrDefault(key, ""); - // escape backslashes and dollars for regex replacement - val = val.replace("\\", "\\\\").replace("$", "\\$"); - m.appendReplacement(sb, val); - } - m.appendTail(sb); - return sb.toString(); - } - // helper in case expanded rest is null private static String expandedRemapNull(String s) { return s == null ? "" : s; diff --git a/src/main/java/de/igslandstuhl/database/client/navigation/HTMLNavigationTemplate.java b/src/main/java/de/igslandstuhl/database/client/navigation/HTMLNavigationTemplate.java new file mode 100644 index 0000000..ccb8956 --- /dev/null +++ b/src/main/java/de/igslandstuhl/database/client/navigation/HTMLNavigationTemplate.java @@ -0,0 +1,14 @@ +package de.igslandstuhl.database.client.navigation; + +import java.util.Map; + +import de.igslandstuhl.database.Registry; +import de.igslandstuhl.database.client.HTMLTemplate; + +public record HTMLNavigationTemplate(NavigationAppearance appearance, NavigationType type) implements HTMLTemplate { + @Override + public String fill(Map args) { + // args are not being used by this template + return appearance().translateToHTML(Registry.navigationRegistry().stream(type()).toList()); + } +} diff --git a/src/main/java/de/igslandstuhl/database/client/navigation/NavigationAppearance.java b/src/main/java/de/igslandstuhl/database/client/navigation/NavigationAppearance.java new file mode 100644 index 0000000..9f51181 --- /dev/null +++ b/src/main/java/de/igslandstuhl/database/client/navigation/NavigationAppearance.java @@ -0,0 +1,37 @@ +package de.igslandstuhl.database.client.navigation; + +import java.util.List; +import java.util.function.Function; + +public enum NavigationAppearance { + LIST_APPEARANCE ((l) -> { + StringBuilder builder = new StringBuilder(""); + return builder.toString(); + }), + BUTTON_APPEARANCE ((l) -> { + StringBuilder builder = new StringBuilder(); + l.forEach((e) -> { + builder.append("") + .append(""); + }); + return builder.toString(); + }); + private final Function,String> translator; + private NavigationAppearance(Function,String> translator) { + this.translator = translator; + } + public String translateToHTML(List navigationElements) { + return translator.apply(navigationElements); + } +} diff --git a/src/main/java/de/igslandstuhl/database/client/navigation/NavigationElement.java b/src/main/java/de/igslandstuhl/database/client/navigation/NavigationElement.java new file mode 100644 index 0000000..efa931f --- /dev/null +++ b/src/main/java/de/igslandstuhl/database/client/navigation/NavigationElement.java @@ -0,0 +1,4 @@ +package de.igslandstuhl.database.client.navigation; + +public record NavigationElement(String path, String label) { +} diff --git a/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java b/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java new file mode 100644 index 0000000..30d97a1 --- /dev/null +++ b/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java @@ -0,0 +1,5 @@ +package de.igslandstuhl.database.client.navigation; + +public enum NavigationType { + ADMIN_DASHBOARD +} diff --git a/src/main/java/de/igslandstuhl/database/server/webserver/responses/GetResponse.java b/src/main/java/de/igslandstuhl/database/server/webserver/responses/GetResponse.java index 6ed9b25..327a575 100644 --- a/src/main/java/de/igslandstuhl/database/server/webserver/responses/GetResponse.java +++ b/src/main/java/de/igslandstuhl/database/server/webserver/responses/GetResponse.java @@ -4,6 +4,7 @@ import java.io.InputStream; import java.io.PrintStream; +import de.igslandstuhl.database.client.TemplatingPreprocessor; import de.igslandstuhl.database.server.Server; import de.igslandstuhl.database.server.resources.ResourceLocation; import de.igslandstuhl.database.server.webserver.AccessManager; diff --git a/src/main/resources/meta/templates/templates.json b/src/main/resources/meta/templates/templates.json new file mode 100644 index 0000000..2950a88 --- /dev/null +++ b/src/main/resources/meta/templates/templates.json @@ -0,0 +1,40 @@ +{ + "class_info": { + "type": "HTMLFileTemplate", + "path": "class_info" + }, + "file_import": { + "type": "HTMLFileTemplate", + "path": "file_import" + }, + "login": { + "type": "HTMLFileTemplate", + "path": "login" + }, + "room_info": { + "type": "HTMLFileTemplate", + "path": "room_info" + }, + "site": { + "type": "HTMLFileTemplate", + "path": "site" + }, + "student_dashboard": { + "type": "HTMLFileTemplate", + "path": "student_dashboard" + }, + "student_results": { + "type": "HTMLFileTemplate", + "path": "student_results" + }, + "subject_info": { + "type": "HTMLFileTemplate", + "path": "subject_info" + }, + + "admin_dashboard_nav": { + "type": "HTMLNavigationTemplate", + "navigation_type": "ADMIN_DASHBOARD", + "appearance": "LIST_APPEARANCE" + } +} \ No newline at end of file From 06c0f3a605346c7ea13676dca66de90bc968b4e9 Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Tue, 31 Mar 2026 01:00:34 +0200 Subject: [PATCH 21/36] Admin dashboard navigation is now extendable by plugins pretty easily --- .../database/client/HTMLTemplate.java | 2 + .../client/navigation/NavigationElement.java | 17 +++++++++ .../server/resources/MergeHelper.java | 4 +- .../server/resources/ResourceManager.java | 3 ++ src/main/resources/html/admin/dashboard.html | 10 +---- .../meta/navigation/navigation_elements.json | 37 +++++++++++++++++++ 6 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 src/main/resources/meta/navigation/navigation_elements.json diff --git a/src/main/java/de/igslandstuhl/database/client/HTMLTemplate.java b/src/main/java/de/igslandstuhl/database/client/HTMLTemplate.java index 804dac4..79a3d20 100644 --- a/src/main/java/de/igslandstuhl/database/client/HTMLTemplate.java +++ b/src/main/java/de/igslandstuhl/database/client/HTMLTemplate.java @@ -6,6 +6,7 @@ import de.igslandstuhl.database.Registry; import de.igslandstuhl.database.client.navigation.HTMLNavigationTemplate; import de.igslandstuhl.database.client.navigation.NavigationAppearance; +import de.igslandstuhl.database.client.navigation.NavigationElement; import de.igslandstuhl.database.client.navigation.NavigationType; import de.igslandstuhl.database.server.Server; import de.igslandstuhl.database.server.resources.ResourceLocation; @@ -17,6 +18,7 @@ private static void register(HTMLTemplate template, String key) { Registry.templateRegistry().register(key, template); } public static void registerAll() { + NavigationElement.registerAll(); Map json = Server.getInstance().getResourceManager().readJsonResourceMerged(meta); json.keySet().forEach((key) -> { @SuppressWarnings("unchecked") diff --git a/src/main/java/de/igslandstuhl/database/client/navigation/NavigationElement.java b/src/main/java/de/igslandstuhl/database/client/navigation/NavigationElement.java index efa931f..3090a5d 100644 --- a/src/main/java/de/igslandstuhl/database/client/navigation/NavigationElement.java +++ b/src/main/java/de/igslandstuhl/database/client/navigation/NavigationElement.java @@ -1,4 +1,21 @@ package de.igslandstuhl.database.client.navigation; +import java.util.List; +import java.util.Map; + +import com.google.gson.reflect.TypeToken; + +import de.igslandstuhl.database.Registry; +import de.igslandstuhl.database.server.Server; +import de.igslandstuhl.database.server.resources.ResourceLocation; + public record NavigationElement(String path, String label) { + private static final ResourceLocation meta = new ResourceLocation("meta", "navigation", "navigation_elements.json"); + + public static void registerAll() { + List> elements = Server.getInstance().getResourceManager().readJsonListMerged(meta, new TypeToken>>() {}); + elements.forEach((e) -> { + Registry.navigationRegistry().register(NavigationType.valueOf(e.get("type")), new NavigationElement(e.get("path"), e.get("label"))); + }); + } } diff --git a/src/main/java/de/igslandstuhl/database/server/resources/MergeHelper.java b/src/main/java/de/igslandstuhl/database/server/resources/MergeHelper.java index bd5d603..ced9017 100644 --- a/src/main/java/de/igslandstuhl/database/server/resources/MergeHelper.java +++ b/src/main/java/de/igslandstuhl/database/server/resources/MergeHelper.java @@ -34,7 +34,7 @@ public class MergeHelper { } ); } - public static List readJsonListMerged(ResourceManager manager, ResourceLocation location) { + public static List readJsonListMerged(ResourceManager manager, ResourceLocation location, TypeToken> listType) { Gson gson = new Gson(); return manager.mergeResources( @@ -52,7 +52,7 @@ public static List readJsonListMerged(ResourceManager manager, ResourceL // parser (is) -> { try (BufferedReader reader = new BufferedReader(new InputStreamReader(is))) { - return gson.fromJson(reader, new TypeToken>(){}.getType()); + return gson.fromJson(reader, listType.getType()); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/src/main/java/de/igslandstuhl/database/server/resources/ResourceManager.java b/src/main/java/de/igslandstuhl/database/server/resources/ResourceManager.java index 3707ac8..567495c 100644 --- a/src/main/java/de/igslandstuhl/database/server/resources/ResourceManager.java +++ b/src/main/java/de/igslandstuhl/database/server/resources/ResourceManager.java @@ -193,6 +193,9 @@ public Map readJsonResourceAsMap(ResourceLocation location) throws IOE public Map readJsonResourceMerged(ResourceLocation location) { return MergeHelper.readJsonObjectMerged(this, location); } + public List readJsonListMerged(ResourceLocation location, TypeToken> token) { + return MergeHelper.readJsonListMerged(this, location, token); + } public String readCodeMerged(ResourceLocation location) { return MergeHelper.readMergedCode(this, location); } diff --git a/src/main/resources/html/admin/dashboard.html b/src/main/resources/html/admin/dashboard.html index 40e882f..0d5bb68 100644 --- a/src/main/resources/html/admin/dashboard.html +++ b/src/main/resources/html/admin/dashboard.html @@ -6,15 +6,7 @@

    Admin Dashboard

    Willkommen im Admin-Dashboard!

    Admin Optionen

    - + %[admin_dashboard_nav]
    %[file_import;header=Datenimport aus Lehrerplanungstool;note=Hinweis: Stellen Sie sicher, dass das jeweilige Fach bereits existiert, und der jeweiligen Klassenstufe zugeordnet ist.;uploadUrl=/lpt-file]
    \ No newline at end of file diff --git a/src/main/resources/meta/navigation/navigation_elements.json b/src/main/resources/meta/navigation/navigation_elements.json new file mode 100644 index 0000000..c494c37 --- /dev/null +++ b/src/main/resources/meta/navigation/navigation_elements.json @@ -0,0 +1,37 @@ +[ + { + "type": "ADMIN_DASHBOARD", + "path": "/manage_classes", + "label": "Klassen verwalten" + }, + { + "type": "ADMIN_DASHBOARD", + "path": "/manage_rooms", + "label": "Räume verwalten" + }, + { + "type": "ADMIN_DASHBOARD", + "path": "/manage_students", + "label": "Schüler verwalten" + }, + { + "type": "ADMIN_DASHBOARD", + "path": "/manage_subjects", + "label": "Fächer verwalten" + }, + { + "type": "ADMIN_DASHBOARD", + "path": "/manage_teachers", + "label": "Lehrer verwalten" + }, + { + "type": "ADMIN_DASHBOARD", + "path": "/plugins", + "label": "Module verwalten" + }, + { + "type": "ADMIN_DASHBOARD", + "path": "/editor", + "label": "HTML Editor" + } +] \ No newline at end of file From 781d22c7c23f638898a5b8f380452c206081db4b Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Tue, 31 Mar 2026 01:09:31 +0200 Subject: [PATCH 22/36] Added student dashboard nav --- .../database/client/navigation/NavigationType.java | 3 ++- src/main/resources/html/user/dashboard.html | 6 +++--- src/main/resources/meta/navigation/navigation_elements.json | 6 ++++++ src/main/resources/meta/templates/templates.json | 5 +++++ 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java b/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java index 30d97a1..d000562 100644 --- a/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java +++ b/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java @@ -1,5 +1,6 @@ package de.igslandstuhl.database.client.navigation; public enum NavigationType { - ADMIN_DASHBOARD + ADMIN_DASHBOARD, + STUDENT_DASHBOARD } diff --git a/src/main/resources/html/user/dashboard.html b/src/main/resources/html/user/dashboard.html index 8b20dad..1111937 100644 --- a/src/main/resources/html/user/dashboard.html +++ b/src/main/resources/html/user/dashboard.html @@ -1,4 +1,4 @@ %[student_dashboard;script=build_dashboard.js;nav= - - -] \ No newline at end of file + !FOLLOWS +] +%[student_dashboard_nav] \ No newline at end of file diff --git a/src/main/resources/meta/navigation/navigation_elements.json b/src/main/resources/meta/navigation/navigation_elements.json index c494c37..1199a9c 100644 --- a/src/main/resources/meta/navigation/navigation_elements.json +++ b/src/main/resources/meta/navigation/navigation_elements.json @@ -33,5 +33,11 @@ "type": "ADMIN_DASHBOARD", "path": "/editor", "label": "HTML Editor" + }, + + { + "type": "STUDENT_DASHBOARD", + "path": "/results", + "label": "Meine Ergebnisse anzeigen" } ] \ No newline at end of file diff --git a/src/main/resources/meta/templates/templates.json b/src/main/resources/meta/templates/templates.json index 2950a88..a4dab22 100644 --- a/src/main/resources/meta/templates/templates.json +++ b/src/main/resources/meta/templates/templates.json @@ -36,5 +36,10 @@ "type": "HTMLNavigationTemplate", "navigation_type": "ADMIN_DASHBOARD", "appearance": "LIST_APPEARANCE" + }, + "student_dashboard_nav": { + "type": "HTMLNavigationTemplate", + "navigation_type": "STUDENT_DASHBOARD", + "appearance": "BUTTON_APPEARANCE" } } \ No newline at end of file From b755c0e5e2057cc0fc6391a3546121d91c6d1e00 Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Tue, 31 Mar 2026 01:16:25 +0200 Subject: [PATCH 23/36] Added STUDENT_OTHER navigation type --- .../database/client/navigation/NavigationType.java | 3 ++- src/main/resources/html/user/partner_search.html | 2 +- src/main/resources/html/user/results.html | 5 +++-- .../meta/navigation/navigation_elements.json | 11 +++++++++++ src/main/resources/meta/templates/templates.json | 5 +++++ 5 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java b/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java index d000562..27f209e 100644 --- a/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java +++ b/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java @@ -2,5 +2,6 @@ public enum NavigationType { ADMIN_DASHBOARD, - STUDENT_DASHBOARD + STUDENT_DASHBOARD, + STUDENT_OTHER } diff --git a/src/main/resources/html/user/partner_search.html b/src/main/resources/html/user/partner_search.html index 0c36837..56d316f 100644 --- a/src/main/resources/html/user/partner_search.html +++ b/src/main/resources/html/user/partner_search.html @@ -16,7 +16,7 @@

    Partnersuche

    \ No newline at end of file diff --git a/src/main/resources/html/user/results.html b/src/main/resources/html/user/results.html index d123147..72b7f22 100644 --- a/src/main/resources/html/user/results.html +++ b/src/main/resources/html/user/results.html @@ -1,3 +1,4 @@ %[student_results;nav= - -] \ No newline at end of file + !FOLLOWS +] +%[student_nav] \ No newline at end of file diff --git a/src/main/resources/meta/navigation/navigation_elements.json b/src/main/resources/meta/navigation/navigation_elements.json index 1199a9c..f796be7 100644 --- a/src/main/resources/meta/navigation/navigation_elements.json +++ b/src/main/resources/meta/navigation/navigation_elements.json @@ -39,5 +39,16 @@ "type": "STUDENT_DASHBOARD", "path": "/results", "label": "Meine Ergebnisse anzeigen" + }, + { + "type": "STUDENT_DASHBOARD", + "path": "/partner_search", + "label": "Partner suchen" + }, + + { + "type": "STUDENT_OTHER", + "path": "/dashboard", + "label": "Zurück zum Dashboard" } ] \ No newline at end of file diff --git a/src/main/resources/meta/templates/templates.json b/src/main/resources/meta/templates/templates.json index a4dab22..04180fc 100644 --- a/src/main/resources/meta/templates/templates.json +++ b/src/main/resources/meta/templates/templates.json @@ -41,5 +41,10 @@ "type": "HTMLNavigationTemplate", "navigation_type": "STUDENT_DASHBOARD", "appearance": "BUTTON_APPEARANCE" + }, + "student_nav": { + "type": "HTMLNavigationTemplate", + "navigation_type": "STUDENT_OTHER", + "appearance": "BUTTON_APPEARANCE" } } \ No newline at end of file From bcc7123e437100eb9be7b694a5036458007a3cda Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Tue, 31 Mar 2026 01:23:47 +0200 Subject: [PATCH 24/36] Bumped version to v1.1.0-SNAPSHOT-0 --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index f5f3620..300b4f8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ plugins { group = "igs-landstuhl" -version = "v1.0.2" +version = "v1.1.0-SNAPSHOT-0" application { mainClass.set("de.igslandstuhl.database.Application") From 3b4eb58ccd72560e47b8791db0c1c51fc6a99db2 Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Tue, 31 Mar 2026 01:29:19 +0200 Subject: [PATCH 25/36] Added new navigation type: admin student view --- .../client/navigation/NavigationType.java | 1 + src/main/resources/html/admin/student.html | 8 ++----- .../meta/navigation/navigation_elements.json | 21 +++++++++++++++++++ .../resources/meta/templates/templates.json | 5 +++++ 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java b/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java index 27f209e..cf79a90 100644 --- a/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java +++ b/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java @@ -2,6 +2,7 @@ public enum NavigationType { ADMIN_DASHBOARD, + ADMIN_STUDENT, STUDENT_DASHBOARD, STUDENT_OTHER } diff --git a/src/main/resources/html/admin/student.html b/src/main/resources/html/admin/student.html index 2ea49f3..ce7590b 100644 --- a/src/main/resources/html/admin/student.html +++ b/src/main/resources/html/admin/student.html @@ -1,6 +1,2 @@ -%[student_dashboard;script=build_student.js;nav= - - - - -] \ No newline at end of file +%[student_dashboard;script=build_student.js;nav=!FOLLOWS] +%[admin_student_nav] \ No newline at end of file diff --git a/src/main/resources/meta/navigation/navigation_elements.json b/src/main/resources/meta/navigation/navigation_elements.json index f796be7..f98b0d9 100644 --- a/src/main/resources/meta/navigation/navigation_elements.json +++ b/src/main/resources/meta/navigation/navigation_elements.json @@ -34,6 +34,27 @@ "path": "/editor", "label": "HTML Editor" }, + + { + "type": "ADMIN_STUDENT", + "path": "/student-results", + "label": "Ergebnisse anzeigen" + }, + { + "type": "ADMIN_STUDENT", + "path": "/teacher", + "label": "Zurück zur Lehrer-Ansicht" + }, + { + "type": "ADMIN_STUDENT", + "path": "/class", + "label": "Zurück zur Klassen-Ansicht" + }, + { + "type": "ADMIN_STUDENT", + "path": "/dashboard", + "label": "Zurück zum Admin-Dashboard" + }, { "type": "STUDENT_DASHBOARD", diff --git a/src/main/resources/meta/templates/templates.json b/src/main/resources/meta/templates/templates.json index 04180fc..7fb6eba 100644 --- a/src/main/resources/meta/templates/templates.json +++ b/src/main/resources/meta/templates/templates.json @@ -37,6 +37,11 @@ "navigation_type": "ADMIN_DASHBOARD", "appearance": "LIST_APPEARANCE" }, + "admin_student_nav": { + "type": "HTMLNavigationTemplate", + "navigation_type": "ADMIN_STUDENT", + "appearance": "BUTTON_APPEARANCE" + }, "student_dashboard_nav": { "type": "HTMLNavigationTemplate", "navigation_type": "STUDENT_DASHBOARD", From af77b4d3206bb2247b396464e4b5decc90da4fde Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Tue, 31 Mar 2026 01:34:23 +0200 Subject: [PATCH 26/36] New Navigation type: admin student other nav --- .../client/navigation/NavigationType.java | 1 + .../resources/html/admin/student-results.html | 38 +++++++++---------- .../meta/navigation/navigation_elements.json | 21 ++++++++++ .../resources/meta/templates/templates.json | 5 +++ 4 files changed, 44 insertions(+), 21 deletions(-) diff --git a/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java b/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java index cf79a90..c15d331 100644 --- a/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java +++ b/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java @@ -3,6 +3,7 @@ public enum NavigationType { ADMIN_DASHBOARD, ADMIN_STUDENT, + ADMIN_STUDENT_OTHER, STUDENT_DASHBOARD, STUDENT_OTHER } diff --git a/src/main/resources/html/admin/student-results.html b/src/main/resources/html/admin/student-results.html index d295208..1001f46 100644 --- a/src/main/resources/html/admin/student-results.html +++ b/src/main/resources/html/admin/student-results.html @@ -1,22 +1,18 @@ -%[student_results;nav= - - - - -
    - - -
    - -] \ No newline at end of file + postDataAndDownload('/student-results-csv', JSON.stringify({ studentId }), 'schueler_ergebnisse_' + studentId + '.csv'); + }); + document.addEventListener('DOMContentLoaded', async () => { + document.getElementById('studentId').value = studentId; + }) + \ No newline at end of file diff --git a/src/main/resources/meta/navigation/navigation_elements.json b/src/main/resources/meta/navigation/navigation_elements.json index f98b0d9..2ed3dd7 100644 --- a/src/main/resources/meta/navigation/navigation_elements.json +++ b/src/main/resources/meta/navigation/navigation_elements.json @@ -56,6 +56,27 @@ "label": "Zurück zum Admin-Dashboard" }, + { + "type": "ADMIN_STUDENT_OTHER", + "path": "/student", + "label": "Zurück zur Schüler-Ansicht" + }, + { + "type": "ADMIN_STUDENT_OTHER", + "path": "/teacher", + "label": "Zurück zur Lehrer-Ansicht" + }, + { + "type": "ADMIN_STUDENT_OTHER", + "path": "/class", + "label": "Zurück zur Klassen-Ansicht" + }, + { + "type": "ADMIN_STUDENT_OTHER", + "path": "/dashboard", + "label": "Zurück zum Admin-Dashboard" + }, + { "type": "STUDENT_DASHBOARD", "path": "/results", diff --git a/src/main/resources/meta/templates/templates.json b/src/main/resources/meta/templates/templates.json index 7fb6eba..0784b76 100644 --- a/src/main/resources/meta/templates/templates.json +++ b/src/main/resources/meta/templates/templates.json @@ -42,6 +42,11 @@ "navigation_type": "ADMIN_STUDENT", "appearance": "BUTTON_APPEARANCE" }, + "admin_student_other_nav": { + "type": "HTMLNavigationTemplate", + "navigation_type": "ADMIN_STUDENT_OTHER", + "appearance": "BUTTON_APPEARANCE" + }, "student_dashboard_nav": { "type": "HTMLNavigationTemplate", "navigation_type": "STUDENT_DASHBOARD", From 12fca759e5632449374cacd95942e8f4e0f3ced8 Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Tue, 31 Mar 2026 02:00:35 +0200 Subject: [PATCH 27/36] Added new navigation type for other admin pages --- .../database/client/navigation/NavigationType.java | 1 + src/main/resources/html/admin/manage_classes.html | 2 +- src/main/resources/html/admin/manage_rooms.html | 2 +- src/main/resources/html/admin/manage_students.html | 2 +- src/main/resources/html/admin/manage_subjects.html | 2 +- src/main/resources/html/admin/manage_teachers.html | 2 +- src/main/resources/html/admin/plugins.html | 3 +++ src/main/resources/meta/navigation/navigation_elements.json | 6 ++++++ src/main/resources/meta/templates/templates.json | 5 +++++ 9 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java b/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java index c15d331..9f4a8aa 100644 --- a/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java +++ b/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java @@ -2,6 +2,7 @@ public enum NavigationType { ADMIN_DASHBOARD, + ADMIN_OTHER, ADMIN_STUDENT, ADMIN_STUDENT_OTHER, STUDENT_DASHBOARD, diff --git a/src/main/resources/html/admin/manage_classes.html b/src/main/resources/html/admin/manage_classes.html index 83df6c8..fbff4d3 100644 --- a/src/main/resources/html/admin/manage_classes.html +++ b/src/main/resources/html/admin/manage_classes.html @@ -32,7 +32,7 @@

    Klassenübersicht

    + \ No newline at end of file diff --git a/src/main/resources/meta/navigation/navigation_elements.json b/src/main/resources/meta/navigation/navigation_elements.json index 2ed3dd7..054d5aa 100644 --- a/src/main/resources/meta/navigation/navigation_elements.json +++ b/src/main/resources/meta/navigation/navigation_elements.json @@ -34,6 +34,12 @@ "path": "/editor", "label": "HTML Editor" }, + + { + "type": "ADMIN_OTHER", + "path": "/dashboard", + "label": "Zurück zum Admin-Dashboard" + }, { "type": "ADMIN_STUDENT", diff --git a/src/main/resources/meta/templates/templates.json b/src/main/resources/meta/templates/templates.json index 0784b76..2e1c975 100644 --- a/src/main/resources/meta/templates/templates.json +++ b/src/main/resources/meta/templates/templates.json @@ -37,6 +37,11 @@ "navigation_type": "ADMIN_DASHBOARD", "appearance": "LIST_APPEARANCE" }, + "admin_nav": { + "type": "HTMLNavigationTemplate", + "navigation_type": "ADMIN_OTHER", + "appearance": "BUTTON_APPEARANCE" + }, "admin_student_nav": { "type": "HTMLNavigationTemplate", "navigation_type": "ADMIN_STUDENT", From b3ea707e399c41b2aa62835e658a0a8f313fcfaa Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Tue, 31 Mar 2026 02:06:15 +0200 Subject: [PATCH 28/36] Added teacher student nav --- .../database/client/navigation/NavigationType.java | 1 + src/main/resources/html/teacher/student.html | 6 ++---- .../meta/navigation/navigation_elements.json | 11 +++++++++++ src/main/resources/meta/templates/templates.json | 5 +++++ 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java b/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java index 9f4a8aa..49099da 100644 --- a/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java +++ b/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java @@ -5,6 +5,7 @@ public enum NavigationType { ADMIN_OTHER, ADMIN_STUDENT, ADMIN_STUDENT_OTHER, + TEACHER_STUDENT, STUDENT_DASHBOARD, STUDENT_OTHER } diff --git a/src/main/resources/html/teacher/student.html b/src/main/resources/html/teacher/student.html index 7b597a9..f747271 100644 --- a/src/main/resources/html/teacher/student.html +++ b/src/main/resources/html/teacher/student.html @@ -1,4 +1,2 @@ -%[student_dashboard;script=build_student.js;nav= - - -] \ No newline at end of file +%[student_dashboard;script=build_student.js;nav=!FOLLOWS] +%[teacher_student_nav] \ No newline at end of file diff --git a/src/main/resources/meta/navigation/navigation_elements.json b/src/main/resources/meta/navigation/navigation_elements.json index 054d5aa..27b10da 100644 --- a/src/main/resources/meta/navigation/navigation_elements.json +++ b/src/main/resources/meta/navigation/navigation_elements.json @@ -83,6 +83,17 @@ "label": "Zurück zum Admin-Dashboard" }, + { + "type": "TEACHER_STUDENT", + "path": "/student-results", + "label": "Ergebnisse anzeigen" + }, + { + "type": "TEACHER_STUDENT", + "path": "/dashboard", + "label": "Zurück zur Lehrer-Ansicht" + }, + { "type": "STUDENT_DASHBOARD", "path": "/results", diff --git a/src/main/resources/meta/templates/templates.json b/src/main/resources/meta/templates/templates.json index 2e1c975..e4902d9 100644 --- a/src/main/resources/meta/templates/templates.json +++ b/src/main/resources/meta/templates/templates.json @@ -52,6 +52,11 @@ "navigation_type": "ADMIN_STUDENT_OTHER", "appearance": "BUTTON_APPEARANCE" }, + "teacher_student_nav": { + "type": "HTMLNavigationTemplate", + "navigation_type": "TEACHER_STUDENT", + "appearance": "BUTTON_APPEARANCE" + }, "student_dashboard_nav": { "type": "HTMLNavigationTemplate", "navigation_type": "STUDENT_DASHBOARD", From 24df2207cd4c8f736e00920f6e74c8d9e87783ff Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Tue, 31 Mar 2026 11:47:28 +0200 Subject: [PATCH 29/36] Added teacher_student_other_nav --- .../database/client/navigation/NavigationType.java | 1 + .../resources/html/teacher/student-results.html | 14 ++++++-------- .../meta/navigation/navigation_elements.json | 13 ++++++++++++- src/main/resources/meta/templates/templates.json | 5 +++++ 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java b/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java index 49099da..9d0dddd 100644 --- a/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java +++ b/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java @@ -6,6 +6,7 @@ public enum NavigationType { ADMIN_STUDENT, ADMIN_STUDENT_OTHER, TEACHER_STUDENT, + TEACHER_STUDENT_OTHER, STUDENT_DASHBOARD, STUDENT_OTHER } diff --git a/src/main/resources/html/teacher/student-results.html b/src/main/resources/html/teacher/student-results.html index 3ffafe5..1090f9f 100644 --- a/src/main/resources/html/teacher/student-results.html +++ b/src/main/resources/html/teacher/student-results.html @@ -1,8 +1,6 @@ -%[student_results;nav= - - -
    - - -
    -] \ No newline at end of file +%[student_results;nav=!FOLLOWS] +%[teacher_student_other_nav] +
    + + +
    \ No newline at end of file diff --git a/src/main/resources/meta/navigation/navigation_elements.json b/src/main/resources/meta/navigation/navigation_elements.json index 27b10da..7cb7704 100644 --- a/src/main/resources/meta/navigation/navigation_elements.json +++ b/src/main/resources/meta/navigation/navigation_elements.json @@ -91,7 +91,18 @@ { "type": "TEACHER_STUDENT", "path": "/dashboard", - "label": "Zurück zur Lehrer-Ansicht" + "label": "Zurück zum Lehrer-Dashboard" + }, + + { + "type": "TEACHER_STUDENT_OTHER", + "path": "/student", + "label": "Zurück zur Schüler-Ansicht" + }, + { + "type": "TEACHER_STUDENT", + "path": "/dashboard", + "label": "Zurück zum Lehrer-Dashboard" }, { diff --git a/src/main/resources/meta/templates/templates.json b/src/main/resources/meta/templates/templates.json index e4902d9..56a9a78 100644 --- a/src/main/resources/meta/templates/templates.json +++ b/src/main/resources/meta/templates/templates.json @@ -57,6 +57,11 @@ "navigation_type": "TEACHER_STUDENT", "appearance": "BUTTON_APPEARANCE" }, + "teacher_student_other_nav": { + "type": "HTMLNavigationTemplate", + "navigation_type": "TEACHER_STUDENT_OTHER", + "appearance": "BUTTON_APPEARANCE" + }, "student_dashboard_nav": { "type": "HTMLNavigationTemplate", "navigation_type": "STUDENT_DASHBOARD", From c2cc11cf02ab5ed78ae6b0a00d21234495c0104e Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Tue, 31 Mar 2026 11:51:28 +0200 Subject: [PATCH 30/36] Added other teacher navigation types that are not currently used but could be useful for plugins --- .../database/client/navigation/NavigationType.java | 2 ++ src/main/resources/html/teacher/dashboard.html | 1 + .../resources/meta/navigation/navigation_elements.json | 6 ++++++ src/main/resources/meta/templates/templates.json | 10 ++++++++++ 4 files changed, 19 insertions(+) diff --git a/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java b/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java index 9d0dddd..bbbe2e1 100644 --- a/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java +++ b/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java @@ -5,6 +5,8 @@ public enum NavigationType { ADMIN_OTHER, ADMIN_STUDENT, ADMIN_STUDENT_OTHER, + TEACHER_DASHBOARD, + TEACHER_OTHER, TEACHER_STUDENT, TEACHER_STUDENT_OTHER, STUDENT_DASHBOARD, diff --git a/src/main/resources/html/teacher/dashboard.html b/src/main/resources/html/teacher/dashboard.html index abcea08..8856c14 100644 --- a/src/main/resources/html/teacher/dashboard.html +++ b/src/main/resources/html/teacher/dashboard.html @@ -6,5 +6,6 @@

    Willkommen, Lehrer

    %[class_info] %[subject_info] %[room_info] + \ No newline at end of file diff --git a/src/main/resources/meta/navigation/navigation_elements.json b/src/main/resources/meta/navigation/navigation_elements.json index 7cb7704..5f8d47b 100644 --- a/src/main/resources/meta/navigation/navigation_elements.json +++ b/src/main/resources/meta/navigation/navigation_elements.json @@ -83,6 +83,12 @@ "label": "Zurück zum Admin-Dashboard" }, + { + "type": "TEACHER_OTHER", + "path": "/dashboard", + "label": "Zurück zum Lehrer-Dashboard" + }, + { "type": "TEACHER_STUDENT", "path": "/student-results", diff --git a/src/main/resources/meta/templates/templates.json b/src/main/resources/meta/templates/templates.json index 56a9a78..0b42972 100644 --- a/src/main/resources/meta/templates/templates.json +++ b/src/main/resources/meta/templates/templates.json @@ -52,6 +52,16 @@ "navigation_type": "ADMIN_STUDENT_OTHER", "appearance": "BUTTON_APPEARANCE" }, + "teacher_dashboard_nav": { + "type": "HTMLNavigationTemplate", + "navigation_type": "TEACHER_DASHBOARD", + "appearance": "LIST_APPEARANCE" + }, + "teacher_other_nav": { + "type": "HTMLNavigationTemplate", + "navigation_type": "TEACHER_OTHER", + "appearance": "BUTTON_APPEARANCE" + }, "teacher_student_nav": { "type": "HTMLNavigationTemplate", "navigation_type": "TEACHER_STUDENT", From 1ff29c458deee491eddc23c42aa81743c234d7cc Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Tue, 31 Mar 2026 13:00:21 +0200 Subject: [PATCH 31/36] Introduced RegistryEnums RegistryEnums basically work like enums, but the constants are initialized at runtime using a registry,that way it can be configured using a config file for example NavigationType is now a RegistryEnum EnumRegistries now use RegistryEnums instead of plain Enums --- .../de/igslandstuhl/database/Registry.java | 18 ++++- .../database/RegistryLockedException.java | 20 ++++++ .../client/navigation/NavigationType.java | 52 +++++++++++---- .../database/utils/RegistryEnum.java | 65 +++++++++++++++++++ .../meta/navigation/navigation_types.json | 14 ++++ 5 files changed, 155 insertions(+), 14 deletions(-) create mode 100644 src/main/java/de/igslandstuhl/database/RegistryLockedException.java create mode 100644 src/main/java/de/igslandstuhl/database/utils/RegistryEnum.java create mode 100644 src/main/resources/meta/navigation/navigation_types.json diff --git a/src/main/java/de/igslandstuhl/database/Registry.java b/src/main/java/de/igslandstuhl/database/Registry.java index d539b1b..6e756e2 100644 --- a/src/main/java/de/igslandstuhl/database/Registry.java +++ b/src/main/java/de/igslandstuhl/database/Registry.java @@ -17,6 +17,7 @@ import de.igslandstuhl.database.server.webserver.handlers.HttpHandler; import de.igslandstuhl.database.server.webserver.requests.APIPostRequest; import de.igslandstuhl.database.server.webserver.requests.GetRequest; +import de.igslandstuhl.database.utils.RegistryEnum; public class Registry implements Closeable { private static final Registry COMMAND_REGISTRY = new Registry<>(); @@ -55,8 +56,13 @@ public static Registry templateRegistry() { } private final Map objects = new HashMap<>(); + private boolean locked = false; + private void checkLocked() { + if (locked) throw new RegistryLockedException(); + } public synchronized void register(K key, V value) { + checkLocked(); objects.put(key, value); } public synchronized Stream stream() { @@ -71,13 +77,19 @@ public synchronized V get(K key) { public synchronized void unregister(K key) { objects.remove(key); } + public synchronized void lock() { + locked = true; + } - public static class EnumRegistry, V> { + public static class EnumRegistry, V> { private final Map> objects = new HashMap<>(); private EnumRegistry(Class clazz) { - for (K k :clazz.getEnumConstants()) { - objects.put(k, new HashSet<>()); + try { + Registry enumRegistry = RegistryEnum.init(clazz); + enumRegistry.stream().forEach((k) -> objects.put(k, new HashSet<>())); + } catch (Exception e) { + throw new RuntimeException("Could not initialize registry enum " + clazz.getName(), e); } } public synchronized void register(K key, V value) { diff --git a/src/main/java/de/igslandstuhl/database/RegistryLockedException.java b/src/main/java/de/igslandstuhl/database/RegistryLockedException.java new file mode 100644 index 0000000..ddbccb8 --- /dev/null +++ b/src/main/java/de/igslandstuhl/database/RegistryLockedException.java @@ -0,0 +1,20 @@ +package de.igslandstuhl.database; + +public class RegistryLockedException extends RuntimeException { + public RegistryLockedException() { + super("Registry already locked"); + } + public RegistryLockedException(String message) { + super(message); + } + public RegistryLockedException(Throwable cause) { + super(cause); + } + public RegistryLockedException(String message, Throwable cause) { + super(message, cause); + } + public RegistryLockedException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java b/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java index bbbe2e1..6d20e3a 100644 --- a/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java +++ b/src/main/java/de/igslandstuhl/database/client/navigation/NavigationType.java @@ -1,14 +1,44 @@ package de.igslandstuhl.database.client.navigation; -public enum NavigationType { - ADMIN_DASHBOARD, - ADMIN_OTHER, - ADMIN_STUDENT, - ADMIN_STUDENT_OTHER, - TEACHER_DASHBOARD, - TEACHER_OTHER, - TEACHER_STUDENT, - TEACHER_STUDENT_OTHER, - STUDENT_DASHBOARD, - STUDENT_OTHER +import java.lang.reflect.InvocationTargetException; +import java.util.List; + +import de.igslandstuhl.database.Registry; +import de.igslandstuhl.database.server.resources.ResourceLocation; +import de.igslandstuhl.database.utils.RegistryEnum; + +public class NavigationType extends RegistryEnum { + protected NavigationType(Registry registry, String key) { + super(registry, key); + } + + private static final ResourceLocation meta = new ResourceLocation("meta", "navigation", "navigation_types.json"); + @Override + protected NavigationType[] values(Registry registry) { + List navigationTypes = registry.stream().toList(); + NavigationType[] arr = new NavigationType[navigationTypes.size()]; + return navigationTypes.toArray(arr); + } + + @Override + protected void initValues() { + initUsingJSONMeta(meta); + } + + @Override + protected NavigationType initValue(Registry registry,String key) { + return new NavigationType(registry, key); + } + + public static NavigationType valueOf(String string) { + return RegistryEnum.valueOf(string, NavigationType.class); + } + public static void init() { + try { + RegistryEnum.init(NavigationType.class); + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException + | NoSuchMethodException | SecurityException e) { + throw new ExceptionInInitializerError(e); + } + } } diff --git a/src/main/java/de/igslandstuhl/database/utils/RegistryEnum.java b/src/main/java/de/igslandstuhl/database/utils/RegistryEnum.java new file mode 100644 index 0000000..b9a6cce --- /dev/null +++ b/src/main/java/de/igslandstuhl/database/utils/RegistryEnum.java @@ -0,0 +1,65 @@ +package de.igslandstuhl.database.utils; + +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.google.gson.reflect.TypeToken; + +import de.igslandstuhl.database.Registry; +import de.igslandstuhl.database.plugins.PluginResourceProvider; +import de.igslandstuhl.database.server.resources.CoreResourceProvider; +import de.igslandstuhl.database.server.resources.ResourceLocation; +import de.igslandstuhl.database.server.resources.ResourceManager; + +public abstract class RegistryEnum> { + private static final Map,Registry> map = new HashMap<>(); + private static final ResourceManager manager = new ResourceManager(new PluginResourceProvider(), new CoreResourceProvider()); + @SuppressWarnings("unchecked") + public static > Registry getRegistry(Class clazz) { + return (Registry) map.get(clazz); + } + private final Registry registry; + private final String key; + + protected RegistryEnum(Registry registry,String key) { + this.registry = registry; + this.key = key; + } + + protected abstract T[] values(Registry registry); + + public T[] values() { + return values(registry); + } + + protected abstract void initValues(); + + protected abstract T initValue(Registry registry, String key); + + protected void initUsingJSONMeta(ResourceLocation meta) { + List constants = manager.readJsonListMerged(meta, new TypeToken>() {}); + constants.forEach((c) -> registry.register(c, initValue(registry, c))); + } + + public static > Registry init(Class clazz) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException { + Registry registry = new Registry<>(); + var constructor = clazz.getDeclaredConstructor(Registry.class, String.class); + constructor.setAccessible(true); + T dummy = constructor.newInstance(registry, "DUMMY"); + dummy.initValues(); + if (registry.stream().anyMatch((t) -> t == dummy)) registry.unregister(registry.keyStream().filter((k) -> registry.get(k) == dummy).findAny().get()); + registry.lock(); + map.put(clazz, registry); + return registry; + } + public static > T valueOf(String s, Class clazz) { + return getRegistry(clazz).get(s); + } + + @Override + public String toString() { + return key; + } +} diff --git a/src/main/resources/meta/navigation/navigation_types.json b/src/main/resources/meta/navigation/navigation_types.json new file mode 100644 index 0000000..d333afc --- /dev/null +++ b/src/main/resources/meta/navigation/navigation_types.json @@ -0,0 +1,14 @@ +[ + "ADMIN_DASHBOARD", + "ADMIN_OTHER", + "ADMIN_STUDENT", + "ADMIN_STUDENT_OTHER", + + "TEACHER_DASHBOARD", + "TEACHER_OTHER", + "TEACHER_STUDENT", + "TEACHER_STUDENT_OTHER", + + "STUDENT_DASHBOARD", + "STUDENT_OTHER" +] \ No newline at end of file From c049f53c8cc55a0863c619b997c59c3297b57ca5 Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Tue, 31 Mar 2026 13:10:18 +0200 Subject: [PATCH 32/36] Added remaining teacher navigation types --- src/main/resources/html/admin/class.html | 3 +- src/main/resources/html/admin/subject.html | 3 +- src/main/resources/html/admin/teacher.html | 3 +- .../meta/navigation/navigation_elements.json | 33 +++++++++++++++++++ .../meta/navigation/navigation_types.json | 3 ++ .../resources/meta/templates/templates.json | 15 +++++++++ 6 files changed, 54 insertions(+), 6 deletions(-) diff --git a/src/main/resources/html/admin/class.html b/src/main/resources/html/admin/class.html index af1c639..cec7d6a 100644 --- a/src/main/resources/html/admin/class.html +++ b/src/main/resources/html/admin/class.html @@ -52,7 +52,6 @@

    Schüler in der Klasse

    \ No newline at end of file diff --git a/src/main/resources/html/admin/subject.html b/src/main/resources/html/admin/subject.html index faea4b5..6db89fe 100644 --- a/src/main/resources/html/admin/subject.html +++ b/src/main/resources/html/admin/subject.html @@ -50,7 +50,6 @@

    Themen in diesem Fach

    \ No newline at end of file diff --git a/src/main/resources/html/admin/teacher.html b/src/main/resources/html/admin/teacher.html index d36a908..e6c43f5 100644 --- a/src/main/resources/html/admin/teacher.html +++ b/src/main/resources/html/admin/teacher.html @@ -24,8 +24,7 @@

    Lehrer bearbeiten

    %[room_info] \ No newline at end of file diff --git a/src/main/resources/meta/navigation/navigation_elements.json b/src/main/resources/meta/navigation/navigation_elements.json index 5f8d47b..14dbbb1 100644 --- a/src/main/resources/meta/navigation/navigation_elements.json +++ b/src/main/resources/meta/navigation/navigation_elements.json @@ -83,6 +83,39 @@ "label": "Zurück zum Admin-Dashboard" }, + { + "type": "ADMIN_CLASS", + "path": "/manage_classes", + "label": "Zurück zur Klassenverwaltung" + }, + { + "type": "ADMIN_CLASS", + "path": "/dashboard", + "label": "Zurück zum Admin-Dashboard" + }, + + { + "type": "ADMIN_SUBJECT", + "path": "/manage_subjects", + "label": "Zurück zur Fächerverwaltung" + }, + { + "type": "ADMIN_SUBJECT", + "path": "/dashboard", + "label": "Zurück zum Admin-Dashboard" + }, + + { + "type": "ADMIN_TEACHER", + "path": "/manage_teachers", + "label": "Zurück zur Lehrerverwaltung" + }, + { + "type": "ADMIN_TEACHER", + "path": "/dashboard", + "label": "Zurück zum Admin-Dashboard" + }, + { "type": "TEACHER_OTHER", "path": "/dashboard", diff --git a/src/main/resources/meta/navigation/navigation_types.json b/src/main/resources/meta/navigation/navigation_types.json index d333afc..ef33ef7 100644 --- a/src/main/resources/meta/navigation/navigation_types.json +++ b/src/main/resources/meta/navigation/navigation_types.json @@ -3,6 +3,9 @@ "ADMIN_OTHER", "ADMIN_STUDENT", "ADMIN_STUDENT_OTHER", + "ADMIN_CLASS", + "ADMIN_SUBJECT", + "ADMIN_TEACHER", "TEACHER_DASHBOARD", "TEACHER_OTHER", diff --git a/src/main/resources/meta/templates/templates.json b/src/main/resources/meta/templates/templates.json index 0b42972..b39d226 100644 --- a/src/main/resources/meta/templates/templates.json +++ b/src/main/resources/meta/templates/templates.json @@ -52,6 +52,21 @@ "navigation_type": "ADMIN_STUDENT_OTHER", "appearance": "BUTTON_APPEARANCE" }, + "admin_class_nav": { + "type": "HTMLNavigationTemplate", + "navigation_type": "ADMIN_CLASS", + "appearance": "BUTTON_APPEARANCE" + }, + "admin_subject_nav": { + "type": "HTMLNavigationTemplate", + "navigation_type": "ADMIN_SUBJECT", + "appearance": "BUTTON_APPEARANCE" + }, + "admin_teacher_nav": { + "type": "HTMLNavigationTemplate", + "navigation_type": "ADMIN_TEACHER", + "appearance": "BUTTON_APPEARANCE" + }, "teacher_dashboard_nav": { "type": "HTMLNavigationTemplate", "navigation_type": "TEACHER_DASHBOARD", From 06604a5bddfc5873213b9cde631cb4f9dfd672e5 Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Tue, 31 Mar 2026 13:13:04 +0200 Subject: [PATCH 33/36] Added nav to editor --- src/main/resources/html/admin/editor.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/resources/html/admin/editor.html b/src/main/resources/html/admin/editor.html index e2f2dbc..257e92a 100644 --- a/src/main/resources/html/admin/editor.html +++ b/src/main/resources/html/admin/editor.html @@ -16,3 +16,6 @@

    HTML Editor

    + \ No newline at end of file From f1d6a2e8aff6e9255ebda0edccc0189669c8cb77 Mon Sep 17 00:00:00 2001 From: Lukas Morgenstern <139539229+Schlaumeier5@users.noreply.github.com> Date: Tue, 31 Mar 2026 13:18:17 +0200 Subject: [PATCH 34/36] Potential fix for code scanning alert no. 13: Incomplete string escaping or encoding Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- src/main/resources/js/site/student-database.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/js/site/student-database.js b/src/main/resources/js/site/student-database.js index 4efa83a..c3fe14b 100644 --- a/src/main/resources/js/site/student-database.js +++ b/src/main/resources/js/site/student-database.js @@ -834,7 +834,7 @@ function loadPluginSection(pluginKey) { const plugin = await fetchPlugin(pluginKey); header.textContent = plugin.name; body.innerHTML = ` -

    ${plugin.description.replace("\n", "

    ")}

    +

    ${plugin.description.replace(/\n/g, "

    ")}

    From 1adc60596be5ae101c83c4916e03cddcc4dba183 Mon Sep 17 00:00:00 2001 From: Lukas Morgenstern <139539229+Schlaumeier5@users.noreply.github.com> Date: Tue, 31 Mar 2026 13:23:07 +0200 Subject: [PATCH 35/36] Potential fix for code scanning alert no. 14: Arbitrary file access during archive extraction ("Zip Slip") Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../server/resources/ResourceLocation.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/igslandstuhl/database/server/resources/ResourceLocation.java b/src/main/java/de/igslandstuhl/database/server/resources/ResourceLocation.java index 6c93f1b..b1f7ca8 100644 --- a/src/main/java/de/igslandstuhl/database/server/resources/ResourceLocation.java +++ b/src/main/java/de/igslandstuhl/database/server/resources/ResourceLocation.java @@ -51,7 +51,19 @@ public static ResourceLocation fromPath(String path) { return fromPath(Path.of(path)); } public static ResourceLocation fromRelativePath(String relativePath) { - String[] parts = relativePath.split(Matcher.quoteReplacement(File.separator)); + if (relativePath == null || relativePath.isEmpty()) { + return null; + } + // Normalize the path to eliminate any "." or ".." segments + Path normalized = Path.of(relativePath).normalize(); + String normalizedStr = normalized.toString(); + // Reject paths that still contain traversal segments after normalization + if (normalizedStr.contains(".." + File.separator) || + normalizedStr.contains(File.separator + "..") || + normalizedStr.equals("..")) { + return null; + } + String[] parts = normalizedStr.split(Matcher.quoteReplacement(File.separator)); if (parts.length != 3) { return null; } From 768f680d7acf558487d9464149d1f60055e43931 Mon Sep 17 00:00:00 2001 From: Schlaumeier5 Date: Tue, 31 Mar 2026 13:33:05 +0200 Subject: [PATCH 36/36] Potential fix for security alert #14 --- .../database/plugins/PluginResourceProvider.java | 6 ++++++ .../database/server/resources/CoreResourceProvider.java | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/igslandstuhl/database/plugins/PluginResourceProvider.java b/src/main/java/de/igslandstuhl/database/plugins/PluginResourceProvider.java index 419dc8f..7b6c2b6 100644 --- a/src/main/java/de/igslandstuhl/database/plugins/PluginResourceProvider.java +++ b/src/main/java/de/igslandstuhl/database/plugins/PluginResourceProvider.java @@ -2,6 +2,8 @@ import java.io.File; import java.io.InputStream; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; import java.util.Enumeration; @@ -10,6 +12,7 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipFile; +import de.igslandstuhl.database.server.resources.CoreResourceProvider; import de.igslandstuhl.database.server.resources.ResourceLocation; import de.igslandstuhl.database.server.resources.ResourceProvider; @@ -51,6 +54,8 @@ public List openAll(ResourceLocation location) { @Override public Collection list(Pattern pattern) { List result = new ArrayList<>(); + // Virtual root – no real filesystem access needed + final Path virtualRoot = Paths.get("").toAbsolutePath().normalize(); for (PreLoadedPlugin module : PluginLoader.getInstance().getPluginInfos()) { try (ZipFile zip = new ZipFile(new File(module.classLoader().getURLs()[0].toURI()))) { @@ -64,6 +69,7 @@ public Collection list(Pattern pattern) { String name = entry.getName(); + if (!CoreResourceProvider.isSafeZipEntryName(name, virtualRoot)) if (pattern.matcher(name).matches()) { ResourceLocation loc = ResourceLocation.fromPath(name); if (loc != null) result.add(loc); diff --git a/src/main/java/de/igslandstuhl/database/server/resources/CoreResourceProvider.java b/src/main/java/de/igslandstuhl/database/server/resources/CoreResourceProvider.java index 758efad..1c67d53 100644 --- a/src/main/java/de/igslandstuhl/database/server/resources/CoreResourceProvider.java +++ b/src/main/java/de/igslandstuhl/database/server/resources/CoreResourceProvider.java @@ -20,7 +20,7 @@ public class CoreResourceProvider implements ResourceProvider { /** * Checks if a zip entry name is safe (to prevent zip slipping). */ - private boolean isSafeZipEntryName(String entryName, Path rootDir) { + public static boolean isSafeZipEntryName(String entryName, Path rootDir) { // Resolve entry against a fixed root and normalize Path resolvedPath = rootDir.resolve(entryName).normalize();
    Key