diff --git a/docs/asciidoc/problem-details.adoc b/docs/asciidoc/problem-details.adoc index 54bf6b9261..c96452cfd2 100644 --- a/docs/asciidoc/problem-details.adoc +++ b/docs/asciidoc/problem-details.adoc @@ -1,4 +1,4 @@ -== Problem Details +=== Problem Details Most APIs have a way to report problems and errors, helping the user understand when something went wrong and what the issue is. The method used depends on the API’s style, technology, and design. @@ -12,7 +12,7 @@ If it suits the API’s needs, using this standard benefits both designers and u `Jooby` provides built-in support for `Problem Details`. -=== Set up ProblemDetails +==== Set up ProblemDetails To enable the `ProblemDetails`, simply add the following line to your configuration: @@ -44,11 +44,11 @@ problem.details { <3> You can optionally mute some exceptions logging completely. -=== Creating problems +==== Creating problems `HttpProblem` class represents the `RFC 7807` model. It is the main entity you need to work with to produce the problem. -==== Static helpers +===== Static helpers There are several handy static methods to produce a simple `HttpProblem`: @@ -111,7 +111,7 @@ Resulting response: } ---- -==== Builder +===== Builder Use builder to create a rich problem instance with all properties: @@ -126,7 +126,7 @@ throw HttpProblem.builder() .build(); ---- -=== Adding extra parameters +==== Adding extra parameters `RFC 7807` has a simple extension model: APIs are free to add any other properties to the problem details object, so all properties other than the five ones listed above are extensions. @@ -163,7 +163,7 @@ Resulting response: } ---- -=== Adding headers +==== Adding headers Some `HTTP` codes (like `413` or `426`) require additional response headers, or it may be required by third-party system/integration. `HttpProblem` support additional headers in response: @@ -177,7 +177,7 @@ throw HttpProblem.builder() .build(); ---- -=== Respond with errors details +==== Respond with errors details `RFC 9457` finally described how errors should be delivered in HTTP APIs. It is basically another extension `errors` on a root level. Adding errors is straight-forward using `error()` or `errors()` for bulk addition in builder: @@ -214,7 +214,7 @@ In response: If you need to enrich errors with more information feel free to extend `HttpProblem.Error` and make your custom errors model. ==== -=== Custom `Exception` to `HttpProblem` +==== Custom `Exception` to `HttpProblem` Apparently, you may already have many custom `Exception` classes in the codebase, and you want to make them `Problem Details` compliant without complete re-write. You can achieve this by implementing `HttpProblemMappable` interface. It allows you to control how exceptions should be transformed into `HttpProblem` if default behaviour doesn't suite your needs: @@ -233,7 +233,7 @@ public class MyException implements HttpProblemMappable { } ---- -=== Custom Problems +==== Custom Problems Extending `HttpProblem` and utilizing builder functionality makes it really easy: @@ -255,7 +255,7 @@ public class OutOfStockProblem extends HttpProblem { } ---- -=== Custom Exception Handlers +==== Custom Exception Handlers All the features described above should give you ability to rely solely on built-in global error handler. But, in case you still need custom exception handler for some reason, you still can do it: diff --git a/docs/asciidoc/routing.adoc b/docs/asciidoc/routing.adoc index d258a19c5d..e4177f8ec5 100644 --- a/docs/asciidoc/routing.adoc +++ b/docs/asciidoc/routing.adoc @@ -1362,6 +1362,56 @@ Output: Done {love}! +=== Multiple routers + +This model let you `run` multiple applications on single server instance. Each application +works like a standalone application, they don't share any kind of services. + +.Multiple routers +[source, java,role="primary"] +---- +public class Foo extends Jooby { + { + setContextPath("/foo"); + get("/hello", ctx -> ...); + } +} + +public class Bar extends Jooby { + { + setContextPath("/bar"); + get("/hello", ctx -> ...); + } +} + +import static io.jooby.Jooby.runApp; + +public class MultiApp { + public static void main(String[] args) { + runApp(args, List.of(Foo::new, Bar::new)); + } +} +---- + +.Kotlin +[source, kotlin,role="secondary"] +---- + +import io.jooby.kt.Kooby.runApp + +fun main(args: Array) { + runApp(args, ::Foo, ::Bar) +} +---- + +You write your application as always and them you deploy them using the `runApp` method. + +[IMPORTANT] +==== +Due to nature of logging framework (static loading and initialization) the logging bootstrap +might not work as you expected. It is recommend to use just the `logback.xml` or `log4j.xml` file. +==== + === Options include::router-hidden-method.adoc[] diff --git a/docs/pom.xml b/docs/pom.xml index a02739c244..ce8ab6d675 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -11,7 +11,7 @@ io.jooby.adoc.DocApp - 3.2.0 + 3.7.0 17 17 17 @@ -20,7 +20,7 @@ io.jooby - jooby-undertow + jooby-netty ${jooby.version} @@ -51,7 +51,7 @@ org.asciidoctor asciidoctorj - 2.5.7 + 3.0.0 @@ -63,7 +63,7 @@ io.methvin directory-watcher - 0.18.0 + 0.19.0 diff --git a/docs/src/main/java/io/jooby/adoc/DocGenerator.java b/docs/src/main/java/io/jooby/adoc/DocGenerator.java index aabac4f4c6..4667f78a31 100644 --- a/docs/src/main/java/io/jooby/adoc/DocGenerator.java +++ b/docs/src/main/java/io/jooby/adoc/DocGenerator.java @@ -251,30 +251,30 @@ private static void processModule( private static Options createOptions(Path basedir, Path outdir, String version, String title) throws IOException { - Attributes attributes = new Attributes(); + var attributes = Attributes.builder(); - attributes.setAttribute("love", "♡"); - attributes.setAttribute("docinfo", "shared"); - attributes.setTitle(title == null ? "jooby: do more! more easily!!" : "jooby: " + title); - attributes.setTableOfContents(Placement.LEFT); - attributes.setAttribute("toclevels", "3"); + attributes.attribute("love", "♡"); + attributes.attribute("docinfo", "shared"); + attributes.title(title == null ? "jooby: do more! more easily!!" : "jooby: " + title); + attributes.tableOfContents(Placement.LEFT); + attributes.attribute("toclevels", "3"); attributes.setAnchors(true); - attributes.setAttribute("sectlinks", ""); - attributes.setSectionNumbers(true); - attributes.setAttribute("sectnumlevels", "3"); - attributes.setLinkAttrs(true); - attributes.setNoFooter(true); - attributes.setAttribute("idprefix", ""); - attributes.setAttribute("idseparator", "-"); - attributes.setIcons("font"); - attributes.setAttribute("description", "The modular micro web framework for Java"); - attributes.setAttribute( + attributes.attribute("sectlinks", ""); + attributes.sectionNumbers(true); + attributes.attribute("sectnumlevels", "3"); + attributes.linkAttrs(true); + attributes.noFooter(true); + attributes.attribute("idprefix", ""); + attributes.attribute("idseparator", "-"); + attributes.icons("font"); + attributes.attribute("description", "The modular micro web framework for Java"); + attributes.attribute( "keywords", "Java, Modern, Micro, Web, Framework, Reactive, Lightweight, Microservices"); - attributes.setImagesDir("images"); - attributes.setSourceHighlighter("highlightjs"); - attributes.setAttribute("highlightjsdir", "js"); - attributes.setAttribute("highlightjs-theme", "agate"); - attributes.setAttribute("favicon", "images/favicon96.png"); + attributes.imagesDir("images"); + attributes.sourceHighlighter("highlightjs"); + attributes.attribute("highlightjsdir", "js"); + attributes.attribute("highlightjs-theme", "agate"); + attributes.attribute("favicon", "images/favicon96.png"); // versions: Document pom = @@ -283,21 +283,20 @@ private static Options createOptions(Path basedir, Path outdir, String version, var tagName = tag.tagName(); var value = tag.text().trim(); Stream.of(tagName, tagName.replaceAll("[.-]", "_"), tagName.replaceAll("[.-]", "-"), toJavaName(tagName)) - .forEach(key -> attributes.setAttribute(key, value)); + .forEach(key -> attributes.attribute(key, value)); }); - attributes.setAttribute("joobyVersion", version); - attributes.setAttribute("date", DateTimeFormatter.ISO_INSTANT.format(Instant.now())); + attributes.attribute("joobyVersion", version); + attributes.attribute("date", DateTimeFormatter.ISO_INSTANT.format(Instant.now())); OptionsBuilder options = Options.builder(); options.backend("html"); - options.attributes(attributes); + options.attributes(attributes.build()); options.baseDir(basedir.toAbsolutePath().toFile()); options.docType("book"); options.toDir(outdir.toFile()); options.mkDirs(true); - options.destinationDir(outdir.resolve("site").toFile()); options.safe(SafeMode.UNSAFE); return options.build(); } diff --git a/docs/src/main/java/io/jooby/adoc/JavadocProcessor.java b/docs/src/main/java/io/jooby/adoc/JavadocProcessor.java index 8b904068cd..7ad818506b 100644 --- a/docs/src/main/java/io/jooby/adoc/JavadocProcessor.java +++ b/docs/src/main/java/io/jooby/adoc/JavadocProcessor.java @@ -13,6 +13,8 @@ import java.util.stream.Collectors; import org.asciidoctor.ast.ContentNode; +import org.asciidoctor.ast.PhraseNode; +import org.asciidoctor.ast.StructuralNode; import org.asciidoctor.extension.InlineMacroProcessor; public class JavadocProcessor extends InlineMacroProcessor { @@ -22,8 +24,7 @@ public JavadocProcessor(String name) { } @Override - public Object process(ContentNode parent, String clazz, Map attributes) { - + public PhraseNode process(StructuralNode parent, String clazz, Map attributes) { StringBuilder link = new StringBuilder("https://www.javadoc.io/doc/io.jooby/jooby/latest/io.jooby/io/jooby/"); StringBuilder text = new StringBuilder(); diff --git a/jooby/src/main/java/io/jooby/Context.java b/jooby/src/main/java/io/jooby/Context.java index 47a4d6b6ea..690e2a58ab 100644 --- a/jooby/src/main/java/io/jooby/Context.java +++ b/jooby/src/main/java/io/jooby/Context.java @@ -46,6 +46,47 @@ */ public interface Context extends Registry { + /** Select an application base on context path prefix matching a provided path. */ + interface Selector { + /** + * Select an application base on context path prefix matching a provided path. + * + * @param applications List of applications. + * @param path Path to match. + * @return Best match application. + */ + Jooby select(List applications, String path); + + static Selector create(List applications) { + return applications.size() == 1 ? single() : multiple(); + } + + /** + * Select an application the best match a given path. If none matches it returns the application + * that has no context path / or the first of the list. + * + * @return Best match application. + */ + static Selector multiple() { + return (applications, path) -> { + var defaultApp = applications.get(0); + for (var app : applications) { + var contextPath = app.getContextPath(); + if ("/".equals(contextPath)) { + defaultApp = app; + } else if (path.startsWith(contextPath)) { + return app; + } + } + return defaultApp; + }; + } + + private static Selector single() { + return (applications, path) -> applications.get(0); + } + } + /** Constant for default HTTP port. */ int PORT = 80; diff --git a/jooby/src/main/java/io/jooby/Jooby.java b/jooby/src/main/java/io/jooby/Jooby.java index f15bc7f9aa..023c58ce7e 100644 --- a/jooby/src/main/java/io/jooby/Jooby.java +++ b/jooby/src/main/java/io/jooby/Jooby.java @@ -7,7 +7,6 @@ import static java.util.Collections.singletonList; import static java.util.Objects.requireNonNull; -import static java.util.Spliterators.spliteratorUnknownSize; import static java.util.stream.StreamSupport.stream; import java.io.IOException; @@ -30,7 +29,6 @@ import java.util.ServiceConfigurationError; import java.util.ServiceLoader; import java.util.Set; -import java.util.Spliterator; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicBoolean; @@ -149,7 +147,9 @@ public Jooby() { * Server options or null. * * @return Server options or null. + * @deprecated Use {@link Server#getOptions()} */ + @Deprecated(since = "3.8.0", forRemoval = true) public @Nullable ServerOptions getServerOptions() { return serverOptions; } @@ -159,7 +159,9 @@ public Jooby() { * * @param serverOptions Server options. * @return This application. + * @deprecated Use {@link Server#setOptions(ServerOptions)} */ + @Deprecated(since = "3.8.0", forRemoval = true) public @NonNull Jooby setServerOptions(@NonNull ServerOptions serverOptions) { this.serverOptions = serverOptions; return this; @@ -970,10 +972,12 @@ public Jooby setStartupSummary(List startupSummary) { * etc.. * * @return Server. + * @deprecated Use {@link Server#start(Jooby[])} */ + @Deprecated(since = "3.8.0", forRemoval = true) public @NonNull Server start() { if (server == null) { - this.server = loadServer(); + this.server = Server.loadServer(); } if (!server.getLoggerOff().isEmpty()) { this.server = MutedServer.mute(this.server); @@ -1004,31 +1008,6 @@ public Jooby setStartupSummary(List startupSummary) { } } - /** - * Load server from classpath using {@link ServiceLoader}. - * - * @return A server. - */ - private Server loadServer() { - List servers = - stream( - spliteratorUnknownSize( - ServiceLoader.load(Server.class).iterator(), Spliterator.ORDERED), - false) - .toList(); - if (servers.isEmpty()) { - throw new StartupException("Server not found."); - } - if (servers.size() > 1) { - List names = - servers.stream() - .map(it -> it.getClass().getSimpleName().toLowerCase()) - .collect(Collectors.toList()); - getLog().warn("Multiple servers found {}. Using: {}", names, names.get(0)); - } - return servers.get(0); - } - /** * Call back method that indicates application was deploy it in the given server. * @@ -1277,28 +1256,81 @@ public static void runApp( @NonNull String[] args, @NonNull ExecutionMode executionMode, @NonNull Supplier provider) { - createApp(args, executionMode, provider).start(); + runApp(args, Server.loadServer(), executionMode, List.of(provider)); } /** * Setup default environment, logging (logback or log4j2) and run application. * * @param args Application arguments. - * @param executionMode Application execution mode. * @param provider Application provider. - * @return Application. */ - public static Jooby createApp( + public static void runApp(@NonNull String[] args, @NonNull List> provider) { + runApp(args, Server.loadServer(), ExecutionMode.DEFAULT, provider); + } + + /** + * Setup default environment, logging (logback or log4j2) and run application. + * + * @param args Application arguments. + * @param executionMode Execution mode. + * @param provider Application provider. + */ + public static void runApp( @NonNull String[] args, @NonNull ExecutionMode executionMode, - @NonNull Supplier provider) { + @NonNull List> provider) { + runApp(args, Server.loadServer(), executionMode, provider); + } - configurePackage(provider.getClass().getPackage()); + /** + * Setup default environment, logging (logback or log4j2) and run application. + * + * @param args Application arguments. + * @param server Server. + * @param provider Application provider. + */ + public static void runApp( + @NonNull String[] args, @NonNull Server server, @NonNull List> provider) { + runApp(args, server, ExecutionMode.DEFAULT, provider); + } - /** Dump command line as system properties. */ + /** + * Setup default environment, logging (logback or log4j2) and run application. + * + * @param args Application arguments. + * @param server Server. + * @param executionMode Execution mode. + * @param provider Application provider. + */ + public static void runApp( + @NonNull String[] args, + @NonNull Server server, + @NonNull ExecutionMode executionMode, + @NonNull List> provider) { + /* Dump command line as system properties. */ parseArguments(args).forEach(System::setProperty); + var apps = new ArrayList(); + var targetServer = server.getLoggerOff().isEmpty() ? server : MutedServer.mute(server); + for (var factory : provider) { + var app = createApp(executionMode, factory); + app.server = targetServer; + apps.add(app); + } + targetServer.start(apps.toArray(new Jooby[0])); + } - /** Find application.env: */ + /** + * Setup default environment, logging (logback or log4j2) and run application. + * + * @param executionMode Application execution mode. + * @param provider Application provider. + * @return Application. + */ + public static Jooby createApp( + @NonNull ExecutionMode executionMode, @NonNull Supplier provider) { + configurePackage(provider.getClass().getPackage()); + /* Find application.env: */ String logfile = LoggingService.configure( provider.getClass().getClassLoader(), new EnvironmentOptions().getActiveNames()); diff --git a/jooby/src/main/java/io/jooby/LoggingService.java b/jooby/src/main/java/io/jooby/LoggingService.java index 1ec3bfe41c..4da3a100f5 100644 --- a/jooby/src/main/java/io/jooby/LoggingService.java +++ b/jooby/src/main/java/io/jooby/LoggingService.java @@ -16,6 +16,7 @@ import java.util.stream.StreamSupport; import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; /** * Describe the underlying logging system. Jooby provides two implementation: jooby-logback and @@ -62,7 +63,7 @@ public interface LoggingService { * @param names Actives environment names. Useful for choosing an environment specific logging * configuration file. */ - static String configure(@NonNull ClassLoader classLoader, @NonNull List names) { + static @Nullable String configure(@NonNull ClassLoader classLoader, @NonNull List names) { // Supported well-know implementation String[] keys = {"logback.configurationFile", "log4j.configurationFile"}; for (String key : keys) { @@ -177,7 +178,7 @@ private static List logFile( return result; } - private static String property(String name) { + private static @Nullable String property(String name) { return System.getProperty(name, System.getenv(name)); } } diff --git a/jooby/src/main/java/io/jooby/Router.java b/jooby/src/main/java/io/jooby/Router.java index 1f47ec2734..544ab357a0 100644 --- a/jooby/src/main/java/io/jooby/Router.java +++ b/jooby/src/main/java/io/jooby/Router.java @@ -794,11 +794,11 @@ default Object execute(@NonNull Context context) { *

If no match exists this method returns a route with a 404 handler. See {@link * Route#NOT_FOUND}. * - * @param method Method to match. + * @param pattern Pattern to match. * @param path Path to match. * @return A route match result. */ - boolean match(@NonNull String method, @NonNull String path); + boolean match(@NonNull String pattern, @NonNull String path); /* Error handler: */ diff --git a/jooby/src/main/java/io/jooby/Server.java b/jooby/src/main/java/io/jooby/Server.java index 908fb77360..51ea5db2c3 100644 --- a/jooby/src/main/java/io/jooby/Server.java +++ b/jooby/src/main/java/io/jooby/Server.java @@ -7,6 +7,8 @@ import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; +import static java.util.Spliterators.spliteratorUnknownSize; +import static java.util.stream.StreamSupport.stream; import java.io.EOFException; import java.io.IOException; @@ -14,13 +16,19 @@ import java.nio.channels.ClosedChannelException; import java.util.List; import java.util.Optional; +import java.util.ServiceLoader; +import java.util.Spliterator; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.slf4j.LoggerFactory; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; +import io.jooby.exception.StartupException; import io.jooby.internal.MutedServer; /** @@ -145,7 +153,7 @@ protected void addShutdownHook() { * @param application Application to start. * @return This server. */ - @NonNull Server start(@NonNull Jooby application); + @NonNull Server start(@NonNull Jooby... application); /** * Utility method to turn off odd logger. This help to ensure same startup log lines across server @@ -219,4 +227,30 @@ static boolean isAddressInUse(@Nullable Throwable cause) { } return false; } + + /** + * Load server from classpath using {@link ServiceLoader}. + * + * @return A server. + */ + static Server loadServer() { + List servers = + stream( + spliteratorUnknownSize( + ServiceLoader.load(Server.class).iterator(), Spliterator.ORDERED), + false) + .toList(); + if (servers.isEmpty()) { + throw new StartupException("Server not found."); + } + if (servers.size() > 1) { + List names = + servers.stream() + .map(it -> it.getClass().getSimpleName().toLowerCase()) + .collect(Collectors.toList()); + var log = LoggerFactory.getLogger(servers.get(0).getClass()); + log.warn("Multiple servers found {}. Using: {}", names, names.get(0)); + } + return servers.get(0); + } } diff --git a/jooby/src/main/java/io/jooby/internal/MutedServer.java b/jooby/src/main/java/io/jooby/internal/MutedServer.java index 2bbedaf8ce..5f6ed6fc03 100644 --- a/jooby/src/main/java/io/jooby/internal/MutedServer.java +++ b/jooby/src/main/java/io/jooby/internal/MutedServer.java @@ -65,7 +65,7 @@ public static Server mute(Server server, String... logger) { return delegate.getOptions(); } - @NonNull public Server start(@NonNull Jooby application) { + @NonNull public Server start(@NonNull Jooby... application) { loggingService.logOff(mute, () -> delegate.start(application)); return delegate; } diff --git a/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/PrefixHandler.java b/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/PrefixHandler.java new file mode 100644 index 0000000000..79a0573c9f --- /dev/null +++ b/modules/jooby-jetty/src/main/java/io/jooby/internal/jetty/PrefixHandler.java @@ -0,0 +1,41 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.jetty; + +import java.util.List; +import java.util.Map; + +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; + +public class PrefixHandler extends Handler.Abstract { + private final List> mapping; + private int defaultHandlerIndex; + + public PrefixHandler(List> mapping) { + this.mapping = mapping; + this.defaultHandlerIndex = 0; + for (int i = 0; i < mapping.size(); i++) { + if (mapping.get(i).getKey().equals("/")) { + defaultHandlerIndex = i; + break; + } + } + } + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception { + for (Map.Entry e : mapping) { + var path = request.getHttpURI().getPath(); + if (path.startsWith(e.getKey())) { + return e.getValue().handle(request, response, callback); + } + } + return mapping.get(defaultHandlerIndex).getValue().handle(request, response, callback); + } +} diff --git a/modules/jooby-jetty/src/main/java/io/jooby/jetty/JettyServer.java b/modules/jooby-jetty/src/main/java/io/jooby/jetty/JettyServer.java index 5508942916..979f805651 100644 --- a/modules/jooby-jetty/src/main/java/io/jooby/jetty/JettyServer.java +++ b/modules/jooby-jetty/src/main/java/io/jooby/jetty/JettyServer.java @@ -9,6 +9,7 @@ import java.time.Duration; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @@ -32,6 +33,7 @@ import io.jooby.*; import io.jooby.internal.jetty.JettyHandler; import io.jooby.internal.jetty.JettyHttpExpectAndContinueHandler; +import io.jooby.internal.jetty.PrefixHandler; import io.jooby.internal.jetty.http2.JettyHttp2Configurer; /** @@ -48,6 +50,8 @@ public class JettyServer extends io.jooby.Server.Base { private ThreadPool threadPool; + private List applications; + private ServerOptions options = new ServerOptions().setServer("jetty").setWorkerThreads(THREADS); private Consumer httpConfigurer; @@ -88,8 +92,9 @@ public JettyServer configure(Consumer configurer) { } @NonNull @Override - public io.jooby.Server start(@NonNull Jooby application) { + public io.jooby.Server start(@NonNull Jooby... application) { try { + this.applications = List.of(application); /* Set max request size attribute: */ System.setProperty( "org.eclipse.jetty.server.Request.maxFormContentSize", @@ -107,7 +112,6 @@ public io.jooby.Server start(@NonNull Jooby application) { var acceptors = 1; var selectors = options.getIoThreads(); this.server = new Server(threadPool); - server.addBean(application); server.setStopAtShutdown(false); JettyHttp2Configurer http2 = @@ -145,9 +149,9 @@ public io.jooby.Server start(@NonNull Jooby application) { } if (options.isSSLEnabled()) { + var classLoader = applications.get(0).getClassLoader(); var sslContextFactory = new SslContextFactory.Server(); - sslContextFactory.setSslContext( - options.getSSLContext(application.getEnvironment().getClassLoader())); + sslContextFactory.setSslContext(options.getSSLContext(classLoader)); var protocol = options.getSsl().getProtocol(); sslContextFactory.setIncludeProtocols(protocol.toArray(new String[0])); // exclude @@ -194,7 +198,7 @@ public io.jooby.Server start(@NonNull Jooby application) { var context = new ContextHandler(); boolean webSockets = - application.getRoutes().stream().anyMatch(it -> it.getMethod().equals(Router.WS)); + application[0].getRoutes().stream().anyMatch(it -> it.getMethod().equals(Router.WS)); /* ********************************* Compression *************************************/ var gzip = options.getCompressionLevel() != null; @@ -207,21 +211,10 @@ public io.jooby.Server start(@NonNull Jooby application) { server.addBean(deflater, true); } - /* ********************************* Servlet *************************************/ - var invocationType = - application.getExecutionMode() == ExecutionMode.EVENT_LOOP - ? Invocable.InvocationType.NON_BLOCKING - : Invocable.InvocationType.BLOCKING; + /* ********************************* Handler *************************************/ + var handlerList = createHandler(applications); Handler handler = - new JettyHandler( - invocationType, - application, - options.getBufferSize(), - options.getMaxRequestSize(), - options.getDefaultHeaders()); - if (options.isExpectContinue() == Boolean.TRUE) { - handler = new JettyHttpExpectAndContinueHandler(handler); - } + handlerList.size() == 1 ? handlerList.get(0).getValue() : new PrefixHandler(handlerList); context.setHandler(handler); /* ********************************* Gzip *************************************/ @@ -231,7 +224,7 @@ public io.jooby.Server start(@NonNull Jooby application) { } /* ********************************* WebSocket *************************************/ if (webSockets) { - Config conf = application.getConfig(); + Config conf = application[0].getConfig(); int maxSize = conf.hasPath("websocket.maxSize") ? conf.getBytes("websocket.maxSize").intValue() @@ -253,7 +246,7 @@ public io.jooby.Server start(@NonNull Jooby application) { server.setHandler(context); server.start(); - fireReady(List.of(application)); + fireReady(applications); } catch (Exception x) { if (io.jooby.Server.isAddressInUse(x.getCause())) { x = new BindException("Address already in use: " + options.getPort()); @@ -264,6 +257,29 @@ public io.jooby.Server start(@NonNull Jooby application) { return this; } + private List> createHandler(List applications) { + return applications.stream() + .map( + application -> { + var invocationType = + applications.get(0).getExecutionMode() == ExecutionMode.EVENT_LOOP + ? Invocable.InvocationType.NON_BLOCKING + : Invocable.InvocationType.BLOCKING; + Handler handler = + new JettyHandler( + invocationType, + applications.get(0), + options.getBufferSize(), + options.getMaxRequestSize(), + options.getDefaultHeaders()); + if (options.isExpectContinue() == Boolean.TRUE) { + handler = new JettyHttpExpectAndContinueHandler(handler); + } + return Map.entry(application.getContextPath(), handler); + }) + .toList(); + } + @NonNull @Override public List getLoggerOff() { return List.of( @@ -288,8 +304,7 @@ private void isNotInUse(List protocols, String protocol, Consumer runApp(args: Array, mode: ExecutionMode, provider: () -> Jooby.runApp(args, mode, provider) } +fun runApp(args: Array, vararg provider: () -> T) { + runApp(args, Server.loadServer(), ExecutionMode.DEFAULT, *provider) +} + +fun runApp(args: Array, mode: ExecutionMode, vararg provider: () -> T) { + runApp(args, Server.loadServer(), mode, *provider) +} + +fun runApp(args: Array, server: Server, vararg provider: () -> T) { + runApp(args, server, ExecutionMode.DEFAULT, *provider) +} + +fun runApp( + args: Array, + server: Server, + mode: ExecutionMode, + vararg provider: () -> T, +) { + Jooby.runApp(args, server, mode, provider.map { Supplier { it.invoke() } }.toList()) +} + internal fun configurePackage(value: Any) { val appname = value::class.java.name val start = appname.indexOf(".").let { if (it == -1) 0 else it + 1 } diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyContext.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyContext.java index 0d1c5dc4c7..a655b89994 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyContext.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyContext.java @@ -81,10 +81,7 @@ import io.netty.channel.DefaultFileRegion; import io.netty.handler.codec.http.*; import io.netty.handler.codec.http.cookie.ServerCookieDecoder; -import io.netty.handler.codec.http.multipart.HttpData; -import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder; -import io.netty.handler.codec.http.multipart.InterfaceHttpData; -import io.netty.handler.codec.http.multipart.InterfaceHttpPostRequestDecoder; +import io.netty.handler.codec.http.multipart.*; import io.netty.handler.codec.http.websocketx.WebSocketDecoderConfig; import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker; import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory; @@ -104,6 +101,7 @@ public class NettyContext implements DefaultContext, ChannelFutureListener { HeadersMultiMap setHeaders = new HeadersMultiMap(); private int bufferSize; InterfaceHttpPostRequestDecoder decoder; + DefaultHttpDataFactory httpDataFactory; private Router router; private Route route; ChannelHandlerContext ctx; diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyHandler.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyHandler.java index f53c0bfd7c..261592f720 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyHandler.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyHandler.java @@ -9,49 +9,47 @@ import static io.netty.handler.codec.http.HttpUtil.isTransferEncodingChunked; import java.nio.charset.StandardCharsets; +import java.util.List; import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import io.jooby.*; +import io.jooby.netty.NettyServer; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.http.*; -import io.netty.handler.codec.http.multipart.HttpDataFactory; -import io.netty.handler.codec.http.multipart.HttpPostMultipartRequestDecoder; -import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder; -import io.netty.handler.codec.http.multipart.HttpPostStandardRequestDecoder; -import io.netty.handler.codec.http.multipart.InterfaceHttpPostRequestDecoder; +import io.netty.handler.codec.http.multipart.*; import io.netty.handler.codec.http.websocketx.WebSocketFrame; import io.netty.handler.timeout.IdleStateEvent; import io.netty.util.AsciiString; public class NettyHandler extends ChannelInboundHandlerAdapter { + private final Logger log = LoggerFactory.getLogger(NettyServer.class); private static final AsciiString server = AsciiString.cached("N"); private final NettyDateService serverDate; - private final Jooby app; - private final Router router; + private final List applications; + private Router router; + private final Context.Selector ctxSelector; private final int bufferSize; private final boolean defaultHeaders; - private final HttpDataFactory factory; private final long maxRequestSize; private long contentLength; private long chunkSize; - private boolean http2; + private final boolean http2; private NettyContext context; public NettyHandler( NettyDateService dateService, - Jooby app, + List applications, long maxRequestSize, int bufferSize, - HttpDataFactory factory, boolean defaultHeaders, boolean http2) { this.serverDate = dateService; - this.app = app; - this.router = app.getRouter(); + this.applications = applications; + this.ctxSelector = Context.Selector.create(applications); this.maxRequestSize = maxRequestSize; - this.factory = factory; this.bufferSize = bufferSize; this.defaultHeaders = defaultHeaders; this.http2 = http2; @@ -61,8 +59,11 @@ public NettyHandler( public void channelRead(ChannelHandlerContext ctx, Object msg) { if (isHttpRequest(msg)) { var req = (HttpRequest) msg; + var path = pathOnly(req.uri()); + var app = ctxSelector.select(applications, path); + this.router = app.getRouter(); - context = new NettyContext(ctx, req, app, pathOnly(req.uri()), bufferSize, http2); + context = new NettyContext(ctx, req, app, path, bufferSize, http2); if (defaultHeaders) { context.setHeaders.set(HttpHeaderNames.DATE, serverDate.date()); @@ -76,7 +77,9 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { // possibly body: contentLength = contentLength(req); if (contentLength > 0 || isTransferEncodingChunked(req)) { - context.decoder = newDecoder(req, factory); + context.httpDataFactory = new DefaultHttpDataFactory(bufferSize); + context.httpDataFactory.setBaseDir(app.getTmpdir().toString()); + context.decoder = newDecoder(req, context.httpDataFactory); } else { // no body, move on router.match(context).execute(context); @@ -145,7 +148,6 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { try { - Logger log = app.getLog(); if (Server.connectionLost(cause)) { if (log.isDebugEnabled()) { if (context == null) { @@ -158,7 +160,7 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { if (context == null) { log.error("execution resulted in exception", cause); } else { - if (app.isStopped()) { + if (context.getRouter().isStopped()) { log.debug("execution resulted in exception while application was shutting down", cause); } else { context.sendError(cause); @@ -186,7 +188,10 @@ private void resetDecoderState(NettyContext context, boolean destroy) { contentLength = -1; if (destroy && context.decoder != null) { var decoder = context.decoder; + var httpDataFactory = context.httpDataFactory; context.decoder = null; + context.httpDataFactory = null; + httpDataFactory.cleanAllHttpData(); decoder.destroy(); } } diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyPipeline.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyPipeline.java index 09b5b16611..44b12e46cc 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyPipeline.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyPipeline.java @@ -5,6 +5,7 @@ */ package io.jooby.internal.netty; +import java.util.List; import java.util.function.Supplier; import io.jooby.Jooby; @@ -14,16 +15,14 @@ import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.http.*; -import io.netty.handler.codec.http.multipart.HttpDataFactory; import io.netty.handler.ssl.SslContext; public class NettyPipeline extends ChannelInitializer { private static final String H2_HANDSHAKE = "h2-handshake"; private final SslContext sslContext; private final NettyDateService serverDate; - private final HttpDataFactory httpDataFactory; private final HttpDecoderConfig decoderConfig; - private final Jooby router; + private final List applications; private final long maxRequestSize; private final int bufferSize; private final boolean defaultHeaders; @@ -34,9 +33,8 @@ public class NettyPipeline extends ChannelInitializer { public NettyPipeline( SslContext sslContext, NettyDateService dateService, - HttpDataFactory httpDataFactory, HttpDecoderConfig decoderConfig, - Jooby router, + List applications, long maxRequestSize, int bufferSize, boolean defaultHeaders, @@ -45,9 +43,8 @@ public NettyPipeline( Integer compressionLevel) { this.sslContext = sslContext; this.serverDate = dateService; - this.httpDataFactory = httpDataFactory; this.decoderConfig = decoderConfig; - this.router = router; + this.applications = applications; this.maxRequestSize = maxRequestSize; this.bufferSize = bufferSize; this.defaultHeaders = defaultHeaders; @@ -58,12 +55,12 @@ public NettyPipeline( private NettyHandler createHandler() { return new NettyHandler( - serverDate, router, maxRequestSize, bufferSize, httpDataFactory, defaultHeaders, http2); + serverDate, applications, maxRequestSize, bufferSize, defaultHeaders, http2); } @Override public void initChannel(SocketChannel ch) { - ChannelPipeline p = ch.pipeline(); + var p = ch.pipeline(); if (sslContext != null) { p.addLast("ssl", sslContext.newHandler(ch.alloc())); } diff --git a/modules/jooby-netty/src/main/java/io/jooby/netty/NettyServer.java b/modules/jooby-netty/src/main/java/io/jooby/netty/NettyServer.java index 808235cae5..9eb6135e38 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/netty/NettyServer.java +++ b/modules/jooby-netty/src/main/java/io/jooby/netty/NettyServer.java @@ -29,10 +29,6 @@ import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.handler.codec.http.HttpDecoderConfig; -import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory; -import io.netty.handler.codec.http.multipart.DiskAttribute; -import io.netty.handler.codec.http.multipart.DiskFileUpload; -import io.netty.handler.codec.http.multipart.HttpDataFactory; import io.netty.handler.ssl.ApplicationProtocolConfig; import io.netty.handler.ssl.ApplicationProtocolNames; import io.netty.handler.ssl.ClientAuth; @@ -58,7 +54,7 @@ public class NettyServer extends Server.Base { private ExecutorService worker; private NettyDataBufferFactory bufferFactory; - private Jooby application; + private List applications; private ServerOptions options = new ServerOptions().setServer("netty"); @@ -112,9 +108,10 @@ public String getName() { } @NonNull @Override - public Server start(@NonNull Jooby application) { + public Server start(@NonNull Jooby... application) { try { - this.application = application; + this.applications = List.of(application); + boolean single = applications.size() == 1; /* Worker: Application blocking code */ if (worker == null) { worker = newFixedThreadPool(options.getWorkerThreads(), new DefaultThreadFactory("worker")); @@ -123,18 +120,15 @@ public Server start(@NonNull Jooby application) { bufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT); } // Make sure context use same buffer factory - application.setBufferFactory(bufferFactory); + applications.forEach(app -> app.setBufferFactory(bufferFactory)); addShutdownHook(); fireStart(List.of(application), worker); - /* Disk attributes: */ - String tmpdir = application.getTmpdir().toString(); - DiskFileUpload.baseDirectory = tmpdir; - DiskAttribute.baseDirectory = tmpdir; + var classLoader = applications.get(0).getClassLoader(); - var transport = NettyTransport.transport(application.getClassLoader()); + var transport = NettyTransport.transport(classLoader); /* Acceptor event-loop */ this.acceptorloop = transport.createEventLoop(1, "acceptor", _50); @@ -144,35 +138,30 @@ public Server start(@NonNull Jooby application) { this.dateLoop = Executors.newSingleThreadScheduledExecutor(); var dateService = new NettyDateService(dateLoop); - /* File data factory: */ - var factory = new DefaultHttpDataFactory(options.getBufferSize()); var allocator = bufferFactory.getByteBufAllocator(); var http2 = options.isHttp2() == Boolean.TRUE; /* Bootstrap: */ if (!options.isHttpsOnly()) { - var http = - newBootstrap(allocator, transport, newPipeline(null, factory, dateService, http2)); + var http = newBootstrap(allocator, transport, newPipeline(null, dateService, http2)); http.bind(options.getHost(), options.getPort()).get(); } if (options.isSSLEnabled()) { - var javaSslContext = options.getSSLContext(application.getEnvironment().getClassLoader()); + var javaSslContext = options.getSSLContext(classLoader); var sslOptions = options.getSsl(); var protocol = sslOptions.getProtocol().toArray(String[]::new); var clientAuth = sslOptions.getClientAuth(); var sslContext = wrap(javaSslContext, toClientAuth(clientAuth), protocol, http2); - var https = - newBootstrap( - allocator, transport, newPipeline(sslContext, factory, dateService, http2)); + var https = newBootstrap(allocator, transport, newPipeline(sslContext, dateService, http2)); https.bind(options.getHost(), options.getSecurePort()).get(); } else if (options.isHttpsOnly()) { throw new IllegalArgumentException( "Server configured for httpsOnly, but ssl options not set"); } - fireReady(List.of(application)); + fireReady(applications); } catch (InterruptedException x) { throw SneakyThrows.propagate(x); } catch (ExecutionException x) { @@ -204,7 +193,7 @@ private ClientAuth toClientAuth(SslOptions.ClientAuth clientAuth) { } private NettyPipeline newPipeline( - SslContext sslContext, HttpDataFactory factory, NettyDateService dateService, boolean http2) { + SslContext sslContext, NettyDateService dateService, boolean http2) { var bufferSize = options.getBufferSize(); var headersFactory = HeadersMultiMap.httpHeadersFactory(); var decoderConfig = @@ -217,9 +206,8 @@ private NettyPipeline newPipeline( return new NettyPipeline( sslContext, dateService, - factory, decoderConfig, - application, + applications, options.getMaxRequestSize(), bufferSize, options.getDefaultHeaders(), @@ -230,7 +218,7 @@ private NettyPipeline newPipeline( @NonNull @Override public synchronized Server stop() { - fireStop(List.of(application)); + fireStop(applications); // only for jooby build where close events may take longer. NettyWebSocket.all.clear(); diff --git a/modules/jooby-test/src/main/java/io/jooby/test/JoobyExtension.java b/modules/jooby-test/src/main/java/io/jooby/test/JoobyExtension.java index 350607f59e..eedbe3a646 100644 --- a/modules/jooby-test/src/main/java/io/jooby/test/JoobyExtension.java +++ b/modules/jooby-test/src/main/java/io/jooby/test/JoobyExtension.java @@ -73,21 +73,25 @@ private Jooby startApp(ExtensionContext context, JoobyTest metadata) throws Exce Jooby app; String factoryMethod = metadata.factoryMethod(); if (factoryMethod.isEmpty()) { - app = - Jooby.createApp( - new String[] {metadata.environment()}, - metadata.executionMode(), - reflectionProvider(metadata.value())); + var defaultEnv = System.getProperty("application.env"); + System.setProperty("application.env", metadata.environment()); + app = Jooby.createApp(metadata.executionMode(), reflectionProvider(metadata.value())); + if (defaultEnv != null) { + System.setProperty("application.env", defaultEnv); + } else { + System.getProperties().remove("application.env"); + } } else { app = fromFactoryMethod(context, metadata, factoryMethod); } + var server = Server.loadServer(); ServerOptions serverOptions = app.getServerOptions(); if (serverOptions == null) { - serverOptions = new ServerOptions(); - app.setServerOptions(serverOptions); + serverOptions = server.getOptions(); } serverOptions.setPort(port(metadata.port(), DEFAULT_PORT)); - Server server = app.start(); + server.setOptions(serverOptions); + server.start(app); ExtensionContext.Store store = getStore(context); store.put("server", server); store.put("application", app); @@ -187,30 +191,41 @@ private Supplier injectionPoint( } if (type == String.class && name.get().equals("serverPath")) { return () -> { - Jooby app = application(context); - return "http://localhost:" + app.getServerOptions().getPort() + app.getContextPath(); + var app = application(context); + var server = server(context); + return "http://localhost:" + server.getOptions().getPort() + app.getContextPath(); }; } if (type == int.class && name.get().equals("serverPort")) { return () -> { - Jooby app = application(context); - return app.getServerOptions().getPort(); + var server = server(context); + return server.getOptions().getPort(); }; } return null; } private Jooby application(ExtensionContext context) { - Jooby application = getStore(context).get("application", Jooby.class); - if (application == null) { - ExtensionContext parent = - context.getParent().orElseThrow(() -> new IllegalStateException("Application not found")); - application = getStore(parent).get("application", Jooby.class); + return service(context, "application", Jooby.class); + } + + private Server server(ExtensionContext context) { + return service(context, "server", Server.class); + } + + private T service(ExtensionContext context, String name, Class type) { + var service = getStore(context).get(name, type); + if (service == null) { + var parent = + context + .getParent() + .orElseThrow(() -> new IllegalStateException("Not found: " + type.getSimpleName())); + service = getStore(parent).get(name, type); } - if (application == null) { - throw new IllegalStateException("Application not found"); + if (service == null) { + throw new IllegalStateException("Not found: " + type.getSimpleName()); } - return application; + return service; } private int port(int port, int fallback) { diff --git a/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowHandler.java b/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowHandler.java index d597427ede..677e91a063 100644 --- a/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowHandler.java +++ b/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowHandler.java @@ -6,11 +6,9 @@ package io.jooby.internal.undertow; import java.nio.charset.StandardCharsets; +import java.util.List; -import io.jooby.Context; -import io.jooby.Route; -import io.jooby.Router; -import io.jooby.StatusCode; +import io.jooby.*; import io.undertow.io.Receiver; import io.undertow.server.HttpHandler; import io.undertow.server.HttpServerExchange; @@ -22,14 +20,16 @@ import io.undertow.util.Headers; public class UndertowHandler implements HttpHandler { - protected final Router router; + protected final List applications; private final long maxRequestSize; private final int bufferSize; private final boolean defaultHeaders; + private final Context.Selector ctxSelector; public UndertowHandler( - Router router, int bufferSize, long maxRequestSize, boolean defaultHeaders) { - this.router = router; + List applications, int bufferSize, long maxRequestSize, boolean defaultHeaders) { + this.applications = applications; + this.ctxSelector = Context.Selector.create(applications); this.maxRequestSize = maxRequestSize; this.bufferSize = bufferSize; this.defaultHeaders = defaultHeaders; @@ -37,9 +37,10 @@ public UndertowHandler( @Override public void handleRequest(HttpServerExchange exchange) throws Exception { - UndertowContext context = new UndertowContext(exchange, router); + var router = ctxSelector.select(applications, exchange.getRequestPath()); + var context = new UndertowContext(exchange, router); - /** default headers: */ + /* default headers: */ HeaderMap responseHeaders = exchange.getResponseHeaders(); responseHeaders.put(Headers.CONTENT_TYPE, "text/plain"); if (defaultHeaders) { @@ -65,7 +66,7 @@ public void handleRequest(HttpServerExchange exchange) throws Exception { return; } - /** Eager body parsing: */ + /* Eager body parsing: */ FormDataParser parser = FormParserFactory.builder(false) .addParser( diff --git a/modules/jooby-undertow/src/main/java/io/jooby/undertow/UndertowServer.java b/modules/jooby-undertow/src/main/java/io/jooby/undertow/UndertowServer.java index 68e65a377c..2726575c8e 100644 --- a/modules/jooby-undertow/src/main/java/io/jooby/undertow/UndertowServer.java +++ b/modules/jooby-undertow/src/main/java/io/jooby/undertow/UndertowServer.java @@ -48,7 +48,7 @@ public class UndertowServer extends Server.Base { private static final int _10 = 10; private Undertow server; - private Jooby application; + private List applications; private ServerOptions options = new ServerOptions().setIoThreads(ServerOptions.IO_THREADS).setServer("utow"); @@ -71,15 +71,15 @@ public String getName() { } @Override - public @NonNull Server start(@NonNull Jooby application) { + public @NonNull Server start(@NonNull Jooby... application) { try { - this.application = application; + this.applications = List.of(application); addShutdownHook(); HttpHandler handler = new UndertowHandler( - application, + this.applications, options.getBufferSize(), options.getMaxRequestSize(), options.getDefaultHeaders()); @@ -134,8 +134,8 @@ public String getName() { // HTTP @ builder.setServerOption(ENABLE_HTTP2, options.isHttp2() == Boolean.TRUE); - - SSLContext sslContext = options.getSSLContext(application.getEnvironment().getClassLoader()); + var classLoader = this.applications.get(0).getClassLoader(); + SSLContext sslContext = options.getSSLContext(classLoader); if (sslContext != null) { builder.addHttpsListener(options.getSecurePort(), options.getHost(), sslContext); SslOptions ssl = options.getSsl(); @@ -148,11 +148,11 @@ public String getName() { } else if (options.isHttpsOnly()) { throw new StartupException("Server configured for httpsOnly, but ssl options not set"); } - fireStart(List.of(application), worker); + fireStart(applications, worker); server = builder.build(); server.start(); - fireReady(List.of(application)); + fireReady(applications); return this; } catch (Exception x) { @@ -181,7 +181,7 @@ private SslClientAuthMode toSslClientAuthMode(SslOptions.ClientAuth clientAuth) @NonNull @Override public synchronized Server stop() { try { - fireStop(List.of(application)); + fireStop(applications); } catch (Exception x) { throw SneakyThrows.propagate(x); } finally { diff --git a/tests/src/test/java/examples/MultiApp.java b/tests/src/test/java/examples/MultiApp.java new file mode 100644 index 0000000000..a5f7c4fd01 --- /dev/null +++ b/tests/src/test/java/examples/MultiApp.java @@ -0,0 +1,21 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package examples; + +import java.util.List; + +import examples.multiapp.BarApp; +import examples.multiapp.FooApp; +import io.jooby.ExecutionMode; +import io.jooby.Jooby; +import io.jooby.jetty.JettyServer; + +public class MultiApp { + + public static void main(String[] args) { + Jooby.runApp(args, new JettyServer(), ExecutionMode.DEFAULT, List.of(BarApp::new, FooApp::new)); + } +} diff --git a/tests/src/test/java/examples/multiapp/BarApp.java b/tests/src/test/java/examples/multiapp/BarApp.java new file mode 100644 index 0000000000..23d593c1c9 --- /dev/null +++ b/tests/src/test/java/examples/multiapp/BarApp.java @@ -0,0 +1,27 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package examples.multiapp; + +import io.jooby.Jooby; + +public class BarApp extends Jooby { + { + var services = getServices(); + services.put(BarService.class, new BarService()); + setContextPath("/bar"); + get( + "/hello", + ctx -> { + return require(BarService.class).hello(ctx.query("name").value("Hello")); + }); + + get( + "/service", + ctx -> { + return require(FooService.class).hello(ctx.query("name").value("Hello")); + }); + } +} diff --git a/tests/src/test/java/examples/multiapp/BarService.java b/tests/src/test/java/examples/multiapp/BarService.java new file mode 100644 index 0000000000..ed5d769c31 --- /dev/null +++ b/tests/src/test/java/examples/multiapp/BarService.java @@ -0,0 +1,14 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package examples.multiapp; + +public class BarService { + public BarService() {} + + public String hello(String name) { + return "Bar: " + name; + } +} diff --git a/tests/src/test/java/examples/multiapp/FooApp.java b/tests/src/test/java/examples/multiapp/FooApp.java new file mode 100644 index 0000000000..58ee1dcf47 --- /dev/null +++ b/tests/src/test/java/examples/multiapp/FooApp.java @@ -0,0 +1,36 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package examples.multiapp; + +import io.jooby.Jooby; + +public class FooApp extends Jooby { + { + var services = getServices(); + services.put(FooService.class, new FooService()); + setContextPath("/foo"); + get( + "/hello", + ctx -> { + return require(FooService.class).hello(ctx.query("name").value("Hello")); + }); + + get( + "/service", + ctx -> { + return require(BarService.class).hello(ctx.query("name").value("Hello")); + }); + + error( + (ctx, cause, code) -> { + ctx.send("Foo error handler: " + cause.getMessage()); + }); + } + + public static void main(String[] args) { + runApp(args, FooApp::new); + } +} diff --git a/tests/src/test/java/examples/multiapp/FooService.java b/tests/src/test/java/examples/multiapp/FooService.java new file mode 100644 index 0000000000..be0c73e7b2 --- /dev/null +++ b/tests/src/test/java/examples/multiapp/FooService.java @@ -0,0 +1,14 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package examples.multiapp; + +public class FooService { + public FooService() {} + + public String hello(String name) { + return "Foo: " + name; + } +} diff --git a/tests/src/test/java/io/jooby/junit/ServerExtensionImpl.java b/tests/src/test/java/io/jooby/junit/ServerExtensionImpl.java index 0914f5b79e..c33252401b 100644 --- a/tests/src/test/java/io/jooby/junit/ServerExtensionImpl.java +++ b/tests/src/test/java/io/jooby/junit/ServerExtensionImpl.java @@ -104,7 +104,7 @@ public Stream provideTestTemplateInvocationContex return serverInfos.stream(); }) .sorted() - .map(info -> invocationContext(info)); + .map(this::invocationContext); } private Class[] defaultServers(String serverName) { @@ -125,7 +125,7 @@ public String getDisplayName(int invocationIndex) { @Override public List getAdditionalExtensions() { - return Arrays.asList(new ServerParameterResolver(serverInfo.server, serverInfo.mode)); + return List.of(new ServerParameterResolver(serverInfo.server, serverInfo.mode)); } }; } diff --git a/tests/src/test/java/io/jooby/junit/ServerTestRunner.java b/tests/src/test/java/io/jooby/junit/ServerTestRunner.java index da33a624ab..18db7540cd 100644 --- a/tests/src/test/java/io/jooby/junit/ServerTestRunner.java +++ b/tests/src/test/java/io/jooby/junit/ServerTestRunner.java @@ -90,7 +90,7 @@ public void ready(SneakyThrows.Consumer2 onReady) { try { System.setProperty("___app_name__", testName); System.setProperty("___server_name__", server.getName()); - Jooby app = provider.get(); + var app = provider.get(); if (!(server instanceof NettyServer)) { app.setBufferFactory(new DefaultDataBufferFactory()); } diff --git a/tests/src/test/kotlin/KtMultiApp.kt b/tests/src/test/kotlin/KtMultiApp.kt new file mode 100644 index 0000000000..d168ba6f2e --- /dev/null +++ b/tests/src/test/kotlin/KtMultiApp.kt @@ -0,0 +1,18 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +import examples.multiapp.BarApp +import examples.multiapp.FooApp +import io.jooby.ExecutionMode +import io.jooby.kt.runApp +import io.jooby.netty.NettyServer + +fun main(args: Array) { + runApp(args, NettyServer(), ExecutionMode.DEFAULT, ::BarApp, ::FooApp) + + runApp(args, NettyServer(), ::BarApp, ::FooApp) + + runApp(args, ::BarApp, ::FooApp) +}