From 42494411297e3c522fe0ac25362f45e9350044b0 Mon Sep 17 00:00:00 2001 From: Josiah Noel <32279667+SentryMan@users.noreply.github.com> Date: Sun, 1 Dec 2024 23:15:56 -0500 Subject: [PATCH] Fuse ServiceManager implementation (#116) * Fuse ServiceManagers * Update ContextUtilTest.java * Update ContextUtilTest.java * Update SpiServiceManager.java --- .../io/avaje/jex/core/BootstrapServer.java | 20 +- .../java/io/avaje/jex/core/Constants.java | 2 +- .../io/avaje/jex/core/CoreServiceManager.java | 208 -------------- .../io/avaje/jex/core/CtxServiceManager.java | 91 ------ .../java/io/avaje/jex/core/JdkContext.java | 6 +- .../io/avaje/jex/core/RoutingHandler.java | 4 +- .../io/avaje/jex/core/SpiServiceManager.java | 262 ++++++++++++++---- .../io/avaje/jex/core/ContextUtilTest.java | 20 +- 8 files changed, 228 insertions(+), 385 deletions(-) delete mode 100644 avaje-jex/src/main/java/io/avaje/jex/core/CoreServiceManager.java delete mode 100644 avaje-jex/src/main/java/io/avaje/jex/core/CtxServiceManager.java diff --git a/avaje-jex/src/main/java/io/avaje/jex/core/BootstrapServer.java b/avaje-jex/src/main/java/io/avaje/jex/core/BootstrapServer.java index 2ed7fd98..7ba28e8c 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/core/BootstrapServer.java +++ b/avaje-jex/src/main/java/io/avaje/jex/core/BootstrapServer.java @@ -32,15 +32,13 @@ public static Jex.Server start(Jex jex) { } final SpiRoutes routes = - new RoutesBuilder(jex.routing(), config.ignoreTrailingSlashes()) - .build(); + new RoutesBuilder(jex.routing(), config.ignoreTrailingSlashes()).build(); return start(jex, routes); } static Jex.Server start(Jex jex, SpiRoutes routes) { - SpiServiceManager serviceManager = CoreServiceManager.create(jex); - try { + try { final var config = jex.config(); final var socketAddress = createSocketAddress(config); final var https = config.httpsConfig(); @@ -56,8 +54,8 @@ static Jex.Server start(Jex jex, SpiRoutes routes) { final var scheme = config.scheme(); final var contextPath = config.contextPath(); - final var manager = new CtxServiceManager(serviceManager, scheme, contextPath); - final var handler = new RoutingHandler(routes, manager, config.compression()); + SpiServiceManager serviceManager = SpiServiceManager.create(jex); + final var handler = new RoutingHandler(routes, serviceManager, config.compression()); server.setExecutor(config.executor()); server.createContext(contextPath, handler); @@ -65,16 +63,18 @@ static Jex.Server start(Jex jex, SpiRoutes routes) { jex.lifecycle().status(AppLifecycle.Status.STARTED); log.log( - INFO, - "started com.sun.net.httpserver.HttpServer on port {0}://{1}", - scheme, socketAddress); + INFO, + "started com.sun.net.httpserver.HttpServer on port {0}://{1}", + scheme, + socketAddress); return new JdkJexServer(server, jex.lifecycle(), handler); } catch (IOException e) { throw new UncheckedIOException(e); } } - private static InetSocketAddress createSocketAddress(JexConfig config) throws UnknownHostException { + private static InetSocketAddress createSocketAddress(JexConfig config) + throws UnknownHostException { final var inetAddress = config.host() == null ? null : InetAddress.getByName(config.host()); return new InetSocketAddress(inetAddress, config.port()); } diff --git a/avaje-jex/src/main/java/io/avaje/jex/core/Constants.java b/avaje-jex/src/main/java/io/avaje/jex/core/Constants.java index 3d644975..5a90e9bb 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/core/Constants.java +++ b/avaje-jex/src/main/java/io/avaje/jex/core/Constants.java @@ -16,7 +16,7 @@ private Constants() {} public static final String HOST = "Host"; public static final String USER_AGENT = "User-Agent"; public static final String ACCEPT_ENCODING = "Accept-Encoding"; - + public static final String TEXT_HTML = "text/html"; public static final String TEXT_PLAIN = "text/plain"; public static final String TEXT_HTML_UTF8 = "text/html;charset=utf-8"; 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 deleted file mode 100644 index 49868cc1..00000000 --- a/avaje-jex/src/main/java/io/avaje/jex/core/CoreServiceManager.java +++ /dev/null @@ -1,208 +0,0 @@ -package io.avaje.jex.core; - -import java.io.InputStream; -import java.io.OutputStream; -import java.io.UncheckedIOException; -import java.io.UnsupportedEncodingException; -import java.lang.System.Logger.Level; -import java.net.URLDecoder; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Stream; - -import io.avaje.jex.Context; -import io.avaje.jex.Jex; -import io.avaje.jex.Routing; -import io.avaje.jex.core.json.JacksonJsonService; -import io.avaje.jex.core.json.JsonbJsonService; -import io.avaje.jex.spi.JsonService; -import io.avaje.jex.spi.TemplateRender; - -/** - * Core implementation of SpiServiceManager provided to specific implementations like jetty etc. - */ -final class CoreServiceManager implements SpiServiceManager { - - private static final System.Logger log = System.getLogger("io.avaje.jex"); - static final String UTF_8 = "UTF-8"; - - private final HttpMethodMap methodMap = new HttpMethodMap(); - private final JsonService jsonService; - private final ExceptionManager exceptionHandler; - private final TemplateManager templateManager; - - static SpiServiceManager create(Jex jex) { - return new Builder(jex).build(); - } - - CoreServiceManager(JsonService jsonService, ExceptionManager manager, TemplateManager templateManager) { - this.jsonService = jsonService; - this.exceptionHandler = manager; - this.templateManager = templateManager; - } - - @Override - public T jsonRead(Class clazz, InputStream is) { - return jsonService.jsonRead(clazz, is); - } - - @Override - public void jsonWrite(Object bean, OutputStream os) { - jsonService.jsonWrite(bean, os); - } - - @Override - public void jsonWriteStream(Stream stream, OutputStream os) { - try (stream) { - jsonService.jsonWriteStream(stream.iterator(), os); - } - } - - @Override - public void jsonWriteStream(Iterator iterator, OutputStream os) { - try { - jsonService.jsonWriteStream(iterator, os); - } finally { - maybeClose(iterator); - } - } - - @Override - public void maybeClose(Object iterator) { - if (iterator instanceof AutoCloseable closeable) { - try { - closeable.close(); - } catch (Exception e) { - throw new RuntimeException("Error closing iterator " + iterator, e); - } - } - } - - @Override - public Routing.Type lookupRoutingType(String method) { - return methodMap.get(method); - } - - @Override - public void handleException(JdkContext ctx, Exception e) { - exceptionHandler.handle(ctx, e); - } - - @Override - public void render(Context ctx, String name, Map model) { - templateManager.render(ctx, name, model); - } - - - @Override - public String requestCharset(Context ctx) { - return parseCharset(ctx.header(Constants.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 parseParamMap(ctx.body(), charset); - } - - @Override - public Map> parseParamMap(String body, String charset) { - if (body == null || 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 final class Builder { - - private final Jex jex; - - Builder(Jex jex) { - this.jex = jex; - } - - SpiServiceManager build() { - return new CoreServiceManager( - initJsonService(), - new ExceptionManager(jex.routing().errorHandlers()), - initTemplateMgr()); - } - - JsonService initJsonService() { - final JsonService jsonService = jex.config().jsonService(); - if (jsonService != null) { - return jsonService; - } - return CoreServiceLoader.jsonService() - .orElseGet(this::defaultJsonService); - } - - /** - * Create a reasonable default JsonService if Jackson or avaje-jsonb are present. - */ - JsonService defaultJsonService() { - if (detectJackson()) { - try { - return new JacksonJsonService(); - } catch (IllegalAccessError errorNotInModulePath) { - // not in module path - log.log(Level.DEBUG, "Not using Jackson due to module path {0}", errorNotInModulePath.getMessage()); - } - } - return detectJsonb() ? new JsonbJsonService() : null; - } - - boolean detectJackson() { - return detectTypeExists("com.fasterxml.jackson.databind.ObjectMapper"); - } - - boolean detectJsonb() { - return detectTypeExists("io.avaje.jsonb.Jsonb"); - } - - private boolean detectTypeExists(String className) { - try { - Class.forName(className); - return true; - } catch (ClassNotFoundException e) { - return false; - } - } - - TemplateManager initTemplateMgr() { - TemplateManager mgr = new TemplateManager(); - mgr.register(jex.config().renderers()); - for (TemplateRender render : CoreServiceLoader.getRenders()) { - mgr.registerDefault(render); - } - return mgr; - } - - } -} diff --git a/avaje-jex/src/main/java/io/avaje/jex/core/CtxServiceManager.java b/avaje-jex/src/main/java/io/avaje/jex/core/CtxServiceManager.java deleted file mode 100644 index 20135c06..00000000 --- a/avaje-jex/src/main/java/io/avaje/jex/core/CtxServiceManager.java +++ /dev/null @@ -1,91 +0,0 @@ -package io.avaje.jex.core; - -import java.io.InputStream; -import java.io.OutputStream; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.stream.Stream; - -import io.avaje.jex.Context; -import io.avaje.jex.Routing; - -final class CtxServiceManager implements SpiServiceManager { - - private final String scheme; - private final String contextPath; - private final SpiServiceManager delegate; - - CtxServiceManager(SpiServiceManager delegate, String scheme, String contextPath) { - this.delegate = delegate; - this.scheme = scheme; - this.contextPath = contextPath; - } - - OutputStream createOutputStream(JdkContext jdkContext) { - return new BufferedOutStream(jdkContext); - } - - String scheme() { - return scheme; - } - - String contextPath() { - return contextPath; - } - - @Override - public T jsonRead(Class clazz, InputStream is) { - return delegate.jsonRead(clazz, is); - } - - @Override - public void jsonWrite(Object bean, OutputStream os) { - delegate.jsonWrite(bean, os); - } - - @Override - public void jsonWriteStream(Stream stream, OutputStream os) { - delegate.jsonWriteStream(stream, os); - } - - @Override - public void jsonWriteStream(Iterator iterator, OutputStream os) { - delegate.jsonWriteStream(iterator, os); - } - - @Override - public void maybeClose(Object iterator) { - delegate.maybeClose(iterator); - } - - @Override - public Routing.Type lookupRoutingType(String method) { - return delegate.lookupRoutingType(method); - } - - @Override - public void handleException(JdkContext 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); - } - - @Override - public Map> parseParamMap(String body, String charset) { - return delegate.parseParamMap(body, charset); - } -} diff --git a/avaje-jex/src/main/java/io/avaje/jex/core/JdkContext.java b/avaje-jex/src/main/java/io/avaje/jex/core/JdkContext.java index 9b13aa13..3c7d7b49 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/core/JdkContext.java +++ b/avaje-jex/src/main/java/io/avaje/jex/core/JdkContext.java @@ -42,7 +42,7 @@ final class JdkContext implements Context { private static final int SC_MOVED_TEMPORARILY = 302; private static final String SET_COOKIE = "Set-Cookie"; private static final String COOKIE = "Cookie"; - private final CtxServiceManager mgr; + private final SpiServiceManager mgr; private final CompressionConfig compressionConfig; private final String path; private final Map pathParams; @@ -57,7 +57,7 @@ final class JdkContext implements Context { private String characterEncoding; JdkContext( - CtxServiceManager mgr, + SpiServiceManager mgr, CompressionConfig compressionConfig, HttpExchange exchange, String path, @@ -73,7 +73,7 @@ final class JdkContext implements Context { /** Create when no route matched. */ JdkContext( - CtxServiceManager mgr, + SpiServiceManager mgr, CompressionConfig compressionConfig, HttpExchange exchange, String path, diff --git a/avaje-jex/src/main/java/io/avaje/jex/core/RoutingHandler.java b/avaje-jex/src/main/java/io/avaje/jex/core/RoutingHandler.java index 17f0c447..46f36eb8 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/core/RoutingHandler.java +++ b/avaje-jex/src/main/java/io/avaje/jex/core/RoutingHandler.java @@ -17,11 +17,11 @@ final class RoutingHandler implements HttpHandler { private final SpiRoutes routes; - private final CtxServiceManager mgr; + private final SpiServiceManager mgr; private final CompressionConfig compressionConfig; private final List filters; - RoutingHandler(SpiRoutes routes, CtxServiceManager mgr, CompressionConfig compressionConfig) { + RoutingHandler(SpiRoutes routes, SpiServiceManager mgr, CompressionConfig compressionConfig) { this.mgr = mgr; this.routes = routes; this.filters = routes.filters(); diff --git a/avaje-jex/src/main/java/io/avaje/jex/core/SpiServiceManager.java b/avaje-jex/src/main/java/io/avaje/jex/core/SpiServiceManager.java index c244adaf..886f56d5 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/core/SpiServiceManager.java +++ b/avaje-jex/src/main/java/io/avaje/jex/core/SpiServiceManager.java @@ -2,71 +2,215 @@ import java.io.InputStream; import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.io.UnsupportedEncodingException; +import java.lang.System.Logger.Level; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Collections; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.stream.Stream; import io.avaje.jex.Context; +import io.avaje.jex.Jex; import io.avaje.jex.Routing; +import io.avaje.jex.core.json.JacksonJsonService; +import io.avaje.jex.core.json.JsonbJsonService; +import io.avaje.jex.spi.JsonService; +import io.avaje.jex.spi.TemplateRender; -/** - * Core service methods available to Context implementations. - */ -public sealed interface SpiServiceManager permits CoreServiceManager, CtxServiceManager { - - /** - * Read and return the type from json request content. - */ - T jsonRead(Class clazz, InputStream ctx); - - /** - * Write as json to response content. - */ - void jsonWrite(Object bean, OutputStream ctx); - - /** - * Write as json stream to response content. - */ - void jsonWriteStream(Stream stream, OutputStream ctx); - - /** - * Write as json stream to response content. - */ - void jsonWriteStream(Iterator iterator, OutputStream 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(JdkContext ctx, Exception e); - - /** - * Render using template manager. - */ - void render(Context ctx, String name, Map model); - - /** - * 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); - - /** - * Parse and return the content as url encoded parameters. - */ - Map> parseParamMap(String body, String charset); +/** Core service methods available to Context implementations. */ +final class SpiServiceManager { + + private static final System.Logger log = System.getLogger("io.avaje.jex"); + static final String UTF_8 = "UTF-8"; + + private final HttpMethodMap methodMap = new HttpMethodMap(); + private final JsonService jsonService; + private final ExceptionManager exceptionHandler; + private final TemplateManager templateManager; + private final String scheme; + private final String contextPath; + + static SpiServiceManager create(Jex jex) { + return new Builder(jex).build(); + } + + SpiServiceManager( + JsonService jsonService, + ExceptionManager manager, + TemplateManager templateManager, + String scheme, + String contextPath) { + this.jsonService = jsonService; + this.exceptionHandler = manager; + this.templateManager = templateManager; + this.scheme = scheme; + this.contextPath = contextPath; + } + + OutputStream createOutputStream(JdkContext jdkContext) { + return new BufferedOutStream(jdkContext); + } + + public T jsonRead(Class clazz, InputStream is) { + return jsonService.jsonRead(clazz, is); + } + + public void jsonWrite(Object bean, OutputStream os) { + jsonService.jsonWrite(bean, os); + } + + public void jsonWriteStream(Stream stream, OutputStream os) { + try (stream) { + jsonService.jsonWriteStream(stream.iterator(), os); + } + } + + public void jsonWriteStream(Iterator iterator, OutputStream os) { + try { + jsonService.jsonWriteStream(iterator, os); + } finally { + maybeClose(iterator); + } + } + + public void maybeClose(Object iterator) { + if (iterator instanceof AutoCloseable closeable) { + try { + closeable.close(); + } catch (Exception e) { + throw new RuntimeException("Error closing iterator " + iterator, e); + } + } + } + + public Routing.Type lookupRoutingType(String method) { + return methodMap.get(method); + } + + public void handleException(JdkContext ctx, Exception e) { + exceptionHandler.handle(ctx, e); + } + + public void render(Context ctx, String name, Map model) { + templateManager.render(ctx, name, model); + } + + public String requestCharset(Context ctx) { + return parseCharset(ctx.header(Constants.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; + } + + public Map> formParamMap(Context ctx, String charset) { + return parseParamMap(ctx.body(), charset); + } + + public Map> parseParamMap(String body, String charset) { + if (body == null || 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); + } + } + + String scheme() { + return scheme; + } + + String contextPath() { + return contextPath; + } + + private static final class Builder { + + private final Jex jex; + + Builder(Jex jex) { + this.jex = jex; + } + + SpiServiceManager build() { + return new SpiServiceManager( + initJsonService(), + new ExceptionManager(jex.routing().errorHandlers()), + initTemplateMgr(), + jex.config().scheme(), + jex.config().contextPath()); + } + + JsonService initJsonService() { + final JsonService jsonService = jex.config().jsonService(); + if (jsonService != null) { + return jsonService; + } + return CoreServiceLoader.jsonService().orElseGet(this::defaultJsonService); + } + + /** Create a reasonable default JsonService if Jackson or avaje-jsonb are present. */ + JsonService defaultJsonService() { + if (detectJackson()) { + try { + return new JacksonJsonService(); + } catch (IllegalAccessError errorNotInModulePath) { + // not in module path + log.log( + Level.DEBUG, + "Not using Jackson due to module path {0}", + errorNotInModulePath.getMessage()); + } + } + return detectJsonb() ? new JsonbJsonService() : null; + } + + boolean detectJackson() { + return detectTypeExists("com.fasterxml.jackson.databind.ObjectMapper"); + } + + boolean detectJsonb() { + return detectTypeExists("io.avaje.jsonb.Jsonb"); + } + + private boolean detectTypeExists(String className) { + try { + Class.forName(className); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + TemplateManager initTemplateMgr() { + TemplateManager mgr = new TemplateManager(); + mgr.register(jex.config().renderers()); + for (TemplateRender render : CoreServiceLoader.getRenders()) { + mgr.registerDefault(render); + } + return mgr; + } + } } 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 79dcb785..95232a94 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 @@ -1,25 +1,23 @@ package io.avaje.jex.core; -import org.junit.jupiter.api.Test; - -import io.avaje.jex.core.CoreServiceManager; - import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Test; + class ContextUtilTest { @Test void parseCharset_defaults() { - assertThat(CoreServiceManager.parseCharset("")).isEqualTo(CoreServiceManager.UTF_8); - assertThat(CoreServiceManager.parseCharset("junk")).isEqualTo(CoreServiceManager.UTF_8); + assertThat(SpiServiceManager.parseCharset("")).isEqualTo(SpiServiceManager.UTF_8); + assertThat(SpiServiceManager.parseCharset("junk")).isEqualTo(SpiServiceManager.UTF_8); } @Test void parseCharset_caseCheck() { - 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"); + assertThat(SpiServiceManager.parseCharset("app/foo; charset=ME")).isEqualTo("ME"); + assertThat(SpiServiceManager.parseCharset("app/foo;charset=ME")).isEqualTo("ME"); + assertThat(SpiServiceManager.parseCharset("app/foo;charset = ME ")).isEqualTo("ME"); + assertThat(SpiServiceManager.parseCharset("app/foo;charset = ME;")).isEqualTo("ME"); + assertThat(SpiServiceManager.parseCharset("app/foo;charset = ME;other=junk")).isEqualTo("ME"); } }