From 801801407bd44d6b8a464c2d1062a9fea8299e41 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sun, 12 May 2024 18:55:28 -0300 Subject: [PATCH] router: Add HTTP predicate filter to `install` - fix #3418 --- jooby/src/main/java/io/jooby/Jooby.java | 81 ++++++++++++++++++- .../java/io/jooby/internal/RouterImpl.java | 45 +++++------ .../src/test/java/io/jooby/i3418/Bar3418.java | 14 ++++ .../src/test/java/io/jooby/i3418/Foo3418.java | 14 ++++ .../test/java/io/jooby/i3418/Issue3418.java | 54 +++++++++++++ 5 files changed, 181 insertions(+), 27 deletions(-) create mode 100644 tests/src/test/java/io/jooby/i3418/Bar3418.java create mode 100644 tests/src/test/java/io/jooby/i3418/Foo3418.java create mode 100644 tests/src/test/java/io/jooby/i3418/Issue3418.java diff --git a/jooby/src/main/java/io/jooby/Jooby.java b/jooby/src/main/java/io/jooby/Jooby.java index 5c8115fe4e..54df7c7b52 100644 --- a/jooby/src/main/java/io/jooby/Jooby.java +++ b/jooby/src/main/java/io/jooby/Jooby.java @@ -90,7 +90,7 @@ public class Jooby implements Router, Registry { private final transient AtomicBoolean stopped = new AtomicBoolean(false); - private static transient Jooby owner; + private static Jooby owner; private RouterImpl router; @@ -380,6 +380,85 @@ public String getContextPath() { } } + /** + * Installs/imports a full application into this one. Applications share services, registry, + * callbacks, etc. + * + *

Application must be instantiated/created lazily via a supplier/factory. This is required due + * to the way an application is usually initialized (constructor initializer). + * + *

Working example: + * + *

{@code
+   * install("/subapp", ctx -> ctx.header("v").value("").equals("1.0"), SubApp::new);
+   *
+   * }
+ * + * Lazy creation allows to configure and setup SubApp correctly, the next example + * won't work: + * + *
{@code
+   * SubApp app = new SubApp();
+   * install("/subapp", ctx -> ctx.header("v").value("").equals("1.0"), app); // WONT WORK
+   *
+   * }
+ * + * Note: you must take care of application services across the applications. For example make sure + * you don't configure the same service twice or more in the main and imported applications too. + * + * @param path Sub path. + * @param predicate HTTP predicate. + * @param factory Application factory. + * @return This application. + */ + @NonNull public Jooby install( + @NonNull String path, + @NonNull Predicate predicate, + @NonNull SneakyThrows.Supplier factory) { + try { + owner = this; + router.install(path, predicate, factory); + return this; + } finally { + owner = null; + } + } + + /** + * Installs/imports a full application into this one. Applications share services, registry, + * callbacks, etc. + * + *

Application must be instantiated/created lazily via a supplier/factory. This is required due + * to the way an application is usually initialized (constructor initializer). + * + *

Working example: + * + *

{@code
+   * install(ctx -> ctx.header("v").value("").equals("1.0"), SubApp::new);
+   *
+   * }
+ * + * Lazy creation allows to configure and setup SubApp correctly, the next example + * won't work: + * + *
{@code
+   * SubApp app = new SubApp();
+   * install(ctx -> ctx.header("v").value("").equals("1.0"), app); // WONT WORK
+   *
+   * }
+ * + * Note: you must take care of application services across the applications. For example make sure + * you don't configure the same service twice or more in the main and imported applications too. + * + * @param predicate HTTP predicate. + * @param factory Application factory. + * @return This application. + */ + @NonNull public Jooby install( + @NonNull Predicate predicate, @NonNull SneakyThrows.Supplier factory) { + return install("/", predicate, factory); + } + /** * The underlying router. * diff --git a/jooby/src/main/java/io/jooby/internal/RouterImpl.java b/jooby/src/main/java/io/jooby/internal/RouterImpl.java index c5a3434723..dd36d7ea2b 100644 --- a/jooby/src/main/java/io/jooby/internal/RouterImpl.java +++ b/jooby/src/main/java/io/jooby/internal/RouterImpl.java @@ -41,30 +41,7 @@ import com.typesafe.config.Config; import edu.umd.cs.findbugs.annotations.NonNull; -import io.jooby.BeanConverter; -import io.jooby.Context; -import io.jooby.Cookie; -import io.jooby.Environment; -import io.jooby.ErrorHandler; -import io.jooby.ExecutionMode; -import io.jooby.Jooby; -import io.jooby.MediaType; -import io.jooby.MessageDecoder; -import io.jooby.MessageEncoder; -import io.jooby.ResultHandler; -import io.jooby.Route; -import io.jooby.RouteSet; -import io.jooby.Router; -import io.jooby.RouterOption; -import io.jooby.ServerOptions; -import io.jooby.ServerSentEmitter; -import io.jooby.ServiceKey; -import io.jooby.ServiceRegistry; -import io.jooby.SessionStore; -import io.jooby.StatusCode; -import io.jooby.ValueConverter; -import io.jooby.WebSocket; -import io.jooby.XSS; +import io.jooby.*; import io.jooby.buffer.DataBufferFactory; import io.jooby.buffer.DefaultDataBufferFactory; import io.jooby.exception.RegistryException; @@ -299,8 +276,8 @@ public Router domain(@NonNull String domain, @NonNull Router subrouter) { @NonNull @Override public RouteSet mount(@NonNull Predicate predicate, @NonNull Runnable body) { - RouteSet routeSet = new RouteSet(); - Chi tree = new Chi(); + var routeSet = new RouteSet(); + var tree = new Chi(); putPredicate(predicate, tree); int start = this.routes.size(); newStack(tree, "/", body); @@ -308,6 +285,22 @@ public RouteSet mount(@NonNull Predicate predicate, @NonNull Runnable b return routeSet; } + public Router install( + @NonNull String path, + @NonNull Predicate predicate, + @NonNull SneakyThrows.Supplier factory) { + var existingRouter = this.chi; + try { + var tree = new Chi(); + this.chi = tree; + putPredicate(predicate, tree); + path(path, factory::get); + return this; + } finally { + this.chi = existingRouter; + } + } + @NonNull @Override public Router mount(@NonNull Predicate predicate, @NonNull Router subrouter) { /** Override services: */ diff --git a/tests/src/test/java/io/jooby/i3418/Bar3418.java b/tests/src/test/java/io/jooby/i3418/Bar3418.java new file mode 100644 index 0000000000..d53cef0ab7 --- /dev/null +++ b/tests/src/test/java/io/jooby/i3418/Bar3418.java @@ -0,0 +1,14 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.i3418; + +import io.jooby.Jooby; + +public class Bar3418 extends Jooby { + { + get("/app", ctx -> getClass().getSimpleName()); + } +} diff --git a/tests/src/test/java/io/jooby/i3418/Foo3418.java b/tests/src/test/java/io/jooby/i3418/Foo3418.java new file mode 100644 index 0000000000..c8157f0fc2 --- /dev/null +++ b/tests/src/test/java/io/jooby/i3418/Foo3418.java @@ -0,0 +1,14 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.i3418; + +import io.jooby.Jooby; + +public class Foo3418 extends Jooby { + { + get("/app", ctx -> getClass().getSimpleName()); + } +} diff --git a/tests/src/test/java/io/jooby/i3418/Issue3418.java b/tests/src/test/java/io/jooby/i3418/Issue3418.java new file mode 100644 index 0000000000..7ac1b96b4b --- /dev/null +++ b/tests/src/test/java/io/jooby/i3418/Issue3418.java @@ -0,0 +1,54 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.i3418; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.jooby.junit.ServerTest; +import io.jooby.junit.ServerTestRunner; + +public class Issue3418 { + + @ServerTest + public void shouldInstallWithPredicate(ServerTestRunner runner) { + runner + .define( + app -> { + app.install(ctx -> ctx.header("v").value("").equals("1.0"), Foo3418::new); + app.install(ctx -> ctx.header("v").value("").equals("2.0"), Bar3418::new); + app.get("/app", ctx -> "App"); + }) + .ready( + http -> { + http.header("v", "1.0") + .get( + "/app", + rsp -> { + assertEquals("Foo3418", rsp.body().string()); + }); + + http.header("v", "2.0") + .get( + "/app", + rsp -> { + assertEquals("Bar3418", rsp.body().string()); + }); + + http.header("v", "somethingElse") + .get( + "/app", + rsp -> { + assertEquals("App", rsp.body().string()); + }); + + http.get( + "/app", + rsp -> { + assertEquals("App", rsp.body().string()); + }); + }); + } +}