diff --git a/cms-api/src/main/java/com/condation/cms/api/extensions/http/APIHandlerExtensionPoint.java b/cms-api/src/main/java/com/condation/cms/api/extensions/http/APIHandlerExtensionPoint.java new file mode 100644 index 00000000..4c94490a --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/extensions/http/APIHandlerExtensionPoint.java @@ -0,0 +1,29 @@ +package com.condation.cms.api.extensions.http; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2024 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; + +public abstract class APIHandlerExtensionPoint extends AbstractExtensionPoint { + abstract public PathMapping getMapping(); +} diff --git a/cms-api/src/main/java/com/condation/cms/api/extensions/http/HttpHandler.java b/cms-api/src/main/java/com/condation/cms/api/extensions/http/HttpHandler.java new file mode 100644 index 00000000..8c495ed4 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/extensions/http/HttpHandler.java @@ -0,0 +1,36 @@ +package com.condation.cms.api.extensions.http; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2024 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 org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; + +/** + * + * @author t.marx + */ +public interface HttpHandler { + + boolean handle (Request request, Response response, Callback callback) throws Exception; +} diff --git a/cms-api/src/main/java/com/condation/cms/api/extensions/http/PathMapping.java b/cms-api/src/main/java/com/condation/cms/api/extensions/http/PathMapping.java new file mode 100644 index 00000000..11f297d6 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/extensions/http/PathMapping.java @@ -0,0 +1,53 @@ +package com.condation.cms.api.extensions.http; + +/*- + * #%L + * cms-api + * %% + * Copyright (C) 2023 - 2024 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.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.eclipse.jetty.http.pathmap.PathSpec; + +public class PathMapping { + + private List mappings; + + public PathMapping () { + mappings = new ArrayList<>(); + } + + public void add (PathSpec pathSpec, String method, HttpHandler handler) { + mappings.add(new Mapping(pathSpec, method, handler)); + } + + public Optional getMatchingHandler (String uri, String method) { + return mappings.stream().filter(entry -> + entry.pathSpec.matches(uri) && entry.method.equalsIgnoreCase(method) + ).map(entry -> entry.handler).findFirst(); + } + + + private static record Mapping (PathSpec pathSpec, String method, HttpHandler handler){}; +} diff --git a/cms-api/src/main/java/com/condation/cms/api/hooks/Hooks.java b/cms-api/src/main/java/com/condation/cms/api/hooks/Hooks.java index 325773f3..61a5ce04 100644 --- a/cms-api/src/main/java/com/condation/cms/api/hooks/Hooks.java +++ b/cms-api/src/main/java/com/condation/cms/api/hooks/Hooks.java @@ -38,6 +38,7 @@ public enum Hooks { SCHEDULER_REMOVE("system/scheduler/remove"), HTTP_EXTENSION("system/server/http/extension"), HTTP_ROUTE("system/server/http/route"), + API_ROUTE("system/server/api/route"), TEMPLATE_SUPPLIER("system/template/supplier"), TEMPLATE_FUNCTION("system/template/function") ; diff --git a/cms-core/src/main/java/com/condation/cms/core/scheduler/SingleCronJobRunner.java b/cms-core/src/main/java/com/condation/cms/core/scheduler/SingleCronJobRunner.java index 7c664b7c..ff42e09a 100644 --- a/cms-core/src/main/java/com/condation/cms/core/scheduler/SingleCronJobRunner.java +++ b/cms-core/src/main/java/com/condation/cms/core/scheduler/SingleCronJobRunner.java @@ -28,6 +28,8 @@ import com.condation.cms.api.scheduler.CronJobContext; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; + +import org.quartz.DisallowConcurrentExecution; import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; @@ -36,25 +38,20 @@ * * @author t.marx */ +@DisallowConcurrentExecution public class SingleCronJobRunner implements Job { public static final String DATA_CRONJOB = "cronjob"; public static final String DATA_CONTEXT = "context"; - private static final Lock lock = new ReentrantLock(true); - @Override public void execute(JobExecutionContext context) throws JobExecutionException { - if (context.getJobDetail().getJobDataMap().get(DATA_CRONJOB) != null) { - CronJobContext jobContext = (CronJobContext) context.getJobDetail().getJobDataMap().get(DATA_CONTEXT); - - lock.lock(); - try { - ((CronJob)context.getJobDetail().getJobDataMap().get(DATA_CRONJOB)).accept(jobContext); - } finally { - lock.unlock(); - } - } + CronJobContext jobContext = (CronJobContext) context.getJobDetail().getJobDataMap().get(DATA_CONTEXT); + CronJob cronJob = (CronJob) context.getJobDetail().getJobDataMap().get(DATA_CRONJOB); + + if (cronJob != null && jobContext != null) { + cronJob.accept(jobContext); + } } } diff --git a/cms-extensions/src/main/java/com/condation/cms/extensions/hooks/ServerHooks.java b/cms-extensions/src/main/java/com/condation/cms/extensions/hooks/ServerHooks.java index 4387f8f4..f4663fb5 100644 --- a/cms-extensions/src/main/java/com/condation/cms/extensions/hooks/ServerHooks.java +++ b/cms-extensions/src/main/java/com/condation/cms/extensions/hooks/ServerHooks.java @@ -59,4 +59,12 @@ public HttpHandlerWrapper getHttpRoutes () { return httpExtensions; } + + public HttpHandlerWrapper getAPIRoutes () { + var httpExtensions = new HttpHandlerWrapper(); + requestContext.get(HookSystemFeature.class).hookSystem() + .execute(Hooks.API_ROUTE.hook(), Map.of("apiRoutes", httpExtensions)); + + return httpExtensions; + } } diff --git a/cms-media/src/test/java/com/condation/cms/media/MediaServiceTest.java b/cms-media/src/test/java/com/condation/cms/media/MediaServiceTest.java index e31b746c..6d15f462 100644 --- a/cms-media/src/test/java/com/condation/cms/media/MediaServiceTest.java +++ b/cms-media/src/test/java/com/condation/cms/media/MediaServiceTest.java @@ -23,7 +23,6 @@ */ -import com.condation.cms.media.FileMediaService; import java.io.IOException; import java.nio.file.Path; import org.assertj.core.api.Assertions; diff --git a/cms-server/src/main/java/com/condation/cms/server/VHost.java b/cms-server/src/main/java/com/condation/cms/server/VHost.java index 8b792c2a..97378549 100644 --- a/cms-server/src/main/java/com/condation/cms/server/VHost.java +++ b/cms-server/src/main/java/com/condation/cms/server/VHost.java @@ -75,6 +75,7 @@ import com.condation.cms.server.filter.InitRequestContextFilter; import com.condation.cms.server.filter.PooledRequestContextFilter; import com.condation.cms.server.filter.RequestLoggingFilter; +import com.condation.cms.server.handler.api.APIHandler; import com.condation.cms.server.handler.auth.JettyAuthenticationHandler; import com.condation.cms.server.handler.cache.CacheHandler; import com.condation.cms.server.handler.content.JettyContentHandler; @@ -271,6 +272,10 @@ public Handler buildHttpHandler() { createExtensionHandler() ); + pathMappingsHandler.addMapping(PathSpec.from("/" + APIHandler.PATH + "/*"), + createAPIHandler() + ); + ContextHandler defaultContextHandler = new ContextHandler( pathMappingsHandler, injector.getInstance(SiteProperties.class).contextPath() @@ -301,6 +306,18 @@ public Handler buildHttpHandler() { return hostHandler; } + private Handler.Wrapper createAPIHandler() { + var authHandler = injector.getInstance(JettyAuthenticationHandler.class); + var initContextHandler = injector.getInstance(InitRequestContextFilter.class); + var apiHandler = injector.getInstance(APIHandler.class); + var handlerSequence = new Handler.Sequence( + authHandler, + initContextHandler, + apiHandler + ); + return requestContextFilter(handlerSequence, injector); + } + private Handler.Wrapper createExtensionHandler() { var authHandler = injector.getInstance(JettyAuthenticationHandler.class); var initContextHandler = injector.getInstance(InitRequestContextFilter.class); diff --git a/cms-server/src/main/java/com/condation/cms/server/configs/SiteHandlerModule.java b/cms-server/src/main/java/com/condation/cms/server/configs/SiteHandlerModule.java index ed91e3d4..9b970c23 100644 --- a/cms-server/src/main/java/com/condation/cms/server/configs/SiteHandlerModule.java +++ b/cms-server/src/main/java/com/condation/cms/server/configs/SiteHandlerModule.java @@ -32,6 +32,7 @@ import com.condation.cms.auth.services.AuthService; import com.condation.cms.auth.services.UserService; import com.condation.cms.media.SiteMediaManager; +import com.condation.cms.server.handler.api.APIHandler; import com.condation.cms.server.handler.auth.JettyAuthenticationHandler; import com.condation.cms.server.filter.InitRequestContextFilter; import com.condation.cms.server.handler.content.JettyContentHandler; @@ -75,6 +76,8 @@ protected void configure() { bind(JettyHttpHandlerExtensionHandler.class).in(Singleton.class); bind(JettyExtensionRouteHandler.class).in(Singleton.class); bind(InitRequestContextFilter.class).in(Singleton.class); + + bind(APIHandler.class).in(Singleton.class); //bind(JettyAuthenticationHandler.class).in(Singleton.class); } diff --git a/cms-server/src/main/java/com/condation/cms/server/handler/api/APIHandler.java b/cms-server/src/main/java/com/condation/cms/server/handler/api/APIHandler.java new file mode 100644 index 00000000..79a1d28a --- /dev/null +++ b/cms-server/src/main/java/com/condation/cms/server/handler/api/APIHandler.java @@ -0,0 +1,133 @@ +package com.condation.cms.server.handler.api; + +import com.condation.cms.api.extensions.HttpRoutesExtensionPoint; +import com.condation.cms.api.extensions.Mapping; +import com.condation.cms.api.extensions.http.APIHandlerExtensionPoint; +import com.condation.cms.api.extensions.http.PathMapping; + +/*- + * #%L + * cms-server + * %% + * Copyright (C) 2023 - 2024 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.request.RequestContext; +import com.condation.cms.extensions.HttpHandlerExtension; +import com.condation.cms.extensions.hooks.ServerHooks; +import com.condation.cms.extensions.http.JettyHttpHandlerWrapper; +import com.condation.cms.server.filter.CreateRequestContextFilter; +import com.condation.modules.api.ModuleManager; +import com.google.inject.Inject; + +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; + +/** + * + * @author t.marx + */ +@RequiredArgsConstructor(onConstructor = @__({ + @Inject })) +@Slf4j +public class APIHandler extends Handler.Abstract { + + public static final String PATH = "api"; + + private final ModuleManager moduleManager; + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception { + + try { + if (handleExtensionRoute(request, response, callback)) { + return true; + } + + if (handleModuleRoute(request, response, callback)) { + return true; + } + + Response.writeError(request, response, callback, 404); + return true; + } catch (Exception e) { + log.error(null, e); + callback.failed(e); + return true; + } + } + + private boolean handleModuleRoute(Request request, Response response, Callback callback) throws Exception { + + String apiRoute = getApiRoute(request); + var method = request.getMethod(); + + Optional firstMatch = moduleManager.extensions(APIHandlerExtensionPoint.class) + .stream() + .filter(extension -> extension.getMapping().getMatchingHandler(apiRoute, method).isPresent()) + .map(extension -> extension.getMapping()) + .findFirst(); + + if (firstMatch.isPresent()) { + var mapping = firstMatch.get(); + var handler = mapping.getMatchingHandler(apiRoute, method).get(); + return handler.handle(request, response, callback); + } + + return false; + } + + private boolean handleExtensionRoute(Request request, Response response, Callback callback) throws Exception { + var requestContext = (RequestContext) request.getAttribute(CreateRequestContextFilter.REQUEST_CONTEXT); + + String extension = getApiRoute(request); + var method = request.getMethod(); + + var httpExtensions = requestContext.get(ServerHooks.class).getAPIRoutes(); + Optional findHttpHandler = httpExtensions.findHttpHandler(method, extension); + + if (findHttpHandler.isPresent()) { + return new JettyHttpHandlerWrapper(findHttpHandler.get().handler()).handle(request, response, callback); + } + + return false; + } + + private String getApiRoute(Request request) { + var path = request.getHttpURI().getPath(); + var contextPath = request.getContext().getContextPath(); + + if (!contextPath.endsWith("/")) { + contextPath += "/"; + } + + contextPath = contextPath + PATH + "/"; + + path = path.replaceFirst(contextPath, ""); + if (!path.startsWith("/")) { + path = "/" + path; + } + return path; + } + +} \ No newline at end of file diff --git a/cms-server/src/main/java/com/condation/cms/server/handler/api/APIRegistry.java b/cms-server/src/main/java/com/condation/cms/server/handler/api/APIRegistry.java new file mode 100644 index 00000000..9d3f8743 --- /dev/null +++ b/cms-server/src/main/java/com/condation/cms/server/handler/api/APIRegistry.java @@ -0,0 +1,41 @@ +package com.condation.cms.server.handler.api; + +/*- + * #%L + * cms-server + * %% + * Copyright (C) 2023 - 2024 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.HashMap; +import java.util.Map; +import java.util.function.BiFunction; + +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; + +public class APIRegistry { + + private Map routes = new HashMap<>(); + + public void register (String path, String method, BiFunction handler) { + routes.put(path, new Route(path, method, handler)); + } + + public static record Route (String path, String method, BiFunction handler) {}; +} diff --git a/cms-server/src/main/java/com/condation/cms/server/handler/extensions/JettyExtensionRouteHandler.java b/cms-server/src/main/java/com/condation/cms/server/handler/extensions/JettyExtensionRouteHandler.java index 8db965cd..d018d1e8 100644 --- a/cms-server/src/main/java/com/condation/cms/server/handler/extensions/JettyExtensionRouteHandler.java +++ b/cms-server/src/main/java/com/condation/cms/server/handler/extensions/JettyExtensionRouteHandler.java @@ -27,7 +27,6 @@ import com.condation.cms.extensions.HttpHandlerExtension; import com.condation.cms.extensions.hooks.ServerHooks; import com.condation.cms.extensions.http.JettyHttpHandlerWrapper; -import com.condation.cms.extensions.request.RequestExtensions; import com.condation.cms.server.filter.CreateRequestContextFilter; import java.util.Optional; import lombok.RequiredArgsConstructor; diff --git a/cms-server/src/main/java/com/condation/cms/server/handler/extensions/JettyHttpHandlerExtensionHandler.java b/cms-server/src/main/java/com/condation/cms/server/handler/extensions/JettyHttpHandlerExtensionHandler.java index f81e55a5..a8ed7ec8 100644 --- a/cms-server/src/main/java/com/condation/cms/server/handler/extensions/JettyHttpHandlerExtensionHandler.java +++ b/cms-server/src/main/java/com/condation/cms/server/handler/extensions/JettyHttpHandlerExtensionHandler.java @@ -27,7 +27,6 @@ import com.condation.cms.extensions.HttpHandlerExtension; import com.condation.cms.extensions.hooks.ServerHooks; import com.condation.cms.extensions.http.JettyHttpHandlerWrapper; -import com.condation.cms.extensions.request.RequestExtensions; import com.condation.cms.server.filter.CreateRequestContextFilter; import java.util.Optional; import lombok.RequiredArgsConstructor; @@ -77,7 +76,7 @@ private String getExtensionName(Request request) { } contextPath = contextPath + PATH + "/"; - path = path.replace(contextPath, ""); + path = path.replaceFirst(contextPath, ""); if (!path.startsWith("/")) { path = "/" + path; } diff --git a/modules/example-module/src/main/java/com/condation/cms/modules/example/ExampleApiHandlerExtension.java b/modules/example-module/src/main/java/com/condation/cms/modules/example/ExampleApiHandlerExtension.java new file mode 100644 index 00000000..e4c66f6d --- /dev/null +++ b/modules/example-module/src/main/java/com/condation/cms/modules/example/ExampleApiHandlerExtension.java @@ -0,0 +1,64 @@ +package com.condation.cms.modules.example; + +/*- + * #%L + * example-module + * %% + * Copyright (C) 2023 - 2024 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.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import org.eclipse.jetty.http.pathmap.PathSpec; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; + +import com.condation.cms.api.extensions.http.APIHandlerExtensionPoint; +import com.condation.cms.api.extensions.http.HttpHandler; +import com.condation.cms.api.extensions.http.PathMapping; +import com.condation.modules.api.annotation.Extension; + +import lombok.RequiredArgsConstructor; + +@Extension(APIHandlerExtensionPoint.class) +public class ExampleApiHandlerExtension extends APIHandlerExtensionPoint { + + @Override + public PathMapping getMapping() { + PathMapping mapping = new PathMapping(); + + mapping.add(PathSpec.from("/test-api"), "GET", new ExampleHandler("CondationCMS test api")); + + return mapping; + } + + @RequiredArgsConstructor + private static class ExampleHandler implements HttpHandler { + + private final String message; + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception { + response.write(true, ByteBuffer.wrap(message.getBytes(StandardCharsets.UTF_8)), callback); + return true; + } + + } +} diff --git a/test-server/hosts/features/extensions/http.extension.js b/test-server/hosts/features/extensions/http.extension.js index 24edf6f8..bb47765e 100644 --- a/test-server/hosts/features/extensions/http.extension.js +++ b/test-server/hosts/features/extensions/http.extension.js @@ -24,3 +24,20 @@ $hooks.registerAction("system/server/http/route", (context) => { ) return null; }) + +$hooks.registerAction("system/server/api/route", (context) => { + context.arguments().get("apiRoutes").add( + "GET", + "/hello", + (request, response) => { + + let data = { + "name": "CondationCMS" + } + + response.addHeader("Content-Type", "application/json; charset=utf-8") + response.write(JSON.stringify(data), UTF_8) + } + ) + return null; +}) diff --git a/test-server/modules/example-module/libs/example-module-7.0.0.jar b/test-server/modules/example-module/libs/example-module-7.0.0.jar deleted file mode 100644 index 1e393994..00000000 Binary files a/test-server/modules/example-module/libs/example-module-7.0.0.jar and /dev/null differ diff --git a/test-server/modules/example-module/libs/example-module-7.2.0.jar b/test-server/modules/example-module/libs/example-module-7.2.0.jar new file mode 100644 index 00000000..46944789 Binary files /dev/null and b/test-server/modules/example-module/libs/example-module-7.2.0.jar differ diff --git a/test-server/modules/example-module/module.properties b/test-server/modules/example-module/module.properties index 2f1300c1..3ac324e0 100644 --- a/test-server/modules/example-module/module.properties +++ b/test-server/modules/example-module/module.properties @@ -1,4 +1,4 @@ id=example-module name=example module -version=7.0.0 +version=7.2.0 priority=HIGH \ No newline at end of file