From 849065c40cb96c645a77848c70db119007b66ad7 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Fri, 9 Jul 2021 23:21:45 +1200 Subject: [PATCH 01/27] Refactor move jetty into separate module --- avaje-jetty-loom/pom.xml | 61 +++++++++++++ .../jetty/threadpool/VirtualThreadPool.java | 0 .../src/main/java9/module-info.java | 0 avaje-jex-freemarker/pom.xml | 23 ++++- .../src/main/java/module-info.java | 2 + avaje-jex-jetty/pom.xml | 56 +++++------- .../java/io/avaje/jex/jetty/ContextUtil.java | 17 +--- .../java/io/avaje/jex/jetty/JettyBuilder.java | 10 +-- .../java/io/avaje/jex/jetty/JettyLaunch.java | 35 ++++---- .../io/avaje/jex/jetty/JettyServerConfig.java | 87 +++++++++++++++++++ .../io/avaje/jex/jetty/JettyStartServer.java | 18 ++++ .../io/avaje/jex/jetty/JexHttpContext.java | 10 +-- .../io/avaje/jex/jetty/JexHttpServlet.java | 13 +-- .../io/avaje/jex/jetty/StaticHandler.java | 4 +- .../avaje/jex/jetty/StaticHandlerFactory.java | 0 .../src/main/java/module-info.java | 22 +++++ .../test/java/io/avaje/jex/base/AppRoles.java | 0 .../io/avaje/jex/base/AutoCloseIterator.java | 0 .../avaje/jex/base/CharacterEncodingTest.java | 0 .../avaje/jex/base/ContextAttributeTest.java | 0 .../io/avaje/jex/base/ContextCookieTest.java | 0 .../avaje/jex/base/ContextFormParamTest.java | 0 .../io/avaje/jex/base/ContextLengthTest.java | 0 .../java/io/avaje/jex/base/ContextTest.java | 0 .../avaje/jex/base/ExceptionManagerTest.java | 0 .../java/io/avaje/jex/base/FilterTest.java | 0 .../test/java/io/avaje/jex/base/HelloDto.java | 0 .../test/java/io/avaje/jex/base/JsonTest.java | 0 .../avaje/jex/base/MultipartFormPostTest.java | 0 .../io/avaje/jex/base/NestedRoutesTest.java | 0 .../java/io/avaje/jex/base/RedirectTest.java | 0 .../test/java/io/avaje/jex/base/Roles.java | 0 .../java/io/avaje/jex/base/RolesTest.java | 0 .../io/avaje/jex/base/RouteRegexTest.java | 0 .../io/avaje/jex/base/RouteSplatTest.java | 2 - .../java/io/avaje/jex/base/SimpleTest.java | 0 .../io/avaje/jex/base/StaticContentTest.java | 0 .../test/java/io/avaje/jex/base/TestPair.java | 0 .../test/java/io/avaje/jex/base/VerbTest.java | 0 .../io/avaje/jex/jetty/ContextUtilTest.java | 0 .../src/test/resources/logback-test.xml | 19 ++++ .../src/test/resources/static-a/goodbye.html | 0 .../src/test/resources/static-a/hello.txt | 0 .../src/test/resources/static-a/hello2.txt | 0 .../test-static-files/basic.html | 0 .../test-static-files/index.html | 0 .../test-static-files/plain-file.txt | 0 avaje-jex-mustache/pom.xml | 7 ++ .../src/main/java/module-info.java | 1 + avaje-jex/pom.xml | 28 ++++-- avaje-jex/src/main/java/io/avaje/jex/Jex.java | 57 ++++++------ .../main/java/io/avaje/jex/ServerConfig.java | 7 ++ .../jex/core/BootstapServiceManager.java | 12 +++ .../io/avaje/jex/core/ExceptionManager.java | 7 +- .../java/io/avaje/jex/core/HttpMethodMap.java | 6 +- .../io/avaje/jex/core/ServiceManager.java | 32 +++++-- .../io/avaje/jex/jetty/JettyStartServer.java | 15 ---- .../io/avaje/jex/routes/BootstrapRoutes.java | 14 +++ .../io/avaje/jex/routes/RoutesBuilder.java | 8 +- .../avaje/jex/{core => spi}/HeaderKeys.java | 2 +- .../io/avaje/jex/spi/SpiRoutesProvider.java | 13 +++ .../io/avaje/jex/spi/SpiServiceManager.java | 37 ++++++++ .../jex/spi/SpiServiceManagerProvider.java | 8 ++ .../java/io/avaje/jex/spi/SpiStartServer.java | 2 +- avaje-jex/src/main/java/module-info.java | 33 +++++-- pom.xml | 4 +- 66 files changed, 506 insertions(+), 166 deletions(-) create mode 100644 avaje-jetty-loom/pom.xml rename {avaje-jex-jetty => avaje-jetty-loom}/src/main/java/io/avaje/jex/jetty/threadpool/VirtualThreadPool.java (100%) rename {avaje-jex-jetty => avaje-jetty-loom}/src/main/java9/module-info.java (100%) rename {avaje-jex => avaje-jex-jetty}/src/main/java/io/avaje/jex/jetty/ContextUtil.java (85%) rename {avaje-jex => avaje-jex-jetty}/src/main/java/io/avaje/jex/jetty/JettyBuilder.java (83%) rename {avaje-jex => avaje-jex-jetty}/src/main/java/io/avaje/jex/jetty/JettyLaunch.java (80%) create mode 100644 avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JettyServerConfig.java create mode 100644 avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JettyStartServer.java rename {avaje-jex => avaje-jex-jetty}/src/main/java/io/avaje/jex/jetty/JexHttpContext.java (96%) rename {avaje-jex => avaje-jex-jetty}/src/main/java/io/avaje/jex/jetty/JexHttpServlet.java (89%) rename {avaje-jex => avaje-jex-jetty}/src/main/java/io/avaje/jex/jetty/StaticHandler.java (100%) rename {avaje-jex => avaje-jex-jetty}/src/main/java/io/avaje/jex/jetty/StaticHandlerFactory.java (100%) create mode 100644 avaje-jex-jetty/src/main/java/module-info.java rename {avaje-jex => avaje-jex-jetty}/src/test/java/io/avaje/jex/base/AppRoles.java (100%) rename {avaje-jex => avaje-jex-jetty}/src/test/java/io/avaje/jex/base/AutoCloseIterator.java (100%) rename {avaje-jex => avaje-jex-jetty}/src/test/java/io/avaje/jex/base/CharacterEncodingTest.java (100%) rename {avaje-jex => avaje-jex-jetty}/src/test/java/io/avaje/jex/base/ContextAttributeTest.java (100%) rename {avaje-jex => avaje-jex-jetty}/src/test/java/io/avaje/jex/base/ContextCookieTest.java (100%) rename {avaje-jex => avaje-jex-jetty}/src/test/java/io/avaje/jex/base/ContextFormParamTest.java (100%) rename {avaje-jex => avaje-jex-jetty}/src/test/java/io/avaje/jex/base/ContextLengthTest.java (100%) rename {avaje-jex => avaje-jex-jetty}/src/test/java/io/avaje/jex/base/ContextTest.java (100%) rename {avaje-jex => avaje-jex-jetty}/src/test/java/io/avaje/jex/base/ExceptionManagerTest.java (100%) rename {avaje-jex => avaje-jex-jetty}/src/test/java/io/avaje/jex/base/FilterTest.java (100%) rename {avaje-jex => avaje-jex-jetty}/src/test/java/io/avaje/jex/base/HelloDto.java (100%) rename {avaje-jex => avaje-jex-jetty}/src/test/java/io/avaje/jex/base/JsonTest.java (100%) rename {avaje-jex => avaje-jex-jetty}/src/test/java/io/avaje/jex/base/MultipartFormPostTest.java (100%) rename {avaje-jex => avaje-jex-jetty}/src/test/java/io/avaje/jex/base/NestedRoutesTest.java (100%) rename {avaje-jex => avaje-jex-jetty}/src/test/java/io/avaje/jex/base/RedirectTest.java (100%) rename {avaje-jex => avaje-jex-jetty}/src/test/java/io/avaje/jex/base/Roles.java (100%) rename {avaje-jex => avaje-jex-jetty}/src/test/java/io/avaje/jex/base/RolesTest.java (100%) rename {avaje-jex => avaje-jex-jetty}/src/test/java/io/avaje/jex/base/RouteRegexTest.java (100%) rename {avaje-jex => avaje-jex-jetty}/src/test/java/io/avaje/jex/base/RouteSplatTest.java (97%) rename {avaje-jex => avaje-jex-jetty}/src/test/java/io/avaje/jex/base/SimpleTest.java (100%) rename {avaje-jex => avaje-jex-jetty}/src/test/java/io/avaje/jex/base/StaticContentTest.java (100%) rename {avaje-jex => avaje-jex-jetty}/src/test/java/io/avaje/jex/base/TestPair.java (100%) rename {avaje-jex => avaje-jex-jetty}/src/test/java/io/avaje/jex/base/VerbTest.java (100%) rename {avaje-jex => avaje-jex-jetty}/src/test/java/io/avaje/jex/jetty/ContextUtilTest.java (100%) create mode 100644 avaje-jex-jetty/src/test/resources/logback-test.xml rename {avaje-jex => avaje-jex-jetty}/src/test/resources/static-a/goodbye.html (100%) rename {avaje-jex => avaje-jex-jetty}/src/test/resources/static-a/hello.txt (100%) rename {avaje-jex => avaje-jex-jetty}/src/test/resources/static-a/hello2.txt (100%) rename {avaje-jex => avaje-jex-jetty}/test-static-files/basic.html (100%) rename {avaje-jex => avaje-jex-jetty}/test-static-files/index.html (100%) rename {avaje-jex => avaje-jex-jetty}/test-static-files/plain-file.txt (100%) create mode 100644 avaje-jex/src/main/java/io/avaje/jex/ServerConfig.java create mode 100644 avaje-jex/src/main/java/io/avaje/jex/core/BootstapServiceManager.java delete mode 100644 avaje-jex/src/main/java/io/avaje/jex/jetty/JettyStartServer.java create mode 100644 avaje-jex/src/main/java/io/avaje/jex/routes/BootstrapRoutes.java rename avaje-jex/src/main/java/io/avaje/jex/{core => spi}/HeaderKeys.java (96%) create mode 100644 avaje-jex/src/main/java/io/avaje/jex/spi/SpiRoutesProvider.java create mode 100644 avaje-jex/src/main/java/io/avaje/jex/spi/SpiServiceManager.java create mode 100644 avaje-jex/src/main/java/io/avaje/jex/spi/SpiServiceManagerProvider.java diff --git a/avaje-jetty-loom/pom.xml b/avaje-jetty-loom/pom.xml new file mode 100644 index 00000000..b713869e --- /dev/null +++ b/avaje-jetty-loom/pom.xml @@ -0,0 +1,61 @@ + + + + 4.0.0 + + java11-oss + org.avaje + 3.2 + + + avaje-jex-loomjetty + io.avaje + 1.0 + + + 17 + 17 + 17 + 17 + 11.0.2 + + + + + + org.eclipse.jetty + jetty-servlet + ${jetty.version} + provided + + + + + + + + org.moditect + moditect-maven-plugin + 1.0.0.RC1 + + + add-module-infos + package + + add-module-info + + + 9 + + src/main/java9/module-info.java + + + + + + + + + diff --git a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/threadpool/VirtualThreadPool.java b/avaje-jetty-loom/src/main/java/io/avaje/jex/jetty/threadpool/VirtualThreadPool.java similarity index 100% rename from avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/threadpool/VirtualThreadPool.java rename to avaje-jetty-loom/src/main/java/io/avaje/jex/jetty/threadpool/VirtualThreadPool.java diff --git a/avaje-jex-jetty/src/main/java9/module-info.java b/avaje-jetty-loom/src/main/java9/module-info.java similarity index 100% rename from avaje-jex-jetty/src/main/java9/module-info.java rename to avaje-jetty-loom/src/main/java9/module-info.java diff --git a/avaje-jex-freemarker/pom.xml b/avaje-jex-freemarker/pom.xml index f95e249d..c842b1bf 100644 --- a/avaje-jex-freemarker/pom.xml +++ b/avaje-jex-freemarker/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 avaje-jex-parent @@ -21,6 +22,13 @@ 1.8-SNAPSHOT + + io.avaje + avaje-jex-jetty + 1.8-SNAPSHOT + provided + + org.freemarker freemarker @@ -36,4 +44,17 @@ + + + + maven-surefire-plugin + + + --add-modules io.avaje.jex.jetty + + + + + + diff --git a/avaje-jex-freemarker/src/main/java/module-info.java b/avaje-jex-freemarker/src/main/java/module-info.java index c854fc42..46e85230 100644 --- a/avaje-jex-freemarker/src/main/java/module-info.java +++ b/avaje-jex-freemarker/src/main/java/module-info.java @@ -4,5 +4,7 @@ requires transitive freemarker; requires java.net.http; + requires static io.avaje.jex.jetty; + provides io.avaje.jex.TemplateRender with io.avaje.jex.render.freemarker.FreeMarkerRender; } diff --git a/avaje-jex-jetty/pom.xml b/avaje-jex-jetty/pom.xml index 3e805368..40d85088 100644 --- a/avaje-jex-jetty/pom.xml +++ b/avaje-jex-jetty/pom.xml @@ -2,58 +2,50 @@ - - 4.0.0 - java11-oss - org.avaje - 3.2 + avaje-jex-parent + io.avaje + 1.8-SNAPSHOT + 4.0.0 avaje-jex-jetty - io.avaje - 1.0 - 17 - 17 - 17 - 17 - 11.0.2 + 11 + 11 + 11.0.6 + + io.avaje + avaje-jex + 1.8-SNAPSHOT + org.eclipse.jetty jetty-servlet ${jetty.version} - provided - - org.moditect - moditect-maven-plugin - 1.0.0.RC1 - - - add-module-infos - package - - add-module-info - - - 9 - - src/main/java9/module-info.java - - - - + maven-surefire-plugin + + + --add-opens io.avaje.jex.jetty/io.avaje.jex.base=com.fasterxml.jackson.databind + --add-modules com.fasterxml.jackson.databind + --add-opens io.avaje.jex.jetty/io.avaje.jex=ALL-UNNAMED + --add-opens io.avaje.jex.jetty/io.avaje.jex.base=ALL-UNNAMED + --add-opens io.avaje.jex.jetty/io.avaje.jex.core=ALL-UNNAMED + --add-opens io.avaje.jex.jetty/io.avaje.jex.routes=ALL-UNNAMED + --add-opens io.avaje.jex.jetty/io.avaje.jex.jetty=ALL-UNNAMED + + diff --git a/avaje-jex/src/main/java/io/avaje/jex/jetty/ContextUtil.java b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/ContextUtil.java similarity index 85% rename from avaje-jex/src/main/java/io/avaje/jex/jetty/ContextUtil.java rename to avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/ContextUtil.java index cec20c6b..f40ce684 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/jetty/ContextUtil.java +++ b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/ContextUtil.java @@ -1,21 +1,12 @@ package io.avaje.jex.jetty; -import io.avaje.jex.core.HeaderKeys; - +import io.avaje.jex.spi.HeaderKeys; import jakarta.servlet.ServletInputStream; import jakarta.servlet.http.HttpServletRequest; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.UncheckedIOException; -import java.io.UnsupportedEncodingException; + +import java.io.*; import java.net.URLDecoder; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; class ContextUtil { diff --git a/avaje-jex/src/main/java/io/avaje/jex/jetty/JettyBuilder.java b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JettyBuilder.java similarity index 83% rename from avaje-jex/src/main/java/io/avaje/jex/jetty/JettyBuilder.java rename to avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JettyBuilder.java index 2707131d..1b14bb71 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/jetty/JettyBuilder.java +++ b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JettyBuilder.java @@ -19,11 +19,11 @@ class JettyBuilder { private static final Logger log = LoggerFactory.getLogger(JettyBuilder.class); private final Jex.Inner inner; - private final Jex.Jetty config; + private final JettyServerConfig config; - JettyBuilder(Jex jex) { + JettyBuilder(Jex jex, JettyServerConfig config) { this.inner = jex.inner; - this.config = jex.jetty; + this.config = config; } Server build() { @@ -38,10 +38,10 @@ Server build() { } private ThreadPool pool() { - if (config.virtualThreads) { + if (config.virtualThreads()) { return virtualThreadBasePool(); } else { - return config.maxThreads == 0 ? new QueuedThreadPool() : new QueuedThreadPool(config.maxThreads); + return config.maxThreads() == 0 ? new QueuedThreadPool() : new QueuedThreadPool(config.maxThreads()); } } diff --git a/avaje-jex/src/main/java/io/avaje/jex/jetty/JettyLaunch.java b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JettyLaunch.java similarity index 80% rename from avaje-jex/src/main/java/io/avaje/jex/jetty/JettyLaunch.java rename to avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JettyLaunch.java index a1000b08..f674e9f4 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/jetty/JettyLaunch.java +++ b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JettyLaunch.java @@ -1,9 +1,9 @@ package io.avaje.jex.jetty; import io.avaje.jex.Jex; +import io.avaje.jex.ServerConfig; import io.avaje.jex.StaticFileSource; -import io.avaje.jex.core.ServiceManager; -import io.avaje.jex.routes.RoutesBuilder; +import io.avaje.jex.spi.SpiServiceManager; import io.avaje.jex.spi.SpiRoutes; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -27,11 +27,19 @@ class JettyLaunch implements Jex.Server { private final Jex jex; private final SpiRoutes routes; + private final SpiServiceManager serviceManager; + private final JettyServerConfig config; private Server server; - JettyLaunch(Jex jex) { + JettyLaunch(Jex jex, SpiRoutes routes, SpiServiceManager serviceManager) { this.jex = jex; - this.routes = new RoutesBuilder(jex.routing(), jex).build(); + this.routes = routes; + this.serviceManager = serviceManager; + this.config = initConfig(jex.serverConfig()); + } + + private JettyServerConfig initConfig(ServerConfig config) { + return config == null ? new JettyServerConfig() : (JettyServerConfig)config; } @Override @@ -62,11 +70,11 @@ protected Server createServer() { } protected Server initServer() { - Server server = jex.jetty.server; + Server server = config.server(); if (server != null) { return server; } - return new JettyBuilder(jex).build(); + return new JettyBuilder(jex, config).build(); } protected ServletContextHandler initContextHandler() { @@ -77,18 +85,17 @@ protected ServletContextHandler initContextHandler() { } protected ServletHolder initServletHolder() { - final ServiceManager manager = serviceManager(); final StaticHandler staticHandler = initStaticHandler(); - return new ServletHolder(new JexHttpServlet(jex, routes, manager, staticHandler)); + return new ServletHolder(new JexHttpServlet(jex, routes, serviceManager, staticHandler)); } protected ServletContextHandler initServletContextHandler() { - final ServletContextHandler ch = jex.jetty.contextHandler; - return ch != null ? ch : new ContextHandler(jex.inner.contextPath, jex.jetty.sessions, jex.jetty.security); + final ServletContextHandler ch = config.contextHandler(); + return ch != null ? ch : new ContextHandler(jex.inner.contextPath, config.sessions(), config.security()); } protected SessionHandler initSessionHandler() { - SessionHandler sh = jex.jetty.sessionHandler; + SessionHandler sh = config.sessionHandler(); return sh == null ? defaultSessionHandler() : sh; } @@ -107,13 +114,9 @@ protected StaticHandler initStaticHandler() { return factory.build(jex, fileSources); } - protected ServiceManager serviceManager() { - return ServiceManager.create(jex); - } - private void logOnStart(org.eclipse.jetty.server.Server server) { for (Connector c : server.getConnectors()) { - String virtualThreads = jex.jetty.virtualThreads ? "with virtualThreads" : ""; + String virtualThreads = config.virtualThreads() ? "with virtualThreads" : ""; if (c instanceof ServerConnector) { ServerConnector sc = (ServerConnector) c; String host = (sc.getHost() == null) ? "0.0.0.0" : sc.getHost(); diff --git a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JettyServerConfig.java b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JettyServerConfig.java new file mode 100644 index 00000000..f09bc5fa --- /dev/null +++ b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JettyServerConfig.java @@ -0,0 +1,87 @@ +package io.avaje.jex.jetty; + +import io.avaje.jex.ServerConfig; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.session.SessionHandler; +import org.eclipse.jetty.servlet.ServletContextHandler; + +public class JettyServerConfig implements ServerConfig { + + private boolean sessions = true; + private boolean security = true; + /** + * Set true to use Loom virtual threads for ThreadPool. + * This requires JDK 17 with Loom included. + */ + private boolean virtualThreads; + /** + * Set maxThreads when using default QueuedThreadPool. Defaults to 200. + */ + private int maxThreads; + private SessionHandler sessionHandler; + private ServletContextHandler contextHandler; + private org.eclipse.jetty.server.Server server; + + public boolean sessions() { + return sessions; + } + + public JettyServerConfig sessions(boolean sessions) { + this.sessions = sessions; + return this; + } + + public boolean security() { + return security; + } + + public JettyServerConfig security(boolean security) { + this.security = security; + return this; + } + + public boolean virtualThreads() { + return virtualThreads; + } + + public JettyServerConfig virtualThreads(boolean virtualThreads) { + this.virtualThreads = virtualThreads; + return this; + } + + public int maxThreads() { + return maxThreads; + } + + public JettyServerConfig maxThreads(int maxThreads) { + this.maxThreads = maxThreads; + return this; + } + + public SessionHandler sessionHandler() { + return sessionHandler; + } + + public JettyServerConfig sessionHandler(SessionHandler sessionHandler) { + this.sessionHandler = sessionHandler; + return this; + } + + public ServletContextHandler contextHandler() { + return contextHandler; + } + + public JettyServerConfig contextHandler(ServletContextHandler contextHandler) { + this.contextHandler = contextHandler; + return this; + } + + public Server server() { + return server; + } + + public JettyServerConfig server(Server server) { + this.server = server; + return this; + } +} diff --git a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JettyStartServer.java b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JettyStartServer.java new file mode 100644 index 00000000..390fd419 --- /dev/null +++ b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JettyStartServer.java @@ -0,0 +1,18 @@ +package io.avaje.jex.jetty; + +import io.avaje.jex.Jex; +import io.avaje.jex.spi.SpiRoutes; +import io.avaje.jex.spi.SpiServiceManager; +import io.avaje.jex.spi.SpiStartServer; + +/** + * Configure and starts the underlying Jetty server. + */ +public class JettyStartServer implements SpiStartServer { + + @Override + public Jex.Server start(Jex jex, SpiRoutes routes, SpiServiceManager serviceManager) { + return new JettyLaunch(jex, routes, serviceManager) + .start(); + } +} diff --git a/avaje-jex/src/main/java/io/avaje/jex/jetty/JexHttpContext.java b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java similarity index 96% rename from avaje-jex/src/main/java/io/avaje/jex/jetty/JexHttpContext.java rename to avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java index 6dac57b2..5cf2ba86 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/jetty/JexHttpContext.java +++ b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java @@ -3,8 +3,8 @@ import io.avaje.jex.Context; import io.avaje.jex.Routing; import io.avaje.jex.UploadedFile; -import io.avaje.jex.core.HeaderKeys; -import io.avaje.jex.core.ServiceManager; +import io.avaje.jex.spi.SpiServiceManager; +import io.avaje.jex.spi.HeaderKeys; import io.avaje.jex.http.RedirectResponse; import io.avaje.jex.spi.SpiContext; import io.avaje.jex.spi.SpiRoutes; @@ -26,7 +26,7 @@ class JexHttpContext implements SpiContext { - private final ServiceManager mgr; + private final SpiServiceManager mgr; protected final HttpServletRequest req; private final HttpServletResponse res; private final Map pathParams; @@ -36,7 +36,7 @@ class JexHttpContext implements SpiContext { private Routing.Type mode; private Map> formParamMap; - JexHttpContext(ServiceManager mgr, HttpServletRequest req, HttpServletResponse res, String matchedPath) { + JexHttpContext(SpiServiceManager mgr, HttpServletRequest req, HttpServletResponse res, String matchedPath) { this.mgr = mgr; this.req = req; this.res = res; @@ -45,7 +45,7 @@ class JexHttpContext implements SpiContext { this.splats = null; } - JexHttpContext(ServiceManager mgr, HttpServletRequest req, HttpServletResponse res, String matchedPath, SpiRoutes.Params params) { + JexHttpContext(SpiServiceManager mgr, HttpServletRequest req, HttpServletResponse res, String matchedPath, SpiRoutes.Params params) { this.mgr = mgr; this.req = req; this.res = res; diff --git a/avaje-jex/src/main/java/io/avaje/jex/jetty/JexHttpServlet.java b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpServlet.java similarity index 89% rename from avaje-jex/src/main/java/io/avaje/jex/jetty/JexHttpServlet.java rename to avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpServlet.java index 69eda78b..3e4f6609 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/jetty/JexHttpServlet.java +++ b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpServlet.java @@ -3,28 +3,23 @@ import io.avaje.jex.Context; import io.avaje.jex.Jex; import io.avaje.jex.Routing; -import io.avaje.jex.core.HttpMethodMap; -import io.avaje.jex.core.ServiceManager; import io.avaje.jex.http.NotFoundResponse; import io.avaje.jex.spi.SpiContext; import io.avaje.jex.spi.SpiRoutes; - +import io.avaje.jex.spi.SpiServiceManager; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import java.util.Collections; -import java.util.Map; class JexHttpServlet extends HttpServlet { //private static final String X_HTTP_METHOD_OVERRIDE = "X-HTTP-Method-Override"; private final SpiRoutes routes; - private final ServiceManager manager; + private final SpiServiceManager manager; private final StaticHandler staticHandler; - private final HttpMethodMap methodMap = new HttpMethodMap(); private final boolean prefer405; - JexHttpServlet(Jex jex, SpiRoutes routes, ServiceManager manager, StaticHandler staticHandler) { + JexHttpServlet(Jex jex, SpiRoutes routes, SpiServiceManager manager, StaticHandler staticHandler) { this.routes = routes; this.manager = manager; this.staticHandler = staticHandler; @@ -100,6 +95,6 @@ private boolean hasGetHandler(String uri) { } private Routing.Type method(HttpServletRequest req) { - return methodMap.get(req.getMethod()); + return manager.lookupRoutingType(req.getMethod()); } } diff --git a/avaje-jex/src/main/java/io/avaje/jex/jetty/StaticHandler.java b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/StaticHandler.java similarity index 100% rename from avaje-jex/src/main/java/io/avaje/jex/jetty/StaticHandler.java rename to avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/StaticHandler.java index 52f071be..1238748e 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/jetty/StaticHandler.java +++ b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/StaticHandler.java @@ -1,6 +1,8 @@ package io.avaje.jex.jetty; import io.avaje.jex.StaticFileSource; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.handler.ResourceHandler; import org.eclipse.jetty.util.resource.EmptyResource; @@ -8,8 +10,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; import java.io.File; import java.io.IOException; import java.util.ArrayList; diff --git a/avaje-jex/src/main/java/io/avaje/jex/jetty/StaticHandlerFactory.java b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/StaticHandlerFactory.java similarity index 100% rename from avaje-jex/src/main/java/io/avaje/jex/jetty/StaticHandlerFactory.java rename to avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/StaticHandlerFactory.java diff --git a/avaje-jex-jetty/src/main/java/module-info.java b/avaje-jex-jetty/src/main/java/module-info.java new file mode 100644 index 00000000..9f767e38 --- /dev/null +++ b/avaje-jex-jetty/src/main/java/module-info.java @@ -0,0 +1,22 @@ +import io.avaje.jex.jetty.JettyStartServer; +import io.avaje.jex.spi.SpiStartServer; + +module io.avaje.jex.jetty { + + exports io.avaje.jex.jetty; + + requires transitive io.avaje.jex; + //requires io.avaje.jex.jettyx; + requires java.net.http; + requires transitive jetty.servlet.api; + requires transitive org.slf4j; + requires transitive org.eclipse.jetty.http; + requires transitive org.eclipse.jetty.servlet; + requires transitive org.eclipse.jetty.server; + requires transitive org.eclipse.jetty.io; + requires transitive org.eclipse.jetty.util; + + requires transitive com.fasterxml.jackson.databind; + + provides SpiStartServer with JettyStartServer; +} diff --git a/avaje-jex/src/test/java/io/avaje/jex/base/AppRoles.java b/avaje-jex-jetty/src/test/java/io/avaje/jex/base/AppRoles.java similarity index 100% rename from avaje-jex/src/test/java/io/avaje/jex/base/AppRoles.java rename to avaje-jex-jetty/src/test/java/io/avaje/jex/base/AppRoles.java diff --git a/avaje-jex/src/test/java/io/avaje/jex/base/AutoCloseIterator.java b/avaje-jex-jetty/src/test/java/io/avaje/jex/base/AutoCloseIterator.java similarity index 100% rename from avaje-jex/src/test/java/io/avaje/jex/base/AutoCloseIterator.java rename to avaje-jex-jetty/src/test/java/io/avaje/jex/base/AutoCloseIterator.java diff --git a/avaje-jex/src/test/java/io/avaje/jex/base/CharacterEncodingTest.java b/avaje-jex-jetty/src/test/java/io/avaje/jex/base/CharacterEncodingTest.java similarity index 100% rename from avaje-jex/src/test/java/io/avaje/jex/base/CharacterEncodingTest.java rename to avaje-jex-jetty/src/test/java/io/avaje/jex/base/CharacterEncodingTest.java diff --git a/avaje-jex/src/test/java/io/avaje/jex/base/ContextAttributeTest.java b/avaje-jex-jetty/src/test/java/io/avaje/jex/base/ContextAttributeTest.java similarity index 100% rename from avaje-jex/src/test/java/io/avaje/jex/base/ContextAttributeTest.java rename to avaje-jex-jetty/src/test/java/io/avaje/jex/base/ContextAttributeTest.java diff --git a/avaje-jex/src/test/java/io/avaje/jex/base/ContextCookieTest.java b/avaje-jex-jetty/src/test/java/io/avaje/jex/base/ContextCookieTest.java similarity index 100% rename from avaje-jex/src/test/java/io/avaje/jex/base/ContextCookieTest.java rename to avaje-jex-jetty/src/test/java/io/avaje/jex/base/ContextCookieTest.java diff --git a/avaje-jex/src/test/java/io/avaje/jex/base/ContextFormParamTest.java b/avaje-jex-jetty/src/test/java/io/avaje/jex/base/ContextFormParamTest.java similarity index 100% rename from avaje-jex/src/test/java/io/avaje/jex/base/ContextFormParamTest.java rename to avaje-jex-jetty/src/test/java/io/avaje/jex/base/ContextFormParamTest.java diff --git a/avaje-jex/src/test/java/io/avaje/jex/base/ContextLengthTest.java b/avaje-jex-jetty/src/test/java/io/avaje/jex/base/ContextLengthTest.java similarity index 100% rename from avaje-jex/src/test/java/io/avaje/jex/base/ContextLengthTest.java rename to avaje-jex-jetty/src/test/java/io/avaje/jex/base/ContextLengthTest.java diff --git a/avaje-jex/src/test/java/io/avaje/jex/base/ContextTest.java b/avaje-jex-jetty/src/test/java/io/avaje/jex/base/ContextTest.java similarity index 100% rename from avaje-jex/src/test/java/io/avaje/jex/base/ContextTest.java rename to avaje-jex-jetty/src/test/java/io/avaje/jex/base/ContextTest.java diff --git a/avaje-jex/src/test/java/io/avaje/jex/base/ExceptionManagerTest.java b/avaje-jex-jetty/src/test/java/io/avaje/jex/base/ExceptionManagerTest.java similarity index 100% rename from avaje-jex/src/test/java/io/avaje/jex/base/ExceptionManagerTest.java rename to avaje-jex-jetty/src/test/java/io/avaje/jex/base/ExceptionManagerTest.java diff --git a/avaje-jex/src/test/java/io/avaje/jex/base/FilterTest.java b/avaje-jex-jetty/src/test/java/io/avaje/jex/base/FilterTest.java similarity index 100% rename from avaje-jex/src/test/java/io/avaje/jex/base/FilterTest.java rename to avaje-jex-jetty/src/test/java/io/avaje/jex/base/FilterTest.java diff --git a/avaje-jex/src/test/java/io/avaje/jex/base/HelloDto.java b/avaje-jex-jetty/src/test/java/io/avaje/jex/base/HelloDto.java similarity index 100% rename from avaje-jex/src/test/java/io/avaje/jex/base/HelloDto.java rename to avaje-jex-jetty/src/test/java/io/avaje/jex/base/HelloDto.java diff --git a/avaje-jex/src/test/java/io/avaje/jex/base/JsonTest.java b/avaje-jex-jetty/src/test/java/io/avaje/jex/base/JsonTest.java similarity index 100% rename from avaje-jex/src/test/java/io/avaje/jex/base/JsonTest.java rename to avaje-jex-jetty/src/test/java/io/avaje/jex/base/JsonTest.java diff --git a/avaje-jex/src/test/java/io/avaje/jex/base/MultipartFormPostTest.java b/avaje-jex-jetty/src/test/java/io/avaje/jex/base/MultipartFormPostTest.java similarity index 100% rename from avaje-jex/src/test/java/io/avaje/jex/base/MultipartFormPostTest.java rename to avaje-jex-jetty/src/test/java/io/avaje/jex/base/MultipartFormPostTest.java diff --git a/avaje-jex/src/test/java/io/avaje/jex/base/NestedRoutesTest.java b/avaje-jex-jetty/src/test/java/io/avaje/jex/base/NestedRoutesTest.java similarity index 100% rename from avaje-jex/src/test/java/io/avaje/jex/base/NestedRoutesTest.java rename to avaje-jex-jetty/src/test/java/io/avaje/jex/base/NestedRoutesTest.java diff --git a/avaje-jex/src/test/java/io/avaje/jex/base/RedirectTest.java b/avaje-jex-jetty/src/test/java/io/avaje/jex/base/RedirectTest.java similarity index 100% rename from avaje-jex/src/test/java/io/avaje/jex/base/RedirectTest.java rename to avaje-jex-jetty/src/test/java/io/avaje/jex/base/RedirectTest.java diff --git a/avaje-jex/src/test/java/io/avaje/jex/base/Roles.java b/avaje-jex-jetty/src/test/java/io/avaje/jex/base/Roles.java similarity index 100% rename from avaje-jex/src/test/java/io/avaje/jex/base/Roles.java rename to avaje-jex-jetty/src/test/java/io/avaje/jex/base/Roles.java diff --git a/avaje-jex/src/test/java/io/avaje/jex/base/RolesTest.java b/avaje-jex-jetty/src/test/java/io/avaje/jex/base/RolesTest.java similarity index 100% rename from avaje-jex/src/test/java/io/avaje/jex/base/RolesTest.java rename to avaje-jex-jetty/src/test/java/io/avaje/jex/base/RolesTest.java diff --git a/avaje-jex/src/test/java/io/avaje/jex/base/RouteRegexTest.java b/avaje-jex-jetty/src/test/java/io/avaje/jex/base/RouteRegexTest.java similarity index 100% rename from avaje-jex/src/test/java/io/avaje/jex/base/RouteRegexTest.java rename to avaje-jex-jetty/src/test/java/io/avaje/jex/base/RouteRegexTest.java diff --git a/avaje-jex/src/test/java/io/avaje/jex/base/RouteSplatTest.java b/avaje-jex-jetty/src/test/java/io/avaje/jex/base/RouteSplatTest.java similarity index 97% rename from avaje-jex/src/test/java/io/avaje/jex/base/RouteSplatTest.java rename to avaje-jex-jetty/src/test/java/io/avaje/jex/base/RouteSplatTest.java index 1ea61c5e..220cf00e 100644 --- a/avaje-jex/src/test/java/io/avaje/jex/base/RouteSplatTest.java +++ b/avaje-jex-jetty/src/test/java/io/avaje/jex/base/RouteSplatTest.java @@ -4,9 +4,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; -import java.net.URLEncoder; import java.net.http.HttpResponse; -import java.nio.charset.StandardCharsets; import static org.assertj.core.api.Assertions.assertThat; diff --git a/avaje-jex/src/test/java/io/avaje/jex/base/SimpleTest.java b/avaje-jex-jetty/src/test/java/io/avaje/jex/base/SimpleTest.java similarity index 100% rename from avaje-jex/src/test/java/io/avaje/jex/base/SimpleTest.java rename to avaje-jex-jetty/src/test/java/io/avaje/jex/base/SimpleTest.java diff --git a/avaje-jex/src/test/java/io/avaje/jex/base/StaticContentTest.java b/avaje-jex-jetty/src/test/java/io/avaje/jex/base/StaticContentTest.java similarity index 100% rename from avaje-jex/src/test/java/io/avaje/jex/base/StaticContentTest.java rename to avaje-jex-jetty/src/test/java/io/avaje/jex/base/StaticContentTest.java diff --git a/avaje-jex/src/test/java/io/avaje/jex/base/TestPair.java b/avaje-jex-jetty/src/test/java/io/avaje/jex/base/TestPair.java similarity index 100% rename from avaje-jex/src/test/java/io/avaje/jex/base/TestPair.java rename to avaje-jex-jetty/src/test/java/io/avaje/jex/base/TestPair.java diff --git a/avaje-jex/src/test/java/io/avaje/jex/base/VerbTest.java b/avaje-jex-jetty/src/test/java/io/avaje/jex/base/VerbTest.java similarity index 100% rename from avaje-jex/src/test/java/io/avaje/jex/base/VerbTest.java rename to avaje-jex-jetty/src/test/java/io/avaje/jex/base/VerbTest.java diff --git a/avaje-jex/src/test/java/io/avaje/jex/jetty/ContextUtilTest.java b/avaje-jex-jetty/src/test/java/io/avaje/jex/jetty/ContextUtilTest.java similarity index 100% rename from avaje-jex/src/test/java/io/avaje/jex/jetty/ContextUtilTest.java rename to avaje-jex-jetty/src/test/java/io/avaje/jex/jetty/ContextUtilTest.java diff --git a/avaje-jex-jetty/src/test/resources/logback-test.xml b/avaje-jex-jetty/src/test/resources/logback-test.xml new file mode 100644 index 00000000..199b4a4f --- /dev/null +++ b/avaje-jex-jetty/src/test/resources/logback-test.xml @@ -0,0 +1,19 @@ + + + + TRACE + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + diff --git a/avaje-jex/src/test/resources/static-a/goodbye.html b/avaje-jex-jetty/src/test/resources/static-a/goodbye.html similarity index 100% rename from avaje-jex/src/test/resources/static-a/goodbye.html rename to avaje-jex-jetty/src/test/resources/static-a/goodbye.html diff --git a/avaje-jex/src/test/resources/static-a/hello.txt b/avaje-jex-jetty/src/test/resources/static-a/hello.txt similarity index 100% rename from avaje-jex/src/test/resources/static-a/hello.txt rename to avaje-jex-jetty/src/test/resources/static-a/hello.txt diff --git a/avaje-jex/src/test/resources/static-a/hello2.txt b/avaje-jex-jetty/src/test/resources/static-a/hello2.txt similarity index 100% rename from avaje-jex/src/test/resources/static-a/hello2.txt rename to avaje-jex-jetty/src/test/resources/static-a/hello2.txt diff --git a/avaje-jex/test-static-files/basic.html b/avaje-jex-jetty/test-static-files/basic.html similarity index 100% rename from avaje-jex/test-static-files/basic.html rename to avaje-jex-jetty/test-static-files/basic.html diff --git a/avaje-jex/test-static-files/index.html b/avaje-jex-jetty/test-static-files/index.html similarity index 100% rename from avaje-jex/test-static-files/index.html rename to avaje-jex-jetty/test-static-files/index.html diff --git a/avaje-jex/test-static-files/plain-file.txt b/avaje-jex-jetty/test-static-files/plain-file.txt similarity index 100% rename from avaje-jex/test-static-files/plain-file.txt rename to avaje-jex-jetty/test-static-files/plain-file.txt diff --git a/avaje-jex-mustache/pom.xml b/avaje-jex-mustache/pom.xml index 2365599e..5fce6132 100644 --- a/avaje-jex-mustache/pom.xml +++ b/avaje-jex-mustache/pom.xml @@ -28,6 +28,13 @@ true + + io.avaje + avaje-jex-jetty + 1.8-SNAPSHOT + provided + + io.avaje avaje-jex-test diff --git a/avaje-jex-mustache/src/main/java/module-info.java b/avaje-jex-mustache/src/main/java/module-info.java index 77d4f013..d2edc28c 100644 --- a/avaje-jex-mustache/src/main/java/module-info.java +++ b/avaje-jex-mustache/src/main/java/module-info.java @@ -3,6 +3,7 @@ requires transitive io.avaje.jex; requires transitive com.github.mustachejava; requires java.net.http; + requires static io.avaje.jex.jetty; provides io.avaje.jex.TemplateRender with io.avaje.jex.render.mustache.MustacheRender; } diff --git a/avaje-jex/pom.xml b/avaje-jex/pom.xml index 6412992a..a40cedcd 100644 --- a/avaje-jex/pom.xml +++ b/avaje-jex/pom.xml @@ -21,16 +21,28 @@ - org.eclipse.jetty - jetty-servlet - ${jetty.version} + org.eclipse.jetty.toolchain + jetty-jakarta-servlet-api + 5.0.2 - - io.avaje - avaje-jex-jetty - 1.0 - + + + + + + + + + + + + + + + + + diff --git a/avaje-jex/src/main/java/io/avaje/jex/Jex.java b/avaje-jex/src/main/java/io/avaje/jex/Jex.java index 0ef22693..46e80eed 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/Jex.java +++ b/avaje-jex/src/main/java/io/avaje/jex/Jex.java @@ -1,10 +1,9 @@ package io.avaje.jex; -import io.avaje.jex.jetty.JettyStartServer; -import io.avaje.jex.spi.JsonService; -import io.avaje.jex.spi.SpiStartServer; -import org.eclipse.jetty.server.session.SessionHandler; -import org.eclipse.jetty.servlet.ServletContextHandler; +import io.avaje.jex.spi.*; +//import io.avaje.jex.jetty.JettyStartServer; +//import org.eclipse.jetty.server.session.SessionHandler; +//import org.eclipse.jetty.servlet.ServletContextHandler; import jakarta.servlet.MultipartConfigElement; import java.util.Collection; @@ -37,7 +36,7 @@ public class Jex { private final StaticFileConfig staticFiles; public final Inner inner = new Inner(); - public final Jetty jetty = new Jetty(); + private ServerConfig serverConfig; private Jex() { this.staticFiles = new DefaultStaticFileConfig(this); @@ -65,25 +64,6 @@ public static class Inner { public final Map renderers = new HashMap<>(); } - /** - * Jetty specific configuration options. - */ - public static class Jetty { - public boolean sessions = true; - public boolean security = true; - /** - * Set true to use Loom virtual threads for ThreadPool. - * This requires JDK 17 with Loom included. - */ - public boolean virtualThreads; - /** - * Set maxThreads when using default QueuedThreadPool. Defaults to 200. - */ - public int maxThreads; - public SessionHandler sessionHandler; - public ServletContextHandler contextHandler; - public org.eclipse.jetty.server.Server server; - } /** * Configure error handlers. @@ -100,6 +80,21 @@ public ErrorHandling errorHandling() { return errorHandling; } + /** + * Return the server specific configuration. + */ + public ServerConfig serverConfig() { + return serverConfig; + } + + /** + * Set the server specific configuration. + */ + public Jex serverConfig(ServerConfig serverConfig) { + this.serverConfig = serverConfig; + return this; + } + /** * Add routes and handlers to the routing. */ @@ -199,11 +194,19 @@ public Jex register(TemplateRender renderer, String... extensions) { * Start the server. */ public Server start() { + final SpiRoutes routes = ServiceLoader.load(SpiRoutesProvider.class) + .findFirst().get() + .create(this.routing, this.inner.accessManager, this.inner.ignoreTrailingSlashes); + + final SpiServiceManager serviceManager = ServiceLoader.load(SpiServiceManagerProvider.class) + .findFirst().get() + .create(this); + final Optional start = ServiceLoader.load(SpiStartServer.class).findFirst(); if (start.isEmpty()) { - return new JettyStartServer().start(this); + throw new IllegalStateException("There is no SpiStartServer? Missing dependency on jex-jetty?"); } - return start.get().start(this); + return start.get().start(this, routes, serviceManager); } /** diff --git a/avaje-jex/src/main/java/io/avaje/jex/ServerConfig.java b/avaje-jex/src/main/java/io/avaje/jex/ServerConfig.java new file mode 100644 index 00000000..7f860f57 --- /dev/null +++ b/avaje-jex/src/main/java/io/avaje/jex/ServerConfig.java @@ -0,0 +1,7 @@ +package io.avaje.jex; + +/** + * Marker for server specific configuration. + */ +public interface ServerConfig { +} diff --git a/avaje-jex/src/main/java/io/avaje/jex/core/BootstapServiceManager.java b/avaje-jex/src/main/java/io/avaje/jex/core/BootstapServiceManager.java new file mode 100644 index 00000000..5ed0b7c7 --- /dev/null +++ b/avaje-jex/src/main/java/io/avaje/jex/core/BootstapServiceManager.java @@ -0,0 +1,12 @@ +package io.avaje.jex.core; + +import io.avaje.jex.Jex; +import io.avaje.jex.spi.SpiServiceManager; +import io.avaje.jex.spi.SpiServiceManagerProvider; + +public class BootstapServiceManager implements SpiServiceManagerProvider { + @Override + public SpiServiceManager create(Jex jex) { + return ServiceManager.create(jex); + } +} diff --git a/avaje-jex/src/main/java/io/avaje/jex/core/ExceptionManager.java b/avaje-jex/src/main/java/io/avaje/jex/core/ExceptionManager.java index 0a5efec4..99393249 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/core/ExceptionManager.java +++ b/avaje-jex/src/main/java/io/avaje/jex/core/ExceptionManager.java @@ -6,20 +6,21 @@ import io.avaje.jex.http.HttpResponseException; import io.avaje.jex.http.InternalServerErrorResponse; import io.avaje.jex.http.RedirectResponse; +import io.avaje.jex.spi.HeaderKeys; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class ExceptionManager { +class ExceptionManager { private static final Logger log = LoggerFactory.getLogger(ExceptionManager.class); private final ErrorHandling errorHandling; - public ExceptionManager(ErrorHandling errorHandling) { + ExceptionManager(ErrorHandling errorHandling) { this.errorHandling = errorHandling; } - public void handle(Context ctx, Exception e) { + void handle(Context ctx, Exception e) { final ExceptionHandler handler = errorHandling.find(e.getClass()); if (handler != null) { handler.handle(e, ctx); diff --git a/avaje-jex/src/main/java/io/avaje/jex/core/HttpMethodMap.java b/avaje-jex/src/main/java/io/avaje/jex/core/HttpMethodMap.java index e0a9884a..d44dca71 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/core/HttpMethodMap.java +++ b/avaje-jex/src/main/java/io/avaje/jex/core/HttpMethodMap.java @@ -5,17 +5,17 @@ import java.util.HashMap; import java.util.Map; -public final class HttpMethodMap { +final class HttpMethodMap { private final Map map = new HashMap<>(); - public HttpMethodMap() { + HttpMethodMap() { for (Routing.Type value : Routing.Type.values()) { map.put(value.name(), value); } } - public Routing.Type get(String method) { + Routing.Type get(String method) { return map.get(method); } } diff --git a/avaje-jex/src/main/java/io/avaje/jex/core/ServiceManager.java b/avaje-jex/src/main/java/io/avaje/jex/core/ServiceManager.java index f1992399..dd392927 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/core/ServiceManager.java +++ b/avaje-jex/src/main/java/io/avaje/jex/core/ServiceManager.java @@ -1,13 +1,10 @@ package io.avaje.jex.core; -import io.avaje.jex.Context; -import io.avaje.jex.ErrorHandling; -import io.avaje.jex.Jex; -import io.avaje.jex.TemplateRender; -import io.avaje.jex.UploadedFile; +import io.avaje.jex.*; import io.avaje.jex.spi.JsonService; import io.avaje.jex.spi.SpiContext; +import io.avaje.jex.spi.SpiServiceManager; import jakarta.servlet.MultipartConfigElement; import jakarta.servlet.http.HttpServletRequest; @@ -17,7 +14,9 @@ import java.util.ServiceLoader; import java.util.stream.Stream; -public class ServiceManager { +class ServiceManager implements SpiServiceManager { + + private final HttpMethodMap methodMap = new HttpMethodMap(); private final JsonService jsonService; @@ -27,7 +26,7 @@ public class ServiceManager { private final MultipartUtil multipartUtil; - public static ServiceManager create(Jex jex) { + static SpiServiceManager create(Jex jex) { return new Builder(jex).build(); } @@ -38,20 +37,24 @@ public static ServiceManager create(Jex jex) { this.multipartUtil = multipartUtil; } + @Override public T jsonRead(Class clazz, SpiContext ctx) { return jsonService.jsonRead(clazz, ctx); } + @Override public void jsonWrite(Object bean, SpiContext ctx) { jsonService.jsonWrite(bean, ctx); } + @Override public void jsonWriteStream(Stream stream, SpiContext ctx) { try (stream) { jsonService.jsonWriteStream(stream.iterator(), ctx); } } + @Override public void jsonWriteStream(Iterator iterator, SpiContext ctx) { try { jsonService.jsonWriteStream(iterator, ctx); @@ -60,7 +63,8 @@ public void jsonWriteStream(Iterator iterator, SpiContext ctx) { } } - private void maybeClose(Object iterator) { + @Override + public void maybeClose(Object iterator) { if (AutoCloseable.class.isAssignableFrom(iterator.getClass())) { try { ((AutoCloseable) iterator).close(); @@ -70,22 +74,32 @@ private void maybeClose(Object iterator) { } } + @Override + public Routing.Type lookupRoutingType(String method) { + return methodMap.get(method); + } + + @Override public void handleException(Context ctx, Exception e) { exceptionHandler.handle(ctx, e); } + @Override public void render(Context ctx, String name, Map model) { templateManager.render(ctx, name, model); } + @Override public List uploadedFiles(HttpServletRequest req) { return multipartUtil.uploadedFiles(req); } + @Override public List uploadedFiles(HttpServletRequest req, String name) { return multipartUtil.uploadedFiles(req, name); } + @Override public Map> multiPartForm(HttpServletRequest req) { return multipartUtil.fieldMap(req); } @@ -97,7 +111,7 @@ private static class Builder { this.jex = jex; } - ServiceManager build() { + SpiServiceManager build() { return new ServiceManager(initJsonService(), jex.errorHandling(), initTemplateMgr(), initMultiPart()); } diff --git a/avaje-jex/src/main/java/io/avaje/jex/jetty/JettyStartServer.java b/avaje-jex/src/main/java/io/avaje/jex/jetty/JettyStartServer.java deleted file mode 100644 index 787566ac..00000000 --- a/avaje-jex/src/main/java/io/avaje/jex/jetty/JettyStartServer.java +++ /dev/null @@ -1,15 +0,0 @@ -package io.avaje.jex.jetty; - -import io.avaje.jex.Jex; -import io.avaje.jex.spi.SpiStartServer; - -/** - * Configure and starts the underlying Jetty server. - */ -public class JettyStartServer implements SpiStartServer { - - @Override - public Jex.Server start(Jex jex) { - return new JettyLaunch(jex).start(); - } -} diff --git a/avaje-jex/src/main/java/io/avaje/jex/routes/BootstrapRoutes.java b/avaje-jex/src/main/java/io/avaje/jex/routes/BootstrapRoutes.java new file mode 100644 index 00000000..49b6449b --- /dev/null +++ b/avaje-jex/src/main/java/io/avaje/jex/routes/BootstrapRoutes.java @@ -0,0 +1,14 @@ +package io.avaje.jex.routes; + +import io.avaje.jex.AccessManager; +import io.avaje.jex.Routing; +import io.avaje.jex.spi.SpiRoutes; +import io.avaje.jex.spi.SpiRoutesProvider; + +public class BootstrapRoutes implements SpiRoutesProvider { + + @Override + public SpiRoutes create(Routing routing, AccessManager accessManager, boolean ignoreTrailingSlashes) { + return new RoutesBuilder(routing, accessManager, ignoreTrailingSlashes).build(); + } +} diff --git a/avaje-jex/src/main/java/io/avaje/jex/routes/RoutesBuilder.java b/avaje-jex/src/main/java/io/avaje/jex/routes/RoutesBuilder.java index aa3ad6c2..96fd816f 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/routes/RoutesBuilder.java +++ b/avaje-jex/src/main/java/io/avaje/jex/routes/RoutesBuilder.java @@ -8,7 +8,7 @@ import java.util.List; import java.util.Set; -public class RoutesBuilder { +class RoutesBuilder { private final EnumMap typeMap = new EnumMap<>(Routing.Type.class); private final List before = new ArrayList<>(); @@ -16,9 +16,9 @@ public class RoutesBuilder { private final boolean ignoreTrailingSlashes; private final AccessManager accessManager; - public RoutesBuilder(Routing routing, Jex jex) { - this.accessManager = jex.inner.accessManager; - this.ignoreTrailingSlashes = jex.inner.ignoreTrailingSlashes; + RoutesBuilder(Routing routing, AccessManager accessManager, boolean ignoreTrailingSlashes) { + this.accessManager = accessManager; + this.ignoreTrailingSlashes = ignoreTrailingSlashes; for (Routing.Entry handler : routing.all()) { switch (handler.getType()) { case BEFORE: diff --git a/avaje-jex/src/main/java/io/avaje/jex/core/HeaderKeys.java b/avaje-jex/src/main/java/io/avaje/jex/spi/HeaderKeys.java similarity index 96% rename from avaje-jex/src/main/java/io/avaje/jex/core/HeaderKeys.java rename to avaje-jex/src/main/java/io/avaje/jex/spi/HeaderKeys.java index bf7a298f..e99047ab 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/core/HeaderKeys.java +++ b/avaje-jex/src/main/java/io/avaje/jex/spi/HeaderKeys.java @@ -1,4 +1,4 @@ -package io.avaje.jex.core; +package io.avaje.jex.spi; public class HeaderKeys { diff --git a/avaje-jex/src/main/java/io/avaje/jex/spi/SpiRoutesProvider.java b/avaje-jex/src/main/java/io/avaje/jex/spi/SpiRoutesProvider.java new file mode 100644 index 00000000..d6a4e118 --- /dev/null +++ b/avaje-jex/src/main/java/io/avaje/jex/spi/SpiRoutesProvider.java @@ -0,0 +1,13 @@ +package io.avaje.jex.spi; + +import io.avaje.jex.AccessManager; +import io.avaje.jex.Routing; + +public interface SpiRoutesProvider { + + /** + * Build and return the Routing. + */ + SpiRoutes create(Routing routing, AccessManager accessManager, boolean ignoreTrailingSlashes); + +} diff --git a/avaje-jex/src/main/java/io/avaje/jex/spi/SpiServiceManager.java b/avaje-jex/src/main/java/io/avaje/jex/spi/SpiServiceManager.java new file mode 100644 index 00000000..e27e223b --- /dev/null +++ b/avaje-jex/src/main/java/io/avaje/jex/spi/SpiServiceManager.java @@ -0,0 +1,37 @@ +package io.avaje.jex.spi; + +import io.avaje.jex.Context; +import io.avaje.jex.Routing; +import io.avaje.jex.UploadedFile; +import io.avaje.jex.spi.SpiContext; +import jakarta.servlet.http.HttpServletRequest; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +public interface SpiServiceManager { + + T jsonRead(Class clazz, SpiContext ctx); + + void jsonWrite(Object bean, SpiContext ctx); + + void jsonWriteStream(Stream stream, SpiContext ctx); + + void jsonWriteStream(Iterator iterator, SpiContext ctx); + + void maybeClose(Object iterator); + + Routing.Type lookupRoutingType(String method); + + void handleException(Context ctx, Exception e); + + void render(Context ctx, String name, Map model); + + List uploadedFiles(HttpServletRequest req); + + List uploadedFiles(HttpServletRequest req, String name); + + Map> multiPartForm(HttpServletRequest req); +} diff --git a/avaje-jex/src/main/java/io/avaje/jex/spi/SpiServiceManagerProvider.java b/avaje-jex/src/main/java/io/avaje/jex/spi/SpiServiceManagerProvider.java new file mode 100644 index 00000000..90449ce5 --- /dev/null +++ b/avaje-jex/src/main/java/io/avaje/jex/spi/SpiServiceManagerProvider.java @@ -0,0 +1,8 @@ +package io.avaje.jex.spi; + +import io.avaje.jex.Jex; + +public interface SpiServiceManagerProvider { + + SpiServiceManager create(Jex jex); +} diff --git a/avaje-jex/src/main/java/io/avaje/jex/spi/SpiStartServer.java b/avaje-jex/src/main/java/io/avaje/jex/spi/SpiStartServer.java index 2d937b4f..fa068a15 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/spi/SpiStartServer.java +++ b/avaje-jex/src/main/java/io/avaje/jex/spi/SpiStartServer.java @@ -10,6 +10,6 @@ public interface SpiStartServer { /** * Return the started server. */ - Jex.Server start(Jex jex); + Jex.Server start(Jex jex, SpiRoutes routes, SpiServiceManager serviceManager); } diff --git a/avaje-jex/src/main/java/module-info.java b/avaje-jex/src/main/java/module-info.java index 41db453a..28c8d068 100644 --- a/avaje-jex/src/main/java/module-info.java +++ b/avaje-jex/src/main/java/module-info.java @@ -1,21 +1,36 @@ +import io.avaje.jex.TemplateRender; +import io.avaje.jex.core.BootstapServiceManager; +import io.avaje.jex.routes.BootstrapRoutes; +import io.avaje.jex.spi.SpiRoutesProvider; +import io.avaje.jex.spi.SpiServiceManagerProvider; +import io.avaje.jex.spi.SpiStartServer; + module io.avaje.jex { exports io.avaje.jex; exports io.avaje.jex.http; exports io.avaje.jex.spi; + exports io.avaje.jex.core; + +// requires io.avaje.jex.jetty; - requires io.avaje.jex.jettyx; +// requires io.avaje.jex.jettyx; requires java.net.http; - requires transitive jetty.servlet.api; requires transitive org.slf4j; - requires transitive org.eclipse.jetty.http; - requires transitive org.eclipse.jetty.servlet; - requires transitive org.eclipse.jetty.server; - requires transitive org.eclipse.jetty.io; - requires transitive org.eclipse.jetty.util; + requires jetty.servlet.api; +// requires transitive org.eclipse.jetty.http; +// requires transitive org.eclipse.jetty.servlet; +// requires transitive org.eclipse.jetty.server; +// requires transitive org.eclipse.jetty.io; +// requires transitive org.eclipse.jetty.util; requires transitive com.fasterxml.jackson.databind; - uses io.avaje.jex.spi.SpiStartServer; - uses io.avaje.jex.TemplateRender; + uses TemplateRender; + uses SpiRoutesProvider; + uses SpiServiceManagerProvider; + uses SpiStartServer; + + provides SpiRoutesProvider with BootstrapRoutes; + provides SpiServiceManagerProvider with BootstapServiceManager; } diff --git a/pom.xml b/pom.xml index 899b54c5..26ff80aa 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 org.avaje @@ -26,6 +27,7 @@ avaje-jex-test avaje-jex-freemarker avaje-jex-mustache + avaje-jex-jetty From 8b3bda70f489129feccab58ab845662f88f57105 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Mon, 12 Jul 2021 09:12:25 +1200 Subject: [PATCH 02/27] Initial jex-jdk module --- avaje-jex-jdk/pom.xml | 44 ++ .../java/io/avaje/jex/jdk/BaseHandler.java | 40 ++ .../io/avaje/jex/jdk/BufferedOutStream.java | 56 +++ .../java/io/avaje/jex/jdk/JdkContext.java | 431 ++++++++++++++++++ .../main/java/io/avaje/jex/jdk/JdkServer.java | 18 + .../java/io/avaje/jex/jdk/JdkServerStart.java | 36 ++ .../io/avaje/jex/jdk/JdkServiceManager.java | 77 ++++ avaje-jex-jdk/src/main/java/module-info.java | 10 + .../java/io/avaje/jex/jdk/HeadersTest.java | 54 +++ .../test/java/io/avaje/jex/jdk/HelloBean.java | 16 + .../java/io/avaje/jex/jdk/JdkServerTest.java | 60 +++ .../src/test/resources/logback-test.xml | 19 + .../java/io/avaje/jex/jetty/ContextUtil.java | 40 -- .../io/avaje/jex/jetty/JexHttpContext.java | 21 +- .../io/avaje/jex/jetty/JexHttpServlet.java | 8 +- .../io/avaje/jex/jetty/ContextUtilTest.java | 25 - .../src/main/java/io/avaje/jex/Context.java | 27 +- .../io/avaje/jex/core/ExceptionManager.java | 2 +- .../io/avaje/jex/core/ServiceManager.java | 51 ++- .../io/avaje/jex/spi/SpiServiceManager.java | 35 +- .../io/avaje/jex/core/ContextUtilTest.java | 23 + pom.xml | 1 + 22 files changed, 988 insertions(+), 106 deletions(-) create mode 100644 avaje-jex-jdk/pom.xml create mode 100644 avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/BaseHandler.java create mode 100644 avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/BufferedOutStream.java create mode 100644 avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java create mode 100644 avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServer.java create mode 100644 avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServerStart.java create mode 100644 avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServiceManager.java create mode 100644 avaje-jex-jdk/src/main/java/module-info.java create mode 100644 avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/HeadersTest.java create mode 100644 avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/HelloBean.java create mode 100644 avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/JdkServerTest.java create mode 100644 avaje-jex-jdk/src/test/resources/logback-test.xml delete mode 100644 avaje-jex-jetty/src/test/java/io/avaje/jex/jetty/ContextUtilTest.java create mode 100644 avaje-jex/src/test/java/io/avaje/jex/core/ContextUtilTest.java diff --git a/avaje-jex-jdk/pom.xml b/avaje-jex-jdk/pom.xml new file mode 100644 index 00000000..49baab4e --- /dev/null +++ b/avaje-jex-jdk/pom.xml @@ -0,0 +1,44 @@ + + + + avaje-jex-parent + io.avaje + 1.8-SNAPSHOT + + 4.0.0 + + avaje-jex-jdk + + + 17 + 17 + + + + + + io.avaje + avaje-jex + 1.8-SNAPSHOT + + + + + + + + maven-surefire-plugin + + + --add-modules com.fasterxml.jackson.databind + --add-opens io.avaje.jex.jdk/io.avaje.jex.jdk=com.fasterxml.jackson.databind + --add-opens io.avaje.jex.jdk/io.avaje.jex.jdk=ALL-UNNAMED + + + + + + + diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/BaseHandler.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/BaseHandler.java new file mode 100644 index 00000000..b159e887 --- /dev/null +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/BaseHandler.java @@ -0,0 +1,40 @@ +package io.avaje.jex.jdk; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import io.avaje.jex.Context; +import io.avaje.jex.Routing; +import io.avaje.jex.spi.SpiRoutes; + +import java.io.IOException; +import java.net.URI; + +class BaseHandler implements HttpHandler { + + final SpiRoutes routes; + final JdkServiceManager mgr; + + BaseHandler(SpiRoutes routes, JdkServiceManager mgr) { + this.mgr = mgr; + this.routes = routes; + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + + final String requestMethod = exchange.getRequestMethod(); + final URI requestURI = exchange.getRequestURI(); + final String fragment = requestURI.getFragment(); + final String uri = requestURI.getPath(); + final Routing.Type type = mgr.lookupRoutingType(requestMethod); + final SpiRoutes.Entry match = routes.match(type, uri); + + if (match != null) { + final SpiRoutes.Params params = match.pathParams(uri); + //SpiContext ctx = new JexHttpContext(manager, req, res, route.matchPath(), params); + Context ctx = new JdkContext(mgr, exchange, match.matchPath(), params); + match.handle(ctx); + } + + } +} diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/BufferedOutStream.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/BufferedOutStream.java new file mode 100644 index 00000000..cc7065f6 --- /dev/null +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/BufferedOutStream.java @@ -0,0 +1,56 @@ +package io.avaje.jex.jdk; + +import com.sun.net.httpserver.HttpExchange; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +class BufferedOutStream extends OutputStream { + + private final JdkContext context; + private final long max; + private ByteArrayOutputStream buffer; + private OutputStream stream; + private long count; + + BufferedOutStream(JdkContext context, long max, int bufferSize) { + this.context = context; + this.max = max; + this.buffer = new ByteArrayOutputStream(bufferSize); + } + + @Override + public void write(int b) throws IOException { + if (stream != null) { + stream.write(b); + } else { + buffer.write(b); + if (count++ > max) { + initialiseChunked(); + } + } + } + + /** + * Use responseLength 0 and chunked response. + */ + private void initialiseChunked() throws IOException { + final HttpExchange exchange = context.exchange(); + exchange.sendResponseHeaders(context.statusCode(), 0); + stream = exchange.getResponseBody(); + // empty the existing buffer + stream.write(buffer.toByteArray()); + buffer = null; + } + + @Override + public void close() throws IOException { + if (stream != null) { + stream.flush(); + stream.close(); + } else { + context.writeBytes(buffer.toByteArray()); + } + } +} diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java new file mode 100644 index 00000000..2ac5f4d7 --- /dev/null +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java @@ -0,0 +1,431 @@ +package io.avaje.jex.jdk; + +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpExchange; +import io.avaje.jex.Context; +import io.avaje.jex.Routing; +import io.avaje.jex.UploadedFile; +import io.avaje.jex.spi.HeaderKeys; +import io.avaje.jex.spi.SpiContext; +import io.avaje.jex.spi.SpiRoutes; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.stream.Stream; + +class JdkContext implements Context, SpiContext { + + private final JdkServiceManager mgr; + private final String path; + private final SpiRoutes.Params params; + private final HttpExchange exchange; + private Routing.Type mode; + + private int statusCode; + + JdkContext(JdkServiceManager mgr, HttpExchange exchange, String path, SpiRoutes.Params params) { + this.mgr = mgr; + this.exchange = exchange; + this.path = path; + this.params = params; + } + + @Override + public String matchedPath() { + return path; + } + + @Override + public Context attribute(String key, Object value) { + return null; + } + + @Override + public T attribute(String key) { + return null; + } + + @Override + public Map attributeMap() { + return null; + } + + @Override + public String cookie(String name) { + return null; + } + + @Override + public Map cookieMap() { + return null; + } + + @Override + public Context cookie(String name, String value) { + return null; + } + + @Override + public Context cookie(String name, String value, int maxAge) { + return null; + } + +// @Override +// public Context cookie(Cookie cookie) { +// return null; +// } + + @Override + public Context removeCookie(String name) { + return null; + } + + @Override + public Context removeCookie(String name, String path) { + return null; + } + + @Override + public void redirect(String location) { + + } + + @Override + public void redirect(String location, int httpStatusCode) { + + } + + @Override + public T bodyAsClass(Class beanType) { + return mgr.jsonRead(beanType, this); + } + + @Override + public byte[] bodyAsBytes() { + try { + return exchange.getRequestBody().readAllBytes(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public String body() { + return new String(bodyAsBytes(), StandardCharsets.UTF_8); + } + + @Override + public long contentLength() { + return 0; + } + + @Override + public String contentType() { + return header(exchange.getRequestHeaders(), HeaderKeys.CONTENT_TYPE); + } + + @Override + public String contentTypeOfResponse() { + return header(exchange.getResponseHeaders(), HeaderKeys.CONTENT_TYPE); + } + + private String header(Headers headers, String name) { + final List values = headers.get(name); + return values.isEmpty() ? null : values.get(0); + } + + @Override + public Context contentType(String contentType) { + exchange.getResponseHeaders().add(HeaderKeys.CONTENT_TYPE, contentType); + return this; + } + + @Override + public String splat(int position) { + return params.splats.get(position); + } + + @Override + public List splats() { + return params.splats; + } + + @Override + public Map pathParamMap() { + return params.pathParams; + } + + @Override + public String pathParam(String name) { + return params.pathParams.get(name); + } + + @Override + public String queryParam(String name) { + return null; + } + + @Override + public List queryParams(String name) { + return null; + } + + @Override + public Map queryParamMap() { + return null; + } + + @Override + public String queryString() { + return null; + } + + @Override + public String formParam(String key) { + return null; + } + + @Override + public String formParam(String key, String defaultValue) { + return null; + } + + @Override + public List formParams(String key) { + return null; + } + + private Map> formParamMap; + + @Override + public Map> formParamMap() { + if (formParamMap == null) { + formParamMap = initFormParamMap(); + } + return formParamMap; + } + + private Map> initFormParamMap() { + final String charset = mgr.requestCharset(this); + return mgr.formParamMap(this, charset); + } + + @Override + public String scheme() { + return null; + } + + @Override + public Context sessionAttribute(String key, Object value) { + return null; + } + + @Override + public T sessionAttribute(String key) { + return null; + } + + @Override + public Map sessionAttributeMap() { + return null; + } + + @Override + public String url() { + return null; + } + + @Override + public String fullUrl() { + return null; + } + + @Override + public String contextPath() { + return null; + } + + @Override + public String userAgent() { + return null; + } + + @Override + public Context status(int statusCode) { + this.statusCode = statusCode; + return this; + } + + @Override + public int status() { + return statusCode; + } + + @Override + public Context text(String content) { + contentType(TEXT_PLAIN); + write(content); + return this; + } + + @Override + public Context html(String content) { + contentType(TEXT_HTML); + write(content); + return this; + } + + @Override + public Context json(Object bean) { + contentType(APPLICATION_JSON); + mgr.jsonWrite(bean, this); + return this; + } + + @Override + public Context jsonStream(Stream stream) { + contentType(APPLICATION_X_JSON_STREAM); + mgr.jsonWriteStream(stream, this); + return this; + } + + @Override + public Context jsonStream(Iterator iterator) { + contentType(APPLICATION_X_JSON_STREAM); + mgr.jsonWriteStream(iterator, this); + return this; + } + + @Override + public Context write(String content) { + try { + writeBytes(content.getBytes(StandardCharsets.UTF_8)); + return this; + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + void writeBytes(byte[] bytes) throws IOException { + exchange.sendResponseHeaders(statusCode(), bytes.length); + final OutputStream os = exchange.getResponseBody(); + os.write(bytes); + os.flush(); + os.close(); + } + + int statusCode() { + return statusCode == 0 ? 200 : statusCode; + } + + @Override + public Context render(String name) { + return render(name, Collections.emptyMap()); + } + + @Override + public Context render(String name, Map model) { + mgr.render(this, name, model); + return this; + } + + @Override + public Map headerMap() { + Map map = new LinkedHashMap<>(); + for (Map.Entry> entry : exchange.getRequestHeaders().entrySet()) { + final List value = entry.getValue(); + if (!value.isEmpty()) { + map.put(entry.getKey(), value.get(0)); + } + } + return map; + } + + @Override + public String header(String key) { + return header(exchange.getRequestHeaders(), key); + } + + @Override + public void header(String key, String value) { + exchange.getResponseHeaders().add(key, value); + } + + @Override + public String host() { + return exchange.getRemoteAddress().getHostString(); + } + + @Override + public String ip() { + return null;//exchange.getRemoteAddress(); + } + + @Override + public boolean isMultipart() { + // not really supported + return false; + } + + @Override + public boolean isMultipartFormData() { + // not really supported + return false; + } + + @Override + public String method() { + return exchange.getRequestMethod(); + } + + @Override + public String path() { + return path; + } + + @Override + public int port() { + return 0; + } + + @Override + public String protocol() { + return exchange.getProtocol(); + } + + @Override + public UploadedFile uploadedFile(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public List uploadedFiles(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public List uploadedFiles() { + throw new UnsupportedOperationException(); + } + + @Override + public OutputStream outputStream() { + return mgr.createOutputStream(this); + } + + @Override + public InputStream inputStream() { + return exchange.getRequestBody(); + } + + @Override + public void setMode(Routing.Type type) { + this.mode = type; + } + + HttpExchange exchange() { + return exchange; + } + +} diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServer.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServer.java new file mode 100644 index 00000000..f5a8af3b --- /dev/null +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServer.java @@ -0,0 +1,18 @@ +package io.avaje.jex.jdk; + +import com.sun.net.httpserver.HttpServer; +import io.avaje.jex.Jex; + +class JdkServer implements Jex.Server { + + private final HttpServer server; + + JdkServer(HttpServer server) { + this.server = server; + } + + @Override + public void shutdown() { + server.stop(0); + } +} diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServerStart.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServerStart.java new file mode 100644 index 00000000..823c3300 --- /dev/null +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServerStart.java @@ -0,0 +1,36 @@ +package io.avaje.jex.jdk; + +import com.sun.net.httpserver.HttpContext; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import com.sun.net.httpserver.HttpsServer; +import io.avaje.jex.Jex; +import io.avaje.jex.spi.SpiRoutes; +import io.avaje.jex.spi.SpiServiceManager; +import io.avaje.jex.spi.SpiStartServer; + +import java.io.IOException; +import java.net.InetSocketAddress; + +public class JdkServerStart implements SpiStartServer { + + @Override + public Jex.Server start(Jex jex, SpiRoutes routes, SpiServiceManager serviceManager) { + + final JdkServiceManager manager = new JdkServiceManager(serviceManager); + HttpHandler handler = new BaseHandler(routes, manager); + try { + final HttpServer server = HttpServer.create(); + server.createContext("/", handler); + + int port = jex.inner.port; + server.bind(new InetSocketAddress(port), 0); + server.start(); + + return new JdkServer(server); + + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServiceManager.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServiceManager.java new file mode 100644 index 00000000..9f51c305 --- /dev/null +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServiceManager.java @@ -0,0 +1,77 @@ +package io.avaje.jex.jdk; + +import io.avaje.jex.Context; +import io.avaje.jex.Routing; +import io.avaje.jex.spi.SpiContext; +import io.avaje.jex.spi.SpiServiceManager; + +import java.io.OutputStream; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +class JdkServiceManager implements SpiServiceManager { + + private final SpiServiceManager serviceManager; + private final long outputBufferMax = 1024; + private final int outputBufferInitial = 256; + + JdkServiceManager(SpiServiceManager serviceManager) { + this.serviceManager = serviceManager; + } + + OutputStream createOutputStream(JdkContext jdkContext) { + return new BufferedOutStream(jdkContext, outputBufferMax, outputBufferInitial); + } + + @Override + public T jsonRead(Class clazz, SpiContext ctx) { + return serviceManager.jsonRead(clazz, ctx); + } + + @Override + public void jsonWrite(Object bean, SpiContext ctx) { + serviceManager.jsonWrite(bean, ctx); + } + + @Override + public void jsonWriteStream(Stream stream, SpiContext ctx) { + serviceManager.jsonWriteStream(stream, ctx); + } + + @Override + public void jsonWriteStream(Iterator iterator, SpiContext ctx) { + serviceManager.jsonWriteStream(iterator, ctx); + } + + @Override + public void maybeClose(Object iterator) { + serviceManager.maybeClose(iterator); + } + + @Override + public Routing.Type lookupRoutingType(String method) { + return serviceManager.lookupRoutingType(method); + } + + @Override + public void handleException(Context ctx, Exception e) { + serviceManager.handleException(ctx, e); + } + + @Override + public void render(Context ctx, String name, Map model) { + serviceManager.render(ctx, name, model); + } + + @Override + public String requestCharset(Context ctx) { + return serviceManager.requestCharset(ctx); + } + + @Override + public Map> formParamMap(Context ctx, String charset) { + return serviceManager.formParamMap(ctx, charset); + } +} diff --git a/avaje-jex-jdk/src/main/java/module-info.java b/avaje-jex-jdk/src/main/java/module-info.java new file mode 100644 index 00000000..31a5ae41 --- /dev/null +++ b/avaje-jex-jdk/src/main/java/module-info.java @@ -0,0 +1,10 @@ +import io.avaje.jex.spi.SpiStartServer; + +module io.avaje.jex.jdk { + + requires transitive io.avaje.jex; + requires java.net.http; + requires jdk.httpserver; + + provides SpiStartServer with io.avaje.jex.jdk.JdkServerStart; +} diff --git a/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/HeadersTest.java b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/HeadersTest.java new file mode 100644 index 00000000..3ff8226e --- /dev/null +++ b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/HeadersTest.java @@ -0,0 +1,54 @@ +package io.avaje.jex.jdk; + +import io.avaje.http.client.HttpClientContext; +import io.avaje.http.client.JacksonBodyAdapter; +import io.avaje.jex.Jex; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpResponse; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Random; + +import static org.assertj.core.api.Assertions.assertThat; + +class HeadersTest { + + static final int port = new Random().nextInt(1000) + 10_000; + static Jex.Server server; + static HttpClientContext client; + + @BeforeAll + static void setup() { + server = Jex.create() + .routing(routing -> routing + .get("/", ctx -> { + final String one = ctx.header("one"); + Map obj = new LinkedHashMap<>(); + obj.put("one", one); + ctx.json(obj); + }) + ) + .port(port) + .start(); + + client = HttpClientContext.newBuilder() + .baseUrl("http://localhost:"+port) + .bodyAdapter(new JacksonBodyAdapter()) + .build(); + } + + @Test + void get() { + + final HttpResponse hres = client.request() + .header("one", "hello") + .GET().asString(); + + assertThat(hres.statusCode()).isEqualTo(200); + assertThat(hres.body()).isEqualTo("{\"one\":\"hello\"}"); + + server.shutdown(); + } +} diff --git a/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/HelloBean.java b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/HelloBean.java new file mode 100644 index 00000000..a89d5f57 --- /dev/null +++ b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/HelloBean.java @@ -0,0 +1,16 @@ +package io.avaje.jex.jdk; + +public class HelloBean { + + public int id; + public String name; + + public HelloBean(int id, String name) { + this.id = id; + this.name = name; + } + + public HelloBean() { + + } +} diff --git a/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/JdkServerTest.java b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/JdkServerTest.java new file mode 100644 index 00000000..d364d309 --- /dev/null +++ b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/JdkServerTest.java @@ -0,0 +1,60 @@ +package io.avaje.jex.jdk; + +import io.avaje.http.client.HttpClientContext; +import io.avaje.http.client.JacksonBodyAdapter; +import io.avaje.jex.Jex; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpResponse; + +import static org.assertj.core.api.Assertions.assertThat; + +class JdkServerTest { + + @Test + void init() { + + HelloBean bean = new HelloBean(42, "rob"); + + final Jex.Server server = Jex.create() + .routing(routing -> routing + .get("/", ctx -> ctx.text("hello world")) + .get("/foo", ctx -> ctx.json(bean)) + .post("/foo", ctx -> { + final HelloBean in = ctx.bodyAsClass(HelloBean.class); + in.name = in.name + "-out"; + ctx.json(in); + })) + .port(8093) + .start(); + + final HttpClientContext client = HttpClientContext.newBuilder() + .baseUrl("http://localhost:8093") + .bodyAdapter(new JacksonBodyAdapter()) + .build(); + + final HttpResponse hres = client.request().GET().asString(); + + assertThat(hres.statusCode()).isEqualTo(200); + assertThat(hres.body()).isEqualTo("hello world"); + + + final HelloBean foo = client.request().path("foo") + .GET() + .bean(HelloBean.class); + + assertThat(foo.id).isEqualTo(42); + + final HelloBean foo2 = client.request().path("foo") + .header("Accepts", "application/json") + .body(bean) + .POST() + .bean(HelloBean.class); + + assertThat(foo2.name).isEqualTo("rob-out"); + + System.out.println("done"); + + server.shutdown(); + } +} diff --git a/avaje-jex-jdk/src/test/resources/logback-test.xml b/avaje-jex-jdk/src/test/resources/logback-test.xml new file mode 100644 index 00000000..ddb21350 --- /dev/null +++ b/avaje-jex-jdk/src/test/resources/logback-test.xml @@ -0,0 +1,19 @@ + + + + TRACE + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + diff --git a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/ContextUtil.java b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/ContextUtil.java index f40ce684..c986ffbe 100644 --- a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/ContextUtil.java +++ b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/ContextUtil.java @@ -1,39 +1,16 @@ package io.avaje.jex.jetty; -import io.avaje.jex.spi.HeaderKeys; import jakarta.servlet.ServletInputStream; import jakarta.servlet.http.HttpServletRequest; import java.io.*; -import java.net.URLDecoder; -import java.util.*; class ContextUtil { - public static final String UTF_8 = "UTF-8"; - private static final int DEFAULT_BUFFER_SIZE = 8 * 1024; private static final int BUFFER_MAX = 65536; - static String getRequestCharset(JexHttpContext ctx) { - final String header = ctx.req.getHeader(HeaderKeys.CONTENT_TYPE); - if (header != null) { - return parseCharset(header); - } - return UTF_8; - } - - static String parseCharset(String header) { - for (String val : header.split(";")) { - val = val.trim(); - if (val.regionMatches(true, 0, "charset", 0, "charset".length())) { - return val.split("=")[1].trim(); - } - } - return "UTF-8"; - } - static byte[] readBody(HttpServletRequest req) { try { final ServletInputStream inputStream = req.getInputStream(); @@ -61,21 +38,4 @@ static void copy(InputStream in, OutputStream out, int bufferSize) throws IOExce } } - static Map> formParamMap(String body, String charset) { - if (body.isEmpty()) { - return Collections.emptyMap(); - } - try { - Map> map = new LinkedHashMap<>(); - for (String pair : body.split("&")) { - final String[] split1 = pair.split("=", 2); - String key = URLDecoder.decode(split1[0], charset); - String val = split1.length > 1 ? URLDecoder.decode(split1[1], charset) : ""; - map.computeIfAbsent(key, s -> new ArrayList<>()).add(val); - } - return map; - } catch (UnsupportedEncodingException e) { - throw new UncheckedIOException(e); - } - } } diff --git a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java index 5cf2ba86..92e077c8 100644 --- a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java +++ b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java @@ -3,11 +3,11 @@ import io.avaje.jex.Context; import io.avaje.jex.Routing; import io.avaje.jex.UploadedFile; -import io.avaje.jex.spi.SpiServiceManager; -import io.avaje.jex.spi.HeaderKeys; import io.avaje.jex.http.RedirectResponse; +import io.avaje.jex.spi.HeaderKeys; import io.avaje.jex.spi.SpiContext; import io.avaje.jex.spi.SpiRoutes; +import io.avaje.jex.spi.SpiServiceManager; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -61,17 +61,15 @@ public void setMode(Routing.Type mode) { private String characterEncoding() { if (characterEncoding == null) { - characterEncoding = ContextUtil.getRequestCharset(this); + characterEncoding = mgr.requestCharset(this); } return characterEncoding; } - @Override public HttpServletRequest req() { return req; } - @Override public HttpServletResponse res() { return res; } @@ -137,7 +135,7 @@ public Context cookie(String name, String value, int maxAge) { return cookie(cookie); } - @Override + //@Override public Context cookie(Cookie cookie) { if (cookie.getPath() == null) { cookie.setPath("/"); @@ -296,7 +294,7 @@ private Map> initFormParamMap() { if (isMultipartFormData()) { return mgr.multiPartForm(req); } else { - return ContextUtil.formParamMap(body(), characterEncoding()); + return mgr.formParamMap(this, characterEncoding()); } } @@ -372,6 +370,11 @@ public Context contentType(String contentType) { return this; } + @Override + public String contentTypeOfResponse() { + return req.getContentType(); + } + public Map headerMap() { Map map = new LinkedHashMap<>(); final Enumeration names = req.getHeaderNames(); @@ -436,13 +439,13 @@ public String protocol() { @Override public Context text(String content) { - res.setContentType(TEXT_PLAIN); + contentType(TEXT_PLAIN); return write(content); } @Override public Context html(String content) { - res.setContentType(TEXT_HTML); + contentType(TEXT_HTML); return write(content); } diff --git a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpServlet.java b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpServlet.java index 3e4f6609..809051d5 100644 --- a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpServlet.java +++ b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpServlet.java @@ -32,7 +32,7 @@ protected void service(HttpServletRequest req, HttpServletResponse res) { final String uri = req.getRequestURI(); SpiRoutes.Entry route = routes.match(routeType, uri); if (route == null) { - SpiContext ctx = new JexHttpContext(manager, req, res, uri); + var ctx = new JexHttpContext(manager, req, res, uri); try { processNoRoute(ctx, uri, routeType); routes.after(uri, ctx); @@ -41,7 +41,7 @@ protected void service(HttpServletRequest req, HttpServletResponse res) { } } else { final SpiRoutes.Params params = route.pathParams(uri); - SpiContext ctx = new JexHttpContext(manager, req, res, route.matchPath(), params); + var ctx = new JexHttpContext(manager, req, res, route.matchPath(), params); try { processRoute(ctx, uri, route); routes.after(uri, ctx); @@ -55,13 +55,13 @@ private void handleException(Context ctx, Exception e) { manager.handleException(ctx, e); } - private void processRoute(SpiContext ctx, String uri, SpiRoutes.Entry route) { + private void processRoute(JexHttpContext ctx, String uri, SpiRoutes.Entry route) { routes.before(uri, ctx); ctx.setMode(null); route.handle(ctx); } - private void processNoRoute(SpiContext ctx, String uri, Routing.Type routeType) { + private void processNoRoute(JexHttpContext ctx, String uri, Routing.Type routeType) { routes.before(uri, ctx); if (routeType == Routing.Type.HEAD && hasGetHandler(uri)) { processHead(ctx); diff --git a/avaje-jex-jetty/src/test/java/io/avaje/jex/jetty/ContextUtilTest.java b/avaje-jex-jetty/src/test/java/io/avaje/jex/jetty/ContextUtilTest.java deleted file mode 100644 index be4f54f4..00000000 --- a/avaje-jex-jetty/src/test/java/io/avaje/jex/jetty/ContextUtilTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.avaje.jex.jetty; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -class ContextUtilTest { - - @Test - void parseCharset_defaults() { - - assertThat(ContextUtil.parseCharset("")).isEqualTo(ContextUtil.UTF_8); - assertThat(ContextUtil.parseCharset("junk")).isEqualTo(ContextUtil.UTF_8); - } - - @Test - void parseCharset_caseCheck() { - - assertThat(ContextUtil.parseCharset("app/foo; charset=ME")).isEqualTo("ME"); - assertThat(ContextUtil.parseCharset("app/foo;charset=ME")).isEqualTo("ME"); - assertThat(ContextUtil.parseCharset("app/foo;charset = ME ")).isEqualTo("ME"); - assertThat(ContextUtil.parseCharset("app/foo;charset = ME;")).isEqualTo("ME"); - assertThat(ContextUtil.parseCharset("app/foo;charset = ME;other=junk")).isEqualTo("ME"); - } -} diff --git a/avaje-jex/src/main/java/io/avaje/jex/Context.java b/avaje-jex/src/main/java/io/avaje/jex/Context.java index 8dad68a8..d9d6f79b 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/Context.java +++ b/avaje-jex/src/main/java/io/avaje/jex/Context.java @@ -1,9 +1,5 @@ package io.avaje.jex; -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - import java.util.Iterator; import java.util.List; import java.util.Map; @@ -55,10 +51,10 @@ public interface Context { */ Context cookie(String name, String value, int maxAge); - /** - * Sets a Cookie. - */ - Context cookie(Cookie cookie); +// /** +// * Sets a Cookie. +// */ +// Context cookie(Cookie cookie); /** * Remove a cookie by name. @@ -112,6 +108,11 @@ public interface Context { */ Context contentType(String contentType); + /** + * Return the content type of the response. + */ + String contentTypeOfResponse(); + /** * Return the splat path value for the given position. * @@ -341,16 +342,6 @@ public interface Context { */ String protocol(); - /** - * Return the underlying http servlet request. - */ - HttpServletRequest req(); - - /** - * Return the underlying http servlet response. - */ - HttpServletResponse res(); - /** * Return the first UploadedFile for the specified name or null. */ diff --git a/avaje-jex/src/main/java/io/avaje/jex/core/ExceptionManager.java b/avaje-jex/src/main/java/io/avaje/jex/core/ExceptionManager.java index 99393249..7aa697b6 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/core/ExceptionManager.java +++ b/avaje-jex/src/main/java/io/avaje/jex/core/ExceptionManager.java @@ -87,7 +87,7 @@ private HttpResponseException unwrap(Exception e) { private boolean useJson(Context ctx) { final String acceptHeader = ctx.header(HeaderKeys.ACCEPT); return (acceptHeader != null && acceptHeader.contains("application/json") - || "application/json".equals(ctx.res().getContentType())); + || "application/json".equals(ctx.contentTypeOfResponse())); } } diff --git a/avaje-jex/src/main/java/io/avaje/jex/core/ServiceManager.java b/avaje-jex/src/main/java/io/avaje/jex/core/ServiceManager.java index dd392927..a7b66af2 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/core/ServiceManager.java +++ b/avaje-jex/src/main/java/io/avaje/jex/core/ServiceManager.java @@ -1,6 +1,7 @@ package io.avaje.jex.core; import io.avaje.jex.*; +import io.avaje.jex.spi.HeaderKeys; import io.avaje.jex.spi.JsonService; import io.avaje.jex.spi.SpiContext; @@ -8,14 +9,16 @@ import jakarta.servlet.MultipartConfigElement; import jakarta.servlet.http.HttpServletRequest; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.ServiceLoader; +import java.io.UncheckedIOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.*; import java.util.stream.Stream; class ServiceManager implements SpiServiceManager { + public static final String UTF_8 = "UTF-8"; + private final HttpMethodMap methodMap = new HttpMethodMap(); private final JsonService jsonService; @@ -104,6 +107,46 @@ public Map> multiPartForm(HttpServletRequest req) { return multipartUtil.fieldMap(req); } + @Override + public String requestCharset(Context ctx) { + return parseCharset(ctx.header(HeaderKeys.CONTENT_TYPE)); + } + + static String parseCharset(String header) { + if (header != null) { + for (String val : header.split(";")) { + val = val.trim(); + if (val.regionMatches(true, 0, "charset", 0, "charset".length())) { + return val.split("=")[1].trim(); + } + } + } + return UTF_8; + } + + @Override + public Map> formParamMap(Context ctx, String charset) { + return formParamMap(ctx.body(), charset); + } + + static Map> formParamMap(String body, String charset) { + if (body.isEmpty()) { + return Collections.emptyMap(); + } + try { + Map> map = new LinkedHashMap<>(); + for (String pair : body.split("&")) { + final String[] split1 = pair.split("=", 2); + String key = URLDecoder.decode(split1[0], charset); + String val = split1.length > 1 ? URLDecoder.decode(split1[1], charset) : ""; + map.computeIfAbsent(key, s -> new ArrayList<>()).add(val); + } + return map; + } catch (UnsupportedEncodingException e) { + throw new UncheckedIOException(e); + } + } + private static class Builder { private final Jex jex; diff --git a/avaje-jex/src/main/java/io/avaje/jex/spi/SpiServiceManager.java b/avaje-jex/src/main/java/io/avaje/jex/spi/SpiServiceManager.java index e27e223b..bdafd682 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/spi/SpiServiceManager.java +++ b/avaje-jex/src/main/java/io/avaje/jex/spi/SpiServiceManager.java @@ -3,7 +3,6 @@ import io.avaje.jex.Context; import io.avaje.jex.Routing; import io.avaje.jex.UploadedFile; -import io.avaje.jex.spi.SpiContext; import jakarta.servlet.http.HttpServletRequest; import java.util.Iterator; @@ -29,9 +28,35 @@ public interface SpiServiceManager { void render(Context ctx, String name, Map model); - List uploadedFiles(HttpServletRequest req); + /** + * Return the character set of the request. + */ + String requestCharset(Context ctx); + + /** + * Parse and return the body as form parameters. + */ + Map> formParamMap(Context ctx, String charset); + + /** + * Return the uploaded files for the request. + */ + default List uploadedFiles(HttpServletRequest req) { + throw new UnsupportedOperationException(); + } + + /** + * Return the uploaded files for the request and name. + */ + default List uploadedFiles(HttpServletRequest req, String name) { + throw new UnsupportedOperationException(); + } + + /** + * Return the form parameters from a multipart form request. + */ + default Map> multiPartForm(HttpServletRequest req) { + throw new UnsupportedOperationException(); + } - List uploadedFiles(HttpServletRequest req, String name); - - Map> multiPartForm(HttpServletRequest req); } diff --git a/avaje-jex/src/test/java/io/avaje/jex/core/ContextUtilTest.java b/avaje-jex/src/test/java/io/avaje/jex/core/ContextUtilTest.java new file mode 100644 index 00000000..92dab133 --- /dev/null +++ b/avaje-jex/src/test/java/io/avaje/jex/core/ContextUtilTest.java @@ -0,0 +1,23 @@ +package io.avaje.jex.core; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class ContextUtilTest { + + @Test + void parseCharset_defaults() { + assertThat(ServiceManager.parseCharset("")).isEqualTo(ServiceManager.UTF_8); + assertThat(ServiceManager.parseCharset("junk")).isEqualTo(ServiceManager.UTF_8); + } + + @Test + void parseCharset_caseCheck() { + assertThat(ServiceManager.parseCharset("app/foo; charset=ME")).isEqualTo("ME"); + assertThat(ServiceManager.parseCharset("app/foo;charset=ME")).isEqualTo("ME"); + assertThat(ServiceManager.parseCharset("app/foo;charset = ME ")).isEqualTo("ME"); + assertThat(ServiceManager.parseCharset("app/foo;charset = ME;")).isEqualTo("ME"); + assertThat(ServiceManager.parseCharset("app/foo;charset = ME;other=junk")).isEqualTo("ME"); + } +} diff --git a/pom.xml b/pom.xml index 26ff80aa..2a031f6a 100644 --- a/pom.xml +++ b/pom.xml @@ -28,6 +28,7 @@ avaje-jex-freemarker avaje-jex-mustache avaje-jex-jetty + avaje-jex-jdk From ccaa67f5b06e5fc252140572c66f255151637681 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Mon, 12 Jul 2021 11:29:51 +1200 Subject: [PATCH 03/27] Move MultipartUtil into Jetty specific module --- .../java/io/avaje/jex/jdk/BaseHandler.java | 4 +- .../java/io/avaje/jex/jdk/JdkContext.java | 60 +++------------ .../jdk/{JdkServer.java => JdkJexServer.java} | 4 +- .../java/io/avaje/jex/jdk/JdkServerStart.java | 6 +- .../io/avaje/jex/jdk/JdkServiceManager.java | 77 ------------------- .../java/io/avaje/jex/jdk/ServiceManager.java | 21 +++++ avaje-jex-jdk/src/main/java/module-info.java | 3 +- ...kServerTest.java => JdkJexServerTest.java} | 2 +- .../java/io/avaje/jex/jetty/JettyLaunch.java | 14 +++- .../io/avaje/jex/jetty/JexHttpContext.java | 41 +--------- .../io/avaje/jex/jetty/JexHttpServlet.java | 6 +- .../io/avaje/jex/jetty}/MultipartUtil.java | 4 +- .../io/avaje/jex/jetty}/PartUploadedFile.java | 4 +- .../io/avaje/jex/jetty/ServiceManager.java | 34 ++++++++ .../src/main/java/io/avaje/jex/Context.java | 34 ++++++-- .../jex/core/BootstapServiceManager.java | 2 +- ...ceManager.java => CoreServiceManager.java} | 37 ++------- .../io/avaje/jex/spi/ProxyServiceManager.java | 74 ++++++++++++++++++ .../io/avaje/jex/spi/SpiServiceManager.java | 50 ++++++------ avaje-jex/src/main/java/module-info.java | 8 -- .../io/avaje/jex/core/ContextUtilTest.java | 14 ++-- 21 files changed, 239 insertions(+), 260 deletions(-) rename avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/{JdkServer.java => JdkJexServer.java} (74%) delete mode 100644 avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServiceManager.java create mode 100644 avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/ServiceManager.java rename avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/{JdkServerTest.java => JdkJexServerTest.java} (98%) rename {avaje-jex/src/main/java/io/avaje/jex/core => avaje-jex-jetty/src/main/java/io/avaje/jex/jetty}/MultipartUtil.java (99%) rename {avaje-jex/src/main/java/io/avaje/jex/core => avaje-jex-jetty/src/main/java/io/avaje/jex/jetty}/PartUploadedFile.java (97%) create mode 100644 avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/ServiceManager.java rename avaje-jex/src/main/java/io/avaje/jex/core/{ServiceManager.java => CoreServiceManager.java} (77%) create mode 100644 avaje-jex/src/main/java/io/avaje/jex/spi/ProxyServiceManager.java diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/BaseHandler.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/BaseHandler.java index b159e887..195e225d 100644 --- a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/BaseHandler.java +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/BaseHandler.java @@ -12,9 +12,9 @@ class BaseHandler implements HttpHandler { final SpiRoutes routes; - final JdkServiceManager mgr; + final ServiceManager mgr; - BaseHandler(SpiRoutes routes, JdkServiceManager mgr) { + BaseHandler(SpiRoutes routes, ServiceManager mgr) { this.mgr = mgr; this.routes = routes; } diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java index 2ac5f4d7..7427f4d0 100644 --- a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java @@ -16,15 +16,15 @@ class JdkContext implements Context, SpiContext { - private final JdkServiceManager mgr; + private final ServiceManager mgr; private final String path; private final SpiRoutes.Params params; private final HttpExchange exchange; private Routing.Type mode; - + private Map> formParamMap; private int statusCode; - JdkContext(JdkServiceManager mgr, HttpExchange exchange, String path, SpiRoutes.Params params) { + JdkContext(ServiceManager mgr, HttpExchange exchange, String path, SpiRoutes.Params params) { this.mgr = mgr; this.exchange = exchange; this.path = path; @@ -38,17 +38,19 @@ public String matchedPath() { @Override public Context attribute(String key, Object value) { - return null; + exchange.setAttribute(key, value); + return this; } @Override + @SuppressWarnings("unchecked") public T attribute(String key) { - return null; + return (T)exchange.getAttribute(key); } @Override public Map attributeMap() { - return null; + throw new UnsupportedOperationException(); } @Override @@ -71,11 +73,6 @@ public Context cookie(String name, String value, int maxAge) { return null; } -// @Override -// public Context cookie(Cookie cookie) { -// return null; -// } - @Override public Context removeCookie(String name) { return null; @@ -117,7 +114,8 @@ public String body() { @Override public long contentLength() { - return 0; + final String len = header(HeaderKeys.CONTENT_LENGTH); + return len == null ? 0 : Long.parseLong(len); } @Override @@ -137,7 +135,7 @@ private String header(Headers headers, String name) { @Override public Context contentType(String contentType) { - exchange.getResponseHeaders().add(HeaderKeys.CONTENT_TYPE, contentType); + exchange.getResponseHeaders().set(HeaderKeys.CONTENT_TYPE, contentType); return this; } @@ -181,23 +179,6 @@ public String queryString() { return null; } - @Override - public String formParam(String key) { - return null; - } - - @Override - public String formParam(String key, String defaultValue) { - return null; - } - - @Override - public List formParams(String key) { - return null; - } - - private Map> formParamMap; - @Override public Map> formParamMap() { if (formParamMap == null) { @@ -246,11 +227,6 @@ public String contextPath() { return null; } - @Override - public String userAgent() { - return null; - } - @Override public Context status(int statusCode) { this.statusCode = statusCode; @@ -262,20 +238,6 @@ public int status() { return statusCode; } - @Override - public Context text(String content) { - contentType(TEXT_PLAIN); - write(content); - return this; - } - - @Override - public Context html(String content) { - contentType(TEXT_HTML); - write(content); - return this; - } - @Override public Context json(Object bean) { contentType(APPLICATION_JSON); diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServer.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkJexServer.java similarity index 74% rename from avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServer.java rename to avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkJexServer.java index f5a8af3b..714ea94a 100644 --- a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServer.java +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkJexServer.java @@ -3,11 +3,11 @@ import com.sun.net.httpserver.HttpServer; import io.avaje.jex.Jex; -class JdkServer implements Jex.Server { +class JdkJexServer implements Jex.Server { private final HttpServer server; - JdkServer(HttpServer server) { + JdkJexServer(HttpServer server) { this.server = server; } diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServerStart.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServerStart.java index 823c3300..fa8fd86c 100644 --- a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServerStart.java +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServerStart.java @@ -1,9 +1,7 @@ package io.avaje.jex.jdk; -import com.sun.net.httpserver.HttpContext; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; -import com.sun.net.httpserver.HttpsServer; import io.avaje.jex.Jex; import io.avaje.jex.spi.SpiRoutes; import io.avaje.jex.spi.SpiServiceManager; @@ -17,7 +15,7 @@ public class JdkServerStart implements SpiStartServer { @Override public Jex.Server start(Jex jex, SpiRoutes routes, SpiServiceManager serviceManager) { - final JdkServiceManager manager = new JdkServiceManager(serviceManager); + final ServiceManager manager = new ServiceManager(serviceManager); HttpHandler handler = new BaseHandler(routes, manager); try { final HttpServer server = HttpServer.create(); @@ -27,7 +25,7 @@ public Jex.Server start(Jex jex, SpiRoutes routes, SpiServiceManager serviceMana server.bind(new InetSocketAddress(port), 0); server.start(); - return new JdkServer(server); + return new JdkJexServer(server); } catch (IOException e) { throw new RuntimeException(e); diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServiceManager.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServiceManager.java deleted file mode 100644 index 9f51c305..00000000 --- a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServiceManager.java +++ /dev/null @@ -1,77 +0,0 @@ -package io.avaje.jex.jdk; - -import io.avaje.jex.Context; -import io.avaje.jex.Routing; -import io.avaje.jex.spi.SpiContext; -import io.avaje.jex.spi.SpiServiceManager; - -import java.io.OutputStream; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.stream.Stream; - -class JdkServiceManager implements SpiServiceManager { - - private final SpiServiceManager serviceManager; - private final long outputBufferMax = 1024; - private final int outputBufferInitial = 256; - - JdkServiceManager(SpiServiceManager serviceManager) { - this.serviceManager = serviceManager; - } - - OutputStream createOutputStream(JdkContext jdkContext) { - return new BufferedOutStream(jdkContext, outputBufferMax, outputBufferInitial); - } - - @Override - public T jsonRead(Class clazz, SpiContext ctx) { - return serviceManager.jsonRead(clazz, ctx); - } - - @Override - public void jsonWrite(Object bean, SpiContext ctx) { - serviceManager.jsonWrite(bean, ctx); - } - - @Override - public void jsonWriteStream(Stream stream, SpiContext ctx) { - serviceManager.jsonWriteStream(stream, ctx); - } - - @Override - public void jsonWriteStream(Iterator iterator, SpiContext ctx) { - serviceManager.jsonWriteStream(iterator, ctx); - } - - @Override - public void maybeClose(Object iterator) { - serviceManager.maybeClose(iterator); - } - - @Override - public Routing.Type lookupRoutingType(String method) { - return serviceManager.lookupRoutingType(method); - } - - @Override - public void handleException(Context ctx, Exception e) { - serviceManager.handleException(ctx, e); - } - - @Override - public void render(Context ctx, String name, Map model) { - serviceManager.render(ctx, name, model); - } - - @Override - public String requestCharset(Context ctx) { - return serviceManager.requestCharset(ctx); - } - - @Override - public Map> formParamMap(Context ctx, String charset) { - return serviceManager.formParamMap(ctx, charset); - } -} diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/ServiceManager.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/ServiceManager.java new file mode 100644 index 00000000..175c3faa --- /dev/null +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/ServiceManager.java @@ -0,0 +1,21 @@ +package io.avaje.jex.jdk; + +import io.avaje.jex.spi.ProxyServiceManager; +import io.avaje.jex.spi.SpiServiceManager; + +import java.io.OutputStream; + +class ServiceManager extends ProxyServiceManager { + + private final long outputBufferMax = 1024; + private final int outputBufferInitial = 256; + + ServiceManager(SpiServiceManager delegate) { + super(delegate); + } + + OutputStream createOutputStream(JdkContext jdkContext) { + return new BufferedOutStream(jdkContext, outputBufferMax, outputBufferInitial); + } + +} diff --git a/avaje-jex-jdk/src/main/java/module-info.java b/avaje-jex-jdk/src/main/java/module-info.java index 31a5ae41..1424e24c 100644 --- a/avaje-jex-jdk/src/main/java/module-info.java +++ b/avaje-jex-jdk/src/main/java/module-info.java @@ -1,3 +1,4 @@ +import io.avaje.jex.jdk.JdkServerStart; import io.avaje.jex.spi.SpiStartServer; module io.avaje.jex.jdk { @@ -6,5 +7,5 @@ requires java.net.http; requires jdk.httpserver; - provides SpiStartServer with io.avaje.jex.jdk.JdkServerStart; + provides SpiStartServer with JdkServerStart; } diff --git a/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/JdkServerTest.java b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/JdkJexServerTest.java similarity index 98% rename from avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/JdkServerTest.java rename to avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/JdkJexServerTest.java index d364d309..87900cdf 100644 --- a/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/JdkServerTest.java +++ b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/JdkJexServerTest.java @@ -9,7 +9,7 @@ import static org.assertj.core.api.Assertions.assertThat; -class JdkServerTest { +class JdkJexServerTest { @Test void init() { diff --git a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JettyLaunch.java b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JettyLaunch.java index f674e9f4..ec15eb32 100644 --- a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JettyLaunch.java +++ b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JettyLaunch.java @@ -5,6 +5,7 @@ import io.avaje.jex.StaticFileSource; import io.avaje.jex.spi.SpiServiceManager; import io.avaje.jex.spi.SpiRoutes; +import jakarta.servlet.MultipartConfigElement; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -27,14 +28,14 @@ class JettyLaunch implements Jex.Server { private final Jex jex; private final SpiRoutes routes; - private final SpiServiceManager serviceManager; + private final ServiceManager serviceManager; private final JettyServerConfig config; private Server server; JettyLaunch(Jex jex, SpiRoutes routes, SpiServiceManager serviceManager) { this.jex = jex; this.routes = routes; - this.serviceManager = serviceManager; + this.serviceManager = new ServiceManager(serviceManager, initMultiPart()); this.config = initConfig(jex.serverConfig()); } @@ -42,6 +43,15 @@ private JettyServerConfig initConfig(ServerConfig config) { return config == null ? new JettyServerConfig() : (JettyServerConfig)config; } + MultipartUtil initMultiPart() { + MultipartConfigElement config = jex.inner.multipartConfig; + if (config == null) { + final int fileThreshold = jex.inner.multipartFileThreshold; + config = new MultipartConfigElement(System.getProperty("java.io.tmpdir"), -1, -1, fileThreshold); + } + return new MultipartUtil(config); + } + @Override public void shutdown() { try { diff --git a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java index 92e077c8..b9d95e33 100644 --- a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java +++ b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java @@ -7,7 +7,6 @@ import io.avaje.jex.spi.HeaderKeys; import io.avaje.jex.spi.SpiContext; import io.avaje.jex.spi.SpiRoutes; -import io.avaje.jex.spi.SpiServiceManager; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -26,7 +25,7 @@ class JexHttpContext implements SpiContext { - private final SpiServiceManager mgr; + private final ServiceManager mgr; protected final HttpServletRequest req; private final HttpServletResponse res; private final Map pathParams; @@ -36,7 +35,7 @@ class JexHttpContext implements SpiContext { private Routing.Type mode; private Map> formParamMap; - JexHttpContext(SpiServiceManager mgr, HttpServletRequest req, HttpServletResponse res, String matchedPath) { + JexHttpContext(ServiceManager mgr, HttpServletRequest req, HttpServletResponse res, String matchedPath) { this.mgr = mgr; this.req = req; this.res = res; @@ -45,7 +44,7 @@ class JexHttpContext implements SpiContext { this.splats = null; } - JexHttpContext(SpiServiceManager mgr, HttpServletRequest req, HttpServletResponse res, String matchedPath, SpiRoutes.Params params) { + JexHttpContext(ServiceManager mgr, HttpServletRequest req, HttpServletResponse res, String matchedPath, SpiRoutes.Params params) { this.mgr = mgr; this.req = req; this.res = res; @@ -265,23 +264,6 @@ public String queryString() { return req.getQueryString(); } - @Override - public String formParam(String key) { - return formParam(key, null); - } - - @Override - public String formParam(String key, String defaultValue) { - final List values = formParamMap().get(key); - return values == null || values.isEmpty() ? defaultValue : values.get(0); - } - - @Override - public List formParams(String key) { - final List values = formParamMap().get(key); - return values != null ? values : emptyList(); - } - @Override public Map> formParamMap() { if (formParamMap == null) { @@ -343,11 +325,6 @@ public String contextPath() { return req.getContextPath(); } - @Override - public String userAgent() { - return req.getHeader(HeaderKeys.USER_AGENT); - } - @Override public Context status(int statusCode) { res.setStatus(statusCode); @@ -437,18 +414,6 @@ public String protocol() { return req.getProtocol(); } - @Override - public Context text(String content) { - contentType(TEXT_PLAIN); - return write(content); - } - - @Override - public Context html(String content) { - contentType(TEXT_HTML); - return write(content); - } - @Override public Context json(Object bean) { contentType(APPLICATION_JSON); diff --git a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpServlet.java b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpServlet.java index 809051d5..60da967f 100644 --- a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpServlet.java +++ b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpServlet.java @@ -4,9 +4,7 @@ import io.avaje.jex.Jex; import io.avaje.jex.Routing; import io.avaje.jex.http.NotFoundResponse; -import io.avaje.jex.spi.SpiContext; import io.avaje.jex.spi.SpiRoutes; -import io.avaje.jex.spi.SpiServiceManager; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -15,11 +13,11 @@ class JexHttpServlet extends HttpServlet { //private static final String X_HTTP_METHOD_OVERRIDE = "X-HTTP-Method-Override"; private final SpiRoutes routes; - private final SpiServiceManager manager; + private final ServiceManager manager; private final StaticHandler staticHandler; private final boolean prefer405; - JexHttpServlet(Jex jex, SpiRoutes routes, SpiServiceManager manager, StaticHandler staticHandler) { + JexHttpServlet(Jex jex, SpiRoutes routes, ServiceManager manager, StaticHandler staticHandler) { this.routes = routes; this.manager = manager; this.staticHandler = staticHandler; diff --git a/avaje-jex/src/main/java/io/avaje/jex/core/MultipartUtil.java b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/MultipartUtil.java similarity index 99% rename from avaje-jex/src/main/java/io/avaje/jex/core/MultipartUtil.java rename to avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/MultipartUtil.java index b417c939..dea863bb 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/core/MultipartUtil.java +++ b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/MultipartUtil.java @@ -1,11 +1,11 @@ -package io.avaje.jex.core; +package io.avaje.jex.jetty; import io.avaje.jex.UploadedFile; - import jakarta.servlet.MultipartConfigElement; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.Part; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; diff --git a/avaje-jex/src/main/java/io/avaje/jex/core/PartUploadedFile.java b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/PartUploadedFile.java similarity index 97% rename from avaje-jex/src/main/java/io/avaje/jex/core/PartUploadedFile.java rename to avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/PartUploadedFile.java index b499dd28..712ed3b6 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/core/PartUploadedFile.java +++ b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/PartUploadedFile.java @@ -1,8 +1,8 @@ -package io.avaje.jex.core; +package io.avaje.jex.jetty; import io.avaje.jex.UploadedFile; - import jakarta.servlet.http.Part; + import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; diff --git a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/ServiceManager.java b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/ServiceManager.java new file mode 100644 index 00000000..5eb0ddbc --- /dev/null +++ b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/ServiceManager.java @@ -0,0 +1,34 @@ +package io.avaje.jex.jetty; + +import io.avaje.jex.UploadedFile; +import io.avaje.jex.spi.ProxyServiceManager; +import io.avaje.jex.spi.SpiServiceManager; +import jakarta.servlet.http.HttpServletRequest; + +import java.util.List; +import java.util.Map; + +/** + * Jetty specific service manager. + */ +class ServiceManager extends ProxyServiceManager { + + private final MultipartUtil multipartUtil; + + ServiceManager(SpiServiceManager delegate, MultipartUtil multipartUtil) { + super(delegate); + this.multipartUtil = multipartUtil; + } + + List uploadedFiles(HttpServletRequest req) { + return multipartUtil.uploadedFiles(req); + } + + List uploadedFiles(HttpServletRequest req, String name) { + return multipartUtil.uploadedFiles(req, name); + } + + Map> multiPartForm(HttpServletRequest req) { + return multipartUtil.fieldMap(req); + } +} diff --git a/avaje-jex/src/main/java/io/avaje/jex/Context.java b/avaje-jex/src/main/java/io/avaje/jex/Context.java index d9d6f79b..bf323180 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/Context.java +++ b/avaje-jex/src/main/java/io/avaje/jex/Context.java @@ -1,10 +1,16 @@ package io.avaje.jex; +import io.avaje.jex.spi.HeaderKeys; + import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.stream.Stream; +import static io.avaje.jex.spi.SpiContext.TEXT_HTML; +import static io.avaje.jex.spi.SpiContext.TEXT_PLAIN; +import static java.util.Collections.emptyList; + /** * Provides access to functions for handling the request and response. */ @@ -164,17 +170,25 @@ public interface Context { /** * Return the first form param value for the specified key or null. */ - String formParam(String key); + default String formParam(String key) { + return formParam(key, null); + } /** * Return the first form param value for the specified key or the default value. */ - String formParam(String key, String defaultValue); + default String formParam(String key, String defaultValue) { + final List values = formParamMap().get(key); + return values == null || values.isEmpty() ? defaultValue : values.get(0); + } /** * Return the form params for the specified key, or empty list. */ - List formParams(String key); + default List formParams(String key) { + final List values = formParamMap().get(key); + return values != null ? values : emptyList(); + } /** * Returns a map with all the form param keys and values. @@ -219,7 +233,9 @@ public interface Context { /** * Return the request user agent, or null. */ - String userAgent(); + default String userAgent() { + return header(HeaderKeys.USER_AGENT); + } /** * Set the status code on the response. @@ -234,12 +250,18 @@ public interface Context { /** * Write plain text content to the response. */ - Context text(String content); + default Context text(String content) { + contentType(TEXT_PLAIN); + return write(content); + } /** * Write html content to the response. */ - Context html(String content); + default Context html(String content) { + contentType(TEXT_HTML); + return write(content); + } /** * Set the response body as JSON for the given bean. diff --git a/avaje-jex/src/main/java/io/avaje/jex/core/BootstapServiceManager.java b/avaje-jex/src/main/java/io/avaje/jex/core/BootstapServiceManager.java index 5ed0b7c7..1895a6e5 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/core/BootstapServiceManager.java +++ b/avaje-jex/src/main/java/io/avaje/jex/core/BootstapServiceManager.java @@ -7,6 +7,6 @@ public class BootstapServiceManager implements SpiServiceManagerProvider { @Override public SpiServiceManager create(Jex jex) { - return ServiceManager.create(jex); + return CoreServiceManager.create(jex); } } diff --git a/avaje-jex/src/main/java/io/avaje/jex/core/ServiceManager.java b/avaje-jex/src/main/java/io/avaje/jex/core/CoreServiceManager.java similarity index 77% rename from avaje-jex/src/main/java/io/avaje/jex/core/ServiceManager.java rename to avaje-jex/src/main/java/io/avaje/jex/core/CoreServiceManager.java index a7b66af2..2f98eb80 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/core/ServiceManager.java +++ b/avaje-jex/src/main/java/io/avaje/jex/core/CoreServiceManager.java @@ -4,10 +4,7 @@ import io.avaje.jex.spi.HeaderKeys; import io.avaje.jex.spi.JsonService; import io.avaje.jex.spi.SpiContext; - import io.avaje.jex.spi.SpiServiceManager; -import jakarta.servlet.MultipartConfigElement; -import jakarta.servlet.http.HttpServletRequest; import java.io.UncheckedIOException; import java.io.UnsupportedEncodingException; @@ -15,7 +12,10 @@ import java.util.*; import java.util.stream.Stream; -class ServiceManager implements SpiServiceManager { +/** + * Core implementation of SpiServiceManager provided to specific implementations like jetty etc. + */ +class CoreServiceManager implements SpiServiceManager { public static final String UTF_8 = "UTF-8"; @@ -27,17 +27,14 @@ class ServiceManager implements SpiServiceManager { private final TemplateManager templateManager; - private final MultipartUtil multipartUtil; - static SpiServiceManager create(Jex jex) { return new Builder(jex).build(); } - ServiceManager(JsonService jsonService, ErrorHandling errorHandling, TemplateManager templateManager, MultipartUtil multipartUtil) { + CoreServiceManager(JsonService jsonService, ErrorHandling errorHandling, TemplateManager templateManager) { this.jsonService = jsonService; this.exceptionHandler = new ExceptionManager(errorHandling); this.templateManager = templateManager; - this.multipartUtil = multipartUtil; } @Override @@ -92,20 +89,6 @@ public void render(Context ctx, String name, Map model) { templateManager.render(ctx, name, model); } - @Override - public List uploadedFiles(HttpServletRequest req) { - return multipartUtil.uploadedFiles(req); - } - - @Override - public List uploadedFiles(HttpServletRequest req, String name) { - return multipartUtil.uploadedFiles(req, name); - } - - @Override - public Map> multiPartForm(HttpServletRequest req) { - return multipartUtil.fieldMap(req); - } @Override public String requestCharset(Context ctx) { @@ -155,7 +138,7 @@ private static class Builder { } SpiServiceManager build() { - return new ServiceManager(initJsonService(), jex.errorHandling(), initTemplateMgr(), initMultiPart()); + return new CoreServiceManager(initJsonService(), jex.errorHandling(), initTemplateMgr()); } JsonService initJsonService() { @@ -188,13 +171,5 @@ TemplateManager initTemplateMgr() { return mgr; } - MultipartUtil initMultiPart() { - MultipartConfigElement config = jex.inner.multipartConfig; - if (config == null) { - final int fileThreshold = jex.inner.multipartFileThreshold; - config = new MultipartConfigElement(System.getProperty("java.io.tmpdir"), -1, -1, fileThreshold); - } - return new MultipartUtil(config); - } } } diff --git a/avaje-jex/src/main/java/io/avaje/jex/spi/ProxyServiceManager.java b/avaje-jex/src/main/java/io/avaje/jex/spi/ProxyServiceManager.java new file mode 100644 index 00000000..eec71784 --- /dev/null +++ b/avaje-jex/src/main/java/io/avaje/jex/spi/ProxyServiceManager.java @@ -0,0 +1,74 @@ +package io.avaje.jex.spi; + +import io.avaje.jex.Context; +import io.avaje.jex.Routing; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +/** + * Provides a delegating proxy to a SpiServiceManager. + *

+ * Can be used by specific implementations like Jetty and JDK Http Server to add core functionality + * to provide to the specific context implementation. + */ +public abstract class ProxyServiceManager implements SpiServiceManager { + + protected final SpiServiceManager delegate; + + public ProxyServiceManager(SpiServiceManager delegate) { + this.delegate = delegate; + } + + @Override + public T jsonRead(Class clazz, SpiContext ctx) { + return delegate.jsonRead(clazz, ctx); + } + + @Override + public void jsonWrite(Object bean, SpiContext ctx) { + delegate.jsonWrite(bean, ctx); + } + + @Override + public void jsonWriteStream(Stream stream, SpiContext ctx) { + delegate.jsonWriteStream(stream, ctx); + } + + @Override + public void jsonWriteStream(Iterator iterator, SpiContext ctx) { + delegate.jsonWriteStream(iterator, ctx); + } + + @Override + public void maybeClose(Object iterator) { + delegate.maybeClose(iterator); + } + + @Override + public Routing.Type lookupRoutingType(String method) { + return delegate.lookupRoutingType(method); + } + + @Override + public void handleException(Context ctx, Exception e) { + delegate.handleException(ctx, e); + } + + @Override + public void render(Context ctx, String name, Map model) { + delegate.render(ctx, name, model); + } + + @Override + public String requestCharset(Context ctx) { + return delegate.requestCharset(ctx); + } + + @Override + public Map> formParamMap(Context ctx, String charset) { + return delegate.formParamMap(ctx, charset); + } +} diff --git a/avaje-jex/src/main/java/io/avaje/jex/spi/SpiServiceManager.java b/avaje-jex/src/main/java/io/avaje/jex/spi/SpiServiceManager.java index bdafd682..97191245 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/spi/SpiServiceManager.java +++ b/avaje-jex/src/main/java/io/avaje/jex/spi/SpiServiceManager.java @@ -2,30 +2,55 @@ import io.avaje.jex.Context; import io.avaje.jex.Routing; -import io.avaje.jex.UploadedFile; -import jakarta.servlet.http.HttpServletRequest; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.stream.Stream; +/** + * Core service methods available to Context implementations. + */ public interface SpiServiceManager { + /** + * Read and return the type from json request content. + */ T jsonRead(Class clazz, SpiContext ctx); + /** + * Write as json to response content. + */ void jsonWrite(Object bean, SpiContext ctx); + /** + * Write as json stream to response content. + */ void jsonWriteStream(Stream stream, SpiContext ctx); + /** + * Write as json stream to response content. + */ void jsonWriteStream(Iterator iterator, SpiContext ctx); + /** + * Maybe close if iterator is a AutoClosable. + */ void maybeClose(Object iterator); + /** + * Return the routing type given the http method. + */ Routing.Type lookupRoutingType(String method); + /** + * Handle the exception. + */ void handleException(Context ctx, Exception e); + /** + * Render using template manager. + */ void render(Context ctx, String name, Map model); /** @@ -38,25 +63,4 @@ public interface SpiServiceManager { */ Map> formParamMap(Context ctx, String charset); - /** - * Return the uploaded files for the request. - */ - default List uploadedFiles(HttpServletRequest req) { - throw new UnsupportedOperationException(); - } - - /** - * Return the uploaded files for the request and name. - */ - default List uploadedFiles(HttpServletRequest req, String name) { - throw new UnsupportedOperationException(); - } - - /** - * Return the form parameters from a multipart form request. - */ - default Map> multiPartForm(HttpServletRequest req) { - throw new UnsupportedOperationException(); - } - } diff --git a/avaje-jex/src/main/java/module-info.java b/avaje-jex/src/main/java/module-info.java index 28c8d068..ea79e586 100644 --- a/avaje-jex/src/main/java/module-info.java +++ b/avaje-jex/src/main/java/module-info.java @@ -12,17 +12,9 @@ exports io.avaje.jex.spi; exports io.avaje.jex.core; -// requires io.avaje.jex.jetty; - -// requires io.avaje.jex.jettyx; requires java.net.http; requires transitive org.slf4j; requires jetty.servlet.api; -// requires transitive org.eclipse.jetty.http; -// requires transitive org.eclipse.jetty.servlet; -// requires transitive org.eclipse.jetty.server; -// requires transitive org.eclipse.jetty.io; -// requires transitive org.eclipse.jetty.util; requires transitive com.fasterxml.jackson.databind; diff --git a/avaje-jex/src/test/java/io/avaje/jex/core/ContextUtilTest.java b/avaje-jex/src/test/java/io/avaje/jex/core/ContextUtilTest.java index 92dab133..944bcc8b 100644 --- a/avaje-jex/src/test/java/io/avaje/jex/core/ContextUtilTest.java +++ b/avaje-jex/src/test/java/io/avaje/jex/core/ContextUtilTest.java @@ -8,16 +8,16 @@ class ContextUtilTest { @Test void parseCharset_defaults() { - assertThat(ServiceManager.parseCharset("")).isEqualTo(ServiceManager.UTF_8); - assertThat(ServiceManager.parseCharset("junk")).isEqualTo(ServiceManager.UTF_8); + assertThat(CoreServiceManager.parseCharset("")).isEqualTo(CoreServiceManager.UTF_8); + assertThat(CoreServiceManager.parseCharset("junk")).isEqualTo(CoreServiceManager.UTF_8); } @Test void parseCharset_caseCheck() { - assertThat(ServiceManager.parseCharset("app/foo; charset=ME")).isEqualTo("ME"); - assertThat(ServiceManager.parseCharset("app/foo;charset=ME")).isEqualTo("ME"); - assertThat(ServiceManager.parseCharset("app/foo;charset = ME ")).isEqualTo("ME"); - assertThat(ServiceManager.parseCharset("app/foo;charset = ME;")).isEqualTo("ME"); - assertThat(ServiceManager.parseCharset("app/foo;charset = ME;other=junk")).isEqualTo("ME"); + assertThat(CoreServiceManager.parseCharset("app/foo; charset=ME")).isEqualTo("ME"); + assertThat(CoreServiceManager.parseCharset("app/foo;charset=ME")).isEqualTo("ME"); + assertThat(CoreServiceManager.parseCharset("app/foo;charset = ME ")).isEqualTo("ME"); + assertThat(CoreServiceManager.parseCharset("app/foo;charset = ME;")).isEqualTo("ME"); + assertThat(CoreServiceManager.parseCharset("app/foo;charset = ME;other=junk")).isEqualTo("ME"); } } From 519f846126e408e093662bb9bd785529dc63f572 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Mon, 12 Jul 2021 11:55:41 +1200 Subject: [PATCH 04/27] Use responseHeader(key) for Context API --- .../main/java/io/avaje/jex/jdk/JdkContext.java | 9 ++------- .../java/io/avaje/jex/jetty/JexHttpContext.java | 15 +++++---------- avaje-jex/src/main/java/io/avaje/jex/Context.java | 15 +++++++++------ .../java/io/avaje/jex/core/ExceptionManager.java | 2 +- 4 files changed, 17 insertions(+), 24 deletions(-) diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java index 7427f4d0..8e321b9b 100644 --- a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java @@ -124,8 +124,8 @@ public String contentType() { } @Override - public String contentTypeOfResponse() { - return header(exchange.getResponseHeaders(), HeaderKeys.CONTENT_TYPE); + public String responseHeader(String key) { + return header(exchange.getResponseHeaders(), key); } private String header(Headers headers, String name) { @@ -281,11 +281,6 @@ int statusCode() { return statusCode == 0 ? 200 : statusCode; } - @Override - public Context render(String name) { - return render(name, Collections.emptyMap()); - } - @Override public Context render(String name, Map model) { mgr.render(this, name, model); diff --git a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java index b9d95e33..eb78349a 100644 --- a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java +++ b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java @@ -347,11 +347,6 @@ public Context contentType(String contentType) { return this; } - @Override - public String contentTypeOfResponse() { - return req.getContentType(); - } - public Map headerMap() { Map map = new LinkedHashMap<>(); final Enumeration names = req.getHeaderNames(); @@ -362,6 +357,11 @@ public Map headerMap() { return map; } + @Override + public String responseHeader(String key) { + return req.getHeader(key); + } + @Override public String header(String key) { return req.getHeader(key); @@ -445,11 +445,6 @@ public Context write(String content) { } } - @Override - public Context render(String name) { - return render(name, emptyMap()); - } - @Override public Context render(String name, Map model) { mgr.render(this, name, model); diff --git a/avaje-jex/src/main/java/io/avaje/jex/Context.java b/avaje-jex/src/main/java/io/avaje/jex/Context.java index bf323180..43013338 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/Context.java +++ b/avaje-jex/src/main/java/io/avaje/jex/Context.java @@ -10,6 +10,7 @@ import static io.avaje.jex.spi.SpiContext.TEXT_HTML; import static io.avaje.jex.spi.SpiContext.TEXT_PLAIN; import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; /** * Provides access to functions for handling the request and response. @@ -114,11 +115,6 @@ public interface Context { */ Context contentType(String contentType); - /** - * Return the content type of the response. - */ - String contentTypeOfResponse(); - /** * Return the splat path value for the given position. * @@ -294,7 +290,9 @@ default Context html(String content) { * * @param name The template name */ - Context render(String name); + default Context render(String name) { + return render(name, emptyMap()); + } /** * Render a template typically as html with the given model. @@ -324,6 +322,11 @@ default Context html(String content) { */ void header(String key, String value); + /** + * Return the response header. + */ + String responseHeader(String key); + /** * Returns the request host, or null. */ diff --git a/avaje-jex/src/main/java/io/avaje/jex/core/ExceptionManager.java b/avaje-jex/src/main/java/io/avaje/jex/core/ExceptionManager.java index 7aa697b6..4c118d45 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/core/ExceptionManager.java +++ b/avaje-jex/src/main/java/io/avaje/jex/core/ExceptionManager.java @@ -87,7 +87,7 @@ private HttpResponseException unwrap(Exception e) { private boolean useJson(Context ctx) { final String acceptHeader = ctx.header(HeaderKeys.ACCEPT); return (acceptHeader != null && acceptHeader.contains("application/json") - || "application/json".equals(ctx.contentTypeOfResponse())); + || "application/json".equals(ctx.responseHeader(HeaderKeys.CONTENT_TYPE))); } } From e311929bdc22735a90e90e3ef91c292b61267029 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Mon, 12 Jul 2021 12:07:02 +1200 Subject: [PATCH 05/27] Remove servlet api dependency - MultipartConfigElement -> UploadConfig --- .../java/io/avaje/jex/jetty/JettyLaunch.java | 12 ++-- avaje-jex/src/main/java/io/avaje/jex/Jex.java | 14 +---- .../main/java/io/avaje/jex/UploadConfig.java | 58 +++++++++++++++++++ avaje-jex/src/main/java/module-info.java | 1 - 4 files changed, 69 insertions(+), 16 deletions(-) create mode 100644 avaje-jex/src/main/java/io/avaje/jex/UploadConfig.java diff --git a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JettyLaunch.java b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JettyLaunch.java index ec15eb32..279c3e13 100644 --- a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JettyLaunch.java +++ b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JettyLaunch.java @@ -3,6 +3,7 @@ import io.avaje.jex.Jex; import io.avaje.jex.ServerConfig; import io.avaje.jex.StaticFileSource; +import io.avaje.jex.UploadConfig; import io.avaje.jex.spi.SpiServiceManager; import io.avaje.jex.spi.SpiRoutes; import jakarta.servlet.MultipartConfigElement; @@ -44,12 +45,15 @@ private JettyServerConfig initConfig(ServerConfig config) { } MultipartUtil initMultiPart() { - MultipartConfigElement config = jex.inner.multipartConfig; - if (config == null) { + return new MultipartUtil(initMultipartConfigElement(jex.inner.multipartConfig)); + } + + MultipartConfigElement initMultipartConfigElement(UploadConfig uploadConfig) { + if (uploadConfig == null) { final int fileThreshold = jex.inner.multipartFileThreshold; - config = new MultipartConfigElement(System.getProperty("java.io.tmpdir"), -1, -1, fileThreshold); + return new MultipartConfigElement(System.getProperty("java.io.tmpdir"), -1, -1, fileThreshold); } - return new MultipartUtil(config); + return new MultipartConfigElement(uploadConfig.location(), uploadConfig.maxFileSize(), uploadConfig.maxRequestSize(), uploadConfig.fileSizeThreshold()); } @Override diff --git a/avaje-jex/src/main/java/io/avaje/jex/Jex.java b/avaje-jex/src/main/java/io/avaje/jex/Jex.java index 46e80eed..120e3142 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/Jex.java +++ b/avaje-jex/src/main/java/io/avaje/jex/Jex.java @@ -1,16 +1,8 @@ package io.avaje.jex; import io.avaje.jex.spi.*; -//import io.avaje.jex.jetty.JettyStartServer; -//import org.eclipse.jetty.server.session.SessionHandler; -//import org.eclipse.jetty.servlet.ServletContextHandler; - -import jakarta.servlet.MultipartConfigElement; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.ServiceLoader; + +import java.util.*; import java.util.function.Consumer; /** @@ -59,7 +51,7 @@ public static class Inner { public boolean preCompressStaticFiles; public JsonService jsonService; public AccessManager accessManager; - public MultipartConfigElement multipartConfig; + public UploadConfig multipartConfig; public int multipartFileThreshold = 8 * 1024; public final Map renderers = new HashMap<>(); } diff --git a/avaje-jex/src/main/java/io/avaje/jex/UploadConfig.java b/avaje-jex/src/main/java/io/avaje/jex/UploadConfig.java new file mode 100644 index 00000000..640bc08a --- /dev/null +++ b/avaje-jex/src/main/java/io/avaje/jex/UploadConfig.java @@ -0,0 +1,58 @@ +package io.avaje.jex; + +/** + * Configuration for server handling of Multipart file uploads etc. + */ +public class UploadConfig { + + private String location; + private long maxFileSize; + private long maxRequestSize; + private int fileSizeThreshold; + + public UploadConfig() { + } + + public UploadConfig(String location, long maxFileSize, long maxRequestSize, int fileSizeThreshold) { + this.location = location; + this.maxFileSize = maxFileSize; + this.maxRequestSize = maxRequestSize; + this.fileSizeThreshold = fileSizeThreshold; + } + + public String location() { + return location; + } + + public UploadConfig location(String location) { + this.location = location; + return this; + } + + public long maxFileSize() { + return maxFileSize; + } + + public UploadConfig maxFileSize(long maxFileSize) { + this.maxFileSize = maxFileSize; + return this; + } + + public long maxRequestSize() { + return maxRequestSize; + } + + public UploadConfig maxRequestSize(long maxRequestSize) { + this.maxRequestSize = maxRequestSize; + return this; + } + + public int fileSizeThreshold() { + return fileSizeThreshold; + } + + public UploadConfig fileSizeThreshold(int fileSizeThreshold) { + this.fileSizeThreshold = fileSizeThreshold; + return this; + } +} diff --git a/avaje-jex/src/main/java/module-info.java b/avaje-jex/src/main/java/module-info.java index ea79e586..d3b08a45 100644 --- a/avaje-jex/src/main/java/module-info.java +++ b/avaje-jex/src/main/java/module-info.java @@ -14,7 +14,6 @@ requires java.net.http; requires transitive org.slf4j; - requires jetty.servlet.api; requires transitive com.fasterxml.jackson.databind; From 6abd636384d3a8b17a3f8722174cb4fd2d73ec6a Mon Sep 17 00:00:00 2001 From: rbygrave Date: Mon, 12 Jul 2021 12:39:58 +1200 Subject: [PATCH 06/27] Add back API to set all Cookie attributes (using non-servlet Context.Cookie type) --- .../java/io/avaje/jex/jdk/JdkContext.java | 5 ++ .../io/avaje/jex/jetty/JexHttpContext.java | 37 +++++---- .../src/main/java/io/avaje/jex/Context.java | 81 ++++++++++++++++++- 3 files changed, 104 insertions(+), 19 deletions(-) diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java index 8e321b9b..17bb411e 100644 --- a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java @@ -53,6 +53,11 @@ public Map attributeMap() { throw new UnsupportedOperationException(); } + @Override + public Context cookie(Cookie cookie) { + return null; + } + @Override public String cookie(String name) { return null; diff --git a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java index eb78349a..fbcde149 100644 --- a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java +++ b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java @@ -7,7 +7,6 @@ import io.avaje.jex.spi.HeaderKeys; import io.avaje.jex.spi.SpiContext; import io.avaje.jex.spi.SpiRoutes; -import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; @@ -98,9 +97,9 @@ public Map attributeMap() { @Override public String cookie(String name) { - final Cookie[] cookies = req.getCookies(); + final jakarta.servlet.http.Cookie[] cookies = req.getCookies(); if (cookies != null) { - for (Cookie cookie : cookies) { + for (jakarta.servlet.http.Cookie cookie : cookies) { if (cookie.getName().equals(name)) { return cookie.getValue(); } @@ -111,17 +110,32 @@ public String cookie(String name) { @Override public Map cookieMap() { - final Cookie[] cookies = req.getCookies(); + final jakarta.servlet.http.Cookie[] cookies = req.getCookies(); if (cookies == null) { return emptyMap(); } final Map map = new LinkedHashMap<>(); - for (Cookie cookie : cookies) { + for (jakarta.servlet.http.Cookie cookie : cookies) { map.put(cookie.getName(), cookie.getValue()); } return map; } + @Override + public Context cookie(Cookie cookie) { + final jakarta.servlet.http.Cookie newCookie = new jakarta.servlet.http.Cookie(cookie.name(), cookie.value()); + newCookie.setPath(cookie.path()); + newCookie.setDomain(cookie.domain()); + newCookie.setMaxAge(cookie.maxAge()); + newCookie.setHttpOnly(cookie.httpOnly()); + newCookie.setSecure(cookie.secure()); + if (newCookie.getPath() == null) { + newCookie.setPath("/"); + } + res.addCookie(newCookie); + return this; + } + @Override public Context cookie(String name, String value) { return cookie(name, value, -1); @@ -129,16 +143,9 @@ public Context cookie(String name, String value) { @Override public Context cookie(String name, String value, int maxAge) { - final Cookie cookie = new Cookie(name, value); + final jakarta.servlet.http.Cookie cookie = new jakarta.servlet.http.Cookie(name, value); + cookie.setPath("/"); cookie.setMaxAge(maxAge); - return cookie(cookie); - } - - //@Override - public Context cookie(Cookie cookie) { - if (cookie.getPath() == null) { - cookie.setPath("/"); - } res.addCookie(cookie); return this; } @@ -153,7 +160,7 @@ public Context removeCookie(String name, String path) { if (path == null) { path = "/"; } - final Cookie cookie = new Cookie(name, ""); + final jakarta.servlet.http.Cookie cookie = new jakarta.servlet.http.Cookie(name, ""); cookie.setPath(path); cookie.setMaxAge(0); res.addCookie(cookie); diff --git a/avaje-jex/src/main/java/io/avaje/jex/Context.java b/avaje-jex/src/main/java/io/avaje/jex/Context.java index 43013338..6716698b 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/Context.java +++ b/avaje-jex/src/main/java/io/avaje/jex/Context.java @@ -58,10 +58,10 @@ public interface Context { */ Context cookie(String name, String value, int maxAge); -// /** -// * Sets a Cookie. -// */ -// Context cookie(Cookie cookie); + /** + * Sets a Cookie. + */ + Context cookie(Cookie cookie); /** * Remove a cookie by name. @@ -382,4 +382,77 @@ default Context render(String name) { */ List uploadedFiles(); + class Cookie { + private final String name; // NAME= ... "$Name" style is reserved + private final String value; // value of NAME + private String domain; // ;Domain=VALUE ... domain that sees cookie + private int maxAge = -1; // ;Max-Age=VALUE ... cookies auto-expire + private String path; // ;Path=VALUE ... URLs that see the cookie + private boolean secure; // ;Secure ... e.g. use SSL + private boolean httpOnly; + + private Cookie(String name, String value) { + if (name == null || name.length() == 0) { + throw new IllegalArgumentException("name required"); + } + this.name = name; + this.value = value; + } + + public static Cookie of(String name, String value) { + return new Cookie(name, value); + } + + public String name() { + return name; + } + public String value() { + return value; + } + + public String domain() { + return domain; + } + + public Cookie domain(String domain) { + this.domain= domain; + return this; + } + + public int maxAge() { + return maxAge; + } + + public Cookie maxAge(int maxAge) { + this.maxAge = maxAge; + return this; + } + + public String path() { + return path; + } + + public Cookie path(String path) { + this.path = path; + return this; + } + + public boolean secure() { + return secure; + } + + public Cookie secure(boolean secure) { + this.secure = secure; + return this; + } + + public boolean httpOnly() { + return httpOnly; + } + + public Cookie httpOnly(boolean httpOnly) { + this.httpOnly = httpOnly; + return this; + } + } } From 2f41ab16ea34b3ac6f8e54b29538f12a91a45df8 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Mon, 12 Jul 2021 12:51:48 +1200 Subject: [PATCH 07/27] JDK explicit charset=utf-8 for plain text and html response --- .../java/io/avaje/jex/jdk/JdkContext.java | 12 ++ .../avaje/jex/jdk/CharacterEncodingTest.java | 49 +++++++ .../avaje/jex/jdk/ContextFormParamTest.java | 135 ++++++++++++++++++ .../test/java/io/avaje/jex/jdk/TestPair.java | 59 ++++++++ .../io/avaje/jex/jetty/JexHttpContext.java | 18 +++ .../src/main/java/io/avaje/jex/Context.java | 10 +- .../java/io/avaje/jex/spi/SpiContext.java | 2 + 7 files changed, 277 insertions(+), 8 deletions(-) create mode 100644 avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/CharacterEncodingTest.java create mode 100644 avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/ContextFormParamTest.java create mode 100644 avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/TestPair.java diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java index 17bb411e..7cdbe124 100644 --- a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java @@ -264,6 +264,18 @@ public Context jsonStream(Iterator iterator) { return this; } + @Override + public Context text(String content) { + contentType(TEXT_PLAIN_UTF8); + return write(content); + } + + @Override + public Context html(String content) { + contentType(TEXT_HTML_UTF8); + return write(content); + } + @Override public Context write(String content) { try { diff --git a/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/CharacterEncodingTest.java b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/CharacterEncodingTest.java new file mode 100644 index 00000000..3d0f7dcb --- /dev/null +++ b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/CharacterEncodingTest.java @@ -0,0 +1,49 @@ +package io.avaje.jex.jdk; + +import io.avaje.jex.Jex; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpResponse; + +import static org.assertj.core.api.Assertions.assertThat; + +class CharacterEncodingTest { + + static TestPair pair = init(); + + static TestPair init() { + Jex app = Jex.create() + .routing(routing -> routing + .get("/text", ctx -> ctx.contentType("text/plain;charset=utf-8").write("суп из капусты")) + .get("/json", ctx -> ctx.json("白菜湯")) + .get("/html", ctx -> ctx.html("kålsuppe"))); + + return TestPair.create(app); + } + + @AfterAll + static void end() { + pair.shutdown(); + } + + @Test + void get() { + + var textRes = pair.request().path("text").GET().asString(); + var jsonRes = pair.request().path("json").GET().asString(); + var htmlRes = pair.request().path("html").GET().asString(); + + assertThat(contentType(jsonRes)).isEqualTo("application/json"); + assertThat(jsonRes.body()).isEqualTo("\"白菜湯\""); + assertThat(contentType(htmlRes)).isEqualTo("text/html;charset=utf-8"); + assertThat(htmlRes.body()).isEqualTo("kålsuppe"); + assertThat(contentType(textRes)).isEqualTo("text/plain;charset=utf-8"); + assertThat(textRes.body()).isEqualTo("суп из капусты"); + } + + private String contentType(HttpResponse res) { + return res.headers().firstValue("Content-Type").get(); + } + +} diff --git a/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/ContextFormParamTest.java b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/ContextFormParamTest.java new file mode 100644 index 00000000..6ca5e2c0 --- /dev/null +++ b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/ContextFormParamTest.java @@ -0,0 +1,135 @@ +package io.avaje.jex.jdk; + +import io.avaje.jex.Jex; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpResponse; + +import static org.assertj.core.api.Assertions.assertThat; + +class ContextFormParamTest { + + static TestPair pair = init(); + + static TestPair init() { + var app = Jex.create() + .routing(routing -> routing + .post("/", ctx -> ctx.text("map:" +ctx.formParamMap())) + .post("/formParams/{key}", ctx -> ctx.text("formParams:" + ctx.formParams(ctx.pathParam("key")))) + .post("/formParam/{key}", ctx -> ctx.text("formParam:" + ctx.formParam(ctx.pathParam("key")))) + .post("/formParamWithDefault/{key}", ctx -> ctx.text("formParam:" + ctx.formParam(ctx.pathParam("key"), "foo"))) + ); + return TestPair.create(app); + } + + @AfterAll + static void end() { + pair.shutdown(); + } + + @Test + void formParamMap() { + HttpResponse res = pair.request() + .formParam("one", "ao") + .formParam("one", "bo") + .formParam("two", "z") + .POST().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("map:{one=[ao, bo], two=[z]}"); + } + + + @Test + void formParams_one() { + HttpResponse res = pair.request() + .formParam("one", "ao") + .formParam("one", "bo") + .formParam("two", "z") + .path("formParams").path("one") + .POST().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("formParams:[ao, bo]"); + } + + @Test + void formParams_two() { + HttpResponse res = pair.request() + .formParam("one", "ao") + .formParam("one", "bo") + .formParam("two", "z") + .path("formParams").path("two") + .POST().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("formParams:[z]"); + } + + + @Test + void formParam_null() { + HttpResponse res = pair.request() + .formParam("one", "ao") + .formParam("one", "bo") + .formParam("two", "z") + .path("formParam").path("doesNotExist") + .POST().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("formParam:null"); + } + + @Test + void formParam_first() { + HttpResponse res = pair.request() + .formParam("one", "ao") + .formParam("one", "bo") + .formParam("two", "z") + .path("formParam").path("one") + .POST().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("formParam:ao"); + } + + @Test + void formParam_default() { + HttpResponse res = pair.request() + .formParam("one", "ao") + .formParam("one", "bo") + .formParam("two", "z") + .path("formParamWithDefault").path("doesNotExist") + .POST().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("formParam:foo"); + } + + @Test + void formParam_default_first() { + HttpResponse res = pair.request() + .formParam("one", "ao") + .formParam("one", "bo") + .formParam("two", "z") + .path("formParamWithDefault").path("one") + .POST().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("formParam:ao"); + } + + @Test + void formParam_default_only() { + HttpResponse res = pair.request() + .formParam("one", "ao") + .formParam("one", "bo") + .formParam("two", "z") + .path("formParamWithDefault").path("two") + .POST().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("formParam:z"); + } +} diff --git a/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/TestPair.java b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/TestPair.java new file mode 100644 index 00000000..087a1d3d --- /dev/null +++ b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/TestPair.java @@ -0,0 +1,59 @@ +package io.avaje.jex.jdk; + +import io.avaje.http.client.HttpClientContext; +import io.avaje.http.client.HttpClientRequest; +import io.avaje.http.client.JacksonBodyAdapter; +import io.avaje.jex.Jex; + +import java.util.Random; + +/** + * Server and Client pair for a test. + */ +public class TestPair { + + private final int port; + + private final Jex.Server server; + + private final HttpClientContext client; + + public TestPair(int port, Jex.Server server, HttpClientContext client) { + this.port = port; + this.server = server; + this.client = client; + } + + public void shutdown() { + server.shutdown(); + } + + public HttpClientRequest request() { + return client.request(); + } + + public int port() { + return port; + } + + public String url() { + return client.url().build(); + } + + /** + * Create a Server and Client pair for a given set of tests. + */ + public static TestPair create(Jex app) { + + int port = 10000 + new Random().nextInt(1000); + var jexServer = app.port(port).start(); + + var url = "http://localhost:" + port; + var client = HttpClientContext.newBuilder() + .baseUrl(url) + .bodyAdapter(new JacksonBodyAdapter()) + .build(); + + return new TestPair(port, jexServer, client); + } +} diff --git a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java index fbcde149..4c7be918 100644 --- a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java +++ b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java @@ -442,6 +442,24 @@ public Context jsonStream(Iterator iterator) { return this; } + /** + * Write plain text content to the response. + */ + @Override + public Context text(String content) { + contentType(TEXT_PLAIN); + return write(content); + } + + /** + * Write html content to the response. + */ + @Override + public Context html(String content) { + contentType(TEXT_HTML); + return write(content); + } + @Override public Context write(String content) { try { diff --git a/avaje-jex/src/main/java/io/avaje/jex/Context.java b/avaje-jex/src/main/java/io/avaje/jex/Context.java index 6716698b..c1d33f25 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/Context.java +++ b/avaje-jex/src/main/java/io/avaje/jex/Context.java @@ -246,18 +246,12 @@ default String userAgent() { /** * Write plain text content to the response. */ - default Context text(String content) { - contentType(TEXT_PLAIN); - return write(content); - } + Context text(String content); /** * Write html content to the response. */ - default Context html(String content) { - contentType(TEXT_HTML); - return write(content); - } + Context html(String content); /** * Set the response body as JSON for the given bean. diff --git a/avaje-jex/src/main/java/io/avaje/jex/spi/SpiContext.java b/avaje-jex/src/main/java/io/avaje/jex/spi/SpiContext.java index 753e6e10..c81b5c4b 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/spi/SpiContext.java +++ b/avaje-jex/src/main/java/io/avaje/jex/spi/SpiContext.java @@ -13,6 +13,8 @@ public interface SpiContext extends Context { String TEXT_HTML = "text/html"; String TEXT_PLAIN = "text/plain"; + String TEXT_HTML_UTF8 = "text/html;charset=utf-8"; + String TEXT_PLAIN_UTF8 = "text/plain;charset=utf-8"; String APPLICATION_JSON = "application/json"; String APPLICATION_X_JSON_STREAM = "application/x-json-stream"; From 66e4304390c55e5616bf40f455918b381905029f Mon Sep 17 00:00:00 2001 From: rbygrave Date: Mon, 12 Jul 2021 13:03:36 +1200 Subject: [PATCH 08/27] Fix Json stream with flush and close generator Plus add JsonTest to jdk module --- .../io/avaje/jex/jdk/AutoCloseIterator.java | 32 +++++ .../test/java/io/avaje/jex/jdk/HelloDto.java | 27 ++++ .../test/java/io/avaje/jex/jdk/JsonTest.java | 121 ++++++++++++++++++ .../io/avaje/jex/core/JacksonJsonService.java | 21 +-- 4 files changed, 193 insertions(+), 8 deletions(-) create mode 100644 avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/AutoCloseIterator.java create mode 100644 avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/HelloDto.java create mode 100644 avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/JsonTest.java diff --git a/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/AutoCloseIterator.java b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/AutoCloseIterator.java new file mode 100644 index 00000000..36ce4986 --- /dev/null +++ b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/AutoCloseIterator.java @@ -0,0 +1,32 @@ +package io.avaje.jex.jdk; + +import java.util.Iterator; + +public class AutoCloseIterator implements Iterator, AutoCloseable { + + private final Iterator it; + private boolean closed; + + public AutoCloseIterator(Iterator it) { + this.it = it; + } + + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + public E next() { + return it.next(); + } + + @Override + public void close() { + closed = true; + } + + public boolean isClosed() { + return closed; + } +} diff --git a/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/HelloDto.java b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/HelloDto.java new file mode 100644 index 00000000..e8861f3f --- /dev/null +++ b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/HelloDto.java @@ -0,0 +1,27 @@ +package io.avaje.jex.jdk; + +public class HelloDto { + + public long id; + public String name; + + @Override + public String toString() { + return "id:" + id + " name:" + name; + } + + public static HelloDto rob() { + return create(42, "rob"); + } + + public static HelloDto fi() { + return create(45, "fi"); + } + + public static HelloDto create(long id, String name) { + HelloDto me = new HelloDto(); + me.id = id; + me.name = name; + return me; + } +} diff --git a/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/JsonTest.java b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/JsonTest.java new file mode 100644 index 00000000..673a60a0 --- /dev/null +++ b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/JsonTest.java @@ -0,0 +1,121 @@ +package io.avaje.jex.jdk; + +import io.avaje.jex.Jex; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpHeaders; +import java.net.http.HttpResponse; +import java.util.List; +import java.util.stream.Stream; + +import static java.util.Arrays.asList; +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; + +class JsonTest { + + static List HELLO_BEANS = asList(HelloDto.rob(), HelloDto.fi()); + + static AutoCloseIterator ITERATOR = createBeanIterator(); + + private static AutoCloseIterator createBeanIterator() { + return new AutoCloseIterator<>(HELLO_BEANS.iterator()); + } + + static TestPair pair = init(); + + static TestPair init() { + Jex app = Jex.create() + .routing(routing -> routing + .get("/", ctx -> ctx.json(HelloDto.rob()).status(200)) + .get("/iterate", ctx -> ctx.jsonStream(ITERATOR)) + .get("/stream", ctx -> ctx.jsonStream(HELLO_BEANS.stream())) + .post("/", ctx -> ctx.text("bean[" + ctx.bodyAsClass(HelloDto.class) + "]"))); + + return TestPair.create(app); + } + + @AfterAll + static void end() { + pair.shutdown(); + } + + @Test + void get() { + + var bean = pair.request() + .GET() + .bean(HelloDto.class); + + assertThat(bean.id).isEqualTo(42); + assertThat(bean.name).isEqualTo("rob"); + + final HttpResponse hres = pair.request() + .GET().asString(); + + final HttpHeaders headers = hres.headers(); + assertThat(headers.firstValue("Content-Type").get()).isEqualTo("application/json"); + } + + @Test + void stream_viaIterator() { + final Stream beanStream = pair.request() + .path("iterate") + .GET() + .stream(HelloDto.class); + + // assert AutoCloseable iterator on the server-side was closed + assertThat(ITERATOR.isClosed()).isTrue(); + // expect client gets the expected stream of beans + assertCollectedStream(beanStream); + } + + @Test + void stream() { + final Stream beanStream = pair.request() + .path("stream") + .GET() + .stream(HelloDto.class); + + assertCollectedStream(beanStream); + } + + private void assertCollectedStream(Stream beanStream) { + final List collectedBeans = beanStream.collect(toList()); + assertThat(collectedBeans).hasSize(2); + + final HelloDto first = collectedBeans.get(0); + assertThat(first.id).isEqualTo(42); + assertThat(first.name).isEqualTo("rob"); + + final HelloDto second = collectedBeans.get(1); + assertThat(second.id).isEqualTo(45); + assertThat(second.name).isEqualTo("fi"); + } + + @Test + void post() { + HelloDto dto = new HelloDto(); + dto.id = 42; + dto.name = "rob was here"; + + var res = pair.request() + .body(dto) + .POST().asString(); + + assertThat(res.body()).isEqualTo("bean[id:42 name:rob was here]"); + assertThat(res.statusCode()).isEqualTo(200); + + dto.id = 99; + dto.name = "fi"; + + res = pair.request() + .body(dto) + .POST().asString(); + + assertThat(res.body()).isEqualTo("bean[id:99 name:fi]"); + assertThat(res.statusCode()).isEqualTo(200); + } + +} diff --git a/avaje-jex/src/main/java/io/avaje/jex/core/JacksonJsonService.java b/avaje-jex/src/main/java/io/avaje/jex/core/JacksonJsonService.java index 800cb244..0c14cdf9 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/core/JacksonJsonService.java +++ b/avaje-jex/src/main/java/io/avaje/jex/core/JacksonJsonService.java @@ -52,16 +52,21 @@ public void jsonWriteStream(Iterator iterator, SpiContext ctx) { try { generator = mapper.createGenerator(ctx.outputStream()); generator.setPrettyPrinter(null); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - while (iterator.hasNext()) { try { - mapper.writeValue(generator, iterator.next()); - generator.writeRaw('\n'); - } catch (IOException e) { - throw new UncheckedIOException(e); + while (iterator.hasNext()) { + try { + mapper.writeValue(generator, iterator.next()); + generator.writeRaw('\n'); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } finally { + generator.flush(); + generator.close(); } + } catch (IOException e) { + throw new UncheckedIOException(e); } } } From 5528df5c373f09b6b1d345f14c79e9a8c8887bb1 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Mon, 12 Jul 2021 13:31:45 +1200 Subject: [PATCH 09/27] JDK module add ExceptionManagerTest --- .../java/io/avaje/jex/jdk/BaseHandler.java | 61 +++++++++++--- .../java/io/avaje/jex/jdk/JdkContext.java | 12 ++- .../avaje/jex/jdk/ExceptionManagerTest.java | 80 +++++++++++++++++++ 3 files changed, 143 insertions(+), 10 deletions(-) create mode 100644 avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/ExceptionManagerTest.java diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/BaseHandler.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/BaseHandler.java index 195e225d..73ddbc26 100644 --- a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/BaseHandler.java +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/BaseHandler.java @@ -4,6 +4,7 @@ import com.sun.net.httpserver.HttpHandler; import io.avaje.jex.Context; import io.avaje.jex.Routing; +import io.avaje.jex.http.NotFoundResponse; import io.avaje.jex.spi.SpiRoutes; import java.io.IOException; @@ -24,17 +25,59 @@ public void handle(HttpExchange exchange) throws IOException { final String requestMethod = exchange.getRequestMethod(); final URI requestURI = exchange.getRequestURI(); - final String fragment = requestURI.getFragment(); final String uri = requestURI.getPath(); - final Routing.Type type = mgr.lookupRoutingType(requestMethod); - final SpiRoutes.Entry match = routes.match(type, uri); - - if (match != null) { - final SpiRoutes.Params params = match.pathParams(uri); - //SpiContext ctx = new JexHttpContext(manager, req, res, route.matchPath(), params); - Context ctx = new JdkContext(mgr, exchange, match.matchPath(), params); - match.handle(ctx); + final Routing.Type routeType = mgr.lookupRoutingType(requestMethod); + final SpiRoutes.Entry route = routes.match(routeType, uri); + + if (route == null) { + var ctx = new JdkContext(mgr, exchange, uri); + try { + processNoRoute(ctx, uri, routeType); + routes.after(uri, ctx); + } catch (Exception e) { + handleException(ctx, e); + } + } else { + final SpiRoutes.Params params = route.pathParams(uri); + JdkContext ctx = new JdkContext(mgr, exchange, route.matchPath(), params); + try { + processRoute(ctx, uri, route); + routes.after(uri, ctx); + } catch (Exception e) { + handleException(ctx, e); + } + } + } + + private void handleException(Context ctx, Exception e) { + mgr.handleException(ctx, e); + } + + private void processRoute(JdkContext ctx, String uri, SpiRoutes.Entry route) { + routes.before(uri, ctx); + ctx.setMode(null); + route.handle(ctx); + } + + private void processNoRoute(JdkContext ctx, String uri, Routing.Type routeType) { + routes.before(uri, ctx); + if (routeType == Routing.Type.HEAD && hasGetHandler(uri)) { + processHead(ctx); + return; } +// if (routeType == Routing.Type.GET || routeType == Routing.Type.HEAD) { +// // check if handled by static resource +// // check if handled by singlePageHandler +// } + throw new NotFoundResponse("uri: " + uri); + } + private void processHead(Context ctx) { + ctx.status(200); } + + private boolean hasGetHandler(String uri) { + return routes.match(Routing.Type.GET, uri) != null; + } + } diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java index 7cdbe124..79054c77 100644 --- a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java @@ -31,6 +31,16 @@ class JdkContext implements Context, SpiContext { this.params = params; } + /** + * Create when no route matched. + */ + JdkContext(ServiceManager mgr, HttpExchange exchange, String path) { + this.mgr = mgr; + this.exchange = exchange; + this.path = path; + this.params = null; + } + @Override public String matchedPath() { return path; @@ -135,7 +145,7 @@ public String responseHeader(String key) { private String header(Headers headers, String name) { final List values = headers.get(name); - return values.isEmpty() ? null : values.get(0); + return (values == null || values.isEmpty()) ? null : values.get(0); } @Override diff --git a/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/ExceptionManagerTest.java b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/ExceptionManagerTest.java new file mode 100644 index 00000000..2e6f60ca --- /dev/null +++ b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/ExceptionManagerTest.java @@ -0,0 +1,80 @@ +package io.avaje.jex.jdk; + +import io.avaje.jex.Jex; +import io.avaje.jex.http.ConflictResponse; +import io.avaje.jex.http.ForbiddenResponse; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpResponse; + +import static org.assertj.core.api.Assertions.assertThat; + +class ExceptionManagerTest { + + static TestPair pair = init(); + + static TestPair init() { + final Jex app = Jex.create() + .routing(routing -> routing + .get("/", ctx -> { + throw new ForbiddenResponse(); + }) + .post("/", ctx -> { + throw new IllegalStateException("foo"); + }) + .get("/conflict", ctx -> { + throw new ConflictResponse("Baz"); + }) + .get("/fiveHundred", ctx -> { + throw new IllegalArgumentException("Bar"); + })) + .exception(NullPointerException.class, (exception, ctx) -> ctx.text("npe")) + .exception(IllegalStateException.class, (exception, ctx) -> ctx.status(222).text("Handled IllegalStateException|" + exception.getMessage())) + .exception(ForbiddenResponse.class, (exception, ctx) -> ctx.status(223).text("Handled ForbiddenResponse|" + exception.getMessage())); + + return TestPair.create(app); + } + + @AfterAll + static void end() { + pair.shutdown(); + } + + @Test + void get() { + HttpResponse res = pair.request().GET().asString(); + assertThat(res.statusCode()).isEqualTo(223); + assertThat(res.body()).isEqualTo("Handled ForbiddenResponse|Forbidden"); + } + + @Test + void post() { + HttpResponse res = pair.request().body("simple").POST().asString(); + assertThat(res.statusCode()).isEqualTo(222); + assertThat(res.body()).isEqualTo("Handled IllegalStateException|foo"); + } + + @Test + void expect_fallback_to_default_asPlainText() { + HttpResponse res = pair.request().path("conflict").GET().asString(); + assertThat(res.statusCode()).isEqualTo(409); + assertThat(res.body()).isEqualTo("Baz"); + assertThat(res.headers().firstValue("Content-Type").get()).contains("text/plain"); + } + + @Test + void expect_fallback_to_default_asJson() { + HttpResponse res = pair.request().path("conflict").header("Accept", "application/json").GET().asString(); + assertThat(res.statusCode()).isEqualTo(409); + assertThat(res.body()).isEqualTo("{\"title\": Baz, \"status\": 409}"); + assertThat(res.headers().firstValue("Content-Type").get()).contains("application/json"); + } + + @Test + void expect_fallback_to_internalServerError() { + HttpResponse res = pair.request().path("fiveHundred").GET().asString(); + assertThat(res.statusCode()).isEqualTo(500); + assertThat(res.body()).isEqualTo("Internal server error"); + } +} From 3a469719dee899be150822fc4b2167ebf3cc9b52 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Mon, 12 Jul 2021 13:55:26 +1200 Subject: [PATCH 10/27] JDK module add FilterTest --- .../java/io/avaje/jex/jdk/FilterTest.java | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/FilterTest.java diff --git a/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/FilterTest.java b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/FilterTest.java new file mode 100644 index 00000000..9280e929 --- /dev/null +++ b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/FilterTest.java @@ -0,0 +1,89 @@ +package io.avaje.jex.jdk; + +import io.avaje.jex.Jex; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpHeaders; +import java.net.http.HttpResponse; +import java.util.concurrent.atomic.AtomicReference; + +import static org.assertj.core.api.Assertions.assertThat; + +class FilterTest { + + static TestPair pair = init(); + static AtomicReference afterAll = new AtomicReference<>(); + static AtomicReference afterTwo = new AtomicReference<>(); + + static TestPair init() { + final Jex app = Jex.create() + .routing(routing -> routing + .get("/", ctx -> ctx.text("roo")) + .get("/one", ctx -> ctx.text("one")) + .get("/two", ctx -> ctx.text("two")) + .get("/two/{id}", ctx -> ctx.text("two-id")) + .before(ctx -> { + ctx.header("before-all", "set"); + }) + .before("/two/*", ctx -> ctx.header("before-two", "set")) + .after(ctx -> { + afterAll.set("set"); + }) + .after("/two/*", ctx -> afterTwo.set("set")) + .get("/dummy", ctx -> ctx.text("dummy")) + ); + + return TestPair.create(app); + } + + @AfterAll + static void end() { + pair.shutdown(); + } + + void clearAfter() { + afterAll.set(null); + afterTwo.set(null); + } + + @Test + void get() { + clearAfter(); + HttpResponse res = pair.request().GET().asString(); + assertHasBeforeAfterAll(res); + assertNoBeforeAfterTwo(res); + + clearAfter(); + res = pair.request().path("one").GET().asString(); + assertHasBeforeAfterAll(res); + assertNoBeforeAfterTwo(res); + + clearAfter(); + res = pair.request().path("two").GET().asString(); + assertHasBeforeAfterAll(res); + assertNoBeforeAfterTwo(res); + } + + + @Test + void get_two_expect_extraFilters() { + clearAfter(); + HttpResponse res = pair.request().path("two/42").GET().asString(); + + final HttpHeaders headers = res.headers(); + assertHasBeforeAfterAll(res); + assertThat(headers.firstValue("before-two")).get().isEqualTo("set"); + assertThat(afterTwo.get()).isEqualTo("set"); + } + + private void assertNoBeforeAfterTwo(HttpResponse res) { + assertThat(res.headers().firstValue("before-two")).isEmpty(); + assertThat(afterTwo.get()).isNull(); + } + + private void assertHasBeforeAfterAll(HttpResponse res) { + assertThat(res.headers().firstValue("before-all")).get().isEqualTo("set"); + assertThat(afterAll.get()).isEqualTo("set"); + } +} From 5d9ad1541b09d05f77f8647b2832e33ab73f769d Mon Sep 17 00:00:00 2001 From: rbygrave Date: Mon, 12 Jul 2021 14:28:58 +1200 Subject: [PATCH 11/27] JDK module add ContextTest with host() and ip() Note that the headerMap shows different case behaviour (does not maintain case) --- .../java/io/avaje/jex/jdk/JdkContext.java | 15 +- .../java/io/avaje/jex/jdk/ContextTest.java | 180 ++++++++++++++++++ 2 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/ContextTest.java diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java index 79054c77..daffe523 100644 --- a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java @@ -10,6 +10,8 @@ import io.avaje.jex.spi.SpiRoutes; import java.io.*; +import java.net.InetAddress; +import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.stream.Stream; @@ -55,7 +57,7 @@ public Context attribute(String key, Object value) { @Override @SuppressWarnings("unchecked") public T attribute(String key) { - return (T)exchange.getAttribute(key); + return (T) exchange.getAttribute(key); } @Override @@ -338,12 +340,17 @@ public void header(String key, String value) { @Override public String host() { - return exchange.getRemoteAddress().getHostString(); + return header(HeaderKeys.HOST); } @Override public String ip() { - return null;//exchange.getRemoteAddress(); + final InetSocketAddress remote = exchange.getRemoteAddress(); + if (remote == null) { + return ""; + } + InetAddress address = remote.getAddress(); + return address == null ? remote.getHostString() : address.getHostAddress(); } @Override @@ -370,7 +377,7 @@ public String path() { @Override public int port() { - return 0; + return exchange.getLocalAddress().getPort(); } @Override diff --git a/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/ContextTest.java b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/ContextTest.java new file mode 100644 index 00000000..478c347b --- /dev/null +++ b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/ContextTest.java @@ -0,0 +1,180 @@ +package io.avaje.jex.jdk; + +import io.avaje.jex.Jex; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpResponse; +import java.util.Optional; + +import static java.util.Objects.requireNonNull; +import static org.assertj.core.api.Assertions.assertThat; + +class ContextTest { + + static TestPair pair = init(); + + static TestPair init() { + final Jex app = Jex.create() + .routing(routing -> routing + .get("/", ctx -> ctx.text("ze-get")) + .post("/", ctx -> ctx.text("ze-post")) + .get("/header", ctx -> { + ctx.header("From-My-Server", "Set-By-Server"); + ctx.text("req-header[" + ctx.header("From-My-Client") + "]"); + }) + .get("/headerMap", ctx -> ctx.text("req-header-map[" + ctx.headerMap() + "]")) + .get("/host", ctx -> { + final String host = ctx.host(); + requireNonNull(host); + ctx.text("host:" + host); + }) + .get("/ip", ctx -> { + final String ip = ctx.ip(); + requireNonNull(ip); + ctx.text("ip:" + ip); + }) + .post("/multipart", ctx -> ctx.text("isMultipart:" + ctx.isMultipart() + " isMultipartFormData:" + ctx.isMultipartFormData())) + .get("/method", ctx -> ctx.text("method:" + ctx.method() + " path:" + ctx.path() + " protocol:" + ctx.protocol() + " port:" + ctx.port())) + .post("/echo", ctx -> ctx.text("req-body[" + ctx.body() + "]")) + .get("/{a}/{b}", ctx -> ctx.text("ze-get-" + ctx.pathParamMap())) + .post("/{a}/{b}", ctx -> ctx.text("ze-post-" + ctx.pathParamMap())) + .get("/status", ctx -> { + ctx.status(201); + ctx.text("status:" + ctx.status()); + })); + + return TestPair.create(app); + } + + @AfterAll + static void end() { + pair.shutdown(); + } + + @Test + void get() { + HttpResponse res = pair.request().GET().asString(); + assertThat(res.body()).isEqualTo("ze-get"); + } + + @Test + void post() { + HttpResponse res = pair.request().body("simple").POST().asString(); + assertThat(res.body()).isEqualTo("ze-post"); + } + + @Test + void ctx_header_getSet() { + HttpResponse res = pair.request().path("header") + .header("From-My-Client", "client-value") + .GET().asString(); + + final Optional serverSetHeader = res.headers().firstValue("From-My-Server"); + assertThat(serverSetHeader.get()).isEqualTo("Set-By-Server"); + assertThat(res.body()).isEqualTo("req-header[client-value]"); + } + + @Test + void ctx_headerMap() { + HttpResponse res = pair.request().path("headerMap") + .header("X-Foo", "a") + .header("X-Bar", "b") + .GET().asString(); + + assertThat(res.body()).contains("X-foo=a"); // not maintaining case? + assertThat(res.body()).contains("X-bar=b"); + } + + @Test + void ctx_status() { + HttpResponse res = pair.request().path("status") + .GET().asString(); + + assertThat(res.body()).isEqualTo("status:201"); + } + + @Test + void ctx_host() { + HttpResponse res = pair.request().path("host") + .GET().asString(); + + assertThat(res.body()).contains("host:localhost"); + } + + @Test + void ctx_ip() { + HttpResponse res = pair.request().path("ip") + .GET().asString(); + + assertThat(res.body()).isEqualTo("ip:127.0.0.1"); + } + + @Test + void ctx_isMultiPart_when_not() { + HttpResponse res = pair.request().path("multipart") + .formParam("a", "aval") + .POST().asString(); + + assertThat(res.body()).isEqualTo("isMultipart:false isMultipartFormData:false"); + } + + + @Test + void ctx_isMultiPart_when_nothing() { + HttpResponse res = pair.request().path("multipart") + .body("junk") + .POST().asString(); + + assertThat(res.body()).isEqualTo("isMultipart:false isMultipartFormData:false"); + } + +// @Test +// void ctx_isMultiPart_when_isMultipart() { +// HttpResponse res = pair.request().path("multipart") +// .header("Content-Type", "multipart/foo") +// .body("junk") +// .POST().asString(); +// +// assertThat(res.body()).isEqualTo("isMultipart:true isMultipartFormData:false"); +// } +// +// @Test +// void ctx_isMultiPart_when_isMultipartFormData() { +// HttpResponse res = pair.request().path("multipart") +// .header("Content-Type", "multipart/form-data") +// .body("junk") +// .POST().asString(); +// +// assertThat(res.body()).isEqualTo("isMultipart:true isMultipartFormData:true"); +// } + + @Test + void ctx_methodPathPortProtocol() { + HttpResponse res = pair.request().path("method") + .GET().asString(); + + assertThat(res.body()).isEqualTo("method:GET path:/method protocol:HTTP/1.1 port:" + pair.port()); + } + + @Test + void post_body() { + HttpResponse res = pair.request().path("echo").body("simple").POST().asString(); + assertThat(res.body()).isEqualTo("req-body[simple]"); + } + + @Test + void get_path_path() { + var res = pair.request() + .path("A").path("B").GET().asString(); + + assertThat(res.body()).isEqualTo("ze-get-{a=A, b=B}"); + + res = pair.request() + .path("one").path("bar").body("simple").POST().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("ze-post-{a=one, b=bar}"); + } + +} From 0b4204f28f2c531219fdc768cd56054f4e30cfe3 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Mon, 12 Jul 2021 15:32:32 +1200 Subject: [PATCH 12/27] JDK module add QueryParamTest with queryParam support --- .../java/io/avaje/jex/jdk/JdkContext.java | 43 +++-- .../java/io/avaje/jex/jdk/JdkServerStart.java | 2 +- .../java/io/avaje/jex/jdk/ServiceManager.java | 7 +- .../java/io/avaje/jex/jdk/QueryParamTest.java | 150 ++++++++++++++++++ .../io/avaje/jex/core/CoreServiceManager.java | 7 +- .../io/avaje/jex/spi/ProxyServiceManager.java | 5 + .../io/avaje/jex/spi/SpiServiceManager.java | 4 + 7 files changed, 204 insertions(+), 14 deletions(-) create mode 100644 avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/QueryParamTest.java diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java index daffe523..435f9b81 100644 --- a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java @@ -16,14 +16,19 @@ import java.util.*; import java.util.stream.Stream; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; + class JdkContext implements Context, SpiContext { + private static final String UTF8 = "UTF8"; private final ServiceManager mgr; private final String path; private final SpiRoutes.Params params; private final HttpExchange exchange; private Routing.Type mode; - private Map> formParamMap; + private Map> formParams; + private Map> queryParams; private int statusCode; JdkContext(ServiceManager mgr, HttpExchange exchange, String path, SpiRoutes.Params params) { @@ -178,30 +183,50 @@ public String pathParam(String name) { @Override public String queryParam(String name) { - return null; + final List vals = queryParams(name); + return vals == null || vals.isEmpty() ? null : vals.get(0); + } + + private Map> queryParams() { + if (queryParams == null) { + queryParams = mgr.parseParamMap(queryString(), UTF8); + } + return queryParams; } @Override public List queryParams(String name) { - return null; + final List vals = queryParams().get(name); + return vals == null ? emptyList() : vals; } @Override public Map queryParamMap() { - return null; + final Map> map = queryParams(); + if (map.isEmpty()) { + return emptyMap(); + } + final Map single = new LinkedHashMap<>(); + for (Map.Entry> entry : map.entrySet()) { + final List value = entry.getValue(); + if (value != null && !value.isEmpty()) { + single.put(entry.getKey(), value.get(0)); + } + } + return single; } @Override public String queryString() { - return null; + return exchange.getRequestURI().getQuery(); } @Override public Map> formParamMap() { - if (formParamMap == null) { - formParamMap = initFormParamMap(); + if (formParams == null) { + formParams = initFormParamMap(); } - return formParamMap; + return formParams; } private Map> initFormParamMap() { @@ -211,7 +236,7 @@ private Map> initFormParamMap() { @Override public String scheme() { - return null; + return mgr.scheme(); } @Override diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServerStart.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServerStart.java index fa8fd86c..2f011fe5 100644 --- a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServerStart.java +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServerStart.java @@ -15,7 +15,7 @@ public class JdkServerStart implements SpiStartServer { @Override public Jex.Server start(Jex jex, SpiRoutes routes, SpiServiceManager serviceManager) { - final ServiceManager manager = new ServiceManager(serviceManager); + final ServiceManager manager = new ServiceManager(serviceManager, "http"); HttpHandler handler = new BaseHandler(routes, manager); try { final HttpServer server = HttpServer.create(); diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/ServiceManager.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/ServiceManager.java index 175c3faa..aa2ec0b9 100644 --- a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/ServiceManager.java +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/ServiceManager.java @@ -7,15 +7,20 @@ class ServiceManager extends ProxyServiceManager { + private final String scheme; private final long outputBufferMax = 1024; private final int outputBufferInitial = 256; - ServiceManager(SpiServiceManager delegate) { + ServiceManager(SpiServiceManager delegate, String scheme) { super(delegate); + this.scheme = scheme; } OutputStream createOutputStream(JdkContext jdkContext) { return new BufferedOutStream(jdkContext, outputBufferMax, outputBufferInitial); } + String scheme() { + return scheme; + } } diff --git a/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/QueryParamTest.java b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/QueryParamTest.java new file mode 100644 index 00000000..64e7ed13 --- /dev/null +++ b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/QueryParamTest.java @@ -0,0 +1,150 @@ +package io.avaje.jex.jdk; + +import io.avaje.jex.Jex; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpResponse; +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +class QueryParamTest { + + static TestPair pair = init(); + + static TestPair init() { + var app = Jex.create() + .routing(routing -> routing + .get("/", ctx -> ctx.text("hello")) + .get("/one/{id}", ctx -> ctx.text("one-" + ctx.pathParam("id") + "|match:" + ctx.matchedPath())) + .get("/one/{id}/{b}", ctx -> ctx.text("path:" + ctx.pathParamMap() + "|query:" + ctx.queryParam("z") + "|match:" + ctx.matchedPath())) + .get("/queryParamMap", ctx -> ctx.text("qpm: "+ctx.queryParamMap())) + .get("/queryParams", ctx -> ctx.text("qps: "+ctx.queryParams("a"))) + .get("/queryString", ctx -> ctx.text("qs: "+ctx.queryString())) + .get("/scheme", ctx -> ctx.text("scheme: "+ctx.scheme())) + ); + return TestPair.create(app); + } + + @AfterAll + static void end() { + pair.shutdown(); + } + + @Test + void get() { + HttpResponse res = pair.request().GET().asString(); + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("hello"); + } + + @Test + void getOne_path() { + var res = pair.request() + .path("one").path("foo").GET().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("one-foo|match:/one/{id}"); + + res = pair.request() + .path("one").path("bar").GET().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("one-bar|match:/one/{id}"); + } + + @Test + void getOne_path_path() { + var res = pair.request() + .path("one").path("foo").path("bar") + .GET().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("path:{id=foo, b=bar}|query:null|match:/one/{id}/{b}"); + + res = pair.request() + .path("one").path("fo").path("ba").queryParam("z", "42") + .GET().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("path:{id=fo, b=ba}|query:42|match:/one/{id}/{b}"); + } + + @Test + void queryParamMap_when_empty() { + HttpResponse res = pair.request().path("queryParamMap").GET().asString(); + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("qpm: {}"); + } + + @Test + void queryParamMap_keyWithMultiValues_expect_firstValueInMap() { + HttpResponse res = pair.request().path("queryParamMap") + .queryParam("a","AVal0") + .queryParam("a","AVal1") + .queryParam("b", "BVal") + .GET().asString(); + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("qpm: {a=AVal0, b=BVal}"); + } + + @Test + void queryParamMap_basic() { + HttpResponse res = pair.request().path("queryParamMap") + .queryParam("a","AVal") + .queryParam("b", "BVal") + .GET().asString(); + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("qpm: {a=AVal, b=BVal}"); + } + + @Test + void queryParams_basic() { + HttpResponse res = pair.request().path("queryParams") + .queryParam("a","one") + .queryParam("a", "two") + .GET().asString(); + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("qps: [one, two]"); + } + + @Test + void queryParams_when_null_expect_emptyList() { + HttpResponse res = pair.request().path("queryParams") + .queryParam("b","one") + .GET().asString(); + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("qps: []"); + } + + @Test + void queryString_when_null() { + HttpResponse res = pair.request().path("queryString") + .GET().asString(); + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("qs: null"); + } + + @Test + void queryString_when_set() { + HttpResponse res = pair.request().path("queryString") + .queryParam("foo","f1") + .queryParam("bar","b1") + .queryParam("bar","b2") + .GET().asString(); + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("qs: foo=f1&bar=b1&bar=b2"); + } + + @Test + void scheme() { + HttpResponse res = pair.request().path("scheme") + .queryParam("foo","f1") + .GET().asString(); + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("scheme: http"); + } + +} diff --git a/avaje-jex/src/main/java/io/avaje/jex/core/CoreServiceManager.java b/avaje-jex/src/main/java/io/avaje/jex/core/CoreServiceManager.java index 2f98eb80..c640c5e2 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/core/CoreServiceManager.java +++ b/avaje-jex/src/main/java/io/avaje/jex/core/CoreServiceManager.java @@ -109,11 +109,12 @@ static String parseCharset(String header) { @Override public Map> formParamMap(Context ctx, String charset) { - return formParamMap(ctx.body(), charset); + return parseParamMap(ctx.body(), charset); } - static Map> formParamMap(String body, String charset) { - if (body.isEmpty()) { + @Override + public Map> parseParamMap(String body, String charset) { + if (body == null || body.isEmpty()) { return Collections.emptyMap(); } try { diff --git a/avaje-jex/src/main/java/io/avaje/jex/spi/ProxyServiceManager.java b/avaje-jex/src/main/java/io/avaje/jex/spi/ProxyServiceManager.java index eec71784..afafa811 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/spi/ProxyServiceManager.java +++ b/avaje-jex/src/main/java/io/avaje/jex/spi/ProxyServiceManager.java @@ -71,4 +71,9 @@ public String requestCharset(Context ctx) { public Map> formParamMap(Context ctx, String charset) { return delegate.formParamMap(ctx, charset); } + + @Override + public Map> parseParamMap(String body, String charset) { + return delegate.parseParamMap(body, charset); + } } diff --git a/avaje-jex/src/main/java/io/avaje/jex/spi/SpiServiceManager.java b/avaje-jex/src/main/java/io/avaje/jex/spi/SpiServiceManager.java index 97191245..88cef787 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/spi/SpiServiceManager.java +++ b/avaje-jex/src/main/java/io/avaje/jex/spi/SpiServiceManager.java @@ -63,4 +63,8 @@ public interface SpiServiceManager { */ Map> formParamMap(Context ctx, String charset); + /** + * Parse and return the content as url encoded parameters. + */ + Map> parseParamMap(String body, String charset); } From f165f746e634abf27cd2a6536fef0a8fb3a7358e Mon Sep 17 00:00:00 2001 From: rbygrave Date: Mon, 12 Jul 2021 16:04:04 +1200 Subject: [PATCH 13/27] JDK module contextPath and fullUrl --- .../java/io/avaje/jex/jdk/JdkContext.java | 11 +- .../java/io/avaje/jex/jdk/JdkServerStart.java | 2 +- .../java/io/avaje/jex/jdk/ServiceManager.java | 12 +- .../avaje/jex/jdk/ContextAttributeTest.java | 63 +++++++++++ .../io/avaje/jex/jdk/ContextLengthTest.java | 106 ++++++++++++++++++ .../io/avaje/jex/jetty/JexHttpContext.java | 3 +- .../src/main/java/io/avaje/jex/Context.java | 13 ++- 7 files changed, 198 insertions(+), 12 deletions(-) create mode 100644 avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/ContextAttributeTest.java create mode 100644 avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/ContextLengthTest.java diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java index 435f9b81..b424e267 100644 --- a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java @@ -256,17 +256,14 @@ public Map sessionAttributeMap() { @Override public String url() { - return null; - } - - @Override - public String fullUrl() { - return null; + StringBuffer url = new StringBuffer(128); + url.append(scheme()).append("://").append(host()).append(path); + return url.toString(); } @Override public String contextPath() { - return null; + return mgr.contextPath(); } @Override diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServerStart.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServerStart.java index 2f011fe5..9639400e 100644 --- a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServerStart.java +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServerStart.java @@ -15,7 +15,7 @@ public class JdkServerStart implements SpiStartServer { @Override public Jex.Server start(Jex jex, SpiRoutes routes, SpiServiceManager serviceManager) { - final ServiceManager manager = new ServiceManager(serviceManager, "http"); + final ServiceManager manager = new ServiceManager(serviceManager, "http", ""); HttpHandler handler = new BaseHandler(routes, manager); try { final HttpServer server = HttpServer.create(); diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/ServiceManager.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/ServiceManager.java index aa2ec0b9..59df313c 100644 --- a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/ServiceManager.java +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/ServiceManager.java @@ -8,12 +8,14 @@ class ServiceManager extends ProxyServiceManager { private final String scheme; + private final String contextPath; private final long outputBufferMax = 1024; private final int outputBufferInitial = 256; - ServiceManager(SpiServiceManager delegate, String scheme) { + ServiceManager(SpiServiceManager delegate, String scheme, String contextPath) { super(delegate); this.scheme = scheme; + this.contextPath = contextPath; } OutputStream createOutputStream(JdkContext jdkContext) { @@ -23,4 +25,12 @@ OutputStream createOutputStream(JdkContext jdkContext) { String scheme() { return scheme; } + + public String url(JdkContext jdkContext) { + return scheme+"://"; + } + + public String contextPath() { + return contextPath; + } } diff --git a/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/ContextAttributeTest.java b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/ContextAttributeTest.java new file mode 100644 index 00000000..116ff93f --- /dev/null +++ b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/ContextAttributeTest.java @@ -0,0 +1,63 @@ +package io.avaje.jex.jdk; + +import io.avaje.jex.Jex; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpResponse; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +class ContextAttributeTest { + + static final UUID uuid = UUID.randomUUID(); + + static TestPair pair = init(); + + static TestPair attrPair; + static UUID attrUuid; + + static TestPair init() { + var app = Jex.create() + .routing(routing -> routing + .before(ctx -> { + ctx.attribute("oneUuid", uuid).attribute(TestPair.class.getName(), pair); + }) + .get("/", ctx -> { + attrUuid = ctx.attribute("oneUuid"); + attrPair = ctx.attribute(TestPair.class.getName()); + + assert attrUuid == uuid; + assert attrPair == pair; + +// ctx.attributeMap() is not supported +// final Map attrMap = ctx.attributeMap(); +// final Object mapUuid = attrMap.get("oneUuid"); +// assert mapUuid == uuid; +// +// final Object mapPair = attrMap.get(TestPair.class.getName()); +// assert mapPair == pair; + ctx.text("all-good"); + }) + ); + return TestPair.create(app); + } + + @AfterAll + static void end() { + pair.shutdown(); + } + + @Test + void get() { + HttpResponse res = pair.request().GET().asString(); + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("all-good"); + + assertThat(attrPair).isSameAs(pair); + assertThat(attrUuid).isSameAs(uuid); + } + + +} diff --git a/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/ContextLengthTest.java b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/ContextLengthTest.java new file mode 100644 index 00000000..f817711a --- /dev/null +++ b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/ContextLengthTest.java @@ -0,0 +1,106 @@ +package io.avaje.jex.jdk; + +import io.avaje.jex.Jex; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpResponse; + +import static org.assertj.core.api.Assertions.assertThat; + +class ContextLengthTest { + + static TestPair pair = init(); + + static TestPair init() { + var app = Jex.create() + .routing(routing -> routing + .post("/", ctx -> ctx.text("contentLength:" + ctx.contentLength() + " type:" + ctx.contentType())) + .get("/url", ctx -> ctx.text("url:" + ctx.url())) + .get("/fullUrl", ctx -> ctx.text("fullUrl:" + ctx.fullUrl())) + .get("/contextPath", ctx -> ctx.text("contextPath:" + ctx.contextPath())) + .get("/userAgent", ctx -> ctx.text("userAgent:" + ctx.userAgent())) + ); + return TestPair.create(app); + } + + @AfterAll + static void end() { + pair.shutdown(); + } + + @Test + void when_noReqContentType() { + HttpResponse res = pair.request().body("MyBodyContent") + .POST().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("contentLength:13 type:null"); + } + + @Test + void requestContentLengthAndType_notReqContentType() { + HttpResponse res = pair.request() + .formParam("a", "my-a-val") + .formParam("b", "my-b-val") + .POST().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("contentLength:21 type:application/x-www-form-urlencoded"); + } + + @Test + void url() { + HttpResponse res = pair.request() + .path("url") + .queryParam("a", "av") + .GET().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("url:http://localhost:" + pair.port() + "/url"); + } + + @Test + void fullUrl_no_queryString() { + HttpResponse res = pair.request() + .path("fullUrl") + .GET().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("fullUrl:http://localhost:" + pair.port() + "/fullUrl"); + } + + @Test + void fullUrl_queryString() { + HttpResponse res = pair.request() + .path("fullUrl") + .queryParam("a", "av") + .queryParam("b", "bv") + .GET().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("fullUrl:http://localhost:" + pair.port() + "/fullUrl?a=av&b=bv"); + } + + @Test + void contextPath() { + HttpResponse res = pair.request() + .path("contextPath") + .queryParam("a", "av") + .GET().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("contextPath:"); + } + + @Test + void userAgent() { + HttpResponse res = pair.request() + .path("userAgent") + .queryParam("a", "av") + .GET().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).contains("userAgent:Java-http-client"); + } +} diff --git a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java index 4c7be918..c519e84a 100644 --- a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java +++ b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java @@ -323,8 +323,9 @@ public String url() { @Override public String fullUrl() { + final String url = url(); final String qs = queryString(); - return qs == null ? url() : url() + "?" + qs; + return qs == null ? url : url + "?" + qs; } @Override diff --git a/avaje-jex/src/main/java/io/avaje/jex/Context.java b/avaje-jex/src/main/java/io/avaje/jex/Context.java index c1d33f25..051cc8b5 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/Context.java +++ b/avaje-jex/src/main/java/io/avaje/jex/Context.java @@ -24,6 +24,7 @@ public interface Context { /** * Sets an attribute on the request. + *

* Attributes are available to other handlers in the request lifecycle */ Context attribute(String key, Object value); @@ -34,8 +35,11 @@ public interface Context { T attribute(String key); /** + * Deprecated for removal - not supported by JDK http server. + *

* Gets a map with all the attribute keys and values on the request. */ + @Deprecated Map attributeMap(); /** @@ -219,7 +223,11 @@ default List formParams(String key) { /** * Return the full request url, including query string (if present) */ - String fullUrl(); + default String fullUrl() { + final String url = url(); + final String qs = queryString(); + return qs == null ? url : url + "?" + qs; + } /** * Return the request context path. @@ -400,6 +408,7 @@ public static Cookie of(String name, String value) { public String name() { return name; } + public String value() { return value; } @@ -409,7 +418,7 @@ public String domain() { } public Cookie domain(String domain) { - this.domain= domain; + this.domain = domain; return this; } From 9ecc246b9b993c2f2668a78407df0d354cc0c762 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Mon, 12 Jul 2021 16:05:54 +1200 Subject: [PATCH 14/27] JDK module add NestedRoutesTest --- .../io/avaje/jex/jdk/NestedRoutesTest.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/NestedRoutesTest.java diff --git a/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/NestedRoutesTest.java b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/NestedRoutesTest.java new file mode 100644 index 00000000..f12a1469 --- /dev/null +++ b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/NestedRoutesTest.java @@ -0,0 +1,73 @@ +package io.avaje.jex.jdk; + +import io.avaje.jex.Jex; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpResponse; + +import static org.assertj.core.api.Assertions.assertThat; + +class NestedRoutesTest { + + static TestPair pair = init(); + + static TestPair init() { + Jex app = Jex.create() + .routing(routing -> routing + .get("/", ctx -> ctx.text("hello")) + .path("api", () -> { + routing.get(ctx -> ctx.text("apiRoot")); + routing.get("{id}", ctx -> ctx.text("api-" + ctx.pathParam("id"))); + }) + .path("extra", () -> { + routing.get(ctx -> ctx.text("extraRoot")); + routing.get("{id}", ctx -> ctx.text("extra-id-" + ctx.pathParam("id"))); + routing.get("more/{id}", ctx -> ctx.text("extraMore-" + ctx.pathParam("id"))); + })); + return TestPair.create(app); + } + + @AfterAll + static void end() { + pair.shutdown(); + } + + @Test + void get() { + HttpResponse res = pair.request().GET().asString(); + assertThat(res.body()).isEqualTo("hello"); + } + + @Test + void get_api_paths() { + var res = pair.request() + .path("api").GET().asString(); + + assertThat(res.body()).isEqualTo("apiRoot"); + + res = pair.request() + .path("api").path("99").GET().asString(); + + assertThat(res.body()).isEqualTo("api-99"); + } + + @Test + void get_extra_paths() { + var res = pair.request() + .path("extra").GET().asString(); + + assertThat(res.body()).isEqualTo("extraRoot"); + + res = pair.request() + .path("extra").path("99").GET().asString(); + + assertThat(res.body()).isEqualTo("extra-id-99"); + + res = pair.request() + .path("extra").path("more").path("42").GET().asString(); + + assertThat(res.body()).isEqualTo("extraMore-42"); + } + +} From 48ffb8c9595c4cc318237e0435c0eae57e581683 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Mon, 12 Jul 2021 16:21:26 +1200 Subject: [PATCH 15/27] JDK module add redirect handling --- .../java/io/avaje/jex/jdk/BaseHandler.java | 3 +- .../java/io/avaje/jex/jdk/JdkContext.java | 32 +++++++++++-- .../java/io/avaje/jex/jdk/RedirectTest.java | 46 +++++++++++++++++++ .../io/avaje/jex/jetty/JexHttpContext.java | 5 ++ .../io/avaje/jex/jetty/JexHttpServlet.java | 3 +- .../io/avaje/jex/core/CoreServiceManager.java | 2 +- .../io/avaje/jex/core/ExceptionManager.java | 13 +++--- .../io/avaje/jex/spi/ProxyServiceManager.java | 2 +- .../java/io/avaje/jex/spi/SpiContext.java | 5 ++ .../io/avaje/jex/spi/SpiServiceManager.java | 2 +- 10 files changed, 97 insertions(+), 16 deletions(-) create mode 100644 avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/RedirectTest.java diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/BaseHandler.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/BaseHandler.java index 73ddbc26..0281fd75 100644 --- a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/BaseHandler.java +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/BaseHandler.java @@ -5,6 +5,7 @@ import io.avaje.jex.Context; import io.avaje.jex.Routing; import io.avaje.jex.http.NotFoundResponse; +import io.avaje.jex.spi.SpiContext; import io.avaje.jex.spi.SpiRoutes; import java.io.IOException; @@ -49,7 +50,7 @@ public void handle(HttpExchange exchange) throws IOException { } } - private void handleException(Context ctx, Exception e) { + private void handleException(SpiContext ctx, Exception e) { mgr.handleException(ctx, e); } diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java index b424e267..3edb016a 100644 --- a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java @@ -5,15 +5,22 @@ import io.avaje.jex.Context; import io.avaje.jex.Routing; import io.avaje.jex.UploadedFile; +import io.avaje.jex.http.RedirectResponse; import io.avaje.jex.spi.HeaderKeys; import io.avaje.jex.spi.SpiContext; import io.avaje.jex.spi.SpiRoutes; -import java.io.*; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; -import java.util.*; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import java.util.stream.Stream; import static java.util.Collections.emptyList; @@ -22,6 +29,7 @@ class JdkContext implements Context, SpiContext { private static final String UTF8 = "UTF8"; + private static final int SC_MOVED_TEMPORARILY = 302; private final ServiceManager mgr; private final String path; private final SpiRoutes.Params params; @@ -107,12 +115,28 @@ public Context removeCookie(String name, String path) { @Override public void redirect(String location) { - + redirect(location, SC_MOVED_TEMPORARILY); } @Override - public void redirect(String location, int httpStatusCode) { + public void redirect(String location, int statusCode) { + header(HeaderKeys.LOCATION, location); + status(statusCode); + if (mode == Routing.Type.BEFORE) { + throw new RedirectResponse(statusCode); + } else { + performRedirect(); + } + } + @Override + public void performRedirect() { + try { + exchange.sendResponseHeaders(statusCode(), 0); + exchange.getResponseBody().close(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } @Override diff --git a/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/RedirectTest.java b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/RedirectTest.java new file mode 100644 index 00000000..418b21be --- /dev/null +++ b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/RedirectTest.java @@ -0,0 +1,46 @@ +package io.avaje.jex.jdk; + +import io.avaje.jex.Jex; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpResponse; + +import static org.assertj.core.api.Assertions.assertThat; + +class RedirectTest { + + + static TestPair pair = init(); + + static TestPair init() { + var app = Jex.create() + .routing(routing -> routing + .before("/other/*", ctx -> ctx.redirect("/two?from=filter")) + .get("/one", ctx -> ctx.text("one")) + .get("/two", ctx -> ctx.text("two")) + .get("/redirect/me", ctx -> ctx.redirect("/one?from=handler")) + .get("/other/me", ctx -> ctx.text("never hit")) + ); + return TestPair.create(app); + } + + @AfterAll + static void end() { + pair.shutdown(); + } + + @Test + void redirect_via_handler() { + HttpResponse res = pair.request().path("redirect/me").GET().asString(); + assertThat(res.body()).isEqualTo("one"); + assertThat(res.statusCode()).isEqualTo(200); + } + + @Test + void redirect_via_beforeHandler() { + HttpResponse res = pair.request().path("other/me").GET().asString(); + assertThat(res.body()).isEqualTo("two"); + assertThat(res.statusCode()).isEqualTo(200); + } +} diff --git a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java index c519e84a..f297b6ac 100644 --- a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java +++ b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java @@ -181,6 +181,11 @@ public void redirect(String location, int statusCode) { } } + @Override + public void performRedirect() { + // do nothing + } + @Override public String matchedPath() { return matchedPath; diff --git a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpServlet.java b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpServlet.java index 60da967f..a7551c80 100644 --- a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpServlet.java +++ b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpServlet.java @@ -4,6 +4,7 @@ import io.avaje.jex.Jex; import io.avaje.jex.Routing; import io.avaje.jex.http.NotFoundResponse; +import io.avaje.jex.spi.SpiContext; import io.avaje.jex.spi.SpiRoutes; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; @@ -49,7 +50,7 @@ protected void service(HttpServletRequest req, HttpServletResponse res) { } } - private void handleException(Context ctx, Exception e) { + private void handleException(SpiContext ctx, Exception e) { manager.handleException(ctx, e); } diff --git a/avaje-jex/src/main/java/io/avaje/jex/core/CoreServiceManager.java b/avaje-jex/src/main/java/io/avaje/jex/core/CoreServiceManager.java index c640c5e2..fba4735f 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/core/CoreServiceManager.java +++ b/avaje-jex/src/main/java/io/avaje/jex/core/CoreServiceManager.java @@ -80,7 +80,7 @@ public Routing.Type lookupRoutingType(String method) { } @Override - public void handleException(Context ctx, Exception e) { + public void handleException(SpiContext ctx, Exception e) { exceptionHandler.handle(ctx, e); } diff --git a/avaje-jex/src/main/java/io/avaje/jex/core/ExceptionManager.java b/avaje-jex/src/main/java/io/avaje/jex/core/ExceptionManager.java index 4c118d45..553ff720 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/core/ExceptionManager.java +++ b/avaje-jex/src/main/java/io/avaje/jex/core/ExceptionManager.java @@ -1,12 +1,12 @@ package io.avaje.jex.core; -import io.avaje.jex.Context; import io.avaje.jex.ErrorHandling; import io.avaje.jex.ExceptionHandler; import io.avaje.jex.http.HttpResponseException; import io.avaje.jex.http.InternalServerErrorResponse; import io.avaje.jex.http.RedirectResponse; import io.avaje.jex.spi.HeaderKeys; +import io.avaje.jex.spi.SpiContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -20,7 +20,7 @@ class ExceptionManager { this.errorHandling = errorHandling; } - void handle(Context ctx, Exception e) { + void handle(SpiContext ctx, Exception e) { final ExceptionHandler handler = errorHandling.find(e.getClass()); if (handler != null) { handler.handle(e, ctx); @@ -33,7 +33,7 @@ void handle(Context ctx, Exception e) { } } - private void unhandledException(Context ctx, Exception e) { + private void unhandledException(SpiContext ctx, Exception e) { log.warn("Uncaught exception", e); defaultHandling(ctx, new InternalServerErrorResponse()); } @@ -46,12 +46,11 @@ private boolean isRedirect(Exception e) { return RedirectResponse.class.isAssignableFrom(e.getClass()); } - private void defaultHandling(Context ctx, Exception exception) { + private void defaultHandling(SpiContext ctx, Exception exception) { final HttpResponseException e = unwrap(exception); ctx.status(e.getStatus()); if (isRedirect(e)) { - // no content - log.trace("redirect"); + ctx.performRedirect(); } else if (useJson(ctx)) { ctx.contentType("application/json").write(asJsonContent(e)); } else { @@ -84,7 +83,7 @@ private HttpResponseException unwrap(Exception e) { //(if (e is CompletionException) e.cause else e) as HttpResponseException } - private boolean useJson(Context ctx) { + private boolean useJson(SpiContext ctx) { final String acceptHeader = ctx.header(HeaderKeys.ACCEPT); return (acceptHeader != null && acceptHeader.contains("application/json") || "application/json".equals(ctx.responseHeader(HeaderKeys.CONTENT_TYPE))); diff --git a/avaje-jex/src/main/java/io/avaje/jex/spi/ProxyServiceManager.java b/avaje-jex/src/main/java/io/avaje/jex/spi/ProxyServiceManager.java index afafa811..994b850e 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/spi/ProxyServiceManager.java +++ b/avaje-jex/src/main/java/io/avaje/jex/spi/ProxyServiceManager.java @@ -53,7 +53,7 @@ public Routing.Type lookupRoutingType(String method) { } @Override - public void handleException(Context ctx, Exception e) { + public void handleException(SpiContext ctx, Exception e) { delegate.handleException(ctx, e); } diff --git a/avaje-jex/src/main/java/io/avaje/jex/spi/SpiContext.java b/avaje-jex/src/main/java/io/avaje/jex/spi/SpiContext.java index c81b5c4b..0f8d46a6 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/spi/SpiContext.java +++ b/avaje-jex/src/main/java/io/avaje/jex/spi/SpiContext.java @@ -32,4 +32,9 @@ public interface SpiContext extends Context { * Set to indicate BEFORE, Handler and AFTER modes of the request. */ void setMode(Routing.Type type); + + /** + * Preform the redirect as part of Exception handling typically due to before handler. + */ + void performRedirect(); } diff --git a/avaje-jex/src/main/java/io/avaje/jex/spi/SpiServiceManager.java b/avaje-jex/src/main/java/io/avaje/jex/spi/SpiServiceManager.java index 88cef787..017f23ba 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/spi/SpiServiceManager.java +++ b/avaje-jex/src/main/java/io/avaje/jex/spi/SpiServiceManager.java @@ -46,7 +46,7 @@ public interface SpiServiceManager { /** * Handle the exception. */ - void handleException(Context ctx, Exception e); + void handleException(SpiContext ctx, Exception e); /** * Render using template manager. From 312011cacaf55edef468d66846ddf73e21bc53a8 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Mon, 12 Jul 2021 16:39:39 +1200 Subject: [PATCH 16/27] test logging trace --- avaje-jex-jetty/pom.xml | 2 +- avaje-jex-jetty/src/test/resources/logback-test.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/avaje-jex-jetty/pom.xml b/avaje-jex-jetty/pom.xml index 40d85088..fbec571c 100644 --- a/avaje-jex-jetty/pom.xml +++ b/avaje-jex-jetty/pom.xml @@ -37,8 +37,8 @@ maven-surefire-plugin - --add-opens io.avaje.jex.jetty/io.avaje.jex.base=com.fasterxml.jackson.databind --add-modules com.fasterxml.jackson.databind + --add-opens io.avaje.jex.jetty/io.avaje.jex.base=com.fasterxml.jackson.databind --add-opens io.avaje.jex.jetty/io.avaje.jex=ALL-UNNAMED --add-opens io.avaje.jex.jetty/io.avaje.jex.base=ALL-UNNAMED --add-opens io.avaje.jex.jetty/io.avaje.jex.core=ALL-UNNAMED diff --git a/avaje-jex-jetty/src/test/resources/logback-test.xml b/avaje-jex-jetty/src/test/resources/logback-test.xml index 199b4a4f..ddb21350 100644 --- a/avaje-jex-jetty/src/test/resources/logback-test.xml +++ b/avaje-jex-jetty/src/test/resources/logback-test.xml @@ -13,7 +13,7 @@ - + From 4aac18953a450e6a06544f6c1e2d34a662845ccc Mon Sep 17 00:00:00 2001 From: rbygrave Date: Mon, 12 Jul 2021 17:48:39 +1200 Subject: [PATCH 17/27] Use java.net.HttpCookie for Context API --- .../java/io/avaje/jex/jdk/JdkContext.java | 10 +-- .../io/avaje/jex/jetty/JexHttpContext.java | 36 ++++---- .../io/avaje/jex/base/ContextCookieTest.java | 28 ++++++- .../test/java/io/avaje/jex/base/TestPair.java | 9 +- .../src/main/java/io/avaje/jex/Context.java | 83 ++----------------- 5 files changed, 57 insertions(+), 109 deletions(-) diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java index 3edb016a..c65d86f6 100644 --- a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java @@ -14,6 +14,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.UncheckedIOException; +import java.net.HttpCookie; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; @@ -79,7 +80,7 @@ public Map attributeMap() { } @Override - public Context cookie(Cookie cookie) { + public Context cookie(HttpCookie cookie) { return null; } @@ -93,14 +94,9 @@ public Map cookieMap() { return null; } - @Override - public Context cookie(String name, String value) { - return null; - } - @Override public Context cookie(String name, String value, int maxAge) { - return null; + return this; } @Override diff --git a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java index f297b6ac..4c2ca75d 100644 --- a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java +++ b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java @@ -7,6 +7,7 @@ import io.avaje.jex.spi.HeaderKeys; import io.avaje.jex.spi.SpiContext; import io.avaje.jex.spi.SpiRoutes; +import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; @@ -15,6 +16,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.UncheckedIOException; +import java.net.HttpCookie; import java.nio.charset.Charset; import java.util.*; import java.util.stream.Stream; @@ -97,9 +99,9 @@ public Map attributeMap() { @Override public String cookie(String name) { - final jakarta.servlet.http.Cookie[] cookies = req.getCookies(); + final Cookie[] cookies = req.getCookies(); if (cookies != null) { - for (jakarta.servlet.http.Cookie cookie : cookies) { + for (Cookie cookie : cookies) { if (cookie.getName().equals(name)) { return cookie.getValue(); } @@ -110,40 +112,38 @@ public String cookie(String name) { @Override public Map cookieMap() { - final jakarta.servlet.http.Cookie[] cookies = req.getCookies(); + final Cookie[] cookies = req.getCookies(); if (cookies == null) { return emptyMap(); } final Map map = new LinkedHashMap<>(); - for (jakarta.servlet.http.Cookie cookie : cookies) { + for (Cookie cookie : cookies) { map.put(cookie.getName(), cookie.getValue()); } return map; } @Override - public Context cookie(Cookie cookie) { - final jakarta.servlet.http.Cookie newCookie = new jakarta.servlet.http.Cookie(cookie.name(), cookie.value()); - newCookie.setPath(cookie.path()); - newCookie.setDomain(cookie.domain()); - newCookie.setMaxAge(cookie.maxAge()); - newCookie.setHttpOnly(cookie.httpOnly()); - newCookie.setSecure(cookie.secure()); + public Context cookie(HttpCookie cookie) { + final Cookie newCookie = new Cookie(cookie.getName(), cookie.getValue()); + newCookie.setPath(cookie.getPath()); if (newCookie.getPath() == null) { newCookie.setPath("/"); } + final String domain = cookie.getDomain(); + if (domain != null) { + newCookie.setDomain(domain); + } + newCookie.setMaxAge((int) cookie.getMaxAge()); + newCookie.setHttpOnly(cookie.isHttpOnly()); + newCookie.setSecure(cookie.getSecure()); res.addCookie(newCookie); return this; } - @Override - public Context cookie(String name, String value) { - return cookie(name, value, -1); - } - @Override public Context cookie(String name, String value, int maxAge) { - final jakarta.servlet.http.Cookie cookie = new jakarta.servlet.http.Cookie(name, value); + final Cookie cookie = new Cookie(name, value); cookie.setPath("/"); cookie.setMaxAge(maxAge); res.addCookie(cookie); @@ -160,7 +160,7 @@ public Context removeCookie(String name, String path) { if (path == null) { path = "/"; } - final jakarta.servlet.http.Cookie cookie = new jakarta.servlet.http.Cookie(name, ""); + final Cookie cookie = new Cookie(name, ""); cookie.setPath(path); cookie.setMaxAge(0); res.addCookie(cookie); diff --git a/avaje-jex-jetty/src/test/java/io/avaje/jex/base/ContextCookieTest.java b/avaje-jex-jetty/src/test/java/io/avaje/jex/base/ContextCookieTest.java index b3fefff2..d0dc2839 100644 --- a/avaje-jex-jetty/src/test/java/io/avaje/jex/base/ContextCookieTest.java +++ b/avaje-jex-jetty/src/test/java/io/avaje/jex/base/ContextCookieTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; +import java.net.HttpCookie; import java.net.http.HttpResponse; import static org.assertj.core.api.Assertions.assertThat; @@ -19,8 +20,15 @@ static TestPair init() { .get("/readCookie/{name}", ctx -> ctx.text("readCookie:" + ctx.cookie(ctx.pathParam("name")))) .get("/readCookieMap", ctx -> ctx.text("cookieMap:" + ctx.cookieMap())) .get("/removeCookie/{name}", ctx -> ctx.removeCookie(ctx.pathParam("name")).text("ok")) + .get("/setCookieAll", ctx -> { + final HttpCookie httpCookie = new HttpCookie("ac", "v_all"); + httpCookie.setPath("/"); + httpCookie.setHttpOnly(true); + httpCookie.setMaxAge(10_000); + ctx.cookie(httpCookie); + }) ); - return TestPair.create(app); + return TestPair.create(app, 9001); } @AfterAll @@ -30,9 +38,10 @@ static void end() { @Test void set_read_readMap_remove_readMap_remove_readMap() { - HttpResponse res = pair.request() - .path("setCookie").GET().asString(); + HttpResponse res = pair.request().path("removeCookie").path("ac").GET().asString(); + assertThat(res.body()).isEqualTo("ok"); + res = pair.request().path("setCookie").GET().asString(); assertThat(res.statusCode()).isEqualTo(200); res = pair.request().path("readCookie").path("ck").GET().asString(); @@ -57,4 +66,17 @@ void set_read_readMap_remove_readMap_remove_readMap() { assertThat(res.body()).isEqualTo("cookieMap:{}"); } + @Test + void setAll() { + HttpResponse res = pair.request().path("setCookieAll").GET().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + + res = pair.request().path("readCookieMap").GET().asString(); + assertThat(res.body()).isEqualTo("cookieMap:{ac=v_all}"); + + res = pair.request().path("readCookie").path("ac").GET().asString(); + assertThat(res.body()).isEqualTo("readCookie:v_all"); + } + } diff --git a/avaje-jex-jetty/src/test/java/io/avaje/jex/base/TestPair.java b/avaje-jex-jetty/src/test/java/io/avaje/jex/base/TestPair.java index 25e5b9be..5ce2290e 100644 --- a/avaje-jex-jetty/src/test/java/io/avaje/jex/base/TestPair.java +++ b/avaje-jex-jetty/src/test/java/io/avaje/jex/base/TestPair.java @@ -40,12 +40,15 @@ public String url() { return client.url().build(); } + public static TestPair create(Jex app) { + int port = 10000 + new Random().nextInt(1000); + return create(app, port); + } + /** * Create a Server and Client pair for a given set of tests. */ - public static TestPair create(Jex app) { - - int port = 10000 + new Random().nextInt(1000); + public static TestPair create(Jex app, int port) { var jexServer = app.port(port).start(); var url = "http://localhost:" + port; diff --git a/avaje-jex/src/main/java/io/avaje/jex/Context.java b/avaje-jex/src/main/java/io/avaje/jex/Context.java index 051cc8b5..37f8dc26 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/Context.java +++ b/avaje-jex/src/main/java/io/avaje/jex/Context.java @@ -2,13 +2,12 @@ import io.avaje.jex.spi.HeaderKeys; +import java.net.HttpCookie; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.stream.Stream; -import static io.avaje.jex.spi.SpiContext.TEXT_HTML; -import static io.avaje.jex.spi.SpiContext.TEXT_PLAIN; import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; @@ -55,7 +54,9 @@ public interface Context { /** * Sets a cookie with name, value with unlimited age. */ - Context cookie(String name, String value); + default Context cookie(String name, String value) { + return cookie(name, value, -1); + } /** * Sets a cookie with name, value, and max-age. @@ -65,7 +66,7 @@ public interface Context { /** * Sets a Cookie. */ - Context cookie(Cookie cookie); + Context cookie(HttpCookie cookie); /** * Remove a cookie by name. @@ -384,78 +385,4 @@ default Context render(String name) { */ List uploadedFiles(); - class Cookie { - private final String name; // NAME= ... "$Name" style is reserved - private final String value; // value of NAME - private String domain; // ;Domain=VALUE ... domain that sees cookie - private int maxAge = -1; // ;Max-Age=VALUE ... cookies auto-expire - private String path; // ;Path=VALUE ... URLs that see the cookie - private boolean secure; // ;Secure ... e.g. use SSL - private boolean httpOnly; - - private Cookie(String name, String value) { - if (name == null || name.length() == 0) { - throw new IllegalArgumentException("name required"); - } - this.name = name; - this.value = value; - } - - public static Cookie of(String name, String value) { - return new Cookie(name, value); - } - - public String name() { - return name; - } - - public String value() { - return value; - } - - public String domain() { - return domain; - } - - public Cookie domain(String domain) { - this.domain = domain; - return this; - } - - public int maxAge() { - return maxAge; - } - - public Cookie maxAge(int maxAge) { - this.maxAge = maxAge; - return this; - } - - public String path() { - return path; - } - - public Cookie path(String path) { - this.path = path; - return this; - } - - public boolean secure() { - return secure; - } - - public Cookie secure(boolean secure) { - this.secure = secure; - return this; - } - - public boolean httpOnly() { - return httpOnly; - } - - public Cookie httpOnly(boolean httpOnly) { - this.httpOnly = httpOnly; - return this; - } - } } From 0061375793af21d245f4075d7476c905b5b3ffd4 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Mon, 12 Jul 2021 20:56:23 +1200 Subject: [PATCH 18/27] JDK module cookie support with CookieParser --- .../java/io/avaje/jex/jdk/CookieParser.java | 130 ++++++++++++++++++ .../java/io/avaje/jex/jdk/JdkContext.java | 76 ++++++++-- .../io/avaje/jex/jdk/CookieParserTest.java | 53 +++++++ .../io/avaje/jex/jdk/CookieServerTest.java | 82 +++++++++++ .../test/java/io/avaje/jex/jdk/TestPair.java | 8 +- 5 files changed, 340 insertions(+), 9 deletions(-) create mode 100644 avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/CookieParser.java create mode 100644 avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/CookieParserTest.java create mode 100644 avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/CookieServerTest.java diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/CookieParser.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/CookieParser.java new file mode 100644 index 00000000..9948fb26 --- /dev/null +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/CookieParser.java @@ -0,0 +1,130 @@ +package io.avaje.jex.jdk; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static java.util.Collections.emptyMap; + +/** + * Parse cookies based on RFC6265 skipping parameters. + */ +class CookieParser { + + private static final String QUOTE = "\""; + private static final char[] QUOTE_CHARS = QUOTE.toCharArray(); + + private CookieParser() { + } + + private static final String RFC2965_VERSION = "$Version"; + private static final String RFC2965_PATH = "$Path"; + private static final String RFC2965_DOMAIN = "$Domain"; + private static final String RFC2965_PORT = "$Port"; + + /** + * Parse cookies based on RFC6265 skipping parameters. + * + *

This does not support cookies with multiple values. + * + * @param rawHeader a value of '{@code Cookie:}' header. + */ + public static Map parse(String rawHeader) { + if (rawHeader == null) { + return emptyMap(); + } + rawHeader = rawHeader.trim(); + if (rawHeader.isEmpty()) { + return emptyMap(); + } + + // Beware RFC2965 + boolean isRfc2965 = false; + if (rawHeader.regionMatches(true, 0, RFC2965_VERSION, 0, RFC2965_VERSION.length())) { + isRfc2965 = true; + int ind = rawHeader.indexOf(';'); + if (ind < 0) { + return emptyMap(); + } else { + rawHeader = rawHeader.substring(ind + 1); + } + } + + Map map = new LinkedHashMap<>(); + for (String baseToken : tokenize(',', rawHeader)) { + for (String token : tokenize(';', baseToken)) { + int eqInd = token.indexOf('='); + if (eqInd > 0) { + String name = token.substring(0, eqInd).trim(); + if (name.isEmpty()) { + continue; // Name MOST NOT be empty; + } + if (isRfc2965 && name.charAt(0) == '$' && ignore(name)) { + continue; // Skip RFC2965 attributes + } + final String value = unwrap(token.substring(eqInd + 1).trim()); + if (!value.isEmpty()) { + map.put(name, value); + } + } + } + } + return map; + } + + private static boolean ignore(String name) { + return (RFC2965_PATH.equalsIgnoreCase(name) || RFC2965_DOMAIN.equalsIgnoreCase(name) + || RFC2965_PORT.equalsIgnoreCase(name) || RFC2965_VERSION.equalsIgnoreCase(name)); + } + + + /** + * Unwrap double-quotes if present. + */ + private static String unwrap(String value) { + if (value.length() >= 2 && '"' == value.charAt(0) && '"' == value.charAt(value.length() - 1)) { + return value.substring(1, value.length() - 1); + } + return value; + } + + /** + * Tokenize with quoted sub-sequences. + */ + static List tokenize(char separator, String text) { + StringBuilder token = new StringBuilder(); + List result = new ArrayList<>(); + boolean quoted = false; + char lastQuoteCharacter = ' '; + for (int i = 0; i < text.length(); i++) { + char ch = text.charAt(i); + if (quoted) { + if (ch == lastQuoteCharacter) { + quoted = false; + } + token.append(ch); + } else { + if (ch == separator) { + if (token.length() > 0) { + result.add(token.toString()); + } + token.setLength(0); + } else { + for (char quote : CookieParser.QUOTE_CHARS) { + if (ch == quote) { + quoted = true; + lastQuoteCharacter = ch; + break; + } + } + token.append(ch); + } + } + } + if (token.length() > 0) { + result.add(token.toString()); + } + return result; + } +} diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java index c65d86f6..e0cf2d5f 100644 --- a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java @@ -29,8 +29,11 @@ class JdkContext implements Context, SpiContext { + private static final String EXPIRE_COOKIE = "; Expires=Sat, 01 Jan 2000 00:00:00 GMT"; private static final String UTF8 = "UTF8"; private static final int SC_MOVED_TEMPORARILY = 302; + private static final String SET_COOKIE2 = "Set-Cookie"; + private static final String COOKIE = "Cookie"; private final ServiceManager mgr; private final String path; private final SpiRoutes.Params params; @@ -38,6 +41,7 @@ class JdkContext implements Context, SpiContext { private Routing.Type mode; private Map> formParams; private Map> queryParams; + private Map cookieMap; private int statusCode; JdkContext(ServiceManager mgr, HttpExchange exchange, String path, SpiRoutes.Params params) { @@ -57,6 +61,21 @@ class JdkContext implements Context, SpiContext { this.params = null; } + private Map internalCookieMap() { + if (cookieMap == null) { + cookieMap = parseCookies(); + } + return cookieMap; + } + + private Map parseCookies() { + final String cookieHeader = header(exchange.getRequestHeaders(), COOKIE); + if (cookieHeader == null || cookieHeader.isEmpty()) { + return emptyMap(); + } + return CookieParser.parse(cookieHeader); + } + @Override public String matchedPath() { return path; @@ -80,33 +99,74 @@ public Map attributeMap() { } @Override - public Context cookie(HttpCookie cookie) { - return null; + public String cookie(String name) { + return internalCookieMap().get(name); } @Override - public String cookie(String name) { - return null; + public Map cookieMap() { + return internalCookieMap(); } @Override - public Map cookieMap() { - return null; + public Context cookie(HttpCookie cookie) { + header(SET_COOKIE2, toCookieHeader(cookie)); + return this; } @Override public Context cookie(String name, String value, int maxAge) { + HttpCookie cookie = new HttpCookie(name, value); + cookie.setMaxAge(maxAge); + cookie.setPath("/"); + header(SET_COOKIE2, toCookieHeader(cookie)); return this; } + private String toCookieHeader(HttpCookie cookie) { + return toCookieHeader(cookie, null); + } + private String toCookieHeader(HttpCookie cookie, String forceExpire) { + StringBuilder sb = new StringBuilder(100); + sb.append(cookie.getName()).append("=").append(cookie.getValue()).append("; Path="); + if (cookie.getPath() == null) { + sb.append("/"); + } else { + sb.append(cookie.getPath()); + } + if (cookie.getDomain() != null) { + sb.append("; Domain=").append(cookie.getDomain()); + } + if (forceExpire != null) { + sb.append(forceExpire); + } + final long maxAge = cookie.getMaxAge(); + if (maxAge > 1) { + sb.append("; Max-Age=").append(maxAge); + } + if (cookie.getSecure()) { + sb.append("; Secure"); + } + if (cookie.isHttpOnly()) { + sb.append("; HttpOnly"); + } + return sb.toString(); + } + + @Override public Context removeCookie(String name) { - return null; + HttpCookie cookie = new HttpCookie(name, ""); + header(SET_COOKIE2, toCookieHeader(cookie, EXPIRE_COOKIE)); + return this; } @Override public Context removeCookie(String name, String path) { - return null; + HttpCookie cookie = new HttpCookie(name, ""); + cookie.setPath(path); + header(SET_COOKIE2, toCookieHeader(cookie, EXPIRE_COOKIE)); + return this; } @Override diff --git a/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/CookieParserTest.java b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/CookieParserTest.java new file mode 100644 index 00000000..46649180 --- /dev/null +++ b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/CookieParserTest.java @@ -0,0 +1,53 @@ +package io.avaje.jex.jdk; + +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class CookieParserTest { + + @Test + void emptyAndNull() { + assertThat(CookieParser.parse(null)).isEmpty(); + assertThat(CookieParser.parse("")).isEmpty(); + } + + @Test + void basicMultiValue() { + Map cookies = CookieParser.parse("foo=bar; aaa=bbb; c=what_the_hell; aaa=ccc"); + assertThat(cookies.get("foo")).isEqualTo("bar"); + assertThat(cookies.get("aaa")).isEqualTo("ccc"); + assertThat(cookies.get("c")).isEqualTo("what_the_hell"); + assertThat(cookies).hasSize(3); + } + + @Test + void rfc2965() { + String header = "$version=1; foo=bar; $Domain=google.com, aaa=bbb, c=cool; $Domain=google.com; $Path=\"/foo\""; + Map cookies = CookieParser.parse(header); + assertThat(cookies.get("foo")).isEqualTo("bar"); + assertThat(cookies.get("aaa")).isEqualTo("bbb"); + assertThat(cookies.get("c")).isEqualTo("cool"); + assertThat(cookies).hasSize(3); + } + + @Test + void unquote() { + Map cookies = CookieParser.parse("foo=\"bar\"; aaa=bbb; c=\"what_the_hell\"; aaa=\"ccc\""); + assertThat(cookies.get("foo")).isEqualTo("bar"); + assertThat(cookies.get("aaa")).isEqualTo("ccc"); + assertThat(cookies).hasSize(3); + } + + @Test + void tokenize() { + String text = ",aa,,fo\"oooo\",\"bar\",co\"o'l,e\"c,df'hk,lm',"; + List tokens = CookieParser.tokenize(',', text); + assertThat(tokens).contains("aa", "fo\"oooo\"", "\"bar\"", "co\"o'l,e\"c", "df'hk", "lm'"); + tokens = CookieParser.tokenize(';', text); + assertThat(tokens).containsExactly(text); + } +} diff --git a/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/CookieServerTest.java b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/CookieServerTest.java new file mode 100644 index 00000000..ecb12b34 --- /dev/null +++ b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/CookieServerTest.java @@ -0,0 +1,82 @@ +package io.avaje.jex.jdk; + +import io.avaje.jex.Jex; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import java.net.HttpCookie; +import java.net.http.HttpResponse; + +import static org.assertj.core.api.Assertions.assertThat; + +class CookieServerTest { + + static TestPair pair = init(); + + static TestPair init() { + var app = Jex.create() + .routing(routing -> routing + .get("/setCookie", ctx -> ctx.cookie("ck", "val").cookie("ck2", "val2").text("ok")) + .get("/readCookie/{name}", ctx -> ctx.text("readCookie:" + ctx.cookie(ctx.pathParam("name")))) + .get("/readCookieMap", ctx -> ctx.text("cookieMap:" + ctx.cookieMap())) + .get("/removeCookie/{name}", ctx -> ctx.removeCookie(ctx.pathParam("name")).text("ok")) + .get("/setCookieAll", ctx -> { + final HttpCookie httpCookie = new HttpCookie("ac", "v_all"); + httpCookie.setPath("/"); + httpCookie.setHttpOnly(true); + httpCookie.setMaxAge(10_000); + ctx.cookie(httpCookie).text("ok"); + }) + ); + return TestPair.create(app, 9001); + } + + @AfterAll + static void end() { + pair.shutdown(); + } + + @Test + void set_read_readMap_remove_readMap_remove_readMap() { + HttpResponse res = pair.request().path("removeCookie").path("ac").GET().asString(); + assertThat(res.body()).isEqualTo("ok"); + + res = pair.request().path("setCookie").GET().asString(); + assertThat(res.statusCode()).isEqualTo(200); + + res = pair.request().path("readCookie").path("ck").GET().asString(); + assertThat(res.body()).isEqualTo("readCookie:val"); + + res = pair.request().path("readCookie").path("ck2").GET().asString(); + assertThat(res.body()).isEqualTo("readCookie:val2"); + + res = pair.request().path("readCookieMap").GET().asString(); + assertThat(res.body()).isEqualTo("cookieMap:{ck=val, ck2=val2}"); + + res = pair.request().path("removeCookie").path("ck").GET().asString(); + assertThat(res.body()).isEqualTo("ok"); + + res = pair.request().path("readCookieMap").GET().asString(); + assertThat(res.body()).isEqualTo("cookieMap:{ck2=val2}"); + + res = pair.request().path("removeCookie").path("ck2").GET().asString(); + assertThat(res.body()).isEqualTo("ok"); + + res = pair.request().path("readCookieMap").GET().asString(); + assertThat(res.body()).isEqualTo("cookieMap:{}"); + } + + @Test + void setAll() { + HttpResponse res = pair.request().path("setCookieAll").GET().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + + res = pair.request().path("readCookieMap").GET().asString(); + assertThat(res.body()).isEqualTo("cookieMap:{ac=v_all}"); + + res = pair.request().path("readCookie").path("ac").GET().asString(); + assertThat(res.body()).isEqualTo("readCookie:v_all"); + } + +} diff --git a/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/TestPair.java b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/TestPair.java index 087a1d3d..59d7c285 100644 --- a/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/TestPair.java +++ b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/TestPair.java @@ -5,6 +5,7 @@ import io.avaje.http.client.JacksonBodyAdapter; import io.avaje.jex.Jex; +import java.time.Duration; import java.util.Random; /** @@ -44,14 +45,19 @@ public String url() { * Create a Server and Client pair for a given set of tests. */ public static TestPair create(Jex app) { - int port = 10000 + new Random().nextInt(1000); + return create(app, port); + } + + public static TestPair create(Jex app, int port) { + var jexServer = app.port(port).start(); var url = "http://localhost:" + port; var client = HttpClientContext.newBuilder() .baseUrl(url) .bodyAdapter(new JacksonBodyAdapter()) + .requestTimeout(Duration.ofMinutes(2)) .build(); return new TestPair(port, jexServer, client); From 1db52f0a1cfd12694bee523de8c4d0b687bb6acb Mon Sep 17 00:00:00 2001 From: rbygrave Date: Mon, 12 Jul 2021 21:38:03 +1200 Subject: [PATCH 19/27] Add Context.Cookie with better expires property and building (and server Set-Cookie focused) --- .../java/io/avaje/jex/jdk/JdkContext.java | 55 ++----- .../io/avaje/jex/jdk/CookieServerTest.java | 9 +- .../io/avaje/jex/jetty/JexHttpContext.java | 37 +++-- .../io/avaje/jex/base/ContextCookieTest.java | 12 +- .../src/main/java/io/avaje/jex/Context.java | 142 +++++++++++++++++- .../test/java/io/avaje/jex/CookieTest.java | 26 ++++ 6 files changed, 207 insertions(+), 74 deletions(-) create mode 100644 avaje-jex/src/test/java/io/avaje/jex/CookieTest.java diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java index e0cf2d5f..8ba2a668 100644 --- a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java @@ -14,10 +14,10 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.UncheckedIOException; -import java.net.HttpCookie; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; @@ -32,7 +32,7 @@ class JdkContext implements Context, SpiContext { private static final String EXPIRE_COOKIE = "; Expires=Sat, 01 Jan 2000 00:00:00 GMT"; private static final String UTF8 = "UTF8"; private static final int SC_MOVED_TEMPORARILY = 302; - private static final String SET_COOKIE2 = "Set-Cookie"; + private static final String SET_COOKIE = "Set-Cookie"; private static final String COOKIE = "Cookie"; private final ServiceManager mgr; private final String path; @@ -109,63 +109,32 @@ public Map cookieMap() { } @Override - public Context cookie(HttpCookie cookie) { - header(SET_COOKIE2, toCookieHeader(cookie)); + public Context cookie(Cookie cookie) { + header(SET_COOKIE, cookie.toString()); return this; } @Override - public Context cookie(String name, String value, int maxAge) { - HttpCookie cookie = new HttpCookie(name, value); - cookie.setMaxAge(maxAge); - cookie.setPath("/"); - header(SET_COOKIE2, toCookieHeader(cookie)); + public Context cookie(String name, String value) { + header(SET_COOKIE, Cookie.of(name, value).toString()); return this; } - private String toCookieHeader(HttpCookie cookie) { - return toCookieHeader(cookie, null); - } - private String toCookieHeader(HttpCookie cookie, String forceExpire) { - StringBuilder sb = new StringBuilder(100); - sb.append(cookie.getName()).append("=").append(cookie.getValue()).append("; Path="); - if (cookie.getPath() == null) { - sb.append("/"); - } else { - sb.append(cookie.getPath()); - } - if (cookie.getDomain() != null) { - sb.append("; Domain=").append(cookie.getDomain()); - } - if (forceExpire != null) { - sb.append(forceExpire); - } - final long maxAge = cookie.getMaxAge(); - if (maxAge > 1) { - sb.append("; Max-Age=").append(maxAge); - } - if (cookie.getSecure()) { - sb.append("; Secure"); - } - if (cookie.isHttpOnly()) { - sb.append("; HttpOnly"); - } - return sb.toString(); + @Override + public Context cookie(String name, String value, int maxAge) { + header(SET_COOKIE, Cookie.of(name, value).maxAge(Duration.ofSeconds(maxAge)).toString()); + return this; } - @Override public Context removeCookie(String name) { - HttpCookie cookie = new HttpCookie(name, ""); - header(SET_COOKIE2, toCookieHeader(cookie, EXPIRE_COOKIE)); + header(SET_COOKIE, Cookie.expired(name).path("/").toString()); return this; } @Override public Context removeCookie(String name, String path) { - HttpCookie cookie = new HttpCookie(name, ""); - cookie.setPath(path); - header(SET_COOKIE2, toCookieHeader(cookie, EXPIRE_COOKIE)); + header(SET_COOKIE, Cookie.expired(name).path(path).toString()); return this; } diff --git a/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/CookieServerTest.java b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/CookieServerTest.java index ecb12b34..20a4610f 100644 --- a/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/CookieServerTest.java +++ b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/CookieServerTest.java @@ -1,11 +1,12 @@ package io.avaje.jex.jdk; +import io.avaje.jex.Context; import io.avaje.jex.Jex; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; -import java.net.HttpCookie; import java.net.http.HttpResponse; +import java.time.Duration; import static org.assertj.core.api.Assertions.assertThat; @@ -21,10 +22,8 @@ static TestPair init() { .get("/readCookieMap", ctx -> ctx.text("cookieMap:" + ctx.cookieMap())) .get("/removeCookie/{name}", ctx -> ctx.removeCookie(ctx.pathParam("name")).text("ok")) .get("/setCookieAll", ctx -> { - final HttpCookie httpCookie = new HttpCookie("ac", "v_all"); - httpCookie.setPath("/"); - httpCookie.setHttpOnly(true); - httpCookie.setMaxAge(10_000); + final Context.Cookie httpCookie = Context.Cookie.of("ac", "v_all") + .path("/").httpOnly(true).maxAge(Duration.ofSeconds(10_000)); ctx.cookie(httpCookie).text("ok"); }) ); diff --git a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java index 4c2ca75d..f3bee8f3 100644 --- a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java +++ b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java @@ -7,7 +7,6 @@ import io.avaje.jex.spi.HeaderKeys; import io.avaje.jex.spi.SpiContext; import io.avaje.jex.spi.SpiRoutes; -import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; @@ -16,8 +15,8 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.UncheckedIOException; -import java.net.HttpCookie; import java.nio.charset.Charset; +import java.time.Duration; import java.util.*; import java.util.stream.Stream; @@ -99,9 +98,9 @@ public Map attributeMap() { @Override public String cookie(String name) { - final Cookie[] cookies = req.getCookies(); + final jakarta.servlet.http.Cookie[] cookies = req.getCookies(); if (cookies != null) { - for (Cookie cookie : cookies) { + for (jakarta.servlet.http.Cookie cookie : cookies) { if (cookie.getName().equals(name)) { return cookie.getValue(); } @@ -112,44 +111,52 @@ public String cookie(String name) { @Override public Map cookieMap() { - final Cookie[] cookies = req.getCookies(); + final jakarta.servlet.http.Cookie[] cookies = req.getCookies(); if (cookies == null) { return emptyMap(); } final Map map = new LinkedHashMap<>(); - for (Cookie cookie : cookies) { + for (jakarta.servlet.http.Cookie cookie : cookies) { map.put(cookie.getName(), cookie.getValue()); } return map; } @Override - public Context cookie(HttpCookie cookie) { - final Cookie newCookie = new Cookie(cookie.getName(), cookie.getValue()); - newCookie.setPath(cookie.getPath()); + public Context cookie(Cookie cookie) { + final jakarta.servlet.http.Cookie newCookie = new jakarta.servlet.http.Cookie(cookie.name(), cookie.value()); + newCookie.setPath(cookie.path()); if (newCookie.getPath() == null) { newCookie.setPath("/"); } - final String domain = cookie.getDomain(); + final String domain = cookie.domain(); if (domain != null) { newCookie.setDomain(domain); } - newCookie.setMaxAge((int) cookie.getMaxAge()); - newCookie.setHttpOnly(cookie.isHttpOnly()); - newCookie.setSecure(cookie.getSecure()); + final Duration duration = cookie.maxAge(); + if (duration != null) { + newCookie.setMaxAge((int)duration.toSeconds()); + } + newCookie.setHttpOnly(cookie.httpOnly()); + newCookie.setSecure(cookie.secure()); res.addCookie(newCookie); return this; } @Override public Context cookie(String name, String value, int maxAge) { - final Cookie cookie = new Cookie(name, value); + final jakarta.servlet.http.Cookie cookie = new jakarta.servlet.http.Cookie(name, value); cookie.setPath("/"); cookie.setMaxAge(maxAge); res.addCookie(cookie); return this; } + @Override + public Context cookie(String name, String value) { + return cookie(name, value, -1); + } + @Override public Context removeCookie(String name) { return removeCookie(name, null); @@ -160,7 +167,7 @@ public Context removeCookie(String name, String path) { if (path == null) { path = "/"; } - final Cookie cookie = new Cookie(name, ""); + final jakarta.servlet.http.Cookie cookie = new jakarta.servlet.http.Cookie(name, ""); cookie.setPath(path); cookie.setMaxAge(0); res.addCookie(cookie); diff --git a/avaje-jex-jetty/src/test/java/io/avaje/jex/base/ContextCookieTest.java b/avaje-jex-jetty/src/test/java/io/avaje/jex/base/ContextCookieTest.java index d0dc2839..a01e72f1 100644 --- a/avaje-jex-jetty/src/test/java/io/avaje/jex/base/ContextCookieTest.java +++ b/avaje-jex-jetty/src/test/java/io/avaje/jex/base/ContextCookieTest.java @@ -1,11 +1,13 @@ package io.avaje.jex.base; +import io.avaje.jex.Context; import io.avaje.jex.Jex; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; -import java.net.HttpCookie; import java.net.http.HttpResponse; +import java.time.Duration; +import java.time.temporal.ChronoUnit; import static org.assertj.core.api.Assertions.assertThat; @@ -21,11 +23,9 @@ static TestPair init() { .get("/readCookieMap", ctx -> ctx.text("cookieMap:" + ctx.cookieMap())) .get("/removeCookie/{name}", ctx -> ctx.removeCookie(ctx.pathParam("name")).text("ok")) .get("/setCookieAll", ctx -> { - final HttpCookie httpCookie = new HttpCookie("ac", "v_all"); - httpCookie.setPath("/"); - httpCookie.setHttpOnly(true); - httpCookie.setMaxAge(10_000); - ctx.cookie(httpCookie); + final Context.Cookie cookie = Context.Cookie.of("ac", "v_all") + .path("/").httpOnly(true).maxAge(Duration.of(10, ChronoUnit.DAYS)); + ctx.cookie(cookie); }) ); return TestPair.create(app, 9001); diff --git a/avaje-jex/src/main/java/io/avaje/jex/Context.java b/avaje-jex/src/main/java/io/avaje/jex/Context.java index 37f8dc26..a2109b11 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/Context.java +++ b/avaje-jex/src/main/java/io/avaje/jex/Context.java @@ -2,7 +2,11 @@ import io.avaje.jex.spi.HeaderKeys; -import java.net.HttpCookie; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -54,9 +58,7 @@ public interface Context { /** * Sets a cookie with name, value with unlimited age. */ - default Context cookie(String name, String value) { - return cookie(name, value, -1); - } + Context cookie(String name, String value); /** * Sets a cookie with name, value, and max-age. @@ -66,7 +68,7 @@ default Context cookie(String name, String value) { /** * Sets a Cookie. */ - Context cookie(HttpCookie cookie); + Context cookie(Cookie cookie); /** * Remove a cookie by name. @@ -385,4 +387,134 @@ default Context render(String name) { */ List uploadedFiles(); + class Cookie { + private static final ZonedDateTime EXPIRED = ZonedDateTime.of(LocalDateTime.of(2000, 1, 1, 0, 0, 0), ZoneId.of("GMT")); + private static final DateTimeFormatter RFC_1123_DATE_TIME = DateTimeFormatter.RFC_1123_DATE_TIME; + private static final String PARAM_SEPARATOR = "; "; + private final String name; // NAME= ... "$Name" style is reserved + private final String value; // value of NAME + private String domain; // ;Domain=VALUE ... domain that sees cookie + private ZonedDateTime expires; + private Duration maxAge;// = -1; // ;Max-Age=VALUE ... cookies auto-expire + private String path; // ;Path=VALUE ... URLs that see the cookie + private boolean secure; // ;Secure ... e.g. use SSL + private boolean httpOnly; + + private Cookie(String name, String value) { + if (name == null || name.length() == 0) { + throw new IllegalArgumentException("name required"); + } + this.name = name; + this.value = value; + } + + public static Cookie expired(String name) { + return new Cookie(name, "").expires(EXPIRED); + } + + public static Cookie of(String name, String value) { + return new Cookie(name, value); + } + + public String name() { + return name; + } + + public String value() { + return value; + } + + public String domain() { + return domain; + } + + public Cookie domain(String domain) { + this.domain = domain; + return this; + } + + public Duration maxAge() { + return maxAge; + } + + public Cookie maxAge(Duration maxAge) { + this.maxAge = maxAge; + return this; + } + + public ZonedDateTime expires() { + return expires; + } + + public Cookie expires(ZonedDateTime expires) { + this.expires = expires; + return this; + } + + public String path() { + return path; + } + + public Cookie path(String path) { + this.path = path; + return this; + } + + public boolean secure() { + return secure; + } + + public Cookie secure(boolean secure) { + this.secure = secure; + return this; + } + + public boolean httpOnly() { + return httpOnly; + } + + public Cookie httpOnly(boolean httpOnly) { + this.httpOnly = httpOnly; + return this; + } + + /** + * Returns content of this instance as a 'Set-Cookie:' header value specified + * by RFC6265. + */ + @Override + public String toString() { + StringBuilder result = new StringBuilder(60); + result.append(name).append('=').append(value); + if (expires != null) { + result.append(PARAM_SEPARATOR); + result.append("Expires="); + result.append(expires.format(RFC_1123_DATE_TIME)); + } + if ((maxAge != null) && !maxAge.isNegative() && !maxAge.isZero()) { + result.append(PARAM_SEPARATOR); + result.append("Max-Age="); + result.append(maxAge.getSeconds()); + } + if (domain != null) { + result.append(PARAM_SEPARATOR); + result.append("Domain="); + result.append(domain); + } + if (path != null) { + result.append(PARAM_SEPARATOR); + result.append("Path="); + result.append(path); + } + if (secure) { + result.append(PARAM_SEPARATOR); + result.append("Secure"); + } + if (httpOnly) { + result.append(PARAM_SEPARATOR); + result.append("HttpOnly"); + } + return result.toString(); + } + } } diff --git a/avaje-jex/src/test/java/io/avaje/jex/CookieTest.java b/avaje-jex/src/test/java/io/avaje/jex/CookieTest.java new file mode 100644 index 00000000..03f2dff9 --- /dev/null +++ b/avaje-jex/src/test/java/io/avaje/jex/CookieTest.java @@ -0,0 +1,26 @@ +package io.avaje.jex; + +import io.avaje.jex.Context.Cookie; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class CookieTest { + + @Test + void format() { + assertEquals("key=val", Cookie.of("key", "val").toString()); + assertEquals("key=val; Domain=dom", Cookie.of("key", "val").domain("dom").toString()); + assertEquals("key=val; Path=/pt", Cookie.of("key", "val").path("/pt").toString()); + //assertEquals("key=val; Path=/; Max-Age=10", Cookie.of("key", "val").maxAge(10).format()); + assertEquals("key=val; Secure", Cookie.of("key", "val").secure(true).toString()); + assertEquals("key=val; HttpOnly", Cookie.of("key", "val").httpOnly(true).toString()); + assertEquals("key=val; Secure; HttpOnly", Cookie.of("key", "val").httpOnly(true).secure(true).toString()); + } + + @Test + void format_all() { + assertEquals("key=val; Domain=dom; Path=/pt; Secure; HttpOnly", Cookie.of("key", "val") + .domain("dom").path("/pt").secure(true).httpOnly(true).toString()); + } +} From 90be9e853773c828d20d72aaea72e327eafccd5e Mon Sep 17 00:00:00 2001 From: rbygrave Date: Mon, 12 Jul 2021 22:08:27 +1200 Subject: [PATCH 20/27] Tidy JdkContext --- avaje-jex-jdk/pom.xml | 4 +- .../java/io/avaje/jex/jdk/JdkContext.java | 59 ++++++++++--------- 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/avaje-jex-jdk/pom.xml b/avaje-jex-jdk/pom.xml index 49baab4e..3f40d697 100644 --- a/avaje-jex-jdk/pom.xml +++ b/avaje-jex-jdk/pom.xml @@ -12,8 +12,8 @@ avaje-jex-jdk - 17 - 17 + 11 + 11 diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java index 8ba2a668..28f4c744 100644 --- a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java @@ -16,6 +16,7 @@ import java.io.UncheckedIOException; import java.net.InetAddress; import java.net.InetSocketAddress; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Iterator; @@ -29,7 +30,6 @@ class JdkContext implements Context, SpiContext { - private static final String EXPIRE_COOKIE = "; Expires=Sat, 01 Jan 2000 00:00:00 GMT"; private static final String UTF8 = "UTF8"; private static final int SC_MOVED_TEMPORARILY = 302; private static final String SET_COOKIE = "Set-Cookie"; @@ -43,6 +43,7 @@ class JdkContext implements Context, SpiContext { private Map> queryParams; private Map cookieMap; private int statusCode; + private String characterEncoding; JdkContext(ServiceManager mgr, HttpExchange exchange, String path, SpiRoutes.Params params) { this.mgr = mgr; @@ -61,21 +62,6 @@ class JdkContext implements Context, SpiContext { this.params = null; } - private Map internalCookieMap() { - if (cookieMap == null) { - cookieMap = parseCookies(); - } - return cookieMap; - } - - private Map parseCookies() { - final String cookieHeader = header(exchange.getRequestHeaders(), COOKIE); - if (cookieHeader == null || cookieHeader.isEmpty()) { - return emptyMap(); - } - return CookieParser.parse(cookieHeader); - } - @Override public String matchedPath() { return path; @@ -98,14 +84,25 @@ public Map attributeMap() { throw new UnsupportedOperationException(); } - @Override - public String cookie(String name) { - return internalCookieMap().get(name); + private Map parseCookies() { + final String cookieHeader = header(exchange.getRequestHeaders(), COOKIE); + if (cookieHeader == null || cookieHeader.isEmpty()) { + return emptyMap(); + } + return CookieParser.parse(cookieHeader); } @Override public Map cookieMap() { - return internalCookieMap(); + if (cookieMap == null) { + cookieMap = parseCookies(); + } + return cookieMap; + } + + @Override + public String cookie(String name) { + return cookieMap().get(name); } @Override @@ -178,9 +175,16 @@ public byte[] bodyAsBytes() { } } + private String characterEncoding() { + if (characterEncoding == null) { + characterEncoding = mgr.requestCharset(this); + } + return characterEncoding; + } + @Override public String body() { - return new String(bodyAsBytes(), StandardCharsets.UTF_8); + return new String(bodyAsBytes(), Charset.forName(characterEncoding())); } @Override @@ -279,8 +283,7 @@ public Map> formParamMap() { } private Map> initFormParamMap() { - final String charset = mgr.requestCharset(this); - return mgr.formParamMap(this, charset); + return mgr.formParamMap(this, characterEncoding()); } @Override @@ -290,24 +293,22 @@ public String scheme() { @Override public Context sessionAttribute(String key, Object value) { - return null; + throw new UnsupportedOperationException(); } @Override public T sessionAttribute(String key) { - return null; + throw new UnsupportedOperationException(); } @Override public Map sessionAttributeMap() { - return null; + throw new UnsupportedOperationException(); } @Override public String url() { - StringBuffer url = new StringBuffer(128); - url.append(scheme()).append("://").append(host()).append(path); - return url.toString(); + return scheme() + "://" + host() + path; } @Override From b0ed5903adac1af1495c6107e8da1ca803c985bb Mon Sep 17 00:00:00 2001 From: rbygrave Date: Tue, 13 Jul 2021 23:01:03 +1200 Subject: [PATCH 21/27] Add AppLifecycle and HealthPlugin --- avaje-jex-jdk/pom.xml | 4 +- .../java/io/avaje/jex/jdk/BaseHandler.java | 56 ++++++++--- .../java/io/avaje/jex/jdk/JdkJexServer.java | 18 +++- .../java/io/avaje/jex/jdk/JdkServerStart.java | 19 +++- avaje-jex-jdk/src/main/java/module-info.java | 6 +- .../services/io.avaje.jex.spi.SpiStartServer | 1 + .../io/avaje/jex/jdk/HealthPluginTest.java | 99 +++++++++++++++++++ .../src/test/java/io/avaje/jex/jdk/Main.java | 18 ++++ .../src/test/resources/logback-test.xml | 2 +- .../services/io.avaje.jex.spi.SpiStartServer | 1 + avaje-jex-mustache/pom.xml | 13 +++ avaje-jex/pom.xml | 7 ++ .../main/java/io/avaje/jex/AppLifecycle.java | 56 +++++++++++ .../java/io/avaje/jex/DefaultLifecycle.java | 89 +++++++++++++++++ avaje-jex/src/main/java/io/avaje/jex/Jex.java | 36 ++++++- .../src/main/java/io/avaje/jex/Plugin.java | 12 +++ .../java/io/avaje/jex/core/HealthPlugin.java | 38 +++++++ .../java/io/avaje/jex/routes/FilterEntry.java | 16 +++ .../java/io/avaje/jex/routes/RouteEntry.java | 18 ++++ .../java/io/avaje/jex/routes/RouteIndex.java | 25 ++++- .../main/java/io/avaje/jex/routes/Routes.java | 22 +++++ .../main/java/io/avaje/jex/spi/SpiRoutes.java | 30 ++++++ avaje-jex/src/main/java/module-info.java | 3 +- .../io.avaje.jex.spi.SpiRoutesProvider | 1 + ...io.avaje.jex.spi.SpiServiceManagerProvider | 1 + examples/example-jex-jdk/logback.xml | 19 ++++ examples/example-jex-jdk/pom.xml | 81 +++++++++++++++ .../src/main/java/org/example/HelloDto.java | 7 ++ .../src/main/java/org/example/Main.java | 53 ++++++++++ .../example-jex-jdk/src/main/module-info.java | 5 + .../src/main/resources/content/basic.html | 1 + .../src/main/resources/content/index.html | 1 + .../src/main/resources/content/plain-file.txt | 1 + .../src/main/resources/logback.xml | 19 ++++ .../src/test/resources/logback-test.xml | 19 ++++ examples/pom.xml | 21 ++++ pom.xml | 2 +- 37 files changed, 785 insertions(+), 35 deletions(-) create mode 100644 avaje-jex-jdk/src/main/resources/META-INF/services/io.avaje.jex.spi.SpiStartServer create mode 100644 avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/HealthPluginTest.java create mode 100644 avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/Main.java create mode 100644 avaje-jex-jetty/src/main/resources/META-INF/services/io.avaje.jex.spi.SpiStartServer create mode 100644 avaje-jex/src/main/java/io/avaje/jex/AppLifecycle.java create mode 100644 avaje-jex/src/main/java/io/avaje/jex/DefaultLifecycle.java create mode 100644 avaje-jex/src/main/java/io/avaje/jex/Plugin.java create mode 100644 avaje-jex/src/main/java/io/avaje/jex/core/HealthPlugin.java create mode 100644 avaje-jex/src/main/resources/META-INF/services/io.avaje.jex.spi.SpiRoutesProvider create mode 100644 avaje-jex/src/main/resources/META-INF/services/io.avaje.jex.spi.SpiServiceManagerProvider create mode 100644 examples/example-jex-jdk/logback.xml create mode 100644 examples/example-jex-jdk/pom.xml create mode 100644 examples/example-jex-jdk/src/main/java/org/example/HelloDto.java create mode 100644 examples/example-jex-jdk/src/main/java/org/example/Main.java create mode 100644 examples/example-jex-jdk/src/main/module-info.java create mode 100644 examples/example-jex-jdk/src/main/resources/content/basic.html create mode 100644 examples/example-jex-jdk/src/main/resources/content/index.html create mode 100644 examples/example-jex-jdk/src/main/resources/content/plain-file.txt create mode 100644 examples/example-jex-jdk/src/main/resources/logback.xml create mode 100644 examples/example-jex-jdk/src/test/resources/logback-test.xml create mode 100644 examples/pom.xml diff --git a/avaje-jex-jdk/pom.xml b/avaje-jex-jdk/pom.xml index 3f40d697..f3b8c3ac 100644 --- a/avaje-jex-jdk/pom.xml +++ b/avaje-jex-jdk/pom.xml @@ -12,8 +12,8 @@ avaje-jex-jdk - 11 - 11 + 11 + 11 diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/BaseHandler.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/BaseHandler.java index 0281fd75..fcbf7c6e 100644 --- a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/BaseHandler.java +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/BaseHandler.java @@ -7,45 +7,71 @@ import io.avaje.jex.http.NotFoundResponse; import io.avaje.jex.spi.SpiContext; import io.avaje.jex.spi.SpiRoutes; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.net.URI; +import java.util.concurrent.locks.LockSupport; class BaseHandler implements HttpHandler { - final SpiRoutes routes; - final ServiceManager mgr; + private static final Logger log = LoggerFactory.getLogger(BaseHandler.class); + + private final SpiRoutes routes; + private final ServiceManager mgr; BaseHandler(SpiRoutes routes, ServiceManager mgr) { this.mgr = mgr; this.routes = routes; } + /** + * Wait based routes.activeRequests(). + */ + void waitForIdle(long maxSeconds) { + final long maxAttempts = maxSeconds * 10; // 100 millis per attempt + long attempts = 0; + while ((routes.activeRequests()) > 0 && ++attempts < maxAttempts) { + LockSupport.parkNanos(100_000_000); + } + if (attempts >= maxAttempts) { + final long active = routes.activeRequests(); + if (active > 0) { + log.warn("Active requests since in process - count:{}", active); + } + } + } + @Override - public void handle(HttpExchange exchange) throws IOException { + public void handle(HttpExchange exchange) { - final String requestMethod = exchange.getRequestMethod(); - final URI requestURI = exchange.getRequestURI(); - final String uri = requestURI.getPath(); - final Routing.Type routeType = mgr.lookupRoutingType(requestMethod); + final String uri = exchange.getRequestURI().getPath(); + final Routing.Type routeType = mgr.lookupRoutingType(exchange.getRequestMethod()); final SpiRoutes.Entry route = routes.match(routeType, uri); if (route == null) { var ctx = new JdkContext(mgr, exchange, uri); + routes.inc(); try { processNoRoute(ctx, uri, routeType); routes.after(uri, ctx); } catch (Exception e) { handleException(ctx, e); + } finally { + routes.dec(); } } else { - final SpiRoutes.Params params = route.pathParams(uri); - JdkContext ctx = new JdkContext(mgr, exchange, route.matchPath(), params); + route.inc(); try { - processRoute(ctx, uri, route); - routes.after(uri, ctx); - } catch (Exception e) { - handleException(ctx, e); + final SpiRoutes.Params params = route.pathParams(uri); + JdkContext ctx = new JdkContext(mgr, exchange, route.matchPath(), params); + try { + processRoute(ctx, uri, route); + routes.after(uri, ctx); + } catch (Exception e) { + handleException(ctx, e); + } + } finally { + route.dec(); } } } diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkJexServer.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkJexServer.java index 714ea94a..48c6a1d8 100644 --- a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkJexServer.java +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkJexServer.java @@ -1,18 +1,34 @@ package io.avaje.jex.jdk; import com.sun.net.httpserver.HttpServer; +import io.avaje.jex.AppLifecycle; import io.avaje.jex.Jex; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; class JdkJexServer implements Jex.Server { + private static final Logger log = LoggerFactory.getLogger(JdkJexServer.class); + private final HttpServer server; + private final AppLifecycle lifecycle; + private final BaseHandler handler; - JdkJexServer(HttpServer server) { + JdkJexServer(HttpServer server, AppLifecycle lifecycle, BaseHandler handler) { this.server = server; + this.lifecycle = lifecycle; + this.handler = handler; + lifecycle.registerShutdownHook(this::shutdown); } @Override public void shutdown() { + lifecycle.status(AppLifecycle.Status.STOPPING); + int maxWaitSeconds = 20; + log.debug("stopping server with maxWaitSeconds {}", maxWaitSeconds); + handler.waitForIdle(maxWaitSeconds); server.stop(0); + log.info("shutdown"); + lifecycle.status(AppLifecycle.Status.STOPPED); } } diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServerStart.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServerStart.java index 9639400e..4e3bbe5b 100644 --- a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServerStart.java +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkServerStart.java @@ -1,31 +1,40 @@ package io.avaje.jex.jdk; -import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; +import io.avaje.jex.AppLifecycle; import io.avaje.jex.Jex; import io.avaje.jex.spi.SpiRoutes; import io.avaje.jex.spi.SpiServiceManager; import io.avaje.jex.spi.SpiStartServer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.InetSocketAddress; +import java.util.concurrent.Executor; public class JdkServerStart implements SpiStartServer { + private static final Logger log = LoggerFactory.getLogger(JdkServerStart.class); + @Override public Jex.Server start(Jex jex, SpiRoutes routes, SpiServiceManager serviceManager) { final ServiceManager manager = new ServiceManager(serviceManager, "http", ""); - HttpHandler handler = new BaseHandler(routes, manager); + BaseHandler handler = new BaseHandler(routes, manager); try { final HttpServer server = HttpServer.create(); server.createContext("/", handler); - + final Executor executor = jex.attribute(Executor.class); + if (executor != null) { + server.setExecutor(executor); + } int port = jex.inner.port; + log.debug("starting server on port {}", port); server.bind(new InetSocketAddress(port), 0); server.start(); - - return new JdkJexServer(server); + jex.lifecycle().status(AppLifecycle.Status.STARTED); + return new JdkJexServer(server, jex.lifecycle(), handler); } catch (IOException e) { throw new RuntimeException(e); diff --git a/avaje-jex-jdk/src/main/java/module-info.java b/avaje-jex-jdk/src/main/java/module-info.java index 1424e24c..c34c352f 100644 --- a/avaje-jex-jdk/src/main/java/module-info.java +++ b/avaje-jex-jdk/src/main/java/module-info.java @@ -4,8 +4,8 @@ module io.avaje.jex.jdk { requires transitive io.avaje.jex; - requires java.net.http; - requires jdk.httpserver; - + requires transitive java.net.http; + requires transitive jdk.httpserver; + requires transitive org.slf4j; provides SpiStartServer with JdkServerStart; } diff --git a/avaje-jex-jdk/src/main/resources/META-INF/services/io.avaje.jex.spi.SpiStartServer b/avaje-jex-jdk/src/main/resources/META-INF/services/io.avaje.jex.spi.SpiStartServer new file mode 100644 index 00000000..79199ebe --- /dev/null +++ b/avaje-jex-jdk/src/main/resources/META-INF/services/io.avaje.jex.spi.SpiStartServer @@ -0,0 +1 @@ +io.avaje.jex.jdk.JdkServerStart diff --git a/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/HealthPluginTest.java b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/HealthPluginTest.java new file mode 100644 index 00000000..d05d1876 --- /dev/null +++ b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/HealthPluginTest.java @@ -0,0 +1,99 @@ +package io.avaje.jex.jdk; + +import io.avaje.http.client.HttpClientContext; +import io.avaje.http.client.JacksonBodyAdapter; +import io.avaje.jex.AppLifecycle; +import io.avaje.jex.Jex; +import io.avaje.jex.core.HealthPlugin; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpResponse; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Random; + +import static org.assertj.core.api.Assertions.assertThat; + +class HealthPluginTest { + + static final int port = new Random().nextInt(1000) + 10_000; + static Jex jex; + static Jex.Server server; + static HttpClientContext client; + + @BeforeAll + static void setup() { + jex = Jex.create() + .routing(routing -> routing + .get("/", ctx -> { + final String one = ctx.header("one"); + Map obj = new LinkedHashMap<>(); + obj.put("one", one); + ctx.json(obj); + }) + ) + .configure(new HealthPlugin()) + .port(port); + + server = jex.start(); + client = HttpClientContext.newBuilder() + .baseUrl("http://localhost:"+port) + .bodyAdapter(new JacksonBodyAdapter()) + .build(); + } + + @AfterAll + static void end() { + server.shutdown(); + } + + @Test + void get() { + final HttpResponse hres = client.request() + .header("one", "hello") + .GET().asString(); + assertThat(hres.statusCode()).isEqualTo(200); + } + + @Test + void healthLiveness() { + assertThat(ready("health/liveness").statusCode()).isEqualTo(200); + } + + @Test + void healthLiveness_various() { + jex.lifecycle().status(AppLifecycle.Status.STOPPED); + assertThat(ready("health/liveness").statusCode()).isEqualTo(500); + jex.lifecycle().status(AppLifecycle.Status.STOPPING); + assertThat(ready("health/liveness").statusCode()).isEqualTo(500); + + jex.lifecycle().status(AppLifecycle.Status.STARTING); + assertThat(ready("health/liveness").statusCode()).isEqualTo(200); + jex.lifecycle().status(AppLifecycle.Status.STARTED); + assertThat(ready("health/liveness").statusCode()).isEqualTo(200); + } + + @Test + void healthReadiness() { + assertThat(ready("health/readiness").statusCode()).isEqualTo(200); + } + + @Test + void healthReadiness_various() { + jex.lifecycle().status(AppLifecycle.Status.STOPPING); + assertThat(ready("health/readiness").statusCode()).isEqualTo(500); + jex.lifecycle().status(AppLifecycle.Status.STOPPED); + assertThat(ready("health/readiness").statusCode()).isEqualTo(500); + jex.lifecycle().status(AppLifecycle.Status.STARTING); + assertThat(ready("health/readiness").statusCode()).isEqualTo(500); + jex.lifecycle().status(AppLifecycle.Status.STARTED); + assertThat(ready("health/readiness").statusCode()).isEqualTo(200); + } + + private HttpResponse ready(String s) { + return client.request().path(s) + .GET().asString(); + } +} diff --git a/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/Main.java b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/Main.java new file mode 100644 index 00000000..d1f8068d --- /dev/null +++ b/avaje-jex-jdk/src/test/java/io/avaje/jex/jdk/Main.java @@ -0,0 +1,18 @@ +package io.avaje.jex.jdk; + +import io.avaje.jex.Jex; +import io.avaje.jex.core.HealthPlugin; + +public class Main { + + public static void main(String[] args) { + + Jex.create() + .routing(routing -> routing + .get("/", ctx -> ctx.text("hello world")) + ) + .configure(new HealthPlugin()) + .port(9009) + .start(); + } +} diff --git a/avaje-jex-jdk/src/test/resources/logback-test.xml b/avaje-jex-jdk/src/test/resources/logback-test.xml index ddb21350..5e5a132a 100644 --- a/avaje-jex-jdk/src/test/resources/logback-test.xml +++ b/avaje-jex-jdk/src/test/resources/logback-test.xml @@ -12,7 +12,7 @@ - + diff --git a/avaje-jex-jetty/src/main/resources/META-INF/services/io.avaje.jex.spi.SpiStartServer b/avaje-jex-jetty/src/main/resources/META-INF/services/io.avaje.jex.spi.SpiStartServer new file mode 100644 index 00000000..8adaab64 --- /dev/null +++ b/avaje-jex-jetty/src/main/resources/META-INF/services/io.avaje.jex.spi.SpiStartServer @@ -0,0 +1 @@ +io.avaje.jex.jetty.JettyStartServer diff --git a/avaje-jex-mustache/pom.xml b/avaje-jex-mustache/pom.xml index 5fce6132..4c07054c 100644 --- a/avaje-jex-mustache/pom.xml +++ b/avaje-jex-mustache/pom.xml @@ -44,4 +44,17 @@ + + + + maven-surefire-plugin + + + --add-modules io.avaje.jex.jetty + + + + + + diff --git a/avaje-jex/pom.xml b/avaje-jex/pom.xml index a40cedcd..db4f2a35 100644 --- a/avaje-jex/pom.xml +++ b/avaje-jex/pom.xml @@ -20,6 +20,13 @@ 1.7.30 + + com.fasterxml.jackson.core + jackson-databind + 2.12.3 + true + + org.eclipse.jetty.toolchain jetty-jakarta-servlet-api diff --git a/avaje-jex/src/main/java/io/avaje/jex/AppLifecycle.java b/avaje-jex/src/main/java/io/avaje/jex/AppLifecycle.java new file mode 100644 index 00000000..8e30a065 --- /dev/null +++ b/avaje-jex/src/main/java/io/avaje/jex/AppLifecycle.java @@ -0,0 +1,56 @@ +package io.avaje.jex; + +/** + * Application lifecycle support. + */ +public interface AppLifecycle { + + enum Status { + STARTING, + STARTED, + STOPPING, + STOPPED + } + + /** + * Register the runnable with the Runtime as a shutdown hook. + */ + void registerShutdownHook(Runnable onShutdown); + + /** + * Return the current status. + */ + Status status(); + + /** + * Set the current status. + */ + void status(Status newStatus); + + /** + * Return true if status starting or started (the server is coming up). + */ + default boolean isAlive() { + Status status = status(); + return status == Status.STARTING || status == Status.STARTED; + } + + /** + * Return true the server has started. + */ + default boolean isReady() { + Status status = status(); + return status == Status.STARTED; + } + +// void register(Listener listener); +// +// interface Listener { +// void onChange(StatusChange change); +// } +// +// interface StatusChange { +// Status newStatus(); +// Status oldStatus(); +// } +} diff --git a/avaje-jex/src/main/java/io/avaje/jex/DefaultLifecycle.java b/avaje-jex/src/main/java/io/avaje/jex/DefaultLifecycle.java new file mode 100644 index 00000000..120cae89 --- /dev/null +++ b/avaje-jex/src/main/java/io/avaje/jex/DefaultLifecycle.java @@ -0,0 +1,89 @@ +package io.avaje.jex; + +import java.util.concurrent.locks.ReentrantLock; + +class DefaultLifecycle implements AppLifecycle { + + //private final List listeners = new ArrayList<>(); + + private final ReentrantLock lock = new ReentrantLock(); + + private Status status = Status.STARTING; + + private Hook hook; + + @Override + public void registerShutdownHook(Runnable onShutdown) { +// final Thread unstarted = Thread.ofVirtual().unstarted(onShutdown); + hook = new Hook(onShutdown); + Runtime.getRuntime().addShutdownHook(hook); + } + +// public void removeShutdownHook() { +// if (hook != null) { +// Runtime.getRuntime().removeShutdownHook(hook); +// } +// } + + static class Hook extends Thread { + Hook(Runnable runnable) { + super(runnable); + } + } + + @Override + public Status status() { + return status; + } + + @Override + public void status(Status newStatus) { + lock.lock(); + try { + if (status != newStatus) { + //Status oldStatus = status; + status = newStatus; + //onChange(new Event(newStatus, oldStatus)); + } + } finally { + lock.unlock(); + } + } + +// @Override +// public void register(Listener listener) { +// lock.lock(); +// try { +// listeners.add(listener); +// } finally { +// lock.unlock(); +// } +// } +// +// private void onChange(Event event) { +// for (Listener listener : listeners) { +// listener.onChange(event); +// } +// } +// +// private static class Event implements AppLifecycle.StatusChange { +// +// final Status newStatus; +// final Status oldStatus; +// +// Event(Status newStatus, Status oldStatus) { +// this.newStatus = newStatus; +// this.oldStatus = oldStatus; +// } +// +// @Override +// public Status newStatus() { +// return newStatus; +// } +// +// @Override +// public Status oldStatus() { +// return oldStatus; +// } +// } +} diff --git a/avaje-jex/src/main/java/io/avaje/jex/Jex.java b/avaje-jex/src/main/java/io/avaje/jex/Jex.java index 120e3142..568e4047 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/Jex.java +++ b/avaje-jex/src/main/java/io/avaje/jex/Jex.java @@ -25,7 +25,9 @@ public class Jex { private final Routing routing = new DefaultRouting(); private final ErrorHandling errorHandling = new DefaultErrorHandling(); + private final AppLifecycle lifecycle = new DefaultLifecycle(); private final StaticFileConfig staticFiles; + private final Map, Object> attributes = new HashMap<>(); public final Inner inner = new Inner(); private ServerConfig serverConfig; @@ -41,6 +43,21 @@ public static Jex create() { return new Jex(); } + /** + * Set a custom attribute that can be used by an implementation. + */ + public Jex attribute(Class cls, T instance) { + attributes.put(cls, instance); + return this; + } + + /** + * Return a custom attribute. + */ + public T attribute(Class cls) { + return (T) attributes.get(cls); + } + public static class Inner { public int port = 7001; public String host; @@ -110,7 +127,7 @@ public Routing routing() { return routing; } - /*** + /** * Set the AccessManager. */ public Jex accessManager(AccessManager accessManager) { @@ -118,7 +135,7 @@ public Jex accessManager(AccessManager accessManager) { return this; } - /*** + /** * Set the JsonService. */ public Jex jsonService(JsonService jsonService) { @@ -126,6 +143,14 @@ public Jex jsonService(JsonService jsonService) { return this; } + /** + * Add Plugin functionality. + */ + public Jex configure(Plugin plugin) { + plugin.apply(this); + return this; + } + /** * Configure via a lambda taking the jex instance. */ @@ -201,6 +226,13 @@ public Server start() { return start.get().start(this, routes, serviceManager); } + /** + * Return the application lifecycle support. + */ + public AppLifecycle lifecycle() { + return lifecycle; + } + /** * The running server. */ diff --git a/avaje-jex/src/main/java/io/avaje/jex/Plugin.java b/avaje-jex/src/main/java/io/avaje/jex/Plugin.java new file mode 100644 index 00000000..a9feb473 --- /dev/null +++ b/avaje-jex/src/main/java/io/avaje/jex/Plugin.java @@ -0,0 +1,12 @@ +package io.avaje.jex; + +/** + * A plugin that can register things like routes, exception handlers etc. + */ +public interface Plugin { + + /** + * Register the plugin features with jex. + */ + void apply(Jex jex); +} diff --git a/avaje-jex/src/main/java/io/avaje/jex/core/HealthPlugin.java b/avaje-jex/src/main/java/io/avaje/jex/core/HealthPlugin.java new file mode 100644 index 00000000..8ea2da11 --- /dev/null +++ b/avaje-jex/src/main/java/io/avaje/jex/core/HealthPlugin.java @@ -0,0 +1,38 @@ +package io.avaje.jex.core; + +import io.avaje.jex.AppLifecycle; +import io.avaje.jex.Context; +import io.avaje.jex.Jex; +import io.avaje.jex.Plugin; + +/** + * Health plugin with liveness and readiness support based on + * the application lifecycle support. + */ +public class HealthPlugin implements Plugin { + + private AppLifecycle lifecycle; + + @Override + public void apply(Jex jex) { + lifecycle = jex.lifecycle(); + jex.routing().get("/health/liveness", this::liveness); + jex.routing().get("/health/readiness", this::readiness); + } + + private void readiness(Context context) { + if (lifecycle.isReady()) { + context.text("ok"); + } else { + context.status(500).text("not-ready"); + } + } + + private void liveness(Context context) { + if (lifecycle.isAlive()) { + context.text("ok"); + } else { + context.status(500).text("not-alive"); + } + } +} diff --git a/avaje-jex/src/main/java/io/avaje/jex/routes/FilterEntry.java b/avaje-jex/src/main/java/io/avaje/jex/routes/FilterEntry.java index b2cbdcae..50f82d4d 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/routes/FilterEntry.java +++ b/avaje-jex/src/main/java/io/avaje/jex/routes/FilterEntry.java @@ -24,6 +24,22 @@ class FilterEntry implements SpiRoutes.Entry { this.handler = entry.getHandler(); } + @Override + public void inc() { + // do nothing + } + + @Override + public void dec() { + // do nothing + } + + @Override + public long activeRequests() { + // always zero for filters + return 0; + } + @Override public String matchPath() { return path; diff --git a/avaje-jex/src/main/java/io/avaje/jex/routes/RouteEntry.java b/avaje-jex/src/main/java/io/avaje/jex/routes/RouteEntry.java index a8d442d8..ef080914 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/routes/RouteEntry.java +++ b/avaje-jex/src/main/java/io/avaje/jex/routes/RouteEntry.java @@ -4,8 +4,11 @@ import io.avaje.jex.Handler; import io.avaje.jex.spi.SpiRoutes; +import java.util.concurrent.atomic.AtomicLong; + class RouteEntry implements SpiRoutes.Entry { + private final AtomicLong active = new AtomicLong(); private final PathParser path; private final Handler handler; @@ -14,6 +17,21 @@ class RouteEntry implements SpiRoutes.Entry { this.handler = handler; } + @Override + public void inc() { + active.incrementAndGet(); + } + + @Override + public void dec() { + active.decrementAndGet(); + } + + @Override + public long activeRequests() { + return active.get(); + } + @Override public boolean matches(String requestUri) { return path.matches(requestUri); diff --git a/avaje-jex/src/main/java/io/avaje/jex/routes/RouteIndex.java b/avaje-jex/src/main/java/io/avaje/jex/routes/RouteIndex.java index aa94aeb9..cbd02b04 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/routes/RouteIndex.java +++ b/avaje-jex/src/main/java/io/avaje/jex/routes/RouteIndex.java @@ -10,16 +10,16 @@ class RouteIndex { /** * Partition entries by the number of path segments. */ - private final Entry[] entries = new Entry[6]; + private final RouteIndex.Entry[] entries = new RouteIndex.Entry[6]; /** * Wildcard/splat based route entries. */ - private List wildcardEntries = new ArrayList<>(); + private final List wildcardEntries = new ArrayList<>(); RouteIndex() { for (int i = 0; i < entries.length; i++) { - entries[i] = new Entry(); + entries[i] = new RouteIndex.Entry(); } } @@ -63,6 +63,17 @@ private int segmentCount(String pathInfo) { return count; } + long activeRequests() { + long total = 0; + for (RouteIndex.Entry entry : entries) { + total += entry.activeRequests(); + } + for (SpiRoutes.Entry entry : wildcardEntries) { + total += entry.activeRequests(); + } + return total; + } + private static class Entry { private final List list = new ArrayList<>(); @@ -79,5 +90,13 @@ SpiRoutes.Entry match(String pathInfo) { } return null; } + + long activeRequests() { + long total = 0; + for (SpiRoutes.Entry entry : list) { + total += entry.activeRequests(); + } + return total; + } } } diff --git a/avaje-jex/src/main/java/io/avaje/jex/routes/Routes.java b/avaje-jex/src/main/java/io/avaje/jex/routes/Routes.java index a640ef1d..d223b50e 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/routes/Routes.java +++ b/avaje-jex/src/main/java/io/avaje/jex/routes/Routes.java @@ -6,6 +6,7 @@ import java.util.EnumMap; import java.util.List; +import java.util.concurrent.atomic.AtomicLong; class Routes implements SpiRoutes { @@ -24,12 +25,33 @@ class Routes implements SpiRoutes { */ private final List after; + private final AtomicLong noRouteCounter = new AtomicLong(); + Routes(EnumMap typeMap, List before, List after) { this.typeMap = typeMap; this.before = before; this.after = after; } + @Override + public void inc() { + noRouteCounter.incrementAndGet(); + } + + @Override + public void dec() { + noRouteCounter.decrementAndGet(); + } + + @Override + public long activeRequests() { + long total = noRouteCounter.get(); + for (RouteIndex value : typeMap.values()) { + total += value.activeRequests(); + } + return total; + } + @Override public Entry match(Routing.Type type, String pathInfo) { return typeMap.get(type).match(pathInfo); diff --git a/avaje-jex/src/main/java/io/avaje/jex/spi/SpiRoutes.java b/avaje-jex/src/main/java/io/avaje/jex/spi/SpiRoutes.java index df059e16..2d2786ea 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/spi/SpiRoutes.java +++ b/avaje-jex/src/main/java/io/avaje/jex/spi/SpiRoutes.java @@ -26,6 +26,21 @@ public interface SpiRoutes { */ void after(String pathInfo, SpiContext ctx); + /** + * Increment active request count for no route match. + */ + void inc(); + + /** + * Decrement active request count for no route match. + */ + void dec(); + + /** + * Return the active request count. + */ + long activeRequests(); + /** * A route entry. */ @@ -60,6 +75,21 @@ interface Entry { * Return true if one of the segments is the wildcard match. */ boolean includesWildcard(); + + /** + * Increment active request count for the route. + */ + void inc(); + + /** + * Decrement active request count for the route. + */ + void dec(); + + /** + * Return the active request count for the route. + */ + long activeRequests(); } /** diff --git a/avaje-jex/src/main/java/module-info.java b/avaje-jex/src/main/java/module-info.java index d3b08a45..7bc3c75c 100644 --- a/avaje-jex/src/main/java/module-info.java +++ b/avaje-jex/src/main/java/module-info.java @@ -12,9 +12,8 @@ exports io.avaje.jex.spi; exports io.avaje.jex.core; - requires java.net.http; + requires transitive java.net.http; requires transitive org.slf4j; - requires transitive com.fasterxml.jackson.databind; uses TemplateRender; diff --git a/avaje-jex/src/main/resources/META-INF/services/io.avaje.jex.spi.SpiRoutesProvider b/avaje-jex/src/main/resources/META-INF/services/io.avaje.jex.spi.SpiRoutesProvider new file mode 100644 index 00000000..8d8e8739 --- /dev/null +++ b/avaje-jex/src/main/resources/META-INF/services/io.avaje.jex.spi.SpiRoutesProvider @@ -0,0 +1 @@ +io.avaje.jex.routes.BootstrapRoutes diff --git a/avaje-jex/src/main/resources/META-INF/services/io.avaje.jex.spi.SpiServiceManagerProvider b/avaje-jex/src/main/resources/META-INF/services/io.avaje.jex.spi.SpiServiceManagerProvider new file mode 100644 index 00000000..deef4fcd --- /dev/null +++ b/avaje-jex/src/main/resources/META-INF/services/io.avaje.jex.spi.SpiServiceManagerProvider @@ -0,0 +1 @@ +io.avaje.jex.core.BootstapServiceManager diff --git a/examples/example-jex-jdk/logback.xml b/examples/example-jex-jdk/logback.xml new file mode 100644 index 00000000..2c7f5454 --- /dev/null +++ b/examples/example-jex-jdk/logback.xml @@ -0,0 +1,19 @@ + + + + TRACE + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + diff --git a/examples/example-jex-jdk/pom.xml b/examples/example-jex-jdk/pom.xml new file mode 100644 index 00000000..314c63d6 --- /dev/null +++ b/examples/example-jex-jdk/pom.xml @@ -0,0 +1,81 @@ + + + + org.avaje + java11-oss + 3.2 + + + 4.0.0 + + example-jex-jdk + + + 17 + 17 + 17 + 17 + + + + + + + io.avaje + avaje-jex-jdk + 1.8-SNAPSHOT + + + + + + + + + + com.fasterxml.jackson.core + jackson-databind + 2.12.3 + + + + org.slf4j + slf4j-api + 1.7.30 + + + + ch.qos.logback + logback-classic + 1.2.3 + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 11 + + + + io.repaint.maven + tiles-maven-plugin + 2.17 + true + + + org.avaje.tile:lib-classpath:1.1 + + + + + + + diff --git a/examples/example-jex-jdk/src/main/java/org/example/HelloDto.java b/examples/example-jex-jdk/src/main/java/org/example/HelloDto.java new file mode 100644 index 00000000..20141e62 --- /dev/null +++ b/examples/example-jex-jdk/src/main/java/org/example/HelloDto.java @@ -0,0 +1,7 @@ +package org.example; + +public class HelloDto { + + public long id; + public String name; +} diff --git a/examples/example-jex-jdk/src/main/java/org/example/Main.java b/examples/example-jex-jdk/src/main/java/org/example/Main.java new file mode 100644 index 00000000..eedffa00 --- /dev/null +++ b/examples/example-jex-jdk/src/main/java/org/example/Main.java @@ -0,0 +1,53 @@ +package org.example; + +import io.avaje.jex.Jex; +import io.avaje.jex.core.HealthPlugin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +public class Main { + + private static final Logger log = LoggerFactory.getLogger(Main.class); + + public static void main(String[] args) { + + Jex.create() + .attribute(Executor.class, Executors.newVirtualThreadExecutor()) + .routing(routing -> routing + .get("/", ctx -> ctx.text("hello world")) + .get("/foo/{id}", ctx -> { + HelloDto bean = new HelloDto(); + bean.id = Integer.parseInt(ctx.pathParam("id")); + bean.name = "Rob"; + ctx.json(bean); + }) + .get("/delay", ctx -> { + log.info("delay start"); + try { + Thread.sleep(5_000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + e.printStackTrace(); + } + ctx.text("delay done"); + log.info("delay done"); + }) + .get("/dump", ctx -> dumpThreadCount()) + ) + .configure(new HealthPlugin()) + .port(7003) + .start(); + } + + private static void dumpThreadCount() { + Map allStackTraces = Thread.getAllStackTraces(); + System.out.println("Thread count: " + allStackTraces.size()); + Set threads = allStackTraces.keySet(); + System.out.println("Threads: " + threads); + } +} diff --git a/examples/example-jex-jdk/src/main/module-info.java b/examples/example-jex-jdk/src/main/module-info.java new file mode 100644 index 00000000..9c0feb80 --- /dev/null +++ b/examples/example-jex-jdk/src/main/module-info.java @@ -0,0 +1,5 @@ +module example.jex.jdk { + + requires transitive io.avaje.jex.jdk; + requires transitive org.slf4j; +} diff --git a/examples/example-jex-jdk/src/main/resources/content/basic.html b/examples/example-jex-jdk/src/main/resources/content/basic.html new file mode 100644 index 00000000..8b4e34d7 --- /dev/null +++ b/examples/example-jex-jdk/src/main/resources/content/basic.html @@ -0,0 +1 @@ +basic diff --git a/examples/example-jex-jdk/src/main/resources/content/index.html b/examples/example-jex-jdk/src/main/resources/content/index.html new file mode 100644 index 00000000..0ce384c3 --- /dev/null +++ b/examples/example-jex-jdk/src/main/resources/content/index.html @@ -0,0 +1 @@ +index diff --git a/examples/example-jex-jdk/src/main/resources/content/plain-file.txt b/examples/example-jex-jdk/src/main/resources/content/plain-file.txt new file mode 100644 index 00000000..6be11da0 --- /dev/null +++ b/examples/example-jex-jdk/src/main/resources/content/plain-file.txt @@ -0,0 +1 @@ +plain-file diff --git a/examples/example-jex-jdk/src/main/resources/logback.xml b/examples/example-jex-jdk/src/main/resources/logback.xml new file mode 100644 index 00000000..2c7f5454 --- /dev/null +++ b/examples/example-jex-jdk/src/main/resources/logback.xml @@ -0,0 +1,19 @@ + + + + TRACE + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + diff --git a/examples/example-jex-jdk/src/test/resources/logback-test.xml b/examples/example-jex-jdk/src/test/resources/logback-test.xml new file mode 100644 index 00000000..2c7f5454 --- /dev/null +++ b/examples/example-jex-jdk/src/test/resources/logback-test.xml @@ -0,0 +1,19 @@ + + + + TRACE + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + diff --git a/examples/pom.xml b/examples/pom.xml new file mode 100644 index 00000000..79da83cb --- /dev/null +++ b/examples/pom.xml @@ -0,0 +1,21 @@ + + + 4.0.0 + + avaje-jex-parent + io.avaje + 1.8-SNAPSHOT + + + examples-jex + 0.1 + pom + + + example-jex-jdk + + + + + diff --git a/pom.xml b/pom.xml index 2a031f6a..456a6d6b 100644 --- a/pom.xml +++ b/pom.xml @@ -30,7 +30,7 @@ avaje-jex-jetty avaje-jex-jdk - + From 276a40a5d81641455bec506d4218ed8953b27d12 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Tue, 13 Jul 2021 23:06:16 +1200 Subject: [PATCH 22/27] Tidy Jex pom --- avaje-jex/pom.xml | 24 ------------------- avaje-jex/src/main/java/io/avaje/jex/Jex.java | 1 + 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/avaje-jex/pom.xml b/avaje-jex/pom.xml index db4f2a35..4743dc2e 100644 --- a/avaje-jex/pom.xml +++ b/avaje-jex/pom.xml @@ -27,30 +27,6 @@ true - - org.eclipse.jetty.toolchain - jetty-jakarta-servlet-api - 5.0.2 - - - - - - - - - - - - - - - - - - - - diff --git a/avaje-jex/src/main/java/io/avaje/jex/Jex.java b/avaje-jex/src/main/java/io/avaje/jex/Jex.java index 568e4047..08a47095 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/Jex.java +++ b/avaje-jex/src/main/java/io/avaje/jex/Jex.java @@ -54,6 +54,7 @@ public Jex attribute(Class cls, T instance) { /** * Return a custom attribute. */ + @SuppressWarnings("unchecked") public T attribute(Class cls) { return (T) attributes.get(cls); } From e0c3281b7e5e5d2789b5f6985d4d7071c4a3c197 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Tue, 13 Jul 2021 23:26:27 +1200 Subject: [PATCH 23/27] Tidy examples modules --- .../{example-jex-jdk => example-jdk}/pom.xml | 22 +++++++------------ .../src/main/java/module-info.java | 7 ++++++ .../src/main/java/org/example/HelloDto.java | 0 .../src/main/java/org/example/Main.java | 0 .../src/main/resources/content/basic.html | 0 .../src/main/resources/content/index.html | 0 .../src/main/resources/content/plain-file.txt | 0 .../src/main/resources}/logback.xml | 0 .../src/test/resources/logback-test.xml | 0 {example => examples/example-jetty}/pom.xml | 19 ++++++++-------- .../src/main/java/org/example/HelloDto.java | 0 .../src/main/java/org/example/Main.java | 0 .../src/main/resources/content/basic.html | 0 .../src/main/resources/content/index.html | 0 .../src/main/resources/content/plain-file.txt | 0 .../src/main/resources/logback.xml | 0 .../example-jex-jdk/src/main/module-info.java | 5 ----- .../src/main/resources/logback.xml | 19 ---------------- examples/pom.xml | 6 ++--- pom.xml | 3 +-- 20 files changed, 29 insertions(+), 52 deletions(-) rename examples/{example-jex-jdk => example-jdk}/pom.xml (81%) create mode 100644 examples/example-jdk/src/main/java/module-info.java rename {example => examples/example-jdk}/src/main/java/org/example/HelloDto.java (100%) rename examples/{example-jex-jdk => example-jdk}/src/main/java/org/example/Main.java (100%) rename {example => examples/example-jdk}/src/main/resources/content/basic.html (100%) rename {example => examples/example-jdk}/src/main/resources/content/index.html (100%) rename {example => examples/example-jdk}/src/main/resources/content/plain-file.txt (100%) rename examples/{example-jex-jdk => example-jdk/src/main/resources}/logback.xml (100%) rename examples/{example-jex-jdk => example-jdk}/src/test/resources/logback-test.xml (100%) rename {example => examples/example-jetty}/pom.xml (81%) rename examples/{example-jex-jdk => example-jetty}/src/main/java/org/example/HelloDto.java (100%) rename {example => examples/example-jetty}/src/main/java/org/example/Main.java (100%) rename examples/{example-jex-jdk => example-jetty}/src/main/resources/content/basic.html (100%) rename examples/{example-jex-jdk => example-jetty}/src/main/resources/content/index.html (100%) rename examples/{example-jex-jdk => example-jetty}/src/main/resources/content/plain-file.txt (100%) rename {example => examples/example-jetty}/src/main/resources/logback.xml (100%) delete mode 100644 examples/example-jex-jdk/src/main/module-info.java delete mode 100644 examples/example-jex-jdk/src/main/resources/logback.xml diff --git a/examples/example-jex-jdk/pom.xml b/examples/example-jdk/pom.xml similarity index 81% rename from examples/example-jex-jdk/pom.xml rename to examples/example-jdk/pom.xml index 314c63d6..b86aa3a2 100644 --- a/examples/example-jex-jdk/pom.xml +++ b/examples/example-jdk/pom.xml @@ -2,24 +2,24 @@ + 4.0.0 org.avaje java11-oss 3.2 - 4.0.0 - - example-jex-jdk + org.example + example-jdk + 1 17 17 - 17 - 17 + + - @@ -27,12 +27,6 @@ avaje-jex-jdk 1.8-SNAPSHOT - - - - - - com.fasterxml.jackson.core @@ -61,13 +55,13 @@ maven-compiler-plugin 3.8.1 - 11 + ${java.release} io.repaint.maven tiles-maven-plugin - 2.17 + 2.22 true diff --git a/examples/example-jdk/src/main/java/module-info.java b/examples/example-jdk/src/main/java/module-info.java new file mode 100644 index 00000000..c4284407 --- /dev/null +++ b/examples/example-jdk/src/main/java/module-info.java @@ -0,0 +1,7 @@ +module example.jdk { + + requires transitive io.avaje.jex.jdk; + requires transitive org.slf4j; + + exports org.example to com.fasterxml.jackson.databind; +} diff --git a/example/src/main/java/org/example/HelloDto.java b/examples/example-jdk/src/main/java/org/example/HelloDto.java similarity index 100% rename from example/src/main/java/org/example/HelloDto.java rename to examples/example-jdk/src/main/java/org/example/HelloDto.java diff --git a/examples/example-jex-jdk/src/main/java/org/example/Main.java b/examples/example-jdk/src/main/java/org/example/Main.java similarity index 100% rename from examples/example-jex-jdk/src/main/java/org/example/Main.java rename to examples/example-jdk/src/main/java/org/example/Main.java diff --git a/example/src/main/resources/content/basic.html b/examples/example-jdk/src/main/resources/content/basic.html similarity index 100% rename from example/src/main/resources/content/basic.html rename to examples/example-jdk/src/main/resources/content/basic.html diff --git a/example/src/main/resources/content/index.html b/examples/example-jdk/src/main/resources/content/index.html similarity index 100% rename from example/src/main/resources/content/index.html rename to examples/example-jdk/src/main/resources/content/index.html diff --git a/example/src/main/resources/content/plain-file.txt b/examples/example-jdk/src/main/resources/content/plain-file.txt similarity index 100% rename from example/src/main/resources/content/plain-file.txt rename to examples/example-jdk/src/main/resources/content/plain-file.txt diff --git a/examples/example-jex-jdk/logback.xml b/examples/example-jdk/src/main/resources/logback.xml similarity index 100% rename from examples/example-jex-jdk/logback.xml rename to examples/example-jdk/src/main/resources/logback.xml diff --git a/examples/example-jex-jdk/src/test/resources/logback-test.xml b/examples/example-jdk/src/test/resources/logback-test.xml similarity index 100% rename from examples/example-jex-jdk/src/test/resources/logback-test.xml rename to examples/example-jdk/src/test/resources/logback-test.xml diff --git a/example/pom.xml b/examples/example-jetty/pom.xml similarity index 81% rename from example/pom.xml rename to examples/example-jetty/pom.xml index ab251363..6d116649 100644 --- a/example/pom.xml +++ b/examples/example-jetty/pom.xml @@ -2,15 +2,16 @@ + 4.0.0 - io.avaje - avaje-jex-parent - 1.3-SNAPSHOT + org.avaje + java11-oss + 3.2 - 4.0.0 - - example + org.example + example-jetty + 1 @@ -22,14 +23,14 @@ io.avaje - avaje-jex - 1.3-SNAPSHOT + avaje-jex-jetty + 1.8-SNAPSHOT com.fasterxml.jackson.core jackson-databind - 2.12.0 + 2.12.3 diff --git a/examples/example-jex-jdk/src/main/java/org/example/HelloDto.java b/examples/example-jetty/src/main/java/org/example/HelloDto.java similarity index 100% rename from examples/example-jex-jdk/src/main/java/org/example/HelloDto.java rename to examples/example-jetty/src/main/java/org/example/HelloDto.java diff --git a/example/src/main/java/org/example/Main.java b/examples/example-jetty/src/main/java/org/example/Main.java similarity index 100% rename from example/src/main/java/org/example/Main.java rename to examples/example-jetty/src/main/java/org/example/Main.java diff --git a/examples/example-jex-jdk/src/main/resources/content/basic.html b/examples/example-jetty/src/main/resources/content/basic.html similarity index 100% rename from examples/example-jex-jdk/src/main/resources/content/basic.html rename to examples/example-jetty/src/main/resources/content/basic.html diff --git a/examples/example-jex-jdk/src/main/resources/content/index.html b/examples/example-jetty/src/main/resources/content/index.html similarity index 100% rename from examples/example-jex-jdk/src/main/resources/content/index.html rename to examples/example-jetty/src/main/resources/content/index.html diff --git a/examples/example-jex-jdk/src/main/resources/content/plain-file.txt b/examples/example-jetty/src/main/resources/content/plain-file.txt similarity index 100% rename from examples/example-jex-jdk/src/main/resources/content/plain-file.txt rename to examples/example-jetty/src/main/resources/content/plain-file.txt diff --git a/example/src/main/resources/logback.xml b/examples/example-jetty/src/main/resources/logback.xml similarity index 100% rename from example/src/main/resources/logback.xml rename to examples/example-jetty/src/main/resources/logback.xml diff --git a/examples/example-jex-jdk/src/main/module-info.java b/examples/example-jex-jdk/src/main/module-info.java deleted file mode 100644 index 9c0feb80..00000000 --- a/examples/example-jex-jdk/src/main/module-info.java +++ /dev/null @@ -1,5 +0,0 @@ -module example.jex.jdk { - - requires transitive io.avaje.jex.jdk; - requires transitive org.slf4j; -} diff --git a/examples/example-jex-jdk/src/main/resources/logback.xml b/examples/example-jex-jdk/src/main/resources/logback.xml deleted file mode 100644 index 2c7f5454..00000000 --- a/examples/example-jex-jdk/src/main/resources/logback.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - TRACE - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - - - - - - - - - - diff --git a/examples/pom.xml b/examples/pom.xml index 79da83cb..c81f5a77 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -8,14 +8,14 @@ 1.8-SNAPSHOT - examples-jex + examples 0.1 pom - example-jex-jdk + example-jdk + example-jetty - diff --git a/pom.xml b/pom.xml index 456a6d6b..411c45d0 100644 --- a/pom.xml +++ b/pom.xml @@ -29,8 +29,7 @@ avaje-jex-mustache avaje-jex-jetty avaje-jex-jdk - - + examples From 373880639395cdc790f78d362affbd9aa04d1c9b Mon Sep 17 00:00:00 2001 From: rbygrave Date: Wed, 14 Jul 2021 09:05:05 +1200 Subject: [PATCH 24/27] Tidy examples modules --- .../src/main/java/org/example/Main.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/examples/example-jetty/src/main/java/org/example/Main.java b/examples/example-jetty/src/main/java/org/example/Main.java index 5cce7bbf..4da31012 100644 --- a/examples/example-jetty/src/main/java/org/example/Main.java +++ b/examples/example-jetty/src/main/java/org/example/Main.java @@ -1,9 +1,13 @@ package org.example; import io.avaje.jex.Jex; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class Main { + private static final Logger log = LoggerFactory.getLogger(Main.class); + public static void main(String[] args) { Jex.create() @@ -15,6 +19,17 @@ public static void main(String[] args) { bean.name = "Rob"; ctx.json(bean); }) + .get("/delay", ctx -> { + log.info("delay start"); + try { + Thread.sleep(5_000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + e.printStackTrace(); + } + ctx.text("delay done"); + log.info("delay done"); + }) ) .staticFiles().addClasspath("/static", "content") // .staticFiles().addExternal("/", "/tmp/junk") From b5f841d6f79aabcbe2c286cdcd26cc3acd3470a6 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Wed, 14 Jul 2021 23:45:38 +1200 Subject: [PATCH 25/27] Add initial Grizzly support - avaje-jex-grizzly --- avaje-jex-grizzly/pom.xml | 51 ++ .../io/avaje/jex/grizzly/ContextUtil.java | 65 +++ .../io/avaje/jex/grizzly/GrizzlyContext.java | 482 ++++++++++++++++++ .../avaje/jex/grizzly/GrizzlyJexServer.java | 51 ++ .../avaje/jex/grizzly/GrizzlyServerStart.java | 42 ++ .../avaje/jex/grizzly/HttpServerBuilder.java | 97 ++++ .../io/avaje/jex/grizzly/RouteHandler.java | 80 +++ .../io/avaje/jex/grizzly/ServiceManager.java | 22 + .../services/io.avaje.jex.spi.SpiStartServer | 1 + .../avaje/jex/grizzly/AutoCloseIterator.java | 32 ++ .../jex/grizzly/ContextFormParamTest.java | 135 +++++ .../avaje/jex/grizzly/ContextLengthTest.java | 106 ++++ .../io/avaje/jex/grizzly/ContextTest.java | 192 +++++++ .../java/io/avaje/jex/grizzly/HelloDto.java | 27 + .../io/avaje/jex/grizzly/HelloWorldTest.java | 50 ++ .../java/io/avaje/jex/grizzly/JsonTest.java | 121 +++++ .../io/avaje/jex/grizzly/QueryParamTest.java | 148 ++++++ .../java/io/avaje/jex/grizzly/TestPair.java | 62 +++ .../io/avaje/jex/grizzly/VanillaMain.java | 42 ++ .../src/test/resources/logback-test.xml | 19 + .../src/test/resources/myres/hello.txt | 1 + .../java/io/avaje/jex/jdk/JdkContext.java | 3 +- .../io/avaje/jex/jetty/JexHttpContext.java | 3 +- .../src/main/java/io/avaje/jex/Context.java | 2 +- examples/example-grizzly/pom.xml | 54 ++ .../src/main/java/org/example/GMain.java | 56 ++ .../src/main/java/org/example/HelloDto.java | 14 + .../src/test/java/org/example/ClientMain.java | 28 + .../src/test/resources/logback-test.xml | 19 + examples/pom.xml | 2 + pom.xml | 12 +- 31 files changed, 2014 insertions(+), 5 deletions(-) create mode 100644 avaje-jex-grizzly/pom.xml create mode 100644 avaje-jex-grizzly/src/main/java/io/avaje/jex/grizzly/ContextUtil.java create mode 100644 avaje-jex-grizzly/src/main/java/io/avaje/jex/grizzly/GrizzlyContext.java create mode 100644 avaje-jex-grizzly/src/main/java/io/avaje/jex/grizzly/GrizzlyJexServer.java create mode 100644 avaje-jex-grizzly/src/main/java/io/avaje/jex/grizzly/GrizzlyServerStart.java create mode 100644 avaje-jex-grizzly/src/main/java/io/avaje/jex/grizzly/HttpServerBuilder.java create mode 100644 avaje-jex-grizzly/src/main/java/io/avaje/jex/grizzly/RouteHandler.java create mode 100644 avaje-jex-grizzly/src/main/java/io/avaje/jex/grizzly/ServiceManager.java create mode 100644 avaje-jex-grizzly/src/main/resources/META-INF/services/io.avaje.jex.spi.SpiStartServer create mode 100644 avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/AutoCloseIterator.java create mode 100644 avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/ContextFormParamTest.java create mode 100644 avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/ContextLengthTest.java create mode 100644 avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/ContextTest.java create mode 100644 avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/HelloDto.java create mode 100644 avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/HelloWorldTest.java create mode 100644 avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/JsonTest.java create mode 100644 avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/QueryParamTest.java create mode 100644 avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/TestPair.java create mode 100644 avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/VanillaMain.java create mode 100644 avaje-jex-grizzly/src/test/resources/logback-test.xml create mode 100644 avaje-jex-grizzly/src/test/resources/myres/hello.txt create mode 100644 examples/example-grizzly/pom.xml create mode 100644 examples/example-grizzly/src/main/java/org/example/GMain.java create mode 100644 examples/example-grizzly/src/main/java/org/example/HelloDto.java create mode 100644 examples/example-grizzly/src/test/java/org/example/ClientMain.java create mode 100644 examples/example-grizzly/src/test/resources/logback-test.xml diff --git a/avaje-jex-grizzly/pom.xml b/avaje-jex-grizzly/pom.xml new file mode 100644 index 00000000..85f09479 --- /dev/null +++ b/avaje-jex-grizzly/pom.xml @@ -0,0 +1,51 @@ + + + + avaje-jex-parent + io.avaje + 1.8-SNAPSHOT + + 4.0.0 + + avaje-jex-grizzly + + + 17 + 17 + + + + + + io.avaje + avaje-jex + 1.8-SNAPSHOT + + + + org.glassfish.grizzly + grizzly-http-server + 3.0.0 + + + + org.glassfish.grizzly + grizzly-http2 + 3.0.0 + true + + + + org.slf4j + jul-to-slf4j + 1.7.30 + test + + + + + + + diff --git a/avaje-jex-grizzly/src/main/java/io/avaje/jex/grizzly/ContextUtil.java b/avaje-jex-grizzly/src/main/java/io/avaje/jex/grizzly/ContextUtil.java new file mode 100644 index 00000000..82705c7d --- /dev/null +++ b/avaje-jex-grizzly/src/main/java/io/avaje/jex/grizzly/ContextUtil.java @@ -0,0 +1,65 @@ +package io.avaje.jex.grizzly; + +import org.glassfish.grizzly.http.server.Request; + +import java.io.*; + +class ContextUtil { + + private static final int DEFAULT_BUFFER_SIZE = 8 * 1024; + + private static final int BUFFER_MAX = 65536; + + static byte[] requestBodyAsBytes(Request req) { + final int len = req.getContentLength(); + try (final InputStream inputStream = req.getInputStream()) { + + int bufferSize = len > -1 ? len : DEFAULT_BUFFER_SIZE; + if (bufferSize > BUFFER_MAX) { + bufferSize = BUFFER_MAX; + } + ByteArrayOutputStream os = new ByteArrayOutputStream(bufferSize); + copy(inputStream, os, bufferSize); + return os.toByteArray(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + static void copy(InputStream in, OutputStream out, int bufferSize) throws IOException { + byte[] buffer = new byte[bufferSize]; + int len; + while ((len = in.read(buffer, 0, bufferSize)) > 0) { + out.write(buffer, 0, len); + } + } + + static String requestBodyAsString(Request request) { + final long requestLength = request.getContentLengthLong(); + if (requestLength == 0) { + return ""; + } + if (requestLength < 0) { + throw new IllegalStateException("No content-length set?"); + } + final int bufferSize = requestLength > 512 ? 512 : (int)requestLength; + + StringWriter writer = new StringWriter((int)requestLength); + final Reader reader = request.getReader(); + try { + long transferred = 0; + char[] buffer = new char[bufferSize]; + int nRead; + while ((nRead = reader.read(buffer, 0, bufferSize)) >= 0) { + writer.write(buffer, 0, nRead); + transferred += nRead; + if (transferred == requestLength) { + break; + } + } + return writer.toString(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/avaje-jex-grizzly/src/main/java/io/avaje/jex/grizzly/GrizzlyContext.java b/avaje-jex-grizzly/src/main/java/io/avaje/jex/grizzly/GrizzlyContext.java new file mode 100644 index 00000000..3b908640 --- /dev/null +++ b/avaje-jex-grizzly/src/main/java/io/avaje/jex/grizzly/GrizzlyContext.java @@ -0,0 +1,482 @@ +package io.avaje.jex.grizzly; + +import io.avaje.jex.Context; +import io.avaje.jex.Routing; +import io.avaje.jex.UploadedFile; +import io.avaje.jex.http.RedirectResponse; +import io.avaje.jex.spi.HeaderKeys; +import io.avaje.jex.spi.SpiContext; +import io.avaje.jex.spi.SpiRoutes; +import org.glassfish.grizzly.http.server.Request; +import org.glassfish.grizzly.http.server.Response; +import org.glassfish.grizzly.http.util.ContentType; + +import java.io.*; +import java.util.*; +import java.util.stream.Stream; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; + +class GrizzlyContext implements Context, SpiContext { + + private static final ContentType JSON = ContentType.newContentType(APPLICATION_JSON); + private static final ContentType JSON_STREAM = ContentType.newContentType(APPLICATION_X_JSON_STREAM); + private static final ContentType HTML_UTF8 = ContentType.newContentType("text/html","utf-8"); + private static final ContentType PLAIN_UTF8 = ContentType.newContentType("text/plain","utf-8"); + + private static final String UTF8 = "UTF8"; + private static final int SC_MOVED_TEMPORARILY = 302; + private final ServiceManager mgr; + private final String path; + private final SpiRoutes.Params params; + private final Request request; + private final Response response; + private Routing.Type mode; + private Map> formParams; + private Map> queryParams; + private Map cookieMap; + + GrizzlyContext(ServiceManager mgr, Request request, Response response, String path, SpiRoutes.Params params) { + this.mgr = mgr; + this.request = request; + this.response = response; + this.path = path; + this.params = params; + } + + /** + * Create when no route matched. + */ + GrizzlyContext(ServiceManager mgr, Request request, Response response, String path) { + this.mgr = mgr; + this.request = request; + this.response = response; + this.path = path; + this.params = null; + } + + @Override + public String matchedPath() { + return path; + } + + @Override + public Context attribute(String key, Object value) { + request.setAttribute(key, value); + return this; + } + + @Override + @SuppressWarnings("unchecked") + public T attribute(String key) { + return (T) request.getAttribute(key); + } + + @Override + public Map attributeMap() { + throw new UnsupportedOperationException(); + } + + @Override + public Map cookieMap() { + if (cookieMap == null) { + cookieMap = new LinkedHashMap<>(); + final org.glassfish.grizzly.http.Cookie[] cookies = request.getCookies(); + for (org.glassfish.grizzly.http.Cookie cookie : cookies) { + cookieMap.put(cookie.getName(), cookie.getValue()); + } + } + return cookieMap; + } + + @Override + public String cookie(String name) { + return cookieMap().get(name); + } + + @Override + public Context cookie(Cookie cookie) { + throw new UnsupportedOperationException(); + } + + @Override + public Context cookie(String name, String value) { + throw new UnsupportedOperationException(); + } + + @Override + public Context cookie(String name, String value, int maxAge) { + throw new UnsupportedOperationException(); + } + + @Override + public Context removeCookie(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public Context removeCookie(String name, String path) { + throw new UnsupportedOperationException(); + } + + @Override + public void redirect(String location) { + redirect(location, SC_MOVED_TEMPORARILY); + } + + @Override + public void redirect(String location, int statusCode) { + status(statusCode); + if (mode == Routing.Type.BEFORE) { + header(HeaderKeys.LOCATION, location); + throw new RedirectResponse(statusCode); + } else { + try { + response.sendRedirect(location); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + + @Override + public void performRedirect() { + // TODO check this + } + + @Override + public T bodyAsClass(Class beanType) { + return mgr.jsonRead(beanType, this); + } + + @Override + public byte[] bodyAsBytes() { + return ContextUtil.requestBodyAsBytes(request); + } + + private String characterEncoding() { + String encoding = request.getCharacterEncoding(); + return encoding != null ? encoding : UTF8; + } + + @Override + public String body() { + return ContextUtil.requestBodyAsString(request); + } + + @Override + public long contentLength() { + return request.getContentLengthLong(); + } + + @Override + public String contentType() { + return request.getContentType(); + } + + @Override + public String responseHeader(String key) { + return response.getHeader(key); + } + + @Override + public Context contentType(String contentType) { + response.setContentType(contentType); + return this; + } + + @Override + public String splat(int position) { + return params.splats.get(position); + } + + @Override + public List splats() { + return params.splats; + } + + @Override + public Map pathParamMap() { + return params.pathParams; + } + + @Override + public String pathParam(String name) { + return params.pathParams.get(name); + } + + @Override + public String queryParam(String name) { + final List values = queryParams(name); + return values == null || values.isEmpty() ? null : values.get(0); + } + + private Map> queryParams() { + if (queryParams == null) { + queryParams = mgr.parseParamMap(queryString(), characterEncoding()); + } + return queryParams; + } + + @Override + public List queryParams(String name) { + final List values = queryParams().get(name); + return values == null ? emptyList() : values; + } + + @Override + public Map queryParamMap() { + final Map> map = queryParams(); + if (map.isEmpty()) { + return emptyMap(); + } + final Map single = new LinkedHashMap<>(); + for (Map.Entry> entry : map.entrySet()) { + final List value = entry.getValue(); + if (value != null && !value.isEmpty()) { + single.put(entry.getKey(), value.get(0)); + } + } + return single; + } + + @Override + public String queryString() { + return request.getQueryString(); + } + + /** + * Return the first form param value for the specified key or null. + */ + @Override + public String formParam(String key) { + return request.getParameter(key); + } + + /** + * Return the first form param value for the specified key or the default value. + */ + @Override + public String formParam(String key, String defaultValue) { + String value = request.getParameter(key); + return value == null ? defaultValue : value; + } + + /** + * Return the form params for the specified key, or empty list. + */ + @Override + public List formParams(String key) { + final String[] values = request.getParameterValues(key); + return values == null ? emptyList() : asList(values); + } + + @Override + public Map> formParamMap() { + if (formParams == null) { + formParams = initFormParamMap(); + } + return formParams; + } + + private Map> initFormParamMap() { + final Map parameterMap = request.getParameterMap(); + if (parameterMap.isEmpty()) { + return emptyMap(); + } + final Set> entries = parameterMap.entrySet(); + Map> map = new LinkedHashMap<>(entries.size()); + for (Map.Entry entry : entries) { + map.put(entry.getKey(), asList(entry.getValue())); + } + return map; + } + + @Override + public String scheme() { + return request.getScheme(); + } + + @Override + public Context sessionAttribute(String key, Object value) { + request.getSession().setAttribute(key, value); + return this; + } + + @Override + @SuppressWarnings("unchecked") + public T sessionAttribute(String key) { + return (T) request.getSession().getAttribute(key); + } + + @Override + public Map sessionAttributeMap() { + return request.getSession().attributes(); + } + + @Override + public String url() { + return scheme() + "://" + host() + ":" + port() + path; + } + + @Override + public String contextPath() { + return mgr.contextPath(); + } + + @Override + public Context status(int statusCode) { + response.setStatus(statusCode); + return this; + } + + @Override + public int status() { + return response.getStatus(); + } + + + @Override + public Context json(Object bean) { + response.setContentType(JSON); + mgr.jsonWrite(bean, this); + return this; + } + + @Override + public Context jsonStream(Stream stream) { + response.setContentType(JSON_STREAM); + mgr.jsonWriteStream(stream, this); + return this; + } + + @Override + public Context jsonStream(Iterator iterator) { + response.setContentType(JSON_STREAM); + mgr.jsonWriteStream(iterator, this); + return this; + } + + @Override + public Context text(String content) { + response.setContentType(PLAIN_UTF8); + return write(content); + } + + @Override + public Context html(String content) { + response.setContentType(HTML_UTF8); + return write(content); + } + + @Override + public Context write(String content) { + try { + response.getOutputBuffer().write(content); + return this; + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public Context render(String name, Map model) { + mgr.render(this, name, model); + return this; + } + + @Override + public Map headerMap() { + Map map = new LinkedHashMap<>(); + for (String headerName : request.getHeaderNames()) { + map.put(headerName, request.getHeader(headerName)); + } + return map; + } + + @Override + public String header(String key) { + return request.getHeader(key); + } + + @Override + public Context header(String key, String value) { + response.setHeader(key, value); + return this; + } + + @Override + public String host() { + return request.getRemoteHost(); + } + + @Override + public String ip() { + return request.getRemoteAddr(); + } + + @Override + public boolean isMultipart() { + // TODO + return false; + } + + @Override + public boolean isMultipartFormData() { + // TODO + return false; + } + + @Override + public String method() { + return request.getMethod().getMethodString(); + } + + @Override + public String path() { + return path; + } + + @Override + public int port() { + return request.getServerPort(); + } + + @Override + public String protocol() { + return request.getProtocol().getProtocolString(); + } + + @Override + public UploadedFile uploadedFile(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public List uploadedFiles(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public List uploadedFiles() { + throw new UnsupportedOperationException(); + } + + @Override + public OutputStream outputStream() { + return response.getOutputStream(); + } + + @Override + public InputStream inputStream() { + return request.getInputStream(); + } + + @Override + public void setMode(Routing.Type type) { + this.mode = type; + } + +} diff --git a/avaje-jex-grizzly/src/main/java/io/avaje/jex/grizzly/GrizzlyJexServer.java b/avaje-jex-grizzly/src/main/java/io/avaje/jex/grizzly/GrizzlyJexServer.java new file mode 100644 index 00000000..5c8d1828 --- /dev/null +++ b/avaje-jex-grizzly/src/main/java/io/avaje/jex/grizzly/GrizzlyJexServer.java @@ -0,0 +1,51 @@ +package io.avaje.jex.grizzly; + +import io.avaje.jex.AppLifecycle; +import io.avaje.jex.Jex; +import org.glassfish.grizzly.http.server.HttpServer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; + +class GrizzlyJexServer implements Jex.Server { + + private static final Logger log = LoggerFactory.getLogger(GrizzlyJexServer.class); + + private final HttpServer server; + private final AppLifecycle lifecycle; + private final ReentrantLock lock = new ReentrantLock(); + private final int maxWaitSeconds = 30; + private boolean shutdown; + + GrizzlyJexServer(HttpServer server, AppLifecycle lifecycle) { + this.server = server; + this.lifecycle = lifecycle; + lifecycle.registerShutdownHook(this::shutdown); + } + + @Override + public void shutdown() { + lock.lock(); + try { + if (shutdown) { + log.trace("shutdown in progress"); + } else { + shutdown = true; + lifecycle.status(AppLifecycle.Status.STOPPING); + log.debug("initiate shutdown with maxWaitSeconds {}", maxWaitSeconds); + try { + server.shutdown(maxWaitSeconds, TimeUnit.SECONDS).get(); + } catch (InterruptedException |ExecutionException e) { + log.error("Error during server shutdown", e); + } + lifecycle.status(AppLifecycle.Status.STOPPED); + log.info("shutdown complete"); + } + } finally { + lock.unlock(); + } + } +} diff --git a/avaje-jex-grizzly/src/main/java/io/avaje/jex/grizzly/GrizzlyServerStart.java b/avaje-jex-grizzly/src/main/java/io/avaje/jex/grizzly/GrizzlyServerStart.java new file mode 100644 index 00000000..9572daa9 --- /dev/null +++ b/avaje-jex-grizzly/src/main/java/io/avaje/jex/grizzly/GrizzlyServerStart.java @@ -0,0 +1,42 @@ +package io.avaje.jex.grizzly; + +import io.avaje.jex.Jex; +import io.avaje.jex.spi.SpiRoutes; +import io.avaje.jex.spi.SpiServiceManager; +import io.avaje.jex.spi.SpiStartServer; +import org.glassfish.grizzly.http.server.HttpServer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.UncheckedIOException; + +public class GrizzlyServerStart implements SpiStartServer { + + private static final Logger log = LoggerFactory.getLogger(GrizzlyJexServer.class); + + @Override + public Jex.Server start(Jex jex, SpiRoutes routes, SpiServiceManager serviceManager) { + + final ServiceManager manager = new ServiceManager(serviceManager, "http", ""); + RouteHandler handler = new RouteHandler(routes, manager); + + final int port = jex.inner.port; + final HttpServer httpServer = new HttpServerBuilder() + //.addHandler(clStaticHttpHandler, "cl") + //.addHandler(staticHttpHandler, "static") + .handler(handler) + .setPort(port) + .build(); + + try { + log.debug("starting server on port {}", port); + httpServer.start(); + log.info("server started on port {}", port); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + + return new GrizzlyJexServer(httpServer, jex.lifecycle()); + } +} diff --git a/avaje-jex-grizzly/src/main/java/io/avaje/jex/grizzly/HttpServerBuilder.java b/avaje-jex-grizzly/src/main/java/io/avaje/jex/grizzly/HttpServerBuilder.java new file mode 100644 index 00000000..f09f5c9f --- /dev/null +++ b/avaje-jex-grizzly/src/main/java/io/avaje/jex/grizzly/HttpServerBuilder.java @@ -0,0 +1,97 @@ +package io.avaje.jex.grizzly; + +import org.glassfish.grizzly.http.server.*; +import org.glassfish.grizzly.ssl.SSLEngineConfigurator; +import org.glassfish.grizzly.utils.Charsets; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class HttpServerBuilder { + + private static final Logger log = LoggerFactory.getLogger(GrizzlyJexServer.class); + + private int port = -1; + private String host = "0.0.0.0"; + private boolean secure; + private SSLEngineConfigurator sslEngineConfigurator; + + private final HttpServer server = new HttpServer(); + + public HttpServerBuilder setPort(int port) { + this.port = port; + return this; + } + + public HttpServerBuilder host(String host) { + this.host = host; + return this; + } + + public HttpServerBuilder sslEngineConfigurator(SSLEngineConfigurator sslEngineConfigurator) { + this.sslEngineConfigurator = sslEngineConfigurator; + return this; + } + + public HttpServerBuilder secure(boolean secure) { + this.secure = secure; + return this; + } + + /** + * Add a handler using root context. + */ + public HttpServerBuilder handler(HttpHandler handler) { + return handler(handler, ""); + } + + /** + * Add a handler with the given context. + */ + public HttpServerBuilder handler(HttpHandler handler, String context) { + handler(handler, HttpHandlerRegistration.fromString("/" + context + "/*")); + return this; + } + + /** + * Add a handler given the paths. + */ + public HttpServerBuilder handler(HttpHandler handler, HttpHandlerRegistration... paths) { + server.getServerConfiguration().addHttpHandler(handler, paths); + return this; + } + + /** + * Build and return the grizzly http server. + */ + public HttpServer build() { + + int serverPort = serverPort(); + NetworkListener listener = new NetworkListener("grizzly", host, serverPort); + + // TODO: Configure to use loom thread factory + // listener.getTransport().getWorkerThreadPoolConfig().setThreadFactory() + listener.setSecure(secure); + if (sslEngineConfigurator != null) { + listener.setSSLEngineConfig(sslEngineConfigurator); + } + addHttp2Support(listener); + server.addListener(listener); + ServerConfiguration config = server.getServerConfiguration(); + config.setPassTraceRequest(true); + config.setDefaultQueryEncoding(Charsets.UTF8_CHARSET); + return server; + } + + protected void addHttp2Support(NetworkListener listener) { + try { + Class.forName("org.glassfish.grizzly.http2.Http2AddOn"); +// listener.registerAddOn(new org.glassfish.grizzly.http2.Http2AddOn()); + } catch (Throwable e) { + log.trace("Http2AddOn was not registered"); + } + } + + protected int serverPort() { + return port != -1 ? port : (secure ? 8443 : 7001); + } +} diff --git a/avaje-jex-grizzly/src/main/java/io/avaje/jex/grizzly/RouteHandler.java b/avaje-jex-grizzly/src/main/java/io/avaje/jex/grizzly/RouteHandler.java new file mode 100644 index 00000000..b74ed4bb --- /dev/null +++ b/avaje-jex-grizzly/src/main/java/io/avaje/jex/grizzly/RouteHandler.java @@ -0,0 +1,80 @@ +package io.avaje.jex.grizzly; + +import io.avaje.jex.Context; +import io.avaje.jex.Routing; +import io.avaje.jex.http.NotFoundResponse; +import io.avaje.jex.spi.SpiContext; +import io.avaje.jex.spi.SpiRoutes; +import org.glassfish.grizzly.http.server.HttpHandler; +import org.glassfish.grizzly.http.server.Request; +import org.glassfish.grizzly.http.server.Response; + +class RouteHandler extends HttpHandler { + + private final SpiRoutes routes; + private final ServiceManager mgr; + + RouteHandler(SpiRoutes routes, ServiceManager mgr) { + this.mgr = mgr; + this.routes = routes; + } + + @Override + public void service(Request request, Response response) { + + final String uri = request.getRequestURI(); + final Routing.Type routeType = mgr.lookupRoutingType(request.getMethod().getMethodString()); + final SpiRoutes.Entry route = routes.match(routeType, uri); + + if (route == null) { + var ctx = new GrizzlyContext(mgr, request, response, uri); + try { + processNoRoute(ctx, uri, routeType); + routes.after(uri, ctx); + } catch (Exception e) { + handleException(ctx, e); + } + } else { + final SpiRoutes.Params params = route.pathParams(uri); + var ctx = new GrizzlyContext(mgr, request, response, route.matchPath(), params); + try { + processRoute(ctx, uri, route); + routes.after(uri, ctx); + } catch (Exception e) { + handleException(ctx, e); + } + } + } + + private void handleException(SpiContext ctx, Exception e) { + mgr.handleException(ctx, e); + } + + private void processRoute(GrizzlyContext ctx, String uri, SpiRoutes.Entry route) { + routes.before(uri, ctx); + ctx.setMode(null); + route.handle(ctx); + } + + private void processNoRoute(GrizzlyContext ctx, String uri, Routing.Type routeType) { + routes.before(uri, ctx); + if (routeType == Routing.Type.HEAD && hasGetHandler(uri)) { + processHead(ctx); + return; + } +// if (routeType == Routing.Type.GET || routeType == Routing.Type.HEAD) { +// // check if handled by static resource +// // check if handled by singlePageHandler +// } + throw new NotFoundResponse("uri: " + uri); + } + + private void processHead(Context ctx) { + ctx.status(200); + } + + private boolean hasGetHandler(String uri) { + return routes.match(Routing.Type.GET, uri) != null; + } + +} diff --git a/avaje-jex-grizzly/src/main/java/io/avaje/jex/grizzly/ServiceManager.java b/avaje-jex-grizzly/src/main/java/io/avaje/jex/grizzly/ServiceManager.java new file mode 100644 index 00000000..b7be3145 --- /dev/null +++ b/avaje-jex-grizzly/src/main/java/io/avaje/jex/grizzly/ServiceManager.java @@ -0,0 +1,22 @@ +package io.avaje.jex.grizzly; + +import io.avaje.jex.spi.ProxyServiceManager; +import io.avaje.jex.spi.SpiServiceManager; + +import java.io.OutputStream; + +class ServiceManager extends ProxyServiceManager { + + private final String scheme; + private final String contextPath; + + ServiceManager(SpiServiceManager delegate, String scheme, String contextPath) { + super(delegate); + this.scheme = scheme; + this.contextPath = contextPath; + } + + String contextPath() { + return contextPath; + } +} diff --git a/avaje-jex-grizzly/src/main/resources/META-INF/services/io.avaje.jex.spi.SpiStartServer b/avaje-jex-grizzly/src/main/resources/META-INF/services/io.avaje.jex.spi.SpiStartServer new file mode 100644 index 00000000..855a5fe8 --- /dev/null +++ b/avaje-jex-grizzly/src/main/resources/META-INF/services/io.avaje.jex.spi.SpiStartServer @@ -0,0 +1 @@ +io.avaje.jex.grizzly.GrizzlyServerStart diff --git a/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/AutoCloseIterator.java b/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/AutoCloseIterator.java new file mode 100644 index 00000000..819941a7 --- /dev/null +++ b/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/AutoCloseIterator.java @@ -0,0 +1,32 @@ +package io.avaje.jex.grizzly; + +import java.util.Iterator; + +public class AutoCloseIterator implements Iterator, AutoCloseable { + + private final Iterator it; + private boolean closed; + + public AutoCloseIterator(Iterator it) { + this.it = it; + } + + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + public E next() { + return it.next(); + } + + @Override + public void close() { + closed = true; + } + + public boolean isClosed() { + return closed; + } +} diff --git a/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/ContextFormParamTest.java b/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/ContextFormParamTest.java new file mode 100644 index 00000000..11ed44c4 --- /dev/null +++ b/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/ContextFormParamTest.java @@ -0,0 +1,135 @@ +package io.avaje.jex.grizzly; + +import io.avaje.jex.Jex; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpResponse; + +import static org.assertj.core.api.Assertions.assertThat; + +class ContextFormParamTest { + + static TestPair pair = init(); + + static TestPair init() { + var app = Jex.create() + .routing(routing -> routing + .post("/", ctx -> ctx.text("map:" +ctx.formParamMap())) + .post("/formParams/{key}", ctx -> ctx.text("formParams:" + ctx.formParams(ctx.pathParam("key")))) + .post("/formParam/{key}", ctx -> ctx.text("formParam:" + ctx.formParam(ctx.pathParam("key")))) + .post("/formParamWithDefault/{key}", ctx -> ctx.text("formParam:" + ctx.formParam(ctx.pathParam("key"), "foo"))) + ); + return TestPair.create(app); + } + + @AfterAll + static void end() { + pair.shutdown(); + } + + @Test + void formParamMap() { + HttpResponse res = pair.request() + .formParam("one", "ao") + .formParam("one", "bo") + .formParam("two", "z") + .POST().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("map:{one=[ao, bo], two=[z]}"); + } + + + @Test + void formParams_one() { + HttpResponse res = pair.request() + .formParam("one", "ao") + .formParam("one", "bo") + .formParam("two", "z") + .path("formParams").path("one") + .POST().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("formParams:[ao, bo]"); + } + + @Test + void formParams_two() { + HttpResponse res = pair.request() + .formParam("one", "ao") + .formParam("one", "bo") + .formParam("two", "z") + .path("formParams").path("two") + .POST().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("formParams:[z]"); + } + + + @Test + void formParam_null() { + HttpResponse res = pair.request() + .formParam("one", "ao") + .formParam("one", "bo") + .formParam("two", "z") + .path("formParam").path("doesNotExist") + .POST().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("formParam:null"); + } + + @Test + void formParam_first() { + HttpResponse res = pair.request() + .formParam("one", "ao") + .formParam("one", "bo") + .formParam("two", "z") + .path("formParam").path("one") + .POST().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("formParam:ao"); + } + + @Test + void formParam_default() { + HttpResponse res = pair.request() + .formParam("one", "ao") + .formParam("one", "bo") + .formParam("two", "z") + .path("formParamWithDefault").path("doesNotExist") + .POST().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("formParam:foo"); + } + + @Test + void formParam_default_first() { + HttpResponse res = pair.request() + .formParam("one", "ao") + .formParam("one", "bo") + .formParam("two", "z") + .path("formParamWithDefault").path("one") + .POST().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("formParam:ao"); + } + + @Test + void formParam_default_only() { + HttpResponse res = pair.request() + .formParam("one", "ao") + .formParam("one", "bo") + .formParam("two", "z") + .path("formParamWithDefault").path("two") + .POST().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("formParam:z"); + } +} diff --git a/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/ContextLengthTest.java b/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/ContextLengthTest.java new file mode 100644 index 00000000..19097ddf --- /dev/null +++ b/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/ContextLengthTest.java @@ -0,0 +1,106 @@ +package io.avaje.jex.grizzly; + +import io.avaje.jex.Jex; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpResponse; + +import static org.assertj.core.api.Assertions.assertThat; + +class ContextLengthTest { + + static TestPair pair = init(); + + static TestPair init() { + var app = Jex.create() + .routing(routing -> routing + .post("/", ctx -> ctx.text("contentLength:" + ctx.contentLength() + " type:" + ctx.contentType())) + .get("/url", ctx -> ctx.text("url:" + ctx.url())) + .get("/fullUrl", ctx -> ctx.text("fullUrl:" + ctx.fullUrl())) + .get("/contextPath", ctx -> ctx.text("contextPath:" + ctx.contextPath())) + .get("/userAgent", ctx -> ctx.text("userAgent:" + ctx.userAgent())) + ); + return TestPair.create(app); + } + + @AfterAll + static void end() { + pair.shutdown(); + } + + @Test + void when_noReqContentType() { + HttpResponse res = pair.request().body("MyBodyContent") + .POST().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("contentLength:13 type:null"); + } + + @Test + void requestContentLengthAndType_notReqContentType() { + HttpResponse res = pair.request() + .formParam("a", "my-a-val") + .formParam("b", "my-b-val") + .POST().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("contentLength:21 type:application/x-www-form-urlencoded"); + } + + @Test + void url() { + HttpResponse res = pair.request() + .path("url") + .queryParam("a", "av") + .GET().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("url:http://localhost:" + pair.port() + "/url"); + } + + @Test + void fullUrl_no_queryString() { + HttpResponse res = pair.request() + .path("fullUrl") + .GET().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("fullUrl:http://localhost:" + pair.port() + "/fullUrl"); + } + + @Test + void fullUrl_queryString() { + HttpResponse res = pair.request() + .path("fullUrl") + .queryParam("a", "av") + .queryParam("b", "bv") + .GET().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("fullUrl:http://localhost:" + pair.port() + "/fullUrl?a=av&b=bv"); + } + + @Test + void contextPath() { + HttpResponse res = pair.request() + .path("contextPath") + .queryParam("a", "av") + .GET().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("contextPath:"); + } + + @Test + void userAgent() { + HttpResponse res = pair.request() + .path("userAgent") + .queryParam("a", "av") + .GET().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).contains("userAgent:Java-http-client"); + } +} diff --git a/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/ContextTest.java b/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/ContextTest.java new file mode 100644 index 00000000..ec2cc637 --- /dev/null +++ b/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/ContextTest.java @@ -0,0 +1,192 @@ +package io.avaje.jex.grizzly; + +import io.avaje.jex.Context; +import io.avaje.jex.Jex; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpResponse; +import java.util.Optional; + +import static java.util.Objects.requireNonNull; +import static org.assertj.core.api.Assertions.assertThat; + +class ContextTest { + + static TestPair pair = init(); + + static TestPair init() { + + var me = new ContextTest(); + + final Jex app = Jex.create() + .routing(routing -> routing + .get("/", ctx -> ctx.text("ze-get")) + .post("/", ctx -> ctx.text("ze-post")) + .get("/header", me::doHeader) + .get("/headerMap", ctx -> ctx.text("req-header-map[" + ctx.headerMap() + "]")) + .get("/host", me::doHost) + .get("/ip", me::doIp) + .post("/multipart", ctx -> ctx.text("isMultipart:" + ctx.isMultipart() + " isMultipartFormData:" + ctx.isMultipartFormData())) + .get("/method", ctx -> ctx.text("method:" + ctx.method() + " path:" + ctx.path() + " protocol:" + ctx.protocol() + " port:" + ctx.port())) + .post("/echo", ctx -> ctx.text("req-body[" + ctx.body() + "]")) + .get("/{a}/{b}", ctx -> ctx.text("ze-get-" + ctx.pathParamMap())) + .post("/{a}/{b}", ctx -> ctx.text("ze-post-" + ctx.pathParamMap())) + .get("/status", me::doStatus)); + + return TestPair.create(app); + } + + private void doStatus(Context ctx) { + ctx.status(201); + ctx.text("status:" + ctx.status()); + } + + private void doIp(Context ctx) { + final String ip = ctx.ip(); + requireNonNull(ip); + ctx.text("ip:" + ip); + } + + private void doHost(Context ctx) { + final String host = ctx.host(); + requireNonNull(host); + ctx.text("host:" + host); + } + + private void doHeader(Context ctx) { + ctx.header("From-My-Server", "Set-By-Server"); + ctx.text("req-header[" + ctx.header("From-My-Client") + "]"); + } + + @AfterAll + static void end() { + pair.shutdown(); + } + + @Test + void get() { + HttpResponse res = pair.request().GET().asString(); + assertThat(res.body()).isEqualTo("ze-get"); + } + + @Test + void post() { + HttpResponse res = pair.request().body("simple").POST().asString(); + assertThat(res.body()).isEqualTo("ze-post"); + } + + @Test + void ctx_header_getSet() { + HttpResponse res = pair.request().path("header") + .header("From-My-Client", "client-value") + .GET().asString(); + + final Optional serverSetHeader = res.headers().firstValue("From-My-Server"); + assertThat(serverSetHeader.get()).isEqualTo("Set-By-Server"); + assertThat(res.body()).isEqualTo("req-header[client-value]"); + } + + @Test + void ctx_headerMap() { + HttpResponse res = pair.request().path("headerMap") + .header("X-Foo", "a") + .header("X-Bar", "b") + .GET().asString(); + + assertThat(res.body()).contains("x-foo=a"); // not maintaining case? + assertThat(res.body()).contains("x-bar=b"); + } + + @Test + void ctx_status() { + HttpResponse res = pair.request().path("status") + .GET().asString(); + + assertThat(res.body()).isEqualTo("status:201"); + } + + @Test + void ctx_host() { + HttpResponse res = pair.request().path("host") + .GET().asString(); + + assertThat(res.body()).contains("host:localhost"); + } + + @Test + void ctx_ip() { + HttpResponse res = pair.request().path("ip") + .GET().asString(); + + assertThat(res.body()).isEqualTo("ip:127.0.0.1"); + } + + @Test + void ctx_isMultiPart_when_not() { + HttpResponse res = pair.request().path("multipart") + .formParam("a", "aval") + .POST().asString(); + + assertThat(res.body()).isEqualTo("isMultipart:false isMultipartFormData:false"); + } + + + @Test + void ctx_isMultiPart_when_nothing() { + HttpResponse res = pair.request().path("multipart") + .body("junk") + .POST().asString(); + + assertThat(res.body()).isEqualTo("isMultipart:false isMultipartFormData:false"); + } + +// @Test +// void ctx_isMultiPart_when_isMultipart() { +// HttpResponse res = pair.request().path("multipart") +// .header("Content-Type", "multipart/foo") +// .body("junk") +// .POST().asString(); +// +// assertThat(res.body()).isEqualTo("isMultipart:true isMultipartFormData:false"); +// } +// +// @Test +// void ctx_isMultiPart_when_isMultipartFormData() { +// HttpResponse res = pair.request().path("multipart") +// .header("Content-Type", "multipart/form-data") +// .body("junk") +// .POST().asString(); +// +// assertThat(res.body()).isEqualTo("isMultipart:true isMultipartFormData:true"); +// } + + @Test + void ctx_methodPathPortProtocol() { + HttpResponse res = pair.request().path("method") + .GET().asString(); + + assertThat(res.body()).isEqualTo("method:GET path:/method protocol:HTTP/1.1 port:" + pair.port()); + } + + @Test + void post_body() { + HttpResponse res = pair.request().path("echo").body("simple").POST().asString(); + assertThat(res.body()).isEqualTo("req-body[simple]"); + } + + @Test + void get_path_path() { + var res = pair.request() + .path("A").path("B").GET().asString(); + + assertThat(res.body()).isEqualTo("ze-get-{a=A, b=B}"); + + res = pair.request() + .path("one").path("bar").body("simple").POST().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("ze-post-{a=one, b=bar}"); + } + +} diff --git a/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/HelloDto.java b/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/HelloDto.java new file mode 100644 index 00000000..6beeb95e --- /dev/null +++ b/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/HelloDto.java @@ -0,0 +1,27 @@ +package io.avaje.jex.grizzly; + +public class HelloDto { + + public long id; + public String name; + + @Override + public String toString() { + return "id:" + id + " name:" + name; + } + + public static HelloDto rob() { + return create(42, "rob"); + } + + public static HelloDto fi() { + return create(45, "fi"); + } + + public static HelloDto create(long id, String name) { + HelloDto me = new HelloDto(); + me.id = id; + me.name = name; + return me; + } +} diff --git a/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/HelloWorldTest.java b/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/HelloWorldTest.java new file mode 100644 index 00000000..af91f00d --- /dev/null +++ b/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/HelloWorldTest.java @@ -0,0 +1,50 @@ +package io.avaje.jex.grizzly; + +import io.avaje.jex.Jex; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.slf4j.bridge.SLF4JBridgeHandler; + +import java.net.http.HttpResponse; +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +class HelloWorldTest { + + static { + SLF4JBridgeHandler.removeHandlersForRootLogger(); + SLF4JBridgeHandler.install(); + } + + static TestPair pair = init(); + + static TestPair init() { + var app = Jex.create() + .routing(routing -> routing + .get("/", ctx -> ctx.text("hello")) + ); + return TestPair.create(app); + } + + @AfterAll + static void end() { + pair.shutdown(); + } + + @Test + void get() { + HttpResponse res = pair.request().GET().asString(); + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("hello"); + } + + @Test + void getAgain() { + HttpResponse res = pair.request().GET().asString(); + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("hello"); + } + +} diff --git a/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/JsonTest.java b/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/JsonTest.java new file mode 100644 index 00000000..73f56469 --- /dev/null +++ b/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/JsonTest.java @@ -0,0 +1,121 @@ +package io.avaje.jex.grizzly; + +import io.avaje.jex.Jex; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpHeaders; +import java.net.http.HttpResponse; +import java.util.List; +import java.util.stream.Stream; + +import static java.util.Arrays.asList; +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; + +class JsonTest { + + static List HELLO_BEANS = asList(HelloDto.rob(), HelloDto.fi()); + + static AutoCloseIterator ITERATOR = createBeanIterator(); + + private static AutoCloseIterator createBeanIterator() { + return new AutoCloseIterator<>(HELLO_BEANS.iterator()); + } + + static TestPair pair = init(); + + static TestPair init() { + Jex app = Jex.create() + .routing(routing -> routing + .get("/", ctx -> ctx.json(HelloDto.rob())) //.header("x2-foo","asd") + .get("/iterate", ctx -> ctx.jsonStream(ITERATOR)) + .get("/stream", ctx -> ctx.jsonStream(HELLO_BEANS.stream())) + .post("/", ctx -> ctx.text("bean[" + ctx.bodyAsClass(HelloDto.class) + "]"))); + + return TestPair.create(app); + } + + @AfterAll + static void end() { + pair.shutdown(); + } + + @Test + void get() { + + var bean = pair.request() + .GET() + .bean(HelloDto.class); + + assertThat(bean.id).isEqualTo(42); + assertThat(bean.name).isEqualTo("rob"); + + final HttpResponse hres = pair.request() + .GET().asString(); + + final HttpHeaders headers = hres.headers(); + assertThat(headers.firstValue("content-type").get()).isEqualTo("application/json"); + } + + @Test + void stream_viaIterator() { + final Stream beanStream = pair.request() + .path("iterate") + .GET() + .stream(HelloDto.class); + + // assert AutoCloseable iterator on the server-side was closed + assertThat(ITERATOR.isClosed()).isTrue(); + // expect client gets the expected stream of beans + assertCollectedStream(beanStream); + } + + @Test + void stream() { + final Stream beanStream = pair.request() + .path("stream") + .GET() + .stream(HelloDto.class); + + assertCollectedStream(beanStream); + } + + private void assertCollectedStream(Stream beanStream) { + final List collectedBeans = beanStream.collect(toList()); + assertThat(collectedBeans).hasSize(2); + + final HelloDto first = collectedBeans.get(0); + assertThat(first.id).isEqualTo(42); + assertThat(first.name).isEqualTo("rob"); + + final HelloDto second = collectedBeans.get(1); + assertThat(second.id).isEqualTo(45); + assertThat(second.name).isEqualTo("fi"); + } + + @Test + void post() { + HelloDto dto = new HelloDto(); + dto.id = 42; + dto.name = "rob was here"; + + var res = pair.request() + .body(dto) + .POST().asString(); + + assertThat(res.body()).isEqualTo("bean[id:42 name:rob was here]"); + assertThat(res.statusCode()).isEqualTo(200); + + dto.id = 99; + dto.name = "fi"; + + res = pair.request() + .body(dto) + .POST().asString(); + + assertThat(res.body()).isEqualTo("bean[id:99 name:fi]"); + assertThat(res.statusCode()).isEqualTo(200); + } + +} diff --git a/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/QueryParamTest.java b/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/QueryParamTest.java new file mode 100644 index 00000000..3ebd8b96 --- /dev/null +++ b/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/QueryParamTest.java @@ -0,0 +1,148 @@ +package io.avaje.jex.grizzly; + +import io.avaje.jex.Jex; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpResponse; + +import static org.assertj.core.api.Assertions.assertThat; + +class QueryParamTest { + + static TestPair pair = init(); + + static TestPair init() { + var app = Jex.create() + .routing(routing -> routing + .get("/", ctx -> ctx.text("hello")) + .get("/one/{id}", ctx -> ctx.text("one-" + ctx.pathParam("id") + "|match:" + ctx.matchedPath())) + .get("/one/{id}/{b}", ctx -> ctx.text("path:" + ctx.pathParamMap() + "|query:" + ctx.queryParam("z") + "|match:" + ctx.matchedPath())) + .get("/queryParamMap", ctx -> ctx.text("qpm: "+ctx.queryParamMap())) + .get("/queryParams", ctx -> ctx.text("qps: "+ctx.queryParams("a"))) + .get("/queryString", ctx -> ctx.text("qs: "+ctx.queryString())) + .get("/scheme", ctx -> ctx.text("scheme: "+ctx.scheme())) + ); + return TestPair.create(app); + } + + @AfterAll + static void end() { + pair.shutdown(); + } + + @Test + void get() { + HttpResponse res = pair.request().GET().asString(); + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("hello"); + } + + @Test + void getOne_path() { + var res = pair.request() + .path("one").path("foo").GET().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("one-foo|match:/one/{id}"); + + res = pair.request() + .path("one").path("bar").GET().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("one-bar|match:/one/{id}"); + } + + @Test + void getOne_path_path() { + var res = pair.request() + .path("one").path("foo").path("bar") + .GET().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("path:{id=foo, b=bar}|query:null|match:/one/{id}/{b}"); + + res = pair.request() + .path("one").path("fo").path("ba").queryParam("z", "42") + .GET().asString(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("path:{id=fo, b=ba}|query:42|match:/one/{id}/{b}"); + } + + @Test + void queryParamMap_when_empty() { + HttpResponse res = pair.request().path("queryParamMap").GET().asString(); + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("qpm: {}"); + } + + @Test + void queryParamMap_keyWithMultiValues_expect_firstValueInMap() { + HttpResponse res = pair.request().path("queryParamMap") + .queryParam("a","AVal0") + .queryParam("a","AVal1") + .queryParam("b", "BVal") + .GET().asString(); + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("qpm: {a=AVal0, b=BVal}"); + } + + @Test + void queryParamMap_basic() { + HttpResponse res = pair.request().path("queryParamMap") + .queryParam("a","AVal") + .queryParam("b", "BVal") + .GET().asString(); + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("qpm: {a=AVal, b=BVal}"); + } + + @Test + void queryParams_basic() { + HttpResponse res = pair.request().path("queryParams") + .queryParam("a","one") + .queryParam("a", "two") + .GET().asString(); + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("qps: [one, two]"); + } + + @Test + void queryParams_when_null_expect_emptyList() { + HttpResponse res = pair.request().path("queryParams") + .queryParam("b","one") + .GET().asString(); + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("qps: []"); + } + + @Test + void queryString_when_null() { + HttpResponse res = pair.request().path("queryString") + .GET().asString(); + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("qs: null"); + } + + @Test + void queryString_when_set() { + HttpResponse res = pair.request().path("queryString") + .queryParam("foo","f1") + .queryParam("bar","b1") + .queryParam("bar","b2") + .GET().asString(); + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("qs: foo=f1&bar=b1&bar=b2"); + } + + @Test + void scheme() { + HttpResponse res = pair.request().path("scheme") + .queryParam("foo","f1") + .GET().asString(); + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body()).isEqualTo("scheme: http"); + } + +} diff --git a/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/TestPair.java b/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/TestPair.java new file mode 100644 index 00000000..28c1dec6 --- /dev/null +++ b/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/TestPair.java @@ -0,0 +1,62 @@ +package io.avaje.jex.grizzly; + +import io.avaje.http.client.HttpClientContext; +import io.avaje.http.client.HttpClientRequest; +import io.avaje.http.client.JacksonBodyAdapter; +import io.avaje.jex.Jex; + +import java.util.Random; + +/** + * Server and Client pair for a test. + */ +public class TestPair { + + private final int port; + + private final Jex.Server server; + + private final HttpClientContext client; + + public TestPair(int port, Jex.Server server, HttpClientContext client) { + this.port = port; + this.server = server; + this.client = client; + } + + public void shutdown() { + server.shutdown(); + } + + public HttpClientRequest request() { + return client.request(); + } + + public int port() { + return port; + } + + public String url() { + return client.url().build(); + } + + public static TestPair create(Jex app) { + int port = 10000 + new Random().nextInt(1000); + return create(app, port); + } + + /** + * Create a Server and Client pair for a given set of tests. + */ + public static TestPair create(Jex app, int port) { + var jexServer = app.port(port).start(); + + var url = "http://localhost:" + port; + var client = HttpClientContext.newBuilder() + .baseUrl(url) + .bodyAdapter(new JacksonBodyAdapter()) + .build(); + + return new TestPair(port, jexServer, client); + } +} diff --git a/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/VanillaMain.java b/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/VanillaMain.java new file mode 100644 index 00000000..004d9091 --- /dev/null +++ b/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/VanillaMain.java @@ -0,0 +1,42 @@ +package io.avaje.jex.grizzly; + +import org.glassfish.grizzly.http.server.*; + +import java.io.File; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +class VanillaMain { + + public static void main(String[] args) throws IOException, InterruptedException { + + File dir = new File("."); + System.out.println("workingDirectory" + dir.getAbsolutePath()); + + CLStaticHttpHandler clStaticHttpHandler = new CLStaticHttpHandler(VanillaMain.class.getClassLoader(), "/myres/"); + StaticHttpHandler staticHttpHandler = new StaticHttpHandler(); + + final HttpServer httpServer = new HttpServerBuilder() + .handler(clStaticHttpHandler, "cl") + .handler(staticHttpHandler, "static") + .handler(new MyHandler()) + .build(); + + httpServer.start(); + Thread.currentThread().join(); + } + + static class MyHandler extends HttpHandler { + + @Override + public void service(Request request, Response response) throws Exception { + final SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); + final String date = format.format(new Date(System.currentTimeMillis())); + response.setContentType("text/plain"); + response.setContentLength(date.length()); + response.getWriter().write(date); + } + } +} diff --git a/avaje-jex-grizzly/src/test/resources/logback-test.xml b/avaje-jex-grizzly/src/test/resources/logback-test.xml new file mode 100644 index 00000000..ddb21350 --- /dev/null +++ b/avaje-jex-grizzly/src/test/resources/logback-test.xml @@ -0,0 +1,19 @@ + + + + TRACE + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + diff --git a/avaje-jex-grizzly/src/test/resources/myres/hello.txt b/avaje-jex-grizzly/src/test/resources/myres/hello.txt new file mode 100644 index 00000000..d6613f5f --- /dev/null +++ b/avaje-jex-grizzly/src/test/resources/myres/hello.txt @@ -0,0 +1 @@ +Hello there diff --git a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java index 28f4c744..21a262d3 100644 --- a/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java +++ b/avaje-jex-jdk/src/main/java/io/avaje/jex/jdk/JdkContext.java @@ -406,8 +406,9 @@ public String header(String key) { } @Override - public void header(String key, String value) { + public Context header(String key, String value) { exchange.getResponseHeaders().add(key, value); + return this; } @Override diff --git a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java index f3bee8f3..7f021296 100644 --- a/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java +++ b/avaje-jex-jetty/src/main/java/io/avaje/jex/jetty/JexHttpContext.java @@ -388,8 +388,9 @@ public String header(String key) { } @Override - public void header(String key, String value) { + public Context header(String key, String value) { res.setHeader(key, value); + return this; } @Override diff --git a/avaje-jex/src/main/java/io/avaje/jex/Context.java b/avaje-jex/src/main/java/io/avaje/jex/Context.java index a2109b11..3383006a 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/Context.java +++ b/avaje-jex/src/main/java/io/avaje/jex/Context.java @@ -325,7 +325,7 @@ default Context render(String name) { * @param key The header key * @param value The header value */ - void header(String key, String value); + Context header(String key, String value); /** * Return the response header. diff --git a/examples/example-grizzly/pom.xml b/examples/example-grizzly/pom.xml new file mode 100644 index 00000000..e6d8e552 --- /dev/null +++ b/examples/example-grizzly/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + org.avaje + java11-oss + 3.2 + + + org.example + example-grizzly + 1 + + + 17 + 17 + + + + + + io.avaje + avaje-jex-grizzly + 1.8-SNAPSHOT + + + + com.fasterxml.jackson.core + jackson-databind + 2.12.3 + + + + io.avaje + avaje-http-client + 1.11 + + + + org.slf4j + slf4j-api + 1.7.30 + + + + ch.qos.logback + logback-classic + 1.2.3 + + + + diff --git a/examples/example-grizzly/src/main/java/org/example/GMain.java b/examples/example-grizzly/src/main/java/org/example/GMain.java new file mode 100644 index 00000000..4c42c83d --- /dev/null +++ b/examples/example-grizzly/src/main/java/org/example/GMain.java @@ -0,0 +1,56 @@ +package org.example; + +import io.avaje.jex.Jex; +import io.avaje.jex.core.HealthPlugin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +public class GMain { + + private static final Logger log = LoggerFactory.getLogger(GMain.class); + + public static void main(String[] args) throws InterruptedException { + + Jex.create() + .attribute(Executor.class, Executors.newVirtualThreadExecutor()) + .routing(routing -> routing + //.get("/", ctx -> ctx.text("hello world")) + .get("/", ctx -> ctx.json(HelloDto.rob())) //.header("x2-foo","asd") + .get("/foo/{id}", ctx -> { + HelloDto bean = new HelloDto(); + bean.id = Integer.parseInt(ctx.pathParam("id")); + bean.name = "Rob"; + ctx.json(bean); + }) + .get("/delay", ctx -> { + log.info("delay start"); + try { + Thread.sleep(5_000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + e.printStackTrace(); + } + ctx.text("delay done"); + log.info("delay done"); + }) + .get("/dump", ctx -> dumpThreadCount()) + ) + .configure(new HealthPlugin()) + .port(7003) + .start(); + + Thread.currentThread().join(); + } + + private static void dumpThreadCount() { + Map allStackTraces = Thread.getAllStackTraces(); + System.out.println("Thread count: " + allStackTraces.size()); + Set threads = allStackTraces.keySet(); + System.out.println("Threads: " + threads); + } +} diff --git a/examples/example-grizzly/src/main/java/org/example/HelloDto.java b/examples/example-grizzly/src/main/java/org/example/HelloDto.java new file mode 100644 index 00000000..de0c8524 --- /dev/null +++ b/examples/example-grizzly/src/main/java/org/example/HelloDto.java @@ -0,0 +1,14 @@ +package org.example; + +public class HelloDto { + + public long id; + public String name; + + public static HelloDto rob() { + HelloDto bean = new HelloDto(); + bean.id = 42; + bean.name = "rob"; + return bean; + } +} diff --git a/examples/example-grizzly/src/test/java/org/example/ClientMain.java b/examples/example-grizzly/src/test/java/org/example/ClientMain.java new file mode 100644 index 00000000..ccef9d5f --- /dev/null +++ b/examples/example-grizzly/src/test/java/org/example/ClientMain.java @@ -0,0 +1,28 @@ +package org.example; + +import io.avaje.http.client.HttpClientContext; +import io.avaje.http.client.JacksonBodyAdapter; + +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpResponse; + +public class ClientMain { + + public static void main(String[] args) { + + final HttpClientContext ctx = HttpClientContext.newBuilder() + .baseUrl("http://localhost:7003") + .bodyAdapter(new JacksonBodyAdapter()) + .version(HttpClient.Version.HTTP_1_1) + .build(); + + final HttpResponse res = ctx.request() + .path("foo/99") + .GET() + .asPlainString(); + final HttpHeaders headers = res.headers(); + System.out.println("got " + res.body()); + + } +} diff --git a/examples/example-grizzly/src/test/resources/logback-test.xml b/examples/example-grizzly/src/test/resources/logback-test.xml new file mode 100644 index 00000000..2c7f5454 --- /dev/null +++ b/examples/example-grizzly/src/test/resources/logback-test.xml @@ -0,0 +1,19 @@ + + + + TRACE + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + diff --git a/examples/pom.xml b/examples/pom.xml index c81f5a77..3b788365 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -15,6 +15,8 @@ example-jdk example-jetty + example-grizzly + diff --git a/pom.xml b/pom.xml index 411c45d0..6c5fedf1 100644 --- a/pom.xml +++ b/pom.xml @@ -29,8 +29,9 @@ avaje-jex-mustache avaje-jex-jetty avaje-jex-jdk - examples - + avaje-jex-grizzly + examples + @@ -71,6 +72,13 @@ test + + org.slf4j + jul-to-slf4j + 1.7.30 + test + + From bb7da8fbcbb9fdabbea34215316a61d403c4ea59 Mon Sep 17 00:00:00 2001 From: rbygrave Date: Fri, 16 Jul 2021 15:05:24 +1200 Subject: [PATCH 26/27] Examples to use java 11 --- avaje-jex-grizzly/pom.xml | 3 +-- .../src/test/java/io/avaje/jex/grizzly/TestPair.java | 2 ++ examples/example-grizzly/pom.xml | 3 +-- examples/example-jdk/pom.xml | 6 ++---- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/avaje-jex-grizzly/pom.xml b/avaje-jex-grizzly/pom.xml index 85f09479..ba000877 100644 --- a/avaje-jex-grizzly/pom.xml +++ b/avaje-jex-grizzly/pom.xml @@ -12,8 +12,7 @@ avaje-jex-grizzly - 17 - 17 + diff --git a/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/TestPair.java b/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/TestPair.java index 28c1dec6..63cafbb7 100644 --- a/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/TestPair.java +++ b/avaje-jex-grizzly/src/test/java/io/avaje/jex/grizzly/TestPair.java @@ -5,6 +5,7 @@ import io.avaje.http.client.JacksonBodyAdapter; import io.avaje.jex.Jex; +import java.net.http.HttpClient; import java.util.Random; /** @@ -55,6 +56,7 @@ public static TestPair create(Jex app, int port) { var client = HttpClientContext.newBuilder() .baseUrl(url) .bodyAdapter(new JacksonBodyAdapter()) + .version(HttpClient.Version.HTTP_1_1) .build(); return new TestPair(port, jexServer, client); diff --git a/examples/example-grizzly/pom.xml b/examples/example-grizzly/pom.xml index e6d8e552..d24cfda7 100644 --- a/examples/example-grizzly/pom.xml +++ b/examples/example-grizzly/pom.xml @@ -14,8 +14,7 @@ 1 - 17 - 17 + 11 diff --git a/examples/example-jdk/pom.xml b/examples/example-jdk/pom.xml index b86aa3a2..f5ed20da 100644 --- a/examples/example-jdk/pom.xml +++ b/examples/example-jdk/pom.xml @@ -14,10 +14,8 @@ 1 - 17 - 17 - - + 11 + 11 From cac4c743626db10412fb41e95f4373b2a03fc89d Mon Sep 17 00:00:00 2001 From: rbygrave Date: Fri, 16 Jul 2021 15:09:23 +1200 Subject: [PATCH 27/27] Examples to use java 11 --- examples/example-grizzly/src/main/java/org/example/GMain.java | 2 +- examples/example-jdk/src/main/java/org/example/Main.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/example-grizzly/src/main/java/org/example/GMain.java b/examples/example-grizzly/src/main/java/org/example/GMain.java index 4c42c83d..4a4682ab 100644 --- a/examples/example-grizzly/src/main/java/org/example/GMain.java +++ b/examples/example-grizzly/src/main/java/org/example/GMain.java @@ -17,7 +17,7 @@ public class GMain { public static void main(String[] args) throws InterruptedException { Jex.create() - .attribute(Executor.class, Executors.newVirtualThreadExecutor()) + //.attribute(Executor.class, Executors.newVirtualThreadExecutor()) .routing(routing -> routing //.get("/", ctx -> ctx.text("hello world")) .get("/", ctx -> ctx.json(HelloDto.rob())) //.header("x2-foo","asd") diff --git a/examples/example-jdk/src/main/java/org/example/Main.java b/examples/example-jdk/src/main/java/org/example/Main.java index eedffa00..54e31fb7 100644 --- a/examples/example-jdk/src/main/java/org/example/Main.java +++ b/examples/example-jdk/src/main/java/org/example/Main.java @@ -17,7 +17,7 @@ public class Main { public static void main(String[] args) { Jex.create() - .attribute(Executor.class, Executors.newVirtualThreadExecutor()) + //.attribute(Executor.class, Executors.newVirtualThreadExecutor()) .routing(routing -> routing .get("/", ctx -> ctx.text("hello world")) .get("/foo/{id}", ctx -> {