From 9c6e9153b6bc6d5c57c671e8c01fa26aad2196a2 Mon Sep 17 00:00:00 2001 From: thmarx Date: Sun, 7 Dec 2025 19:06:30 +0100 Subject: [PATCH] extension for ui actions and sources --- .../extensions/UIActionsExtensionPoint.java | 5 +- .../api/ui/extensions/UIExtensionPoint.java | 35 ++++++++ .../UILocalizationExtensionPoint.java | 5 +- .../UIRemoteMethodExtensionPoint.java | 4 +- .../UIScriptActionSourceExtension.java | 34 ++++++++ .../cms/api/utils/AnnotationsUtil.java | 2 +- .../ExampleUiScriptActionSourceExtension.java | 77 ++++++++++++++++++ .../AbstractRemoteMethodeExtension.java | 3 +- .../remotemethods/LocalizationEnpoints.java | 3 +- .../RemoteContentEndpointsExtension.java | 3 +- .../remotemethods/RemoteMediaEnpoints.java | 3 +- .../cms/modules/ui/http/JSActionHandler.java | 49 ++++++++--- .../cms/modules/ui/utils/ActionFactory.java | 4 +- .../example-module/configuration.properties | 0 test-server/hosts/demo/site.toml | 3 + .../libs/example-module-8.0.0.jar | Bin 0 -> 18668 bytes .../modules/example-module/module.properties | 4 + 17 files changed, 204 insertions(+), 30 deletions(-) create mode 100644 cms-api/src/main/java/com/condation/cms/api/ui/extensions/UIExtensionPoint.java create mode 100644 cms-api/src/main/java/com/condation/cms/api/ui/extensions/UIScriptActionSourceExtension.java create mode 100644 modules/example-module/src/main/java/com/condation/cms/modules/example/ui/ExampleUiScriptActionSourceExtension.java create mode 100644 test-server/hosts/demo/modules_data/example-module/configuration.properties create mode 100644 test-server/modules/example-module/libs/example-module-8.0.0.jar create mode 100644 test-server/modules/example-module/module.properties diff --git a/cms-api/src/main/java/com/condation/cms/api/ui/extensions/UIActionsExtensionPoint.java b/cms-api/src/main/java/com/condation/cms/api/ui/extensions/UIActionsExtensionPoint.java index 1b88e4e58..d422eb48f 100644 --- a/cms-api/src/main/java/com/condation/cms/api/ui/extensions/UIActionsExtensionPoint.java +++ b/cms-api/src/main/java/com/condation/cms/api/ui/extensions/UIActionsExtensionPoint.java @@ -22,16 +22,13 @@ * #L% */ -import com.condation.cms.api.module.SiteModuleContext; -import com.condation.cms.api.module.SiteRequestContext; import com.condation.cms.api.ui.elements.Menu; -import com.condation.modules.api.ExtensionPoint; /** * * @author t.marx */ -public interface UIActionsExtensionPoint extends ExtensionPoint { +public interface UIActionsExtensionPoint extends UIExtensionPoint { public default void addMenuItems (Menu menu) {}; } diff --git a/cms-api/src/main/java/com/condation/cms/api/ui/extensions/UIExtensionPoint.java b/cms-api/src/main/java/com/condation/cms/api/ui/extensions/UIExtensionPoint.java new file mode 100644 index 000000000..6d4754117 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/ui/extensions/UIExtensionPoint.java @@ -0,0 +1,35 @@ +package com.condation.cms.api.ui.extensions; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import com.condation.cms.api.module.SiteModuleContext; +import com.condation.cms.api.module.SiteRequestContext; +import com.condation.modules.api.ExtensionPoint; + +/** + * + * @author thmar + */ +public interface UIExtensionPoint extends ExtensionPoint { + +} diff --git a/cms-api/src/main/java/com/condation/cms/api/ui/extensions/UILocalizationExtensionPoint.java b/cms-api/src/main/java/com/condation/cms/api/ui/extensions/UILocalizationExtensionPoint.java index 61873a3e0..d4152570b 100644 --- a/cms-api/src/main/java/com/condation/cms/api/ui/extensions/UILocalizationExtensionPoint.java +++ b/cms-api/src/main/java/com/condation/cms/api/ui/extensions/UILocalizationExtensionPoint.java @@ -22,16 +22,13 @@ * #L% */ -import com.condation.cms.api.module.SiteModuleContext; -import com.condation.cms.api.module.SiteRequestContext; -import com.condation.modules.api.ExtensionPoint; import java.util.Map; /** * * @author t.marx */ -public interface UILocalizationExtensionPoint extends ExtensionPoint { +public interface UILocalizationExtensionPoint extends UIExtensionPoint { public Map> getLocalizations (); } diff --git a/cms-api/src/main/java/com/condation/cms/api/ui/extensions/UIRemoteMethodExtensionPoint.java b/cms-api/src/main/java/com/condation/cms/api/ui/extensions/UIRemoteMethodExtensionPoint.java index 896a8a8b2..f23383532 100644 --- a/cms-api/src/main/java/com/condation/cms/api/ui/extensions/UIRemoteMethodExtensionPoint.java +++ b/cms-api/src/main/java/com/condation/cms/api/ui/extensions/UIRemoteMethodExtensionPoint.java @@ -22,12 +22,10 @@ * #L% */ -import com.condation.cms.api.extensions.AbstractExtensionPoint; - /** * * @author t.marx */ -public abstract class UIRemoteMethodExtensionPoint extends AbstractExtensionPoint { +public interface UIRemoteMethodExtensionPoint extends UIExtensionPoint{ } diff --git a/cms-api/src/main/java/com/condation/cms/api/ui/extensions/UIScriptActionSourceExtension.java b/cms-api/src/main/java/com/condation/cms/api/ui/extensions/UIScriptActionSourceExtension.java new file mode 100644 index 000000000..0287194bf --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/ui/extensions/UIScriptActionSourceExtension.java @@ -0,0 +1,34 @@ +package com.condation.cms.api.ui.extensions; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +import java.util.Map; + +/** + * + * @author thmar + */ +public interface UIScriptActionSourceExtension extends UIExtensionPoint { + + public Map getActionSources (); +} diff --git a/cms-api/src/main/java/com/condation/cms/api/utils/AnnotationsUtil.java b/cms-api/src/main/java/com/condation/cms/api/utils/AnnotationsUtil.java index 17eaa95fa..5c4a03358 100644 --- a/cms-api/src/main/java/com/condation/cms/api/utils/AnnotationsUtil.java +++ b/cms-api/src/main/java/com/condation/cms/api/utils/AnnotationsUtil.java @@ -51,7 +51,7 @@ public static List> process(Object List> result = new ArrayList(); Class clazz = target.getClass(); - for (Method method : clazz.getDeclaredMethods()) { + for (Method method : clazz.getMethods()) { if (!method.isAnnotationPresent(annotationClass)) { continue; } diff --git a/modules/example-module/src/main/java/com/condation/cms/modules/example/ui/ExampleUiScriptActionSourceExtension.java b/modules/example-module/src/main/java/com/condation/cms/modules/example/ui/ExampleUiScriptActionSourceExtension.java new file mode 100644 index 000000000..f47c8c772 --- /dev/null +++ b/modules/example-module/src/main/java/com/condation/cms/modules/example/ui/ExampleUiScriptActionSourceExtension.java @@ -0,0 +1,77 @@ +package com.condation.cms.modules.example.ui; + +/*- + * #%L + * example-module + * %% + * Copyright (C) 2023 - 2025 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +import com.condation.cms.api.auth.Permissions; +import com.condation.cms.api.extensions.AbstractExtensionPoint; +import com.condation.cms.api.ui.annotations.MenuEntry; +import com.condation.cms.api.ui.annotations.ShortCut; +import com.condation.cms.api.ui.extensions.UIActionsExtensionPoint; +import com.condation.cms.api.ui.extensions.UIScriptActionSourceExtension; +import com.condation.modules.api.annotation.Extension; +import com.condation.modules.api.annotation.Extensions; +import java.util.Map; + +/** + * + * @author thmar + */ +@Extensions({ + @Extension(UIActionsExtensionPoint.class), + @Extension(UIScriptActionSourceExtension.class) +}) +public class ExampleUiScriptActionSourceExtension extends AbstractExtensionPoint implements UIScriptActionSourceExtension, UIActionsExtensionPoint { + + @Override + public Map getActionSources() { + return Map.of("example/source", "// this is an example script source"); + } + + @MenuEntry( + id = "exampleMenu", + name = "Example", + permissions = {Permissions.CONTENT_EDIT}, + position = 10 + ) + public void exampleMenu() { + + } + + @MenuEntry( + parent = "exampleMenu", + id = "example-action", + name = "Example action", + permissions = {Permissions.CONTENT_EDIT}, + position = 1, + scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/example/source") + ) + @ShortCut( + id = "example-action", + title = "Example Action", + permissions = {Permissions.CONTENT_EDIT}, + section = "Example", + scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/example/source") + ) + public void example_action() { + + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/AbstractRemoteMethodeExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/AbstractRemoteMethodeExtension.java index 066be7d45..1d16e2129 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/AbstractRemoteMethodeExtension.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/AbstractRemoteMethodeExtension.java @@ -23,6 +23,7 @@ */ import com.condation.cms.api.db.DB; +import com.condation.cms.api.extensions.AbstractExtensionPoint; import com.condation.cms.api.feature.features.AuthFeature; import com.condation.cms.api.feature.features.DBFeature; import com.condation.cms.api.feature.features.HookSystemFeature; @@ -36,7 +37,7 @@ * * @author thorstenmarx */ -public abstract class AbstractRemoteMethodeExtension extends UIRemoteMethodExtensionPoint { +public abstract class AbstractRemoteMethodeExtension extends AbstractExtensionPoint implements UIRemoteMethodExtensionPoint { protected String getUserName() { if (getRequestContext().has(AuthFeature.class)) { return getRequestContext().get(AuthFeature.class).username(); diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/LocalizationEnpoints.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/LocalizationEnpoints.java index 197e21b93..fbf3ca8f3 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/LocalizationEnpoints.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/LocalizationEnpoints.java @@ -23,6 +23,7 @@ */ import com.condation.cms.api.auth.Permissions; +import com.condation.cms.api.extensions.AbstractExtensionPoint; import com.condation.cms.api.feature.features.HookSystemFeature; import com.condation.cms.api.feature.features.ModuleManagerFeature; import com.condation.cms.api.ui.extensions.UILocalizationExtensionPoint; @@ -41,7 +42,7 @@ */ @Slf4j @Extension(UIRemoteMethodExtensionPoint.class) -public class LocalizationEnpoints extends UIRemoteMethodExtensionPoint { +public class LocalizationEnpoints extends AbstractExtensionPoint implements UIRemoteMethodExtensionPoint { diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtension.java index 275dcc988..1611f6788 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtension.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtension.java @@ -27,6 +27,7 @@ import com.condation.cms.api.db.cms.ReadOnlyFile; import com.condation.cms.api.eventbus.events.InvalidateContentCacheEvent; import com.condation.cms.api.eventbus.events.ReIndexContentMetaDataEvent; +import com.condation.cms.api.extensions.AbstractExtensionPoint; import com.condation.cms.api.feature.features.DBFeature; import com.condation.cms.api.feature.features.EventBusFeature; import com.condation.cms.api.feature.features.RequestFeature; @@ -58,7 +59,7 @@ */ @Slf4j @Extension(UIRemoteMethodExtensionPoint.class) -public class RemoteContentEndpointsExtension extends UIRemoteMethodExtensionPoint { +public class RemoteContentEndpointsExtension extends AbstractExtensionPoint implements UIRemoteMethodExtensionPoint { @RemoteMethod(name = "content.get", permissions = {Permissions.CONTENT_EDIT}) public Object getContent(Map parameters) { diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteMediaEnpoints.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteMediaEnpoints.java index ca306d4b5..cdde69274 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteMediaEnpoints.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteMediaEnpoints.java @@ -24,6 +24,7 @@ import com.condation.cms.api.Constants; import com.condation.cms.api.auth.Permissions; import com.condation.cms.api.eventbus.events.InvalidateMediaCache; +import com.condation.cms.api.extensions.AbstractExtensionPoint; import com.condation.cms.api.feature.features.DBFeature; import com.condation.cms.api.feature.features.EventBusFeature; import com.condation.cms.api.feature.features.SiteMediaServiceFeature; @@ -46,7 +47,7 @@ */ @Slf4j @Extension(UIRemoteMethodExtensionPoint.class) -public class RemoteMediaEnpoints extends UIRemoteMethodExtensionPoint { +public class RemoteMediaEnpoints extends AbstractExtensionPoint implements UIRemoteMethodExtensionPoint { @RemoteMethod(name = "media.meta.get", permissions = {Permissions.CONTENT_EDIT}) public Object getMediaMeta(Map parameters) throws RPCException { diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/JSActionHandler.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/JSActionHandler.java index ed65ddd7d..df07fefa8 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/JSActionHandler.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/JSActionHandler.java @@ -21,11 +21,15 @@ * . * #L% */ +import com.condation.cms.api.feature.features.ModuleManagerFeature; import com.condation.cms.api.module.SiteModuleContext; +import com.condation.cms.api.ui.extensions.UIScriptActionSourceExtension; import com.condation.cms.api.utils.HTTPUtil; +import com.google.common.base.Strings; import java.nio.charset.StandardCharsets; import java.nio.file.FileSystem; import java.nio.file.Files; +import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.eclipse.jetty.http.HttpHeader; @@ -48,24 +52,45 @@ public class JSActionHandler extends JettyHandler { @Override public boolean handle(Request request, Response response, Callback callback) throws Exception { - var resource = request.getHttpURI().getPath().replace( - managerURL("/manager/actions/", context), "") + ".js"; - - var files = fileSystem.getPath(base); - - if (resource.startsWith("/")) { - resource = resource.substring(1); + var resourceName = request.getHttpURI().getPath().replace( + managerURL("/manager/actions/", context), ""); + + if (resourceName.startsWith("/")) { + resourceName = resourceName.substring(1); } - - var path = files.resolve(resource); - if (Files.exists(path)) { - response.getHeaders().put(HttpHeader.CONTENT_TYPE, "%s; charset=UTF-8".formatted(Files.probeContentType(path))); - Content.Sink.write(response, true, Files.readString(path, StandardCharsets.UTF_8), callback); + + String scriptContent = ""; + + var moduleContent = getScriptContentFromModules(resourceName); + if (moduleContent.isPresent()) { + scriptContent = moduleContent.get(); + } else { + var resourceFile = resourceName + ".js"; + var files = fileSystem.getPath(base); + var path = files.resolve(resourceFile); + if (Files.exists(path)) { + scriptContent = Files.readString(path); + } + } + + + if (!Strings.isNullOrEmpty(scriptContent)) { + response.getHeaders().put(HttpHeader.CONTENT_TYPE, "application/javascript; charset=UTF-8"); + Content.Sink.write(response, true, scriptContent, callback); } else { callback.succeeded(); } return true; } + + private Optional getScriptContentFromModules (String filename) { + return context.get(ModuleManagerFeature.class).moduleManager().extensions(UIScriptActionSourceExtension.class) + .stream() + .map(UIScriptActionSourceExtension::getActionSources) + .filter(source -> source.containsKey(filename)) + .map(source -> source.get(filename)) + .findFirst(); + } } diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/ActionFactory.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/ActionFactory.java index c6245df5e..ece6937bc 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/ActionFactory.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/ActionFactory.java @@ -102,7 +102,7 @@ public Menu createMenu() { private List scanShortCuts(Object moduleInstance) { List shortCuts = new ArrayList<>(); - for (Method method : moduleInstance.getClass().getDeclaredMethods()) { + for (Method method : moduleInstance.getClass().getMethods()) { var shortcutAnnotation = method.getAnnotation(com.condation.cms.api.ui.annotations.ShortCut.class); if (shortcutAnnotation == null) { continue; @@ -156,7 +156,7 @@ private List scanMenuEntries(Object moduleInstance) { List entries = new ArrayList<>(); - for (Method method : moduleInstance.getClass().getDeclaredMethods()) { + for (Method method : moduleInstance.getClass().getMethods()) { var menuAnn = method.getAnnotation(com.condation.cms.api.ui.annotations.MenuEntry.class); if (menuAnn == null) { continue; diff --git a/test-server/hosts/demo/modules_data/example-module/configuration.properties b/test-server/hosts/demo/modules_data/example-module/configuration.properties new file mode 100644 index 000000000..e69de29bb diff --git a/test-server/hosts/demo/site.toml b/test-server/hosts/demo/site.toml index 1cc7a435b..15716870b 100644 --- a/test-server/hosts/demo/site.toml +++ b/test-server/hosts/demo/site.toml @@ -27,6 +27,9 @@ target = "/Users/thorstenmarx/entwicklung/backups" [cache] content = true +[modules] +active = ["example-module"] + [translation] enabled = true languages = ["de", "en"] diff --git a/test-server/modules/example-module/libs/example-module-8.0.0.jar b/test-server/modules/example-module/libs/example-module-8.0.0.jar new file mode 100644 index 0000000000000000000000000000000000000000..41c48396057e604f4030d7b4fc3f165bfc4a1360 GIT binary patch literal 18668 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