diff --git a/CHANGELOG.md b/CHANGELOG.md index e14582048..bc3f5044c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,61 @@ see wiki for more information: [wiki](https://github.com/thmarx/cms/wiki) * **MAINTENANCE** multiple dependencies updated * **MAINTENANCE** maven wrapper added to project +### Developer experience + +In this release we introduced some features to make life of developers easier. + +#### Registering hooks via annotations + +It is now possible to pass an object with annotated hook definitions to the HookSystem.register method. + +```java +@Filter("test/annotation/filter1") +public List filter (FilterContext> context) { + context.value().remove("2"); + return context.value(); +} +@Action("test/annotation/action1") +public void action1 (ActionContext context) { + // do something +} +``` + +#### HTTP-Controllers + +The RoutesExtensionPoint is an extension point for defining HTTP routes. +It allows developers to provide a list of objects whose methods can be registered as routes using annotations. + +```java +@Route("/test2") +public boolean handle2 (Request request, Response response, Callback callback) { + return true; +} +``` + +#### ShortCodes + +The RegisterShortCodesExtensionPoint interface now includes a new method, shortCodeDefinitions, which returns a list of objects that contain shortcode definitions provided through annotations. + +```java +@ShortCode("printHello") +public String printHello (Parameter parameter) { + return "hello " + parameter.getOrDefault("name", ""); +} +``` + +#### TemplateComponents + +A new method, componentDefinitions, has been added to the RegisterTemplateComponentExtensionPoint interface. It returns a list of objects that define template components using annotations. + +```java +@TemplateComponent("tag3") +public String tag3 (Parameter parameter) { + return "
%s
".formatted(parameter.get("_content")); +} + +``` + ## 7.8.0 * **BUG** Namespaces not set when executing content pipeline [#416](https://github.com/CondationCMS/cms-server/pull/416) diff --git a/cms-api/src/main/java/com/condation/cms/api/annotations/Action.java b/cms-api/src/main/java/com/condation/cms/api/annotations/Action.java new file mode 100644 index 000000000..5700ad687 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/annotations/Action.java @@ -0,0 +1,39 @@ +package com.condation.cms.api.annotations; + +/*- + * #%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.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + * @author thorstenmarx + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface Action { + String value (); + int priority () default 10; +} diff --git a/cms-api/src/main/java/com/condation/cms/api/annotations/Filter.java b/cms-api/src/main/java/com/condation/cms/api/annotations/Filter.java new file mode 100644 index 000000000..f0afee6fe --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/annotations/Filter.java @@ -0,0 +1,39 @@ +package com.condation.cms.api.annotations; + +/*- + * #%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.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + * @author thorstenmarx + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface Filter { + String value (); + int priority () default 10; +} diff --git a/cms-api/src/main/java/com/condation/cms/api/annotations/Route.java b/cms-api/src/main/java/com/condation/cms/api/annotations/Route.java new file mode 100644 index 000000000..8f82df0d3 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/annotations/Route.java @@ -0,0 +1,35 @@ +package com.condation.cms.api.annotations; + +/*- + * #%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.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Route { + String value(); + String method() default "GET"; // Optional: GET, POST, PUT, DELETE ... +} diff --git a/cms-api/src/main/java/com/condation/cms/api/annotations/ShortCode.java b/cms-api/src/main/java/com/condation/cms/api/annotations/ShortCode.java new file mode 100644 index 000000000..9cef1bf04 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/annotations/ShortCode.java @@ -0,0 +1,38 @@ +package com.condation.cms.api.annotations; + +/*- + * #%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.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + * @author thorstenmarx + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface ShortCode { + String value (); +} diff --git a/cms-api/src/main/java/com/condation/cms/api/annotations/TemplateComponent.java b/cms-api/src/main/java/com/condation/cms/api/annotations/TemplateComponent.java new file mode 100644 index 000000000..b7250e70a --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/annotations/TemplateComponent.java @@ -0,0 +1,38 @@ +package com.condation.cms.api.annotations; + +/*- + * #%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.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + * @author thorstenmarx + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface TemplateComponent { + String value (); +} diff --git a/cms-api/src/main/java/com/condation/cms/api/extensions/HttpHandler.java b/cms-api/src/main/java/com/condation/cms/api/extensions/HttpHandler.java index 7916688f1..9006a97bc 100644 --- a/cms-api/src/main/java/com/condation/cms/api/extensions/HttpHandler.java +++ b/cms-api/src/main/java/com/condation/cms/api/extensions/HttpHandler.java @@ -33,5 +33,13 @@ */ public interface HttpHandler { + /** + * + * @param request + * @param response + * @param callback + * @return true if the request is handled by the HttpHandler, otherwise false + * @throws Exception + */ boolean handle (Request request, Response response, Callback callback) throws Exception; } diff --git a/cms-api/src/main/java/com/condation/cms/api/extensions/RegisterShortCodesExtensionPoint.java b/cms-api/src/main/java/com/condation/cms/api/extensions/RegisterShortCodesExtensionPoint.java index 1e02a5f53..a5adadf36 100644 --- a/cms-api/src/main/java/com/condation/cms/api/extensions/RegisterShortCodesExtensionPoint.java +++ b/cms-api/src/main/java/com/condation/cms/api/extensions/RegisterShortCodesExtensionPoint.java @@ -24,6 +24,8 @@ import com.condation.cms.api.model.Parameter; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.function.Function; @@ -33,5 +35,11 @@ */ public abstract class RegisterShortCodesExtensionPoint extends AbstractExtensionPoint { - public abstract Map> shortCodes (); + public Map> shortCodes () { + return Collections.emptyMap(); + } + + public List shortCodeDefinitions () { + return Collections.emptyList(); + } } diff --git a/cms-api/src/main/java/com/condation/cms/api/extensions/RegisterTemplateComponentExtensionPoint.java b/cms-api/src/main/java/com/condation/cms/api/extensions/RegisterTemplateComponentExtensionPoint.java index 21f099930..d980b6175 100644 --- a/cms-api/src/main/java/com/condation/cms/api/extensions/RegisterTemplateComponentExtensionPoint.java +++ b/cms-api/src/main/java/com/condation/cms/api/extensions/RegisterTemplateComponentExtensionPoint.java @@ -24,6 +24,8 @@ import com.condation.cms.api.model.Parameter; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.function.Function; @@ -33,5 +35,11 @@ */ public abstract class RegisterTemplateComponentExtensionPoint extends AbstractExtensionPoint { - public abstract Map> components (); + public Map> components () { + return Collections.emptyMap(); + } + + public List componentDefinitions () { + return Collections.emptyList(); + } } diff --git a/cms-api/src/main/java/com/condation/cms/api/extensions/http/routes/RoutesExtensionPoint.java b/cms-api/src/main/java/com/condation/cms/api/extensions/http/routes/RoutesExtensionPoint.java new file mode 100644 index 000000000..496cd7da4 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/extensions/http/routes/RoutesExtensionPoint.java @@ -0,0 +1,34 @@ +package com.condation.cms.api.extensions.http.routes; + +/*- + * #%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.extensions.AbstractExtensionPoint; +import java.util.List; + +/** + * + * @author thorstenmarx + */ +public abstract class RoutesExtensionPoint extends AbstractExtensionPoint { + abstract public List getRouteDefinitions (); +} diff --git a/cms-api/src/main/java/com/condation/cms/api/extensions/http/routes/RoutesManager.java b/cms-api/src/main/java/com/condation/cms/api/extensions/http/routes/RoutesManager.java new file mode 100644 index 000000000..d3ef35d61 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/extensions/http/routes/RoutesManager.java @@ -0,0 +1,82 @@ +package com.condation.cms.api.extensions.http.routes; + +/*- + * #%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.annotations.Route; +import com.condation.cms.api.extensions.http.HttpHandler; +import com.condation.cms.api.extensions.http.PathMapping; +import org.eclipse.jetty.http.pathmap.PathSpec; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; + +@Slf4j +public class RoutesManager { + + private final PathMapping pathMapping = new PathMapping(); + + public Optional findFirst (String path, String method) { + return pathMapping.getMatchingHandler(path, method); + } + + public void register(Object controller) { + Class clazz = controller.getClass(); + + for (Method method : clazz.getDeclaredMethods()) { + Route route = method.getAnnotation(Route.class); + if (route != null && isValidHandlerMethod(method)) { + method.setAccessible(true); + + PathSpec pathSpec = PathSpec.from(route.value()); + + HttpHandler handler = (request, response, callback) -> { + try { + return (Boolean) method.invoke(controller, request, response, callback); + } catch (Exception e) { + log.error("", e); + response.setStatus(500); + return true; + } + }; + pathMapping.add(pathSpec, route.method(), handler); + } + } + } + + private boolean isValidHandlerMethod(Method method) { + // Muss "boolean handle(Request, Response, Callback)" sein + if (!Modifier.isPublic(method.getModifiers())) return false; + if (!method.getReturnType().equals(boolean.class)) return false; + + Class[] params = method.getParameterTypes(); + return params.length == 3 && + Request.class.isAssignableFrom(params[0]) && + Response.class.isAssignableFrom(params[1]) && + Callback.class.isAssignableFrom(params[2]); + } +} diff --git a/cms-api/src/main/java/com/condation/cms/api/hooks/Action.java b/cms-api/src/main/java/com/condation/cms/api/hooks/ActionHook.java similarity index 89% rename from cms-api/src/main/java/com/condation/cms/api/hooks/Action.java rename to cms-api/src/main/java/com/condation/cms/api/hooks/ActionHook.java index e1e849a3d..281d421da 100644 --- a/cms-api/src/main/java/com/condation/cms/api/hooks/Action.java +++ b/cms-api/src/main/java/com/condation/cms/api/hooks/ActionHook.java @@ -27,5 +27,5 @@ * * @author t.marx */ -public record Action(String name, int priority, ActionFunction function) implements Hook { +record ActionHook(String name, int priority, ActionFunction function) implements Hook { } diff --git a/cms-api/src/main/java/com/condation/cms/api/hooks/Filter.java b/cms-api/src/main/java/com/condation/cms/api/hooks/FilterHook.java similarity index 89% rename from cms-api/src/main/java/com/condation/cms/api/hooks/Filter.java rename to cms-api/src/main/java/com/condation/cms/api/hooks/FilterHook.java index 1ae6e0b22..2e3a2f3a5 100644 --- a/cms-api/src/main/java/com/condation/cms/api/hooks/Filter.java +++ b/cms-api/src/main/java/com/condation/cms/api/hooks/FilterHook.java @@ -27,5 +27,5 @@ * * @author t.marx */ -public record Filter(String name, int priority, FilterFunction function) implements Hook { +record FilterHook(String name, int priority, FilterFunction function) implements Hook { } diff --git a/cms-api/src/main/java/com/condation/cms/api/hooks/HookSystem.java b/cms-api/src/main/java/com/condation/cms/api/hooks/HookSystem.java index bd714568f..b173e4d6e 100644 --- a/cms-api/src/main/java/com/condation/cms/api/hooks/HookSystem.java +++ b/cms-api/src/main/java/com/condation/cms/api/hooks/HookSystem.java @@ -21,9 +21,11 @@ * . * #L% */ - +import com.condation.cms.api.annotations.Filter; +import com.condation.cms.api.annotations.Action; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; @@ -40,40 +42,90 @@ @RequiredArgsConstructor public class HookSystem { - Multimap actions = ArrayListMultimap.create(); - - Multimap filters = ArrayListMultimap.create(); - - private HookSystem (HookSystem source) { + Multimap actions = ArrayListMultimap.create(); + + Multimap filters = ArrayListMultimap.create(); + + private HookSystem(HookSystem source) { this.actions.putAll(source.actions); this.filters.putAll(source.filters); } - - public HookSystem clone () { + + public HookSystem clone() { return new HookSystem(this); } + public void register(Object sourceObject) { + Class objectClass = sourceObject.getClass(); + for (Method method : objectClass.getDeclaredMethods()) { + // regsiter actions + var actionAnnotation = method.getAnnotation(Action.class); + if (actionAnnotation != null) { + + var parameters = method.getParameterTypes(); + if (parameters.length != 1 || !ActionContext.class.isAssignableFrom(parameters[0])) { + log.warn("Method {}.{} ignored: must have exactly one parameter of type ActionContext", + objectClass.getSimpleName(), method.getName()); + } else { + if (!method.canAccess(sourceObject)) { + method.setAccessible(true); + } + + registerAction(actionAnnotation.value(), context -> { + try { + return method.invoke(sourceObject, context); + } catch (Exception e) { + log.error("Error invoking action method {}.{}", objectClass.getSimpleName(), method.getName(), e); + throw new RuntimeException(e); + } + }, actionAnnotation.priority()); + } + } + + // register filters + var filterAnnotation = method.getAnnotation(Filter.class); + if (filterAnnotation != null) { + var parameters = method.getParameterTypes(); + if (parameters.length != 1 || !FilterContext.class.isAssignableFrom(parameters[0])) { + log.warn("Method {}.{} ignored for Filter: must have exactly one parameter of type FilterContext", + objectClass.getSimpleName(), method.getName()); + } else { + if (!method.canAccess(sourceObject)) { + method.setAccessible(true); + } + registerFilter(filterAnnotation.value(), context -> { + try { + return method.invoke(sourceObject, context); + } catch (Exception e) { + log.error("Error invoking filter method {}.{}", objectClass.getSimpleName(), method.getName(), e); + throw new RuntimeException(e); + } + }, filterAnnotation.priority()); + } + } + } + } + public void registerAction(final String name, final ActionFunction hookFunction) { registerAction(name, hookFunction, 10); } public void registerAction(final String name, final ActionFunction hookFunction, int priority) { - actions.put(name, new Action<>(name, priority, hookFunction)); + actions.put(name, new ActionHook<>(name, priority, hookFunction)); } - + public void registerFilter(final String name, final FilterFunction hookFunction) { registerFilter(name, hookFunction, 10); } public void registerFilter(final String name, final FilterFunction hookFunction, int priority) { - filters.put(name, new Filter<>(name, priority, hookFunction)); + filters.put(name, new FilterHook<>(name, priority, hookFunction)); } - + public ActionContext execute(final String name) { return execute(name, Map.of()); } - - + public ActionContext execute(final String name, final Map arguments) { var context = new ActionContext(new HashMap<>(arguments), new ArrayList<>()); actions.get(name).stream() @@ -91,15 +143,15 @@ public ActionContext execute(final String name, final Map * @param name * @param parameters - * @return + * @return */ public FilterContext filter(final String name, final T parameters) { final FilterContext returnContext = new FilterContext( @@ -111,7 +163,7 @@ public FilterContext filter(final String name, final T parameters) { try { var context = new FilterContext(returnContext.value()); var result = action.function().apply(context); - returnContext.value((T)result); + returnContext.value((T) result); } catch (Exception e) { log.error("error on filter", e); } diff --git a/cms-api/src/test/java/com/condation/cms/api/extensions/http/routes/RoutesManagerTest.java b/cms-api/src/test/java/com/condation/cms/api/extensions/http/routes/RoutesManagerTest.java new file mode 100644 index 000000000..9139f8b1f --- /dev/null +++ b/cms-api/src/test/java/com/condation/cms/api/extensions/http/routes/RoutesManagerTest.java @@ -0,0 +1,78 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/UnitTests/JUnit5TestClass.java to edit this template + */ +package com.condation.cms.api.extensions.http.routes; + +/*- + * #%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.annotations.Route; +import org.assertj.core.api.Assertions; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; +import org.junit.jupiter.api.Test; + +/** + * + * @author thorstenmarx + */ +public class RoutesManagerTest { + + @Test + public void testRegister() { + Object controller = new MyRoutes(); + RoutesManager instance = new RoutesManager(); + instance.register(controller); + + var handler1 = instance.findFirst("/test1", "GET"); + Assertions.assertThat(handler1).isPresent(); + var handler2 = instance.findFirst("/test2", "GET"); + Assertions.assertThat(handler2).isPresent(); + } + + @Test + public void test_no_handler() { + Object controller = new MyRoutes(); + RoutesManager instance = new RoutesManager(); + instance.register(controller); + + var handler = instance.findFirst("/test3", "GET"); + Assertions.assertThat(handler).isEmpty(); + } + + public class MyRoutes { + + @Route("/test1") + public boolean handle1 (Request request, Response response, Callback callback) { + return true; + } + + @Route("/test2") + public boolean handle2 (Request request, Response response, Callback callback) { + return true; + } + } + + +} diff --git a/cms-api/src/test/java/com/condation/cms/api/hooks/HookSystemTest.java b/cms-api/src/test/java/com/condation/cms/api/hooks/HookSystemTest.java index ae5081095..1f2155166 100644 --- a/cms-api/src/test/java/com/condation/cms/api/hooks/HookSystemTest.java +++ b/cms-api/src/test/java/com/condation/cms/api/hooks/HookSystemTest.java @@ -23,9 +23,12 @@ */ +import com.condation.cms.api.annotations.Filter; +import com.condation.cms.api.annotations.Action; import com.condation.cms.api.hooks.HookSystem; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -110,4 +113,47 @@ public void test_filter_remove () { var context = hookSystem.filter("test/list", new ArrayList<>(List.of("1", "2", "3"))); Assertions.assertThat(context.value()).containsExactly("1", "3"); } + + @Test + void test_action_annotation () { + var actionObject = new MyActions(); + + hookSystem.register(actionObject); + + hookSystem.execute("test/annotation/action1"); + + Assertions.assertThat(actionObject.counter).hasValue(2); + } + + @Test + void test_filter_annotations () { + var myFilters = new MyFilters(); + + hookSystem.register(myFilters); + + var context = hookSystem.filter("test/annotation/filter1", new ArrayList<>(List.of("1", "2", "3"))); + Assertions.assertThat(context.value()).containsExactly("1", "3"); + } + + public class MyFilters { + @Filter("test/annotation/filter1") + public List filter (FilterContext> context) { + context.value().remove("2"); + return context.value(); + } + } + + public class MyActions { + + private AtomicInteger counter = new AtomicInteger(0); + + @Action("test/annotation/action1") + public void action1 (ActionContext context) { + counter.incrementAndGet(); + } + @Action("test/annotation/action1") + public void action2 (ActionContext context) { + counter.incrementAndGet(); + } + } } diff --git a/cms-content/src/main/java/com/condation/cms/content/shortcodes/ShortCodes.java b/cms-content/src/main/java/com/condation/cms/content/shortcodes/ShortCodes.java index 047a5e36a..ba0d1c6a4 100644 --- a/cms-content/src/main/java/com/condation/cms/content/shortcodes/ShortCodes.java +++ b/cms-content/src/main/java/com/condation/cms/content/shortcodes/ShortCodes.java @@ -21,11 +21,13 @@ * . * #L% */ - - +import com.condation.cms.api.annotations.ShortCode; import com.condation.cms.api.model.Parameter; import com.condation.cms.api.request.RequestContext; +import java.lang.reflect.Method; import java.util.Collections; +import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; @@ -43,29 +45,29 @@ public class ShortCodes { private final TagMap tagMap; private final TagParser parser; - public ShortCodes (Map> codes, TagParser tagParser) { + public ShortCodes(Map> codes, TagParser tagParser) { this.parser = tagParser; this.tagMap = new TagMap(); this.tagMap.putAll(codes); } - - public Set getShortCodeNames () { + + public Set getShortCodeNames() { return tagMap.names(); } - - public String replace (final String content) { + + public String replace(final String content) { return replace(content, Collections.emptyMap(), null); } - - public String replace (final String content, Map contextModel) { + + public String replace(final String content, Map contextModel) { return replace(content, contextModel, null); } - - public String replace (final String content, Map contextModel, RequestContext requestContext) { + + public String replace(final String content, Map contextModel, RequestContext requestContext) { return parser.parse(content, tagMap, contextModel, requestContext); } - - public String execute (String name, Map parameters, RequestContext requestContext) { + + public String execute(String name, Map parameters, RequestContext requestContext) { if (!tagMap.has(name)) { return ""; } @@ -78,8 +80,76 @@ public String execute (String name, Map parameters, RequestConte } return tagMap.get(name).apply(params); } catch (Exception e) { - log.error("",e); + log.error("", e); } return ""; } + + public static ShortCodes.Builder builder(TagParser tagParser) { + return new Builder(tagParser); + } + + public static class Builder { + + private final TagParser tagParser; + + private final Map> codes = new HashMap<>(); + + private Builder(TagParser tagParser) { + this.tagParser = tagParser; + } + + public Builder register(String name, Function shortCodeFN) { + codes.put(name, shortCodeFN); + return this; + } + + public Builder register (Map> codes) { + this.codes.putAll(codes); + return this; + } + + public Builder register (List handlers) { + if (handlers == null || handlers.isEmpty()) { + return this; + } + + handlers.forEach(this::register); + + return this; + } + + public Builder register(Object handler) { + if (handler == null) { + return this; + } + + Class clazz = handler.getClass(); + for (Method method : clazz.getDeclaredMethods()) { + if (method.isAnnotationPresent(ShortCode.class)) { + if (method.getParameterCount() == 1 && method.getParameterTypes()[0] == Parameter.class) { + method.setAccessible(true); // falls private + ShortCode annotation = method.getAnnotation(ShortCode.class); + String key = annotation.value(); + + codes.put(key, param -> { + try { + return (String) method.invoke(handler, param); + } catch (Exception e) { + throw new RuntimeException("Error calling shortcode: " + key, e); + } + }); + } else { + log.error("ignore methode" + method.getName() + " – wrong signature."); + } + } + } + + return this; + } + + public ShortCodes build() { + return new ShortCodes(codes, tagParser); + } + } } diff --git a/cms-content/src/test/java/com/condation/cms/content/shortcodes/ShortCodesTest.java b/cms-content/src/test/java/com/condation/cms/content/shortcodes/ShortCodesTest.java index 8c0924ba3..618577a5a 100644 --- a/cms-content/src/test/java/com/condation/cms/content/shortcodes/ShortCodesTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/shortcodes/ShortCodesTest.java @@ -23,6 +23,7 @@ */ +import com.condation.cms.api.annotations.ShortCode; import com.condation.cms.api.model.Parameter; import com.condation.cms.api.request.RequestContext; import com.condation.cms.content.ContentBaseTest; @@ -43,44 +44,47 @@ public class ShortCodesTest extends ContentBaseTest { @BeforeEach public void init () { - Map> tags = new HashMap<>(); - tags.put( + var builder = ShortCodes.builder(getTagParser()); + + builder.register( "youtube", (params) -> "".formatted(params.getOrDefault("id", ""))); - tags.put( + builder.register( "hello_from", (params) -> "

%s

from %s

".formatted(params.getOrDefault("name", ""), params.getOrDefault("from", ""))); - tags.put( + builder.register( "mark", params -> "%s".formatted(params.get("_content")) ); - tags.put( + builder.register( "mark2", params -> "%s".formatted(params.get("class"), params.get("_content")) ); - tags.put( + builder.register( "exp", params -> "%s".formatted(params.get("expression")) ); - tags.put( + builder.register( "set_var", params -> { params.getRequestContext().getVariables().put("myVar", "Hello world!"); return ""; } ); - tags.put( + builder.register( "get_var", params -> { return (String)params.getRequestContext().getVariables().getOrDefault("myVar", "DEFAULT"); } ); - shortCodes = new ShortCodes(tags, getTagParser()); + builder.register(new ShortCodesHandler()); + + shortCodes = builder.build(); } @@ -229,4 +233,20 @@ void test_variables() { Assertions.assertThat(result).isEqualTo("Hello world!"); } + + @Test + void test_handler () { + RequestContext requestContext = new RequestContext(); + + var result = shortCodes.replace("[[printHello name='CondationCMS' /]]", Map.of(), requestContext); + + Assertions.assertThat(result).isEqualTo("hello CondationCMS"); + } + + public static class ShortCodesHandler { + @ShortCode("printHello") + public String printHello (Parameter parameter) { + return "hello " + parameter.getOrDefault("name", ""); + } + } } diff --git a/cms-server/src/main/java/com/condation/cms/request/RequestContextFactory.java b/cms-server/src/main/java/com/condation/cms/request/RequestContextFactory.java index c8fdc93f4..151485564 100644 --- a/cms-server/src/main/java/com/condation/cms/request/RequestContextFactory.java +++ b/cms-server/src/main/java/com/condation/cms/request/RequestContextFactory.java @@ -171,16 +171,23 @@ private HookSystem setupAndGetHookSystem() { } private ShortCodes initShortCodes(RequestContext requestContext) { - Map> codes = new HashMap<>(); + var parser = injector.getInstance(TagParser.class); + var builder = ShortCodes.builder(parser); + injector.getInstance(ModuleManager.class).extensions(RegisterShortCodesExtensionPoint.class) - .forEach(extension -> codes.putAll(extension.shortCodes())); + .forEach(extension -> { + builder.register(extension.shortCodes()); + + builder.register(extension.shortCodeDefinitions()); + }); + var codes = new HashMap>(); var wrapper = requestContext.get(ContentHooks.class).getShortCodes(codes); - var parser = injector.getInstance(TagParser.class); + builder.register(wrapper.getShortCodes()); - return new ShortCodes(wrapper.getShortCodes(), parser); + return builder.build(); } public RequestContext create() throws IOException { @@ -219,7 +226,7 @@ public RequestContext create() throws IOException { RenderContext renderContext = new RenderContext( markdownRenderer, - createShortCodes(requestContext), + initShortCodes(requestContext), theme); requestContext.add(RenderContext.class, renderContext); requestContext.add(MarkdownRendererFeature.class, new MarkdownRendererFeature(markdownRenderer)); @@ -253,16 +260,4 @@ public RequestContext create( return requestContext; } - - private ShortCodes createShortCodes(RequestContext requestContext) { - Map> codes = new HashMap<>(); - - injector.getInstance(ModuleManager.class).extensions(RegisterShortCodesExtensionPoint.class) - .forEach(extension -> codes.putAll(extension.shortCodes())); - - var wrapper = requestContext.get(ContentHooks.class).getShortCodes(codes); - var parser = injector.getInstance(TagParser.class); - return new ShortCodes(wrapper.getShortCodes(), parser); - } - } diff --git a/cms-server/src/main/java/com/condation/cms/server/handler/http/RoutesHandler.java b/cms-server/src/main/java/com/condation/cms/server/handler/http/RoutesHandler.java index 2171ded65..04c52d49f 100644 --- a/cms-server/src/main/java/com/condation/cms/server/handler/http/RoutesHandler.java +++ b/cms-server/src/main/java/com/condation/cms/server/handler/http/RoutesHandler.java @@ -24,6 +24,8 @@ import com.condation.cms.api.extensions.HttpRoutesExtensionPoint; import com.condation.cms.api.extensions.Mapping; +import com.condation.cms.api.extensions.http.routes.RoutesExtensionPoint; +import com.condation.cms.api.extensions.http.routes.RoutesManager; import com.condation.cms.api.request.RequestContext; import com.condation.cms.api.utils.RequestUtil; import com.condation.cms.extensions.HttpHandlerExtension; @@ -32,6 +34,7 @@ import com.condation.cms.server.filter.CreateRequestContextFilter; import com.condation.modules.api.ModuleManager; import com.google.inject.Inject; +import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -62,8 +65,8 @@ public boolean handle(Request request, Response response, Callback callback) thr if (tryModuleRoutes(request, response, callback)) { return true; } - - return false; + + return tryRoutesManager(request, response, callback); } catch (Exception e) { log.error(null, e); callback.failed(e); @@ -71,6 +74,27 @@ public boolean handle(Request request, Response response, Callback callback) thr } } + private boolean tryRoutesManager (Request request, Response response, Callback callback) throws Exception { + String route = "/" + RequestUtil.getContentPath(request); + + RoutesManager routesManager = new RoutesManager(); + + + moduleManager.extensions(RoutesExtensionPoint.class) + .stream() + .map(RoutesExtensionPoint::getRouteDefinitions) + .filter(routeDefinitions -> routeDefinitions != null && !routeDefinitions.isEmpty()) + .flatMap(List::stream) + .forEach(controller -> routesManager.register(controller)); + + var handler = routesManager.findFirst(route, request.getMethod()); + if (handler.isPresent()) { + return handler.get().handle(request, response, callback); + } + + return false; + } + private boolean tryModuleRoutes(Request request, Response response, Callback callback) throws Exception { String route = "/" + RequestUtil.getContentPath(request); diff --git a/cms-templates/src/main/java/com/condation/cms/templates/DynamicConfiguration.java b/cms-templates/src/main/java/com/condation/cms/templates/DynamicConfiguration.java index bca613a7d..945c62cda 100644 --- a/cms-templates/src/main/java/com/condation/cms/templates/DynamicConfiguration.java +++ b/cms-templates/src/main/java/com/condation/cms/templates/DynamicConfiguration.java @@ -37,7 +37,7 @@ public record DynamicConfiguration(TemplateComponents templateComponents, Map components, RequestContext requestContext) { public static final DynamicConfiguration EMPTY = new DynamicConfiguration( - new TemplateComponents(Collections.emptyMap()), + new TemplateComponents(), Collections.emptyMap(), null ); diff --git a/cms-templates/src/main/java/com/condation/cms/templates/components/ComponentMap.java b/cms-templates/src/main/java/com/condation/cms/templates/components/ComponentMap.java index 185fa791d..75223403f 100644 --- a/cms-templates/src/main/java/com/condation/cms/templates/components/ComponentMap.java +++ b/cms-templates/src/main/java/com/condation/cms/templates/components/ComponentMap.java @@ -21,26 +21,27 @@ * . * #L% */ - import com.condation.cms.api.model.Parameter; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.function.Function; +import lombok.extern.slf4j.Slf4j; /** * * @author t.marx */ +@Slf4j public class ComponentMap { private final Map> tags = new HashMap<>(); - public Set names () { + public Set names() { return Collections.unmodifiableSet(tags.keySet()); } - + public void put(String codeName, Function function) { tags.put(codeName, function); } @@ -48,7 +49,7 @@ public void put(String codeName, Function function) { public void putAll(Map> tags) { this.tags.putAll(tags); } - + public boolean has(String codeName) { return tags.containsKey(codeName); } diff --git a/cms-templates/src/main/java/com/condation/cms/templates/components/TemplateComponents.java b/cms-templates/src/main/java/com/condation/cms/templates/components/TemplateComponents.java index 5055f9764..93befd56e 100644 --- a/cms-templates/src/main/java/com/condation/cms/templates/components/TemplateComponents.java +++ b/cms-templates/src/main/java/com/condation/cms/templates/components/TemplateComponents.java @@ -21,10 +21,11 @@ * . * #L% */ - - +import com.condation.cms.api.annotations.TemplateComponent; import com.condation.cms.api.model.Parameter; import com.condation.cms.api.request.RequestContext; +import java.lang.reflect.Method; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; @@ -39,16 +40,57 @@ public class TemplateComponents { private final ComponentMap componentMap; - public TemplateComponents (Map> components) { + public TemplateComponents() { this.componentMap = new ComponentMap(); + } + + public void register(String name, Function templateComponentFN) { + componentMap.put(name, templateComponentFN); + } + + public void register(Map> components) { this.componentMap.putAll(components); } - - public Set getComponentNames () { + + public void register(List handlers) { + if (handlers == null || handlers.isEmpty()) { + return; + } + handlers.forEach(this::register); + } + + public void register(Object handler) { + if (handler == null) { + return; + } + + Class clazz = handler.getClass(); + for (Method method : clazz.getDeclaredMethods()) { + if (method.isAnnotationPresent(TemplateComponent.class)) { + if (method.getParameterCount() == 1 && method.getParameterTypes()[0] == Parameter.class) { + method.setAccessible(true); + var annotation = method.getAnnotation(TemplateComponent.class); + String key = annotation.value(); + + componentMap.put(key, param -> { + try { + return (String) method.invoke(handler, param); + } catch (Exception e) { + throw new RuntimeException("Error calling component: " + key, e); + } + }); + } else { + log.error("ignore methode" + method.getName() + " – wrong signature."); + } + } + } + } + + public Set getComponentNames() { return componentMap.names(); } - - public String execute (String name, Map parameters, RequestContext requestContext) { + + public String execute(String name, Map parameters, RequestContext requestContext) { if (!componentMap.has(name)) { return ""; } @@ -61,7 +103,7 @@ public String execute (String name, Map parameters, RequestConte } return componentMap.get(name).apply(params); } catch (Exception e) { - log.error("",e); + log.error("", e); } return ""; } diff --git a/cms-templates/src/main/java/com/condation/cms/templates/module/CMSModuleTemplateEngine.java b/cms-templates/src/main/java/com/condation/cms/templates/module/CMSModuleTemplateEngine.java index 3951e64c1..9f9ac3ee5 100644 --- a/cms-templates/src/main/java/com/condation/cms/templates/module/CMSModuleTemplateEngine.java +++ b/cms-templates/src/main/java/com/condation/cms/templates/module/CMSModuleTemplateEngine.java @@ -129,16 +129,22 @@ private DynamicConfiguration createDynamicConfiguration(Model model) { } private TemplateComponents createTemplateComponents(RequestContext requestContext) { - Map> components = new HashMap<>(); - var injector = requestContext.get(InjectorFeature.class).injector(); + var templateComponents = new TemplateComponents(); injector.getInstance(ModuleManager.class) .extensions(RegisterTemplateComponentExtensionPoint.class) - .forEach(extension -> components.putAll(extension.components())); + .forEach(extension -> { + templateComponents.register(extension.components()); + templateComponents.register(extension.componentDefinitions()); + }); + Map> components = new HashMap<>(); var wrapper = requestContext.get(TemplateHooks.class).getComponents(components); - return new TemplateComponents(wrapper.getComponents()); + + templateComponents.register(wrapper.getComponents()); + + return templateComponents; } @Override diff --git a/cms-templates/src/test/java/com/condation/cms/templates/TemplateEngineComponentTest.java b/cms-templates/src/test/java/com/condation/cms/templates/TemplateEngineComponentTest.java index e72981be5..7b205476f 100644 --- a/cms-templates/src/test/java/com/condation/cms/templates/TemplateEngineComponentTest.java +++ b/cms-templates/src/test/java/com/condation/cms/templates/TemplateEngineComponentTest.java @@ -21,8 +21,8 @@ * . * #L% */ -import com.condation.cms.content.shortcodes.ShortCodes; -import com.condation.cms.content.shortcodes.TagParser; +import com.condation.cms.api.annotations.TemplateComponent; +import com.condation.cms.api.model.Parameter; import com.condation.cms.templates.components.TemplateComponents; import com.condation.cms.templates.exceptions.ParserException; import com.condation.cms.templates.exceptions.RenderException; @@ -44,20 +44,24 @@ public class TemplateEngineComponentTest extends AbstractTemplateEngineTest { @BeforeAll public void setupShortCodes() { - components = new TemplateComponents( - Map.of( + components = new TemplateComponents(); + components.register(Map.of( "tag1", (params) -> { return "Hello"; }, "tag2", (param) -> { return "Hello " + param.get("name") + "!"; - }, - "tag3", (param) -> { - return "
%s
".formatted(param.get("_content")); - }) - ); + })); + components.register(new MyComponents()); dynamicConfiguration = new DynamicConfiguration(components, null); } + + public static class MyComponents { + @TemplateComponent("tag3") + public String tag3 (Parameter parameter) { + return "
%s
".formatted(parameter.get("_content")); + } + } @Override public TemplateLoader getLoader() { diff --git a/cms-templates/src/test/java/com/condation/cms/templates/TemplateFeatureTest.java b/cms-templates/src/test/java/com/condation/cms/templates/TemplateFeatureTest.java index 998c233a5..4e122296a 100644 --- a/cms-templates/src/test/java/com/condation/cms/templates/TemplateFeatureTest.java +++ b/cms-templates/src/test/java/com/condation/cms/templates/TemplateFeatureTest.java @@ -21,7 +21,6 @@ * . * #L% */ -import com.condation.cms.content.shortcodes.TagParser; import com.condation.cms.templates.components.TemplateComponents; import com.condation.cms.templates.loaders.StringTemplateLoader; import com.google.gson.Gson; @@ -43,7 +42,7 @@ public class TemplateFeatureTest extends AbstractTemplateEngineTest { private StringTemplateLoader templateLoader = new StringTemplateLoader(); - + private Gson gson = new GsonBuilder() .setStrictness(Strictness.LENIENT) .create(); @@ -66,7 +65,7 @@ void test_features(String templateFile) throws Exception { var expectedContent = readContent(templateFile + "_expected.html"); var data = getData(templateFile); - + templateLoader.add(templateFile, templateContent); var template = SUT.getTemplate(templateFile); @@ -76,25 +75,25 @@ void test_features(String templateFile) throws Exception { Assertions.assertThat(rendered).isEqualToIgnoringWhitespace(expectedContent); } - private DynamicConfiguration createDynamicConfig () { - TemplateComponents components = new TemplateComponents( - Map.of( - "hello", (params) -> "hello " + params.get("name") - )); + private DynamicConfiguration createDynamicConfig() { + TemplateComponents components = new TemplateComponents(); + components.register(Map.of( + "hello", (params) -> "hello " + params.get("name") + )); return new DynamicConfiguration(components, null); } - - private Map getData (String filename) throws IOException { + + private Map getData(String filename) throws IOException { String dataFile = filename + "_data.json"; if (!exists(dataFile)) { return Collections.emptyMap(); } - + var dataContent = readContent(dataFile); - + return gson.fromJson(dataContent, HashMap.class); } - + private boolean exists(String filename) { var resourcePath = "testdata/" + filename; var url = TemplateFeatureTest.class.getResource(resourcePath);