diff --git a/core/deployment/src/main/java/io/quarkus/deployment/util/UriNormalizationUtil.java b/core/deployment/src/main/java/io/quarkus/deployment/util/UriNormalizationUtil.java new file mode 100644 index 0000000000000..bb6b858232c5a --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/util/UriNormalizationUtil.java @@ -0,0 +1,107 @@ +package io.quarkus.deployment.util; + +import java.net.URI; +import java.net.URISyntaxException; + +/** + * Common URI path resolution + */ +public class UriNormalizationUtil { + private UriNormalizationUtil() { + } + + /** + * Create a URI path from a string. The specified path can not contain + * relative {@literal ..} segments or {@literal %} characters. + *

+ * Examples: + *

+ * + * + * @param path String to convert into a URI + * @param trailingSlash true if resulting URI must end with a '/' + * @throws IllegalArgumentException if the path contains invalid characters or path segments. + */ + public static URI toURI(String path, boolean trailingSlash) { + try { + // replace inbound // with / + path = path.replaceAll("//", "/"); + // remove trailing slash if result shouldn't have one + if (!trailingSlash && path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + + if (path.contains("..") || path.contains("%")) { + throw new IllegalArgumentException("Specified path can not contain '..' or '%'. Path was " + path); + } + URI uri = new URI(path).normalize(); + if (uri.getPath().equals("")) { + return trailingSlash ? new URI("/") : new URI(""); + } else if (trailingSlash && !path.endsWith("/")) { + uri = new URI(uri.getPath() + "/"); + } + return uri; + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Specified path is an invalid URI. Path was " + path, e); + } + } + + /** + * Resolve a string path against a URI base. The specified path can not contain + * relative {@literal ..} segments or {@literal %} characters. + * + * Relative paths will be resolved against the specified base URI. + * Absolute paths will be normalized and returned. + *

+ * Examples: + *

+ * + * @param base URI to resolve relative paths. Use {@link #toURI(String, boolean)} to construct this parameter. + * + * @param segment Relative or absolute path + * @param trailingSlash true if resulting URI must end with a '/' + * @throws IllegalArgumentException if the path contains invalid characters or path segments. + */ + public static URI normalizeWithBase(URI base, String segment, boolean trailingSlash) { + if (segment == null || segment.trim().isEmpty()) { + return base; + } + URI segmentUri = toURI(segment, trailingSlash); + URI resolvedUri = base.resolve(segmentUri); + return resolvedUri; + } +} diff --git a/core/deployment/src/test/java/io/quarkus/deployment/util/UriNormalizationTest.java b/core/deployment/src/test/java/io/quarkus/deployment/util/UriNormalizationTest.java new file mode 100644 index 0000000000000..6cc52d80e2c4e --- /dev/null +++ b/core/deployment/src/test/java/io/quarkus/deployment/util/UriNormalizationTest.java @@ -0,0 +1,62 @@ +package io.quarkus.deployment.util; + +import java.net.URI; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class UriNormalizationTest { + @Test + void testToUri() { + Assertions.assertEquals("/", UriNormalizationUtil.toURI("/", true).getPath()); + Assertions.assertEquals("", UriNormalizationUtil.toURI("/", false).getPath()); + + Assertions.assertEquals("/", UriNormalizationUtil.toURI("./", true).getPath()); + Assertions.assertEquals("", UriNormalizationUtil.toURI("./", false).getPath()); + + Assertions.assertEquals("bob/", UriNormalizationUtil.toURI("bob/", true).getPath()); + Assertions.assertEquals("bob", UriNormalizationUtil.toURI("bob/", false).getPath()); + } + + @Test + void testExamples() { + URI root = UriNormalizationUtil.toURI("/", true); + URI prefix = UriNormalizationUtil.toURI("/prefix", true); + URI foo = UriNormalizationUtil.toURI("foo", true); + + Assertions.assertEquals("/example/", UriNormalizationUtil.normalizeWithBase(root, "example", true).getPath()); + Assertions.assertEquals("/example", UriNormalizationUtil.normalizeWithBase(root, "example", false).getPath()); + Assertions.assertEquals("/example/", UriNormalizationUtil.normalizeWithBase(root, "/example", true).getPath()); + Assertions.assertEquals("/example", UriNormalizationUtil.normalizeWithBase(root, "/example", false).getPath()); + + Assertions.assertEquals("/prefix/example/", UriNormalizationUtil.normalizeWithBase(prefix, "example", true).getPath()); + Assertions.assertEquals("/prefix/example", UriNormalizationUtil.normalizeWithBase(prefix, "example", false).getPath()); + Assertions.assertEquals("/example/", UriNormalizationUtil.normalizeWithBase(prefix, "/example", true).getPath()); + Assertions.assertEquals("/example", UriNormalizationUtil.normalizeWithBase(prefix, "/example", false).getPath()); + + Assertions.assertEquals("foo/example/", UriNormalizationUtil.normalizeWithBase(foo, "example", true).getPath()); + Assertions.assertEquals("foo/example", UriNormalizationUtil.normalizeWithBase(foo, "example", false).getPath()); + Assertions.assertEquals("/example/", UriNormalizationUtil.normalizeWithBase(foo, "/example", true).getPath()); + Assertions.assertEquals("/example", UriNormalizationUtil.normalizeWithBase(foo, "/example", false).getPath()); + } + + @Test + void testDubiousUriPaths() { + URI root = UriNormalizationUtil.toURI("/", true); + + Assertions.assertEquals("/", UriNormalizationUtil.normalizeWithBase(root, "#example", false).getPath()); + Assertions.assertEquals("/", UriNormalizationUtil.normalizeWithBase(root, "?example", false).getPath()); + + Assertions.assertEquals("/example", UriNormalizationUtil.normalizeWithBase(root, "./example", false).getPath()); + Assertions.assertEquals("/example", UriNormalizationUtil.normalizeWithBase(root, "//example", false).getPath()); + + Assertions.assertThrows(IllegalArgumentException.class, + () -> UriNormalizationUtil.normalizeWithBase(root, "/%2fexample", false)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> UriNormalizationUtil.normalizeWithBase(root, "junk/../example", false)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> UriNormalizationUtil.normalizeWithBase(root, "junk/../../example", false)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> UriNormalizationUtil.normalizeWithBase(root, "../example", false)); + } +} diff --git a/docs/src/main/asciidoc/openapi-swaggerui.adoc b/docs/src/main/asciidoc/openapi-swaggerui.adoc index 3e80e4f830608..8af97f105de1c 100644 --- a/docs/src/main/asciidoc/openapi-swaggerui.adoc +++ b/docs/src/main/asciidoc/openapi-swaggerui.adoc @@ -467,12 +467,13 @@ You can update the `/swagger-ui` sub path by setting the `quarkus.swagger-ui.pat [source, properties] ---- -quarkus.swagger-ui.path=/my-custom-path +quarkus.swagger-ui.path=my-custom-path ---- [WARNING] ==== The value `/` is not allowed as it blocks the application from serving anything else. +A value prefixed with '/' makes it absolute and not relative. ==== Now, we are ready to run our application: diff --git a/docs/src/main/asciidoc/writing-extensions.adoc b/docs/src/main/asciidoc/writing-extensions.adoc index 98cbf49b487a7..97e774a399f98 100644 --- a/docs/src/main/asciidoc/writing-extensions.adoc +++ b/docs/src/main/asciidoc/writing-extensions.adoc @@ -1453,9 +1453,57 @@ As a CDI based runtime, Quarkus extensions often make CDI beans available as par However, Quarkus DI solution does not support CDI Portable Extensions. Instead, Quarkus extensions can make use of various link:cdi-reference[Build Time Extension Points]. -=== Quarkus DEV Console +=== Quarkus DEV UI -You can make your extension support the link:dev-console[Quarkus DEV Console] for a greater developer experience. +You can make your extension support the link:dev-ui[Quarkus DEV UI] for a greater developer experience. + +=== Extension-defined endpoints + +Your extension can add additional, non-application endpoints to be served alongside endpoints +for Health, Metrics, OpenAPI, Swagger UI, etc. + +Use a `NonApplicationRootPathBuildItem` to define an endpoint: + +[source%nowrap,java] +---- +@BuildStep +RouteBuildItem myExtensionRoute(NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) { + return nonApplicationRootPathBuildItem.routeBuilder() + .route("custom-endpoint") + .handler(new MyCustomHandler()) + .displayOnNotFoundPage() + .build(); +} +---- + +Note that the path above does not start with a '/', indicating it is a relative path. The above +endpoint will be served relative to the configured non-application endpoint root. The non-application +endpoint root is `/q` by default, which means the resulting endpoint will be found at `/q/custom-endpoint`. + +Absolute paths are handled differently. If the above called `route("/custom-endpoint")`, the resulting +endpoint will be found at `/custom-endpoint`. + +If an extension needs nested non-application endpoints: + +[source%nowrap,java] +---- +@BuildStep +RouteBuildItem myNestedExtensionRoute(NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) { + return nonApplicationRootPathBuildItem.routeBuilder() + .nestedRoute("custom-endpoint", "deep") + .handler(new MyCustomHandler()) + .displayOnNotFoundPage() + .build(); +} +---- + +Given a default non-application endpoint root of `/q`, this will create an endpoint at `/q/custom-endpoint/deep`. + +Absolute paths also have an impact on nested endpoints. If the above called `nestedRoute("custom-endpoint", "/deep")`, +the resulting endpoint will be found at `/deep`. + +Refer to the link:all-config#quarkus-vertx-http_quarkus.http.non-application-root-path[Quarkus Vertx HTTP configuration reference] +for details on how the non-application root path is configured. === Extension Health Check diff --git a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/MicrometerProcessor.java b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/MicrometerProcessor.java index b4faaa601bb76..aeb2e8fae7c49 100644 --- a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/MicrometerProcessor.java +++ b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/MicrometerProcessor.java @@ -122,7 +122,7 @@ MetricsCapabilityBuildItem metricsCapabilityBuildItem() { MetricsCapabilityBuildItem metricsCapabilityPrometheusBuildItem( NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) { return new MetricsCapabilityBuildItem(MetricsFactory.MICROMETER::equals, - nonApplicationRootPathBuildItem.adjustPath(mConfig.export.prometheus.path)); + nonApplicationRootPathBuildItem.resolvePath(mConfig.export.prometheus.path)); } @BuildStep(onlyIf = MicrometerEnabled.class) diff --git a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/export/JsonRegistryProcessor.java b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/export/JsonRegistryProcessor.java index dbfd208e41966..9f4da05b43992 100644 --- a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/export/JsonRegistryProcessor.java +++ b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/export/JsonRegistryProcessor.java @@ -14,6 +14,7 @@ import io.quarkus.micrometer.runtime.export.JsonMeterRegistryProvider; import io.quarkus.micrometer.runtime.export.JsonRecorder; import io.quarkus.micrometer.runtime.registry.json.JsonMeterRegistry; +import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; import io.quarkus.vertx.http.deployment.RouteBuildItem; public class JsonRegistryProcessor { @@ -34,17 +35,21 @@ public void initializeJsonRegistry(MicrometerConfig config, BuildProducer registryProviders, BuildProducer routes, BuildProducer additionalBeans, + NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, JsonRecorder recorder) { additionalBeans.produce(AdditionalBeanBuildItem.builder() .addBeanClass(JsonMeterRegistryProvider.class) .setUnremovable().build()); registryProviders.produce(new MicrometerRegistryProviderBuildItem(JsonMeterRegistry.class)); - routes.produce(new RouteBuildItem.Builder() - .routeFunction(recorder.route(config.export.json.path)) + + routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .routeFunction(config.export.json.path, recorder.route()) .handler(recorder.getHandler()) - .nonApplicationRoute(true) + .requiresLegacyRedirect() .build()); - log.debug("Initialized a JSON meter registry on path=" + config.export.json.path); + + log.debug("Initialized a JSON meter registry on path=" + + nonApplicationRootPathBuildItem.resolvePath(config.export.json.path)); } } diff --git a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/export/PrometheusRegistryProcessor.java b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/export/PrometheusRegistryProcessor.java index 33ee7261b7386..7c512dd094d44 100644 --- a/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/export/PrometheusRegistryProcessor.java +++ b/extensions/micrometer/deployment/src/main/java/io/quarkus/micrometer/deployment/export/PrometheusRegistryProcessor.java @@ -15,6 +15,7 @@ import io.quarkus.micrometer.runtime.config.PrometheusConfigGroup; import io.quarkus.micrometer.runtime.export.PrometheusMeterRegistryProvider; import io.quarkus.micrometer.runtime.export.PrometheusRecorder; +import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; import io.quarkus.vertx.http.deployment.RouteBuildItem; /** @@ -53,24 +54,25 @@ MicrometerRegistryProviderBuildItem createPrometheusRegistry( @Record(value = ExecutionTime.STATIC_INIT) void createPrometheusRoute(BuildProducer routes, MicrometerConfig mConfig, + NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, PrometheusRecorder recorder) { PrometheusConfigGroup pConfig = mConfig.export.prometheus; log.debug("PROMETHEUS CONFIG: " + pConfig); // Exact match for resources matched to the root path - routes.produce(new RouteBuildItem.Builder() - .routeFunction(recorder.route(pConfig.path)) + routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .routeFunction(pConfig.path, recorder.route()) .handler(recorder.getHandler()) - .nonApplicationRoute() + .requiresLegacyRedirect() + .displayOnNotFoundPage("Metrics", pConfig.path) .build()); // Match paths that begin with the deployment path - String matchPath = pConfig.path + (pConfig.path.endsWith("/") ? "*" : "/*"); - routes.produce(new RouteBuildItem.Builder() - .routeFunction(recorder.route(matchPath)) + routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .routeFunction(pConfig.path + (pConfig.path.endsWith("/") ? "*" : "/*"), recorder.route()) .handler(recorder.getHandler()) - .nonApplicationRoute() + .requiresLegacyRedirect() .build()); } } diff --git a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/export/JsonRegistryEnabledTest.java b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/export/JsonRegistryEnabledTest.java new file mode 100644 index 0000000000000..3c2d574940bbc --- /dev/null +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/export/JsonRegistryEnabledTest.java @@ -0,0 +1,57 @@ +package io.quarkus.micrometer.deployment.export; + +import java.util.Set; + +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.composite.CompositeMeterRegistry; +import io.quarkus.micrometer.runtime.registry.json.JsonMeterRegistry; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class JsonRegistryEnabledTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withConfigurationResource("test-logging.properties") + .overrideConfigKey("quarkus.http.root-path", "/app") + .overrideConfigKey("quarkus.http.non-application-root-path", "relative") + .overrideConfigKey("quarkus.micrometer.binder-enabled-default", "false") + .overrideConfigKey("quarkus.micrometer.export.json.enabled", "true") + .overrideConfigKey("quarkus.micrometer.registry-enabled-default", "false") + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClass(PrometheusRegistryProcessor.REGISTRY_CLASS)); + + @Inject + MeterRegistry registry; + + @Inject + JsonMeterRegistry jsonMeterRegistry; + + @Test + public void testMeterRegistryPresent() { + // Prometheus is enabled (only registry) + Assertions.assertNotNull(registry, "A registry should be configured"); + Set subRegistries = ((CompositeMeterRegistry) registry).getRegistries(); + JsonMeterRegistry subPromRegistry = (JsonMeterRegistry) subRegistries.iterator().next(); + Assertions.assertEquals(JsonMeterRegistry.class, subPromRegistry.getClass(), "Should be JsonMeterRegistry"); + Assertions.assertEquals(subPromRegistry, jsonMeterRegistry, + "The only MeterRegistry should be the same bean as the JsonMeterRegistry"); + } + + @Test + public void metricsEndpoint() { + // RestAssured prepends /app for us + RestAssured.given() + .contentType("application/json") + .get("/relative/metrics") + .then() + .statusCode(200); + } +} diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/JsonConfigGroup.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/JsonConfigGroup.java index f243dfe8c3c20..446ff3da2e25e 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/JsonConfigGroup.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/JsonConfigGroup.java @@ -16,9 +16,9 @@ public class JsonConfigGroup implements MicrometerConfig.CapabilityEnabled { /** * The path for the JSON metrics endpoint. - * The default value is {@code /metrics}. + * The default value is {@code metrics}. */ - @ConfigItem(defaultValue = "/metrics") + @ConfigItem(defaultValue = "metrics") public String path; /** diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/PrometheusConfigGroup.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/PrometheusConfigGroup.java index 321b92066e1de..c8fccd1b65b8c 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/PrometheusConfigGroup.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/config/PrometheusConfigGroup.java @@ -20,9 +20,9 @@ public class PrometheusConfigGroup implements MicrometerConfig.CapabilityEnabled /** * The path for the prometheus metrics endpoint (produces text/plain). - * The default value is {@code /metrics}. + * The default value is {@code metrics}. */ - @ConfigItem(defaultValue = "/metrics") + @ConfigItem(defaultValue = "metrics") public String path; @Override diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/JsonRecorder.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/JsonRecorder.java index f58b99b51e1d8..9ab0a33fb7d90 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/JsonRecorder.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/JsonRecorder.java @@ -1,11 +1,10 @@ package io.quarkus.micrometer.runtime.export; -import java.util.function.Function; +import java.util.function.Consumer; import io.quarkus.micrometer.runtime.export.handlers.JsonHandler; import io.quarkus.runtime.annotations.Recorder; import io.vertx.ext.web.Route; -import io.vertx.ext.web.Router; @Recorder public class JsonRecorder { @@ -19,11 +18,11 @@ public JsonHandler getHandler() { return handler; } - public Function route(String path) { - return new Function() { + public Consumer route() { + return new Consumer() { @Override - public Route apply(Router router) { - return router.route(path).order(2).produces("application/json"); + public void accept(Route route) { + route.order(2).produces("application/json"); } }; } diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/PrometheusRecorder.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/PrometheusRecorder.java index f2feea32521d9..998d3c2af4aab 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/PrometheusRecorder.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/export/PrometheusRecorder.java @@ -1,11 +1,10 @@ package io.quarkus.micrometer.runtime.export; -import java.util.function.Function; +import java.util.function.Consumer; import io.quarkus.micrometer.runtime.export.handlers.PrometheusHandler; import io.quarkus.runtime.annotations.Recorder; import io.vertx.ext.web.Route; -import io.vertx.ext.web.Router; @Recorder public class PrometheusRecorder { @@ -19,11 +18,11 @@ public PrometheusHandler getHandler() { return handler; } - public Function route(String path) { - return new Function() { + public Consumer route() { + return new Consumer() { @Override - public Route apply(Router router) { - return router.route(path).order(1).produces("text/plain"); + public void accept(Route route) { + route.order(1).produces("text/plain"); } }; } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveDevModeProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveDevModeProcessor.java index 73fe8fae78fa3..62e710115e15c 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveDevModeProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveDevModeProcessor.java @@ -62,7 +62,7 @@ void addAdditionalEndpointsExceptionMapper(List endpoints = displayableEndpoints .stream() - .map(v -> new AdditionalRouteDescription(v.getEndpoint(), v.getDescription())) + .map(v -> new AdditionalRouteDescription(v.getEndpoint(httpRoot), v.getDescription())) .sorted() .collect(Collectors.toList()); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/NotFoundExceptionMapper.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/NotFoundExceptionMapper.java index a19e9c7b54dde..f0953b12a33fc 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/NotFoundExceptionMapper.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/NotFoundExceptionMapper.java @@ -173,7 +173,7 @@ private Response respond(List descriptions, HttpHeaders hea if (!additionalEndpoints.isEmpty()) { sb.resourcesStart("Additional endpoints"); for (AdditionalRouteDescription additionalEndpoint : additionalEndpoints) { - sb.staticResourcePath(adjustRoot(httpRoot, additionalEndpoint.getUri()), + sb.staticResourcePath(additionalEndpoint.getUri(), additionalEndpoint.getDescription()); } sb.resourcesEnd(); diff --git a/extensions/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyBuiltinsProcessor.java b/extensions/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyBuiltinsProcessor.java index 7caa2245b78ff..0de72fccfedde 100644 --- a/extensions/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyBuiltinsProcessor.java +++ b/extensions/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyBuiltinsProcessor.java @@ -126,7 +126,7 @@ void addAdditionalEndpointsExceptionMapper(List endpoints = displayableEndpoints .stream() .map(displayableAdditionalBuildItem -> new AdditionalRouteDescription( - displayableAdditionalBuildItem.getEndpoint(), displayableAdditionalBuildItem.getDescription())) + displayableAdditionalBuildItem.getEndpoint(httpRoot), displayableAdditionalBuildItem.getDescription())) .sorted() .collect(Collectors.toList()); diff --git a/extensions/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyServletProcessor.java b/extensions/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyServletProcessor.java index bfce28a9bc7a4..0508e4e6ffe69 100644 --- a/extensions/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyServletProcessor.java +++ b/extensions/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyServletProcessor.java @@ -56,8 +56,9 @@ public void jaxrsConfig( BuildProducer resteasyJaxrsConfig, HttpRootPathBuildItem httpRootPathBuildItem) { if (resteasyServerConfig.isPresent()) { - String rootPath = httpRootPathBuildItem.adjustPath(resteasyServerConfig.get().getRootPath()); - String defaultPath = httpRootPathBuildItem.adjustPath(resteasyServerConfig.get().getPath()); + String rp = resteasyServerConfig.get().getRootPath(); + String rootPath = httpRootPathBuildItem.resolvePath(rp.startsWith("/") ? rp.substring(1) : rp); + String defaultPath = httpRootPathBuildItem.resolvePath(resteasyServerConfig.get().getPath()); deprecatedResteasyJaxrsConfig.produce(new ResteasyJaxrsConfigBuildItem(defaultPath)); resteasyJaxrsConfig diff --git a/extensions/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/NotFoundExceptionMapper.java b/extensions/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/NotFoundExceptionMapper.java index a5ff1c04db5b7..d8553592e98ba 100644 --- a/extensions/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/NotFoundExceptionMapper.java +++ b/extensions/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/NotFoundExceptionMapper.java @@ -309,7 +309,7 @@ private Response respond(List descriptions) { if (!additionalEndpoints.isEmpty()) { sb.resourcesStart("Additional endpoints"); for (AdditionalRouteDescription additionalEndpoint : additionalEndpoints) { - sb.staticResourcePath(adjustRoot(httpRoot, additionalEndpoint.getUri()), + sb.staticResourcePath(additionalEndpoint.getUri(), additionalEndpoint.getDescription()); } sb.resourcesEnd(); diff --git a/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java b/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java index 4fa39309e35a9..50a616dc55d9b 100644 --- a/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java +++ b/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java @@ -26,6 +26,7 @@ import io.quarkus.deployment.Feature; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.Consume; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; @@ -189,6 +190,7 @@ void buildSchemaEndpoint( routeProducer.produce(new RouteBuildItem.Builder() .route(graphQLConfig.rootPath + SCHEMA_PATH) .handler(schemaHandler) + .displayOnNotFoundPage("MicroProfile GraphQL Schema") .blockingRoute() .build()); @@ -196,17 +198,15 @@ void buildSchemaEndpoint( @Record(ExecutionTime.RUNTIME_INIT) @BuildStep + @Consume(BeanContainerBuildItem.class) void buildExecutionEndpoint( BuildProducer routeProducer, - BuildProducer notFoundPageDisplayableEndpointProducer, SmallRyeGraphQLInitializedBuildItem graphQLInitializedBuildItem, SmallRyeGraphQLRecorder recorder, ShutdownContextBuildItem shutdownContext, LaunchModeBuildItem launchMode, BodyHandlerBuildItem bodyHandlerBuildItem, - SmallRyeGraphQLConfig graphQLConfig, - BeanContainerBuildItem beanContainerBuildItem // don't remove this - makes sure beanContainer is initialized - ) { + SmallRyeGraphQLConfig graphQLConfig) { /* * Ugly Hack @@ -220,23 +220,15 @@ void buildExecutionEndpoint( if (launchMode.getLaunchMode() == LaunchMode.DEVELOPMENT) { recorder.setupClDevMode(shutdownContext); } - // add graphql endpoint for not found display in dev or test mode - if (launchMode.getLaunchMode().isDevOrTest()) { - notFoundPageDisplayableEndpointProducer - .produce(new NotFoundPageDisplayableEndpointBuildItem(graphQLConfig.rootPath, - "MicroProfile GraphQL Endpoint")); - notFoundPageDisplayableEndpointProducer - .produce(new NotFoundPageDisplayableEndpointBuildItem(graphQLConfig.rootPath + SCHEMA_PATH, - "MicroProfile GraphQL Schema")); - } Boolean allowGet = ConfigProvider.getConfig().getOptionalValue(ConfigKey.ALLOW_GET, boolean.class).orElse(false); Handler executionHandler = recorder.executionHandler(graphQLInitializedBuildItem.getInitialized(), allowGet); routeProducer.produce(new RouteBuildItem.Builder() - .routeFunction(recorder.routeFunction(graphQLConfig.rootPath, bodyHandlerBuildItem.getHandler())) + .routeFunction(graphQLConfig.rootPath, recorder.routeFunction(bodyHandlerBuildItem.getHandler())) .handler(executionHandler) + .displayOnNotFoundPage("MicroProfile GraphQL Endpoint") .blockingRoute() .build()); @@ -469,9 +461,8 @@ void getGraphqlUiFinalDestination( "quarkus.smallrye-graphql.root-path-ui was set to \"/\", this is not allowed as it blocks the application from serving anything else."); } - String graphQLPath = httpRootPath.adjustPath(graphQLConfig.rootPath); - String graphQLUiPath = httpRootPath - .adjustPath(nonApplicationRootPathBuildItem.adjustPath(graphQLConfig.ui.rootPath)); + String graphQLPath = httpRootPath.resolvePath(graphQLConfig.rootPath); + String graphQLUiPath = nonApplicationRootPathBuildItem.resolvePath(graphQLConfig.ui.rootPath); AppArtifact artifact = WebJarUtil.getAppArtifact(curateOutcomeBuildItem, GRAPHQL_UI_WEBJAR_GROUP_ID, GRAPHQL_UI_WEBJAR_ARTIFACT_ID); @@ -484,10 +475,6 @@ void getGraphqlUiFinalDestination( smallRyeGraphQLBuildProducer .produce(new SmallRyeGraphQLBuildItem(tempPath.toAbsolutePath().toString(), graphQLUiPath)); - notFoundPageDisplayableEndpointProducer - .produce(new NotFoundPageDisplayableEndpointBuildItem( - nonApplicationRootPathBuildItem.adjustPath(graphQLConfig.ui.rootPath) + "/", - "MicroProfile GraphQL UI")); // Handle live reload of branding files if (liveReloadBuildItem.isLiveReload() && !liveReloadBuildItem.getChangedResources().isEmpty()) { @@ -532,20 +519,22 @@ void registerGraphQLUiHandler( SmallRyeGraphQLRuntimeConfig runtimeConfig, SmallRyeGraphQLBuildItem smallRyeGraphQLBuildItem, LaunchModeBuildItem launchMode, + NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, SmallRyeGraphQLConfig graphQLConfig) throws Exception { if (shouldInclude(launchMode, graphQLConfig)) { Handler handler = recorder.uiHandler(smallRyeGraphQLBuildItem.getGraphqlUiFinalDestination(), smallRyeGraphQLBuildItem.getGraphqlUiPath(), runtimeConfig); - routeProducer.produce(new RouteBuildItem.Builder() - .nonApplicationRoute(true) + routeProducer.produce(nonApplicationRootPathBuildItem.routeBuilder() .route(graphQLConfig.ui.rootPath) + .displayOnNotFoundPage("MicroProfile GraphQL UI") .handler(handler) + .requiresLegacyRedirect() .build()); - routeProducer.produce(new RouteBuildItem.Builder() - .nonApplicationRoute(true) + routeProducer.produce(nonApplicationRootPathBuildItem.routeBuilder() .route(graphQLConfig.ui.rootPath + "/*") .handler(handler) + .requiresLegacyRedirect() .build()); } } diff --git a/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLUIConfig.java b/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLUIConfig.java index 9c426cffbecbf..19cee4b8415a6 100644 --- a/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLUIConfig.java +++ b/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLUIConfig.java @@ -10,7 +10,7 @@ public class SmallRyeGraphQLUIConfig { * The path where GraphQL UI is available. * The value `/` is not allowed as it blocks the application from serving anything else. */ - @ConfigItem(defaultValue = "/graphql-ui") + @ConfigItem(defaultValue = "graphql-ui") String rootPath; /** diff --git a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLRecorder.java b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLRecorder.java index c2ca6c4ed1742..4ed8144b50219 100644 --- a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLRecorder.java +++ b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLRecorder.java @@ -1,6 +1,6 @@ package io.quarkus.smallrye.graphql.runtime; -import java.util.function.Function; +import java.util.function.Consumer; import javax.enterprise.inject.Instance; import javax.enterprise.inject.spi.CDI; @@ -18,7 +18,6 @@ import io.smallrye.graphql.schema.model.Schema; import io.vertx.core.Handler; import io.vertx.ext.web.Route; -import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; @Recorder @@ -76,11 +75,11 @@ public void run() { }); } - public Function routeFunction(String rootPath, Handler bodyHandler) { - return new Function() { + public Consumer routeFunction(Handler bodyHandler) { + return new Consumer() { @Override - public Route apply(Router router) { - return router.route(rootPath).handler(bodyHandler); + public void accept(Route route) { + route.handler(bodyHandler); } }; } diff --git a/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthConfig.java b/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthConfig.java index b7cba3263c84f..9a80dcc1bed0a 100644 --- a/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthConfig.java +++ b/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthConfig.java @@ -10,31 +10,31 @@ public class SmallRyeHealthConfig { /** * Root path for health-checking endpoints. */ - @ConfigItem(defaultValue = "/health") + @ConfigItem(defaultValue = "health") String rootPath; /** * The relative path of the liveness health-checking endpoint. */ - @ConfigItem(defaultValue = "/live") + @ConfigItem(defaultValue = "live") String livenessPath; /** * The relative path of the readiness health-checking endpoint. */ - @ConfigItem(defaultValue = "/ready") + @ConfigItem(defaultValue = "ready") String readinessPath; /** * The relative path of the health group endpoint. */ - @ConfigItem(defaultValue = "/group") + @ConfigItem(defaultValue = "group") String groupPath; /** * The relative path of the wellness health-checking endpoint. */ - @ConfigItem(defaultValue = "/well") + @ConfigItem(defaultValue = "well") String wellnessPath; /** diff --git a/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthProcessor.java b/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthProcessor.java index 99e57df6b6ed1..571a7a321c72b 100644 --- a/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthProcessor.java +++ b/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthProcessor.java @@ -159,21 +159,6 @@ void build(SmallRyeHealthRecorder recorder, feature.produce(new FeatureBuildItem(Feature.SMALLRYE_HEALTH)); - // add health endpoints to not found page - if (launchMode.getLaunchMode().isDevOrTest()) { - String basePath = nonApplicationRootPathBuildItem.adjustPath(healthConfig.rootPath); - displayableEndpoints.produce(new NotFoundPageDisplayableEndpointBuildItem(basePath)); - String subcontextBasePath = adjustSubcontextBasePath(basePath); - displayableEndpoints - .produce(new NotFoundPageDisplayableEndpointBuildItem(subcontextBasePath + healthConfig.livenessPath)); - displayableEndpoints - .produce(new NotFoundPageDisplayableEndpointBuildItem(subcontextBasePath + healthConfig.readinessPath)); - displayableEndpoints - .produce(new NotFoundPageDisplayableEndpointBuildItem(subcontextBasePath + healthConfig.groupPath)); - displayableEndpoints - .produce(new NotFoundPageDisplayableEndpointBuildItem(subcontextBasePath + healthConfig.wellnessPath)); - } - // Discover the beans annotated with @Health, @Liveness, @Readiness, @HealthGroup, // @HealthGroups and @Wellness even if no scope is defined beanDefiningAnnotation.produce(new BeanDefiningAnnotationBuildItem(HEALTH)); @@ -209,6 +194,7 @@ void build(SmallRyeHealthRecorder recorder, @BuildStep public void defineHealthRoutes(BuildProducer routes, BeanArchiveIndexBuildItem beanArchiveIndex, + NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, SmallRyeHealthConfig healthConfig) { IndexView index = beanArchiveIndex.getIndex(); @@ -219,29 +205,30 @@ public void defineHealthRoutes(BuildProducer routes, warnIfJaxRsPathUsed(index, WELLNESS); // Register the health handler - routes.produce(new RouteBuildItem.Builder() + routes.produce(nonApplicationRootPathBuildItem.routeBuilder() .route(healthConfig.rootPath) .handler(new SmallRyeHealthHandler()) + .requiresLegacyRedirect() + .displayOnNotFoundPage() .blockingRoute() - .nonApplicationRoute() .build()); - String subcontextBasePath = adjustSubcontextBasePath(healthConfig.rootPath); - // Register the liveness handler - routes.produce(new RouteBuildItem.Builder() - .route(subcontextBasePath + healthConfig.livenessPath) + routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .nestedRoute(healthConfig.rootPath, healthConfig.livenessPath) .handler(new SmallRyeLivenessHandler()) + .requiresLegacyRedirect() + .displayOnNotFoundPage() .blockingRoute() - .nonApplicationRoute() .build()); // Register the readiness handler - routes.produce(new RouteBuildItem.Builder() - .route(subcontextBasePath + healthConfig.readinessPath) + routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .nestedRoute(healthConfig.rootPath, healthConfig.readinessPath) .handler(new SmallRyeReadinessHandler()) + .requiresLegacyRedirect() + .displayOnNotFoundPage() .blockingRoute() - .nonApplicationRoute() .build()); // Find all health groups @@ -258,29 +245,32 @@ public void defineHealthRoutes(BuildProducer routes, } // Register the health group handlers - routes.produce(new RouteBuildItem.Builder() - .route(subcontextBasePath + healthConfig.groupPath) + routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .nestedRoute(healthConfig.rootPath, healthConfig.groupPath) .handler(new SmallRyeHealthGroupHandler()) + .requiresLegacyRedirect() + .displayOnNotFoundPage() .blockingRoute() - .nonApplicationRoute() .build()); SmallRyeIndividualHealthGroupHandler handler = new SmallRyeIndividualHealthGroupHandler(); for (String healthGroup : healthGroups) { - routes.produce(new RouteBuildItem.Builder() - .route(subcontextBasePath + healthConfig.groupPath + "/" + healthGroup) + routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .nestedRoute(healthConfig.rootPath, healthConfig.groupPath + "/" + healthGroup) .handler(handler) + .requiresLegacyRedirect() + .displayOnNotFoundPage() .blockingRoute() - .nonApplicationRoute() .build()); } // Register the wellness handler - routes.produce(new RouteBuildItem.Builder() - .route(subcontextBasePath + healthConfig.wellnessPath) + routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .nestedRoute(healthConfig.rootPath, healthConfig.wellnessPath) .handler(new SmallRyeWellnessHandler()) + .requiresLegacyRedirect() + .displayOnNotFoundPage() .blockingRoute() - .nonApplicationRoute() .build()); } @@ -294,11 +284,10 @@ public void includeInOpenAPIEndpoint(BuildProducer livenessPathItemProducer, BuildProducer readinessPathItemProducer) { - String subcontextBasePath = adjustSubcontextBasePath(healthConfig.rootPath); - livenessPathItemProducer.produce( new KubernetesHealthLivenessPathBuildItem( - httpConfig - .adjustPath(frameworkRootPath.adjustPath(subcontextBasePath + healthConfig.livenessPath)))); + frameworkRootPath.resolveNestedPath(healthConfig.rootPath, healthConfig.livenessPath))); readinessPathItemProducer.produce( new KubernetesHealthReadinessPathBuildItem( - httpConfig - .adjustPath(frameworkRootPath.adjustPath(subcontextBasePath + healthConfig.readinessPath)))); + frameworkRootPath.resolveNestedPath(healthConfig.rootPath, healthConfig.readinessPath))); } @BuildStep @@ -400,7 +385,6 @@ public void transform(TransformationContext ctx) { void registerUiExtension( BuildProducer generatedResourceProducer, BuildProducer nativeImageResourceProducer, - BuildProducer notFoundPageDisplayableEndpointProducer, BuildProducer smallRyeHealthBuildProducer, HttpRootPathBuildItem httpRootPath, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, @@ -416,8 +400,9 @@ void registerUiExtension( "quarkus.smallrye-health.root-path-ui was set to \"/\", this is not allowed as it blocks the application from serving anything else."); } - String healthPath = httpRootPath.adjustPath(nonApplicationRootPathBuildItem.adjustPath(healthConfig.rootPath)); - String healthUiPath = httpRootPath.adjustPath(nonApplicationRootPathBuildItem.adjustPath(healthConfig.ui.rootPath)); + String healthPath = httpRootPath.resolvePath(nonApplicationRootPathBuildItem.resolvePath(healthConfig.rootPath)); + String healthUiPath = httpRootPath + .resolvePath(nonApplicationRootPathBuildItem.resolvePath(healthConfig.ui.rootPath)); AppArtifact artifact = WebJarUtil.getAppArtifact(curateOutcomeBuildItem, HEALTH_UI_WEBJAR_GROUP_ID, HEALTH_UI_WEBJAR_ARTIFACT_ID); @@ -430,11 +415,6 @@ void registerUiExtension( smallRyeHealthBuildProducer .produce(new SmallRyeHealthBuildItem(tempPath.toAbsolutePath().toString(), healthUiPath)); - notFoundPageDisplayableEndpointProducer - .produce(new NotFoundPageDisplayableEndpointBuildItem( - nonApplicationRootPathBuildItem - .adjustPath(healthConfig.ui.rootPath + "/"))); - // Handle live reload of branding files if (liveReloadBuildItem.isLiveReload() && !liveReloadBuildItem.getChangedResources().isEmpty()) { WebJarUtil.hotReloadBrandingChanges(curateOutcomeBuildItem, launchMode, artifact, @@ -470,21 +450,23 @@ void registerHealthUiHandler( SmallRyeHealthRecorder recorder, SmallRyeHealthRuntimeConfig runtimeConfig, SmallRyeHealthBuildItem smallRyeHealthBuildItem, + NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, LaunchModeBuildItem launchMode, SmallRyeHealthConfig healthConfig) throws Exception { if (shouldInclude(launchMode, healthConfig)) { Handler handler = recorder.uiHandler(smallRyeHealthBuildItem.getHealthUiFinalDestination(), smallRyeHealthBuildItem.getHealthUiPath(), runtimeConfig); - routeProducer.produce(new RouteBuildItem.Builder() + routeProducer.produce(nonApplicationRootPathBuildItem.routeBuilder() .route(healthConfig.ui.rootPath) + .displayOnNotFoundPage("Health UI") + .requiresLegacyRedirect() .handler(handler) - .nonApplicationRoute() .build()); - routeProducer.produce(new RouteBuildItem.Builder() + routeProducer.produce(nonApplicationRootPathBuildItem.routeBuilder() .route(healthConfig.ui.rootPath + "/*") + .requiresLegacyRedirect() .handler(handler) - .nonApplicationRoute() .build()); } } @@ -504,8 +486,4 @@ public String updateApiUrl(String original, String healthPath) { private static boolean shouldInclude(LaunchModeBuildItem launchMode, SmallRyeHealthConfig healthConfig) { return launchMode.getLaunchMode().isDevOrTest() || healthConfig.ui.alwaysInclude; } - - private String adjustSubcontextBasePath(String basePath) { - return basePath.equals("/") ? "" : basePath; - } } diff --git a/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthUIConfig.java b/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthUIConfig.java index 04a04c45ac421..f85e119bbce3c 100644 --- a/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthUIConfig.java +++ b/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthUIConfig.java @@ -9,7 +9,7 @@ public class SmallRyeHealthUIConfig { * The path where Health UI is available. * The value `/` is not allowed as it blocks the application from serving anything else. */ - @ConfigItem(defaultValue = "/health-ui") + @ConfigItem(defaultValue = "health-ui") String rootPath; /** diff --git a/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/ui/CustomConfigTest.java b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/ui/CustomConfigTest.java index 105b68b0eace6..4068bb4e3adba 100644 --- a/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/ui/CustomConfigTest.java +++ b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/ui/CustomConfigTest.java @@ -20,7 +20,7 @@ public class CustomConfigTest { @Test public void shouldUseCustomConfig() { - RestAssured.when().get("/q/custom").then().statusCode(200).body(containsString("SmallRye Health")); - RestAssured.when().get("/q/custom/index.html").then().statusCode(200).body(containsString("SmallRye Health")); + RestAssured.when().get("/custom").then().statusCode(200).body(containsString("SmallRye Health")); + RestAssured.when().get("/custom/index.html").then().statusCode(200).body(containsString("SmallRye Health")); } } diff --git a/extensions/smallrye-metrics/deployment/src/main/java/io/quarkus/smallrye/metrics/deployment/SmallRyeMetricsProcessor.java b/extensions/smallrye-metrics/deployment/src/main/java/io/quarkus/smallrye/metrics/deployment/SmallRyeMetricsProcessor.java index 2173745fc5279..5bfbadd59f33d 100644 --- a/extensions/smallrye-metrics/deployment/src/main/java/io/quarkus/smallrye/metrics/deployment/SmallRyeMetricsProcessor.java +++ b/extensions/smallrye-metrics/deployment/src/main/java/io/quarkus/smallrye/metrics/deployment/SmallRyeMetricsProcessor.java @@ -23,7 +23,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.function.Function; import java.util.function.Predicate; import javax.enterprise.context.Dependent; @@ -92,8 +91,6 @@ import io.smallrye.metrics.interceptors.MetricsInterceptor; import io.smallrye.metrics.interceptors.SimplyTimedInterceptor; import io.smallrye.metrics.interceptors.TimedInterceptor; -import io.vertx.ext.web.Route; -import io.vertx.ext.web.Router; public class SmallRyeMetricsProcessor { static final Logger LOGGER = Logger.getLogger("io.quarkus.smallrye.metrics.deployment.SmallRyeMetricsProcessor"); @@ -104,7 +101,7 @@ static final class SmallRyeMetricsConfig { /** * The path to the metrics handler. */ - @ConfigItem(defaultValue = "/metrics") + @ConfigItem(defaultValue = "metrics") String path; /** @@ -124,7 +121,7 @@ static final class SmallRyeMetricsConfig { /** * Whether or not detailed JAX-RS metrics should be enabled. - * + *

* See MicroProfile * Metrics: Optional REST metrics. @@ -144,7 +141,7 @@ MetricsConfigurationBuildItem metricsConfigurationBuildItem() { MetricsCapabilityBuildItem metricsCapabilityBuildItem(NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) { if (metrics.extensionsEnabled) { return new MetricsCapabilityBuildItem(MetricsFactory.MP_METRICS::equals, - nonApplicationRootPathBuildItem.adjustPath(metrics.path)); + nonApplicationRootPathBuildItem.resolvePath(metrics.path)); } return null; } @@ -157,24 +154,23 @@ void createRoute(BuildProducer routes, BuildProducer displayableEndpoints, LaunchModeBuildItem launchModeBuildItem, BeanContainerBuildItem beanContainer) { - Function route = recorder.route(metrics.path + (metrics.path.endsWith("/") ? "*" : "/*")); - Function slash = recorder.route(metrics.path); // add metrics endpoint for not found display in dev or test mode if (launchModeBuildItem.getLaunchMode().isDevOrTest()) { displayableEndpoints.produce(new NotFoundPageDisplayableEndpointBuildItem(metrics.path)); } - routes.produce(new RouteBuildItem.Builder() - .routeFunction(route) - .handler(recorder.handler(frameworkRoot.adjustPathIncludingHttpRootPath(metrics.path))) + routes.produce(frameworkRoot.routeBuilder() + .route(metrics.path + (metrics.path.endsWith("/") ? "*" : "/*")) + .handler(recorder.handler(frameworkRoot.resolvePath(metrics.path))) + .requiresLegacyRedirect() + .displayOnNotFoundPage("Metrics", metrics.path) .blockingRoute() - .nonApplicationRoute() .build()); - routes.produce(new RouteBuildItem.Builder() - .routeFunction(slash) - .handler(recorder.handler(frameworkRoot.adjustPathIncludingHttpRootPath(metrics.path))) + routes.produce(frameworkRoot.routeBuilder() + .route(metrics.path) + .handler(recorder.handler(frameworkRoot.resolvePath(metrics.path))) + .requiresLegacyRedirect() .blockingRoute() - .nonApplicationRoute() .build()); } diff --git a/extensions/smallrye-metrics/deployment/src/test/java/io/quarkus/smallrye/metrics/deployment/DevModeMetricsTest.java b/extensions/smallrye-metrics/deployment/src/test/java/io/quarkus/smallrye/metrics/deployment/DevModeMetricsTest.java index 79651f30a5772..5a9a0736694a7 100644 --- a/extensions/smallrye-metrics/deployment/src/test/java/io/quarkus/smallrye/metrics/deployment/DevModeMetricsTest.java +++ b/extensions/smallrye-metrics/deployment/src/test/java/io/quarkus/smallrye/metrics/deployment/DevModeMetricsTest.java @@ -96,6 +96,7 @@ public void test() { // jax-rs metrics are enabled when().get("/q/metrics").then() + .statusCode(200) .body(containsString("io.quarkus.smallrye.metrics.deployment.DevModeMetricsTest$MetricsResource")); // disable jax-rs metrics via quarkus.resteasy.metrics.enabled diff --git a/extensions/smallrye-metrics/deployment/src/test/java/io/quarkus/smallrye/metrics/test/MetricsHandlerPathTest.java b/extensions/smallrye-metrics/deployment/src/test/java/io/quarkus/smallrye/metrics/test/MetricsHandlerPathTest.java index 3255a3f8309dc..70be9b1bd0822 100644 --- a/extensions/smallrye-metrics/deployment/src/test/java/io/quarkus/smallrye/metrics/test/MetricsHandlerPathTest.java +++ b/extensions/smallrye-metrics/deployment/src/test/java/io/quarkus/smallrye/metrics/test/MetricsHandlerPathTest.java @@ -22,6 +22,7 @@ public class MetricsHandlerPathTest { @Test public void testMetricsEndpointAccessibility() { + RestAssured.basePath = "/"; RestAssured.when() // no need to prepend the /custom here because it will be reflected in RestAssured.basePath .get("/framework/metrics") diff --git a/extensions/smallrye-metrics/runtime/src/main/java/io/quarkus/smallrye/metrics/runtime/SmallRyeMetricsHandler.java b/extensions/smallrye-metrics/runtime/src/main/java/io/quarkus/smallrye/metrics/runtime/SmallRyeMetricsHandler.java index fdeb4df0d5b30..d9db296a4fadd 100644 --- a/extensions/smallrye-metrics/runtime/src/main/java/io/quarkus/smallrye/metrics/runtime/SmallRyeMetricsHandler.java +++ b/extensions/smallrye-metrics/runtime/src/main/java/io/quarkus/smallrye/metrics/runtime/SmallRyeMetricsHandler.java @@ -31,6 +31,8 @@ public void handle(RoutingContext routingContext) { HttpServerResponse response = routingContext.response(); HttpServerRequest request = routingContext.request(); Stream acceptHeaders = request.headers().getAll("Accept").stream(); + routingContext.currentRoute().getPath(); + routingContext.mountPoint(); try { internalHandler.handleRequest(request.path(), metricsPath, request.rawMethod(), acceptHeaders, diff --git a/extensions/smallrye-metrics/runtime/src/main/java/io/quarkus/smallrye/metrics/runtime/SmallRyeMetricsRecorder.java b/extensions/smallrye-metrics/runtime/src/main/java/io/quarkus/smallrye/metrics/runtime/SmallRyeMetricsRecorder.java index f39d3eadac858..bf5651bc52ec3 100644 --- a/extensions/smallrye-metrics/runtime/src/main/java/io/quarkus/smallrye/metrics/runtime/SmallRyeMetricsRecorder.java +++ b/extensions/smallrye-metrics/runtime/src/main/java/io/quarkus/smallrye/metrics/runtime/SmallRyeMetricsRecorder.java @@ -14,7 +14,6 @@ import java.util.Arrays; import java.util.List; import java.util.function.Consumer; -import java.util.function.Function; import javax.enterprise.inject.spi.BeanManager; import javax.enterprise.inject.spi.CDI; @@ -50,8 +49,6 @@ import io.smallrye.metrics.elementdesc.MemberInfo; import io.smallrye.metrics.interceptors.MetricResolver; import io.smallrye.metrics.setup.MetricsMetadata; -import io.vertx.ext.web.Route; -import io.vertx.ext.web.Router; @Recorder public class SmallRyeMetricsRecorder { @@ -90,15 +87,6 @@ public class SmallRyeMetricsRecorder { private static final SmallRyeMetricsFactory factory = new SmallRyeMetricsFactory(); - public Function route(String name) { - return new Function() { - @Override - public Route apply(Router router) { - return router.route(name); - } - }; - } - public SmallRyeMetricsHandler handler(String metricsPath) { SmallRyeMetricsHandler handler = new SmallRyeMetricsHandler(); handler.setMetricsPath(metricsPath); diff --git a/extensions/smallrye-openapi-common/deployment/src/main/java/io/quarkus/smallrye/openapi/common/deployment/SmallRyeOpenApiConfig.java b/extensions/smallrye-openapi-common/deployment/src/main/java/io/quarkus/smallrye/openapi/common/deployment/SmallRyeOpenApiConfig.java index 5879bd62994f9..beb930cdacefd 100644 --- a/extensions/smallrye-openapi-common/deployment/src/main/java/io/quarkus/smallrye/openapi/common/deployment/SmallRyeOpenApiConfig.java +++ b/extensions/smallrye-openapi-common/deployment/src/main/java/io/quarkus/smallrye/openapi/common/deployment/SmallRyeOpenApiConfig.java @@ -11,7 +11,7 @@ public final class SmallRyeOpenApiConfig { /** * The path at which to register the OpenAPI Servlet. */ - @ConfigItem(defaultValue = "/openapi") + @ConfigItem(defaultValue = "openapi") public String path; /** diff --git a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java index 69a697fe1d61e..c4d12bd1551f0 100644 --- a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java +++ b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java @@ -167,16 +167,15 @@ RouteBuildItem handler(LaunchModeBuildItem launch, */ if (launch.getLaunchMode() == LaunchMode.DEVELOPMENT) { recorder.setupClDevMode(shutdownContext); - displayableEndpoints.produce(new NotFoundPageDisplayableEndpointBuildItem( - nonApplicationRootPathBuildItem.adjustPath(openApiConfig.path), "Open API Schema document")); } Handler handler = recorder.handler(openApiRuntimeConfig); - return new RouteBuildItem.Builder() + return nonApplicationRootPathBuildItem.routeBuilder() .route(openApiConfig.path) .handler(handler) + .displayOnNotFoundPage("Open API Schema document") + .requiresLegacyRedirect() .blockingRoute() - .nonApplicationRoute() .build(); } diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiPathWithSegmentsTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiPathWithSegmentsTestCase.java index 40cc39f9d5576..3ea5304666de2 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiPathWithSegmentsTestCase.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiPathWithSegmentsTestCase.java @@ -22,16 +22,16 @@ public class OpenApiPathWithSegmentsTestCase { @Test public void testOpenApiPathAccessResource() { RestAssured.given().header("Accept", "application/yaml") - .when().get("/q" + OPEN_API_PATH) + .when().get(OPEN_API_PATH) .then().header("Content-Type", "application/yaml;charset=UTF-8"); RestAssured.given().queryParam("format", "YAML") - .when().get("/q" + OPEN_API_PATH) + .when().get(OPEN_API_PATH) .then().header("Content-Type", "application/yaml;charset=UTF-8"); RestAssured.given().header("Accept", "application/json") - .when().get("/q" + OPEN_API_PATH) + .when().get(OPEN_API_PATH) .then().header("Content-Type", "application/json;charset=UTF-8"); RestAssured.given().queryParam("format", "JSON") - .when().get("/q" + OPEN_API_PATH) + .when().get(OPEN_API_PATH) .then().header("Content-Type", "application/json;charset=UTF-8"); } } diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiPathWithoutSegmentsTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiPathWithoutSegmentsTestCase.java index 36e0bd5644fa8..a8a34f7c2b17f 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiPathWithoutSegmentsTestCase.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiPathWithoutSegmentsTestCase.java @@ -10,7 +10,7 @@ import io.restassured.RestAssured; public class OpenApiPathWithoutSegmentsTestCase { - private static final String OPEN_API_PATH = "/path-without-segments"; + private static final String OPEN_API_PATH = "path-without-segments"; @RegisterExtension static QuarkusUnitTest runner = new QuarkusUnitTest() @@ -22,16 +22,16 @@ public class OpenApiPathWithoutSegmentsTestCase { @Test public void testOpenApiPathAccessResource() { RestAssured.given().header("Accept", "application/yaml") - .when().get("/q" + OPEN_API_PATH) + .when().get("/q/" + OPEN_API_PATH) .then().header("Content-Type", "application/yaml;charset=UTF-8"); RestAssured.given().queryParam("format", "YAML") - .when().get("/q" + OPEN_API_PATH) + .when().get("/q/" + OPEN_API_PATH) .then().header("Content-Type", "application/yaml;charset=UTF-8"); RestAssured.given().header("Accept", "application/json") - .when().get("/q" + OPEN_API_PATH) + .when().get("/q/" + OPEN_API_PATH) .then().header("Content-Type", "application/json;charset=UTF-8"); RestAssured.given().queryParam("format", "JSON") - .when().get("/q" + OPEN_API_PATH) + .when().get("/q/" + OPEN_API_PATH) .then().header("Content-Type", "application/json;charset=UTF-8"); } } diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/SwaggerAndOpenAPIWithCommonPrefixTest.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/SwaggerAndOpenAPIWithCommonPrefixTest.java index 5453c5c2b0f81..eeb12e9dc260f 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/SwaggerAndOpenAPIWithCommonPrefixTest.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/SwaggerAndOpenAPIWithCommonPrefixTest.java @@ -20,7 +20,7 @@ public class SwaggerAndOpenAPIWithCommonPrefixTest { static final QuarkusUnitTest config = new QuarkusUnitTest() .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) .addClasses(OpenApiResource.class, ResourceBean.class) - .addAsResource(new StringAsset("quarkus.smallrye-openapi.path=/swagger"), "application.properties")); + .addAsResource(new StringAsset("quarkus.smallrye-openapi.path=swagger"), "application.properties")); @Test public void shouldWorkEvenWithCommonPrefix() { diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/OpenApiPathWithSegmentsTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/OpenApiPathWithSegmentsTestCase.java index 95285a8b6624a..0229070b87efd 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/OpenApiPathWithSegmentsTestCase.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/OpenApiPathWithSegmentsTestCase.java @@ -22,16 +22,16 @@ public class OpenApiPathWithSegmentsTestCase { @Test public void testOpenApiPathAccessResource() { RestAssured.given().header("Accept", "application/yaml") - .when().get("/q" + OPEN_API_PATH) + .when().get(OPEN_API_PATH) .then().header("Content-Type", "application/yaml;charset=UTF-8"); RestAssured.given().queryParam("format", "YAML") - .when().get("/q" + OPEN_API_PATH) + .when().get(OPEN_API_PATH) .then().header("Content-Type", "application/yaml;charset=UTF-8"); RestAssured.given().header("Accept", "application/json") - .when().get("/q" + OPEN_API_PATH) + .when().get(OPEN_API_PATH) .then().header("Content-Type", "application/json;charset=UTF-8"); RestAssured.given().queryParam("format", "JSON") - .when().get("/q" + OPEN_API_PATH) + .when().get(OPEN_API_PATH) .then().header("Content-Type", "application/json;charset=UTF-8"); } } \ No newline at end of file diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/OpenApiPathWithoutSegmentsTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/OpenApiPathWithoutSegmentsTestCase.java index 1972db5393f88..d9094818eb9a0 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/OpenApiPathWithoutSegmentsTestCase.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/OpenApiPathWithoutSegmentsTestCase.java @@ -10,7 +10,7 @@ import io.restassured.RestAssured; public class OpenApiPathWithoutSegmentsTestCase { - private static final String OPEN_API_PATH = "/path-without-segments"; + private static final String OPEN_API_PATH = "path-without-segments"; @RegisterExtension static QuarkusUnitTest runner = new QuarkusUnitTest() @@ -22,16 +22,16 @@ public class OpenApiPathWithoutSegmentsTestCase { @Test public void testOpenApiPathAccessResource() { RestAssured.given().header("Accept", "application/yaml") - .when().get("/q" + OPEN_API_PATH) + .when().get("/q/" + OPEN_API_PATH) .then().header("Content-Type", "application/yaml;charset=UTF-8"); RestAssured.given().queryParam("format", "YAML") - .when().get("/q" + OPEN_API_PATH) + .when().get("/q/" + OPEN_API_PATH) .then().header("Content-Type", "application/yaml;charset=UTF-8"); RestAssured.given().header("Accept", "application/json") - .when().get("/q" + OPEN_API_PATH) + .when().get("/q/" + OPEN_API_PATH) .then().header("Content-Type", "application/json;charset=UTF-8"); RestAssured.given().queryParam("format", "JSON") - .when().get("/q" + OPEN_API_PATH) + .when().get("/q/" + OPEN_API_PATH) .then().header("Content-Type", "application/json;charset=UTF-8"); } } \ No newline at end of file diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/SwaggerAndOpenAPIWithCommonPrefixTest.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/SwaggerAndOpenAPIWithCommonPrefixTest.java index a98a957550e5a..99973f8e17855 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/SwaggerAndOpenAPIWithCommonPrefixTest.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/spring/SwaggerAndOpenAPIWithCommonPrefixTest.java @@ -20,7 +20,7 @@ public class SwaggerAndOpenAPIWithCommonPrefixTest { static final QuarkusUnitTest config = new QuarkusUnitTest() .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) .addClass(OpenApiController.class) - .addAsResource(new StringAsset("quarkus.smallrye-openapi.path=/swagger"), "application.properties")); + .addAsResource(new StringAsset("quarkus.smallrye-openapi.path=swagger"), "application.properties")); @Test public void shouldWorkEvenWithCommonPrefix() { diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/vertx/OpenApiPathWithSegmentsTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/vertx/OpenApiPathWithSegmentsTestCase.java index 0fc066d70de20..b742edd7581c5 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/vertx/OpenApiPathWithSegmentsTestCase.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/vertx/OpenApiPathWithSegmentsTestCase.java @@ -10,7 +10,7 @@ import io.restassured.RestAssured; public class OpenApiPathWithSegmentsTestCase { - private static final String OPEN_API_PATH = "/path/with/segments"; + private static final String OPEN_API_PATH = "path/with/segments"; @RegisterExtension static QuarkusUnitTest runner = new QuarkusUnitTest() @@ -22,16 +22,16 @@ public class OpenApiPathWithSegmentsTestCase { @Test public void testOpenApiPathAccessResource() { RestAssured.given().header("Accept", "application/yaml") - .when().get("/q" + OPEN_API_PATH) + .when().get("/q/" + OPEN_API_PATH) .then().header("Content-Type", "application/yaml;charset=UTF-8"); RestAssured.given().queryParam("format", "YAML") - .when().get("/q" + OPEN_API_PATH) + .when().get("/q/" + OPEN_API_PATH) .then().header("Content-Type", "application/yaml;charset=UTF-8"); RestAssured.given().header("Accept", "application/json") - .when().get("/q" + OPEN_API_PATH) + .when().get("/q/" + OPEN_API_PATH) .then().header("Content-Type", "application/json;charset=UTF-8"); RestAssured.given().queryParam("format", "JSON") - .when().get("/q" + OPEN_API_PATH) + .when().get("/q/" + OPEN_API_PATH) .then().header("Content-Type", "application/json;charset=UTF-8"); } } \ No newline at end of file diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/vertx/OpenApiPathWithoutSegmentsTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/vertx/OpenApiPathWithoutSegmentsTestCase.java index cd7f7152e1adc..ff26a8fd929c5 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/vertx/OpenApiPathWithoutSegmentsTestCase.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/vertx/OpenApiPathWithoutSegmentsTestCase.java @@ -10,7 +10,7 @@ import io.restassured.RestAssured; public class OpenApiPathWithoutSegmentsTestCase { - private static final String OPEN_API_PATH = "/path-without-segments"; + private static final String OPEN_API_PATH = "path-without-segments"; @RegisterExtension static QuarkusUnitTest runner = new QuarkusUnitTest() @@ -22,16 +22,16 @@ public class OpenApiPathWithoutSegmentsTestCase { @Test public void testOpenApiPathAccessResource() { RestAssured.given().header("Accept", "application/yaml") - .when().get("/q" + OPEN_API_PATH) + .when().get("/q/" + OPEN_API_PATH) .then().header("Content-Type", "application/yaml;charset=UTF-8"); RestAssured.given().queryParam("format", "YAML") - .when().get("/q" + OPEN_API_PATH) + .when().get("/q/" + OPEN_API_PATH) .then().header("Content-Type", "application/yaml;charset=UTF-8"); RestAssured.given().header("Accept", "application/json") - .when().get("/q" + OPEN_API_PATH) + .when().get("/q/" + OPEN_API_PATH) .then().header("Content-Type", "application/json;charset=UTF-8"); RestAssured.given().queryParam("format", "JSON") - .when().get("/q" + OPEN_API_PATH) + .when().get("/q/" + OPEN_API_PATH) .then().header("Content-Type", "application/json;charset=UTF-8"); } } \ No newline at end of file diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/vertx/SwaggerAndOpenAPIWithCommonPrefixTest.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/vertx/SwaggerAndOpenAPIWithCommonPrefixTest.java index ef74228ddd17f..20fc632d57b2b 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/vertx/SwaggerAndOpenAPIWithCommonPrefixTest.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/vertx/SwaggerAndOpenAPIWithCommonPrefixTest.java @@ -20,7 +20,7 @@ public class SwaggerAndOpenAPIWithCommonPrefixTest { static final QuarkusUnitTest config = new QuarkusUnitTest() .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) .addClass(OpenApiRoute.class) - .addAsResource(new StringAsset("quarkus.smallrye-openapi.path=/swagger"), "application.properties")); + .addAsResource(new StringAsset("quarkus.smallrye-openapi.path=swagger"), "application.properties")); @Test public void shouldWorkEvenWithCommonPrefix() { diff --git a/extensions/swagger-ui/deployment/src/main/java/io/quarkus/swaggerui/deployment/SwaggerUiConfig.java b/extensions/swagger-ui/deployment/src/main/java/io/quarkus/swaggerui/deployment/SwaggerUiConfig.java index 789da6caa6110..ccb86c99987ab 100644 --- a/extensions/swagger-ui/deployment/src/main/java/io/quarkus/swaggerui/deployment/SwaggerUiConfig.java +++ b/extensions/swagger-ui/deployment/src/main/java/io/quarkus/swaggerui/deployment/SwaggerUiConfig.java @@ -19,7 +19,7 @@ public class SwaggerUiConfig { *

* The value `/` is not allowed as it blocks the application from serving anything else. */ - @ConfigItem(defaultValue = "/swagger-ui") + @ConfigItem(defaultValue = "swagger-ui") String path; /** diff --git a/extensions/swagger-ui/deployment/src/main/java/io/quarkus/swaggerui/deployment/SwaggerUiProcessor.java b/extensions/swagger-ui/deployment/src/main/java/io/quarkus/swaggerui/deployment/SwaggerUiProcessor.java index 9fd5b0e8b1ee5..40c828dd7f9e3 100644 --- a/extensions/swagger-ui/deployment/src/main/java/io/quarkus/swaggerui/deployment/SwaggerUiProcessor.java +++ b/extensions/swagger-ui/deployment/src/main/java/io/quarkus/swaggerui/deployment/SwaggerUiProcessor.java @@ -93,9 +93,9 @@ public void getSwaggerUiFinalDestination( "quarkus.swagger-ui.path was set to \"/\", this is not allowed as it blocks the application from serving anything else."); } - String openApiPath = httpRootPathBuildItem.adjustPath(nonApplicationRootPathBuildItem.adjustPath(openapi.path)); + String openApiPath = httpRootPathBuildItem.resolvePath(nonApplicationRootPathBuildItem.resolvePath(openapi.path)); String swaggerUiPath = httpRootPathBuildItem - .adjustPath(nonApplicationRootPathBuildItem.adjustPath(swaggerUiConfig.path)); + .resolvePath(nonApplicationRootPathBuildItem.resolvePath(swaggerUiConfig.path)); AppArtifact artifact = WebJarUtil.getAppArtifact(curateOutcomeBuildItem, SWAGGER_UI_WEBJAR_GROUP_ID, SWAGGER_UI_WEBJAR_ARTIFACT_ID); @@ -108,8 +108,6 @@ public void getSwaggerUiFinalDestination( generateIndexHtml(openApiPath, swaggerUiPath, swaggerUiConfig)); swaggerUiBuildProducer.produce(new SwaggerUiBuildItem(tempPath.toAbsolutePath().toString(), swaggerUiPath)); - displayableEndpoints.produce(new NotFoundPageDisplayableEndpointBuildItem( - nonApplicationRootPathBuildItem.adjustPath(swaggerUiConfig.path + "/"), "Open API UI")); // Handle live reload of branding files if (liveReloadBuildItem.isLiveReload() && !liveReloadBuildItem.getChangedResources().isEmpty()) { @@ -144,6 +142,7 @@ public void getSwaggerUiFinalDestination( @Record(ExecutionTime.RUNTIME_INIT) public void registerSwaggerUiHandler(SwaggerUiRecorder recorder, BuildProducer routes, + NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, SwaggerUiBuildItem finalDestinationBuildItem, SwaggerUiRuntimeConfig runtimeConfig, LaunchModeBuildItem launchMode, @@ -154,18 +153,17 @@ public void registerSwaggerUiHandler(SwaggerUiRecorder recorder, finalDestinationBuildItem.getSwaggerUiPath(), runtimeConfig); - routes.produce( - new RouteBuildItem.Builder() - .route(swaggerUiConfig.path) - .handler(handler) - .nonApplicationRoute() - .build()); - routes.produce( - new RouteBuildItem.Builder() - .route(swaggerUiConfig.path + "/*") - .handler(handler) - .nonApplicationRoute() - .build()); + routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .route(swaggerUiConfig.path) + .displayOnNotFoundPage("Open API UI") + .handler(handler) + .requiresLegacyRedirect() + .build()); + routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .route(swaggerUiConfig.path + "/*") + .handler(handler) + .requiresLegacyRedirect() + .build()); } } diff --git a/extensions/swagger-ui/deployment/src/test/java/io/quarkus/swaggerui/deployment/CustomConfigTest.java b/extensions/swagger-ui/deployment/src/test/java/io/quarkus/swaggerui/deployment/CustomConfigTest.java index f334a37646f06..7f29285cbddf0 100644 --- a/extensions/swagger-ui/deployment/src/test/java/io/quarkus/swaggerui/deployment/CustomConfigTest.java +++ b/extensions/swagger-ui/deployment/src/test/java/io/quarkus/swaggerui/deployment/CustomConfigTest.java @@ -20,7 +20,7 @@ public class CustomConfigTest { @Test public void shouldUseCustomConfig() { - RestAssured.when().get("/q/custom").then().statusCode(200).body(containsString("/openapi")); - RestAssured.when().get("/q/custom/index.html").then().statusCode(200).body(containsString("/openapi")); + RestAssured.when().get("/custom").then().statusCode(200).body(containsString("/openapi")); + RestAssured.when().get("/custom/index.html").then().statusCode(200).body(containsString("/openapi")); } } diff --git a/extensions/vertx-graphql/deployment/src/main/java/io/quarkus/vertx/graphql/deployment/VertxGraphqlConfig.java b/extensions/vertx-graphql/deployment/src/main/java/io/quarkus/vertx/graphql/deployment/VertxGraphqlConfig.java index 179f070cfb603..ecc6596581c90 100644 --- a/extensions/vertx-graphql/deployment/src/main/java/io/quarkus/vertx/graphql/deployment/VertxGraphqlConfig.java +++ b/extensions/vertx-graphql/deployment/src/main/java/io/quarkus/vertx/graphql/deployment/VertxGraphqlConfig.java @@ -26,7 +26,7 @@ public static class VertxGraphqlUiConfig { *

* The value `/` is not allowed as it blocks the application from serving anything else. */ - @ConfigItem(defaultValue = "/graphql-ui") + @ConfigItem(defaultValue = "graphql-ui") String path; } } diff --git a/extensions/vertx-graphql/deployment/src/main/java/io/quarkus/vertx/graphql/deployment/VertxGraphqlProcessor.java b/extensions/vertx-graphql/deployment/src/main/java/io/quarkus/vertx/graphql/deployment/VertxGraphqlProcessor.java index 1d80b073d28d5..08cce8ddce3ec 100644 --- a/extensions/vertx-graphql/deployment/src/main/java/io/quarkus/vertx/graphql/deployment/VertxGraphqlProcessor.java +++ b/extensions/vertx-graphql/deployment/src/main/java/io/quarkus/vertx/graphql/deployment/VertxGraphqlProcessor.java @@ -16,9 +16,9 @@ import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.vertx.graphql.runtime.VertxGraphqlRecorder; +import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; import io.quarkus.vertx.http.deployment.RouteBuildItem; import io.quarkus.vertx.http.deployment.WebsocketSubProtocolsBuildItem; -import io.quarkus.vertx.http.deployment.devmode.NotFoundPageDisplayableEndpointBuildItem; import io.vertx.core.Handler; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.handler.graphql.impl.GraphQLBatch; @@ -50,7 +50,8 @@ List registerForReflection() { @Record(ExecutionTime.STATIC_INIT) void registerVertxGraphqlUI(VertxGraphqlRecorder recorder, BuildProducer nativeResourcesProducer, VertxGraphqlConfig config, - LaunchModeBuildItem launchMode, BuildProducer displayableEndpoints, + LaunchModeBuildItem launchMode, + NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, BuildProducer routes) { boolean includeVertxGraphqlUi = launchMode.getLaunchMode().isDevOrTest() || config.ui.alwaysInclude; @@ -66,10 +67,14 @@ void registerVertxGraphqlUI(VertxGraphqlRecorder recorder, + "\", this is not allowed as it blocks the application from serving anything else."); } - Handler handler = recorder.handler(path); - routes.produce(new RouteBuildItem(path, handler)); - routes.produce(new RouteBuildItem(path + "/*", handler)); - displayableEndpoints.produce(new NotFoundPageDisplayableEndpointBuildItem(path + "/")); + Handler handler = recorder.handler(); + routes.produce(nonApplicationRootPathBuildItem.routeBuilder().route(path).handler(handler) + .requiresLegacyRedirect() + .displayOnNotFoundPage("GraphQL UI", path + "/") + .build()); + routes.produce( + nonApplicationRootPathBuildItem.routeBuilder().route(path + "/*").handler(handler).requiresLegacyRedirect() + .build()); nativeResourcesProducer.produce(new NativeImageResourceDirectoryBuildItem("io/vertx/ext/web/handler/graphiql")); } } diff --git a/extensions/vertx-graphql/runtime/src/main/java/io/quarkus/vertx/graphql/runtime/VertxGraphqlRecorder.java b/extensions/vertx-graphql/runtime/src/main/java/io/quarkus/vertx/graphql/runtime/VertxGraphqlRecorder.java index a720daf837cc3..1ed8d191ceb35 100644 --- a/extensions/vertx-graphql/runtime/src/main/java/io/quarkus/vertx/graphql/runtime/VertxGraphqlRecorder.java +++ b/extensions/vertx-graphql/runtime/src/main/java/io/quarkus/vertx/graphql/runtime/VertxGraphqlRecorder.java @@ -9,7 +9,7 @@ @Recorder public class VertxGraphqlRecorder { - public Handler handler(String path) { + public Handler handler() { GraphiQLHandlerOptions options = new GraphiQLHandlerOptions(); options.setEnabled(true); @@ -19,10 +19,11 @@ public Handler handler(String path) { return new Handler() { @Override public void handle(RoutingContext event) { - if (event.normalisedPath().length() == path.length()) { - + if (event.normalisedPath().length() == (event.currentRoute().getPath().length() + + (event.mountPoint() == null ? 0 : event.mountPoint().length() - 1))) { event.response().setStatusCode(302); - event.response().headers().set(HttpHeaders.LOCATION, path + "/"); + event.response().headers().set(HttpHeaders.LOCATION, (event.mountPoint() == null ? "" : event.mountPoint()) + + event.currentRoute().getPath().substring(1) + "/"); event.response().end(); return; } diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpRootPathBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpRootPathBuildItem.java index 91548d820f4b5..a1bf1775c2867 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpRootPathBuildItem.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpRootPathBuildItem.java @@ -1,26 +1,68 @@ package io.quarkus.vertx.http.deployment; +import java.net.URI; + import io.quarkus.builder.item.SimpleBuildItem; +import io.quarkus.deployment.util.UriNormalizationUtil; public final class HttpRootPathBuildItem extends SimpleBuildItem { - private final String rootPath; + /** + * Normalized from quarkus.http.root-path. + * This path will always end in a slash + */ + private final URI rootPath; public HttpRootPathBuildItem(String rootPath) { - this.rootPath = rootPath; + this.rootPath = UriNormalizationUtil.toURI(rootPath, true); } + /** + * Return normalized Http root path configured from {@literal quarkus.http.root-path}. + * This path will always end in a slash. + *

+ * Use {@link #resolvePath(String)} if you need to construct a Uri from the Http root path. + * + * @return Normalized Http root path ending with a slash + * @see #resolvePath(String) + */ public String getRootPath() { - return rootPath; + return rootPath.getPath(); } + /** + * Adjusts the path in relation to `quarkus.http.root-path`. + * Any leading slash will be removed to resolve relatively. + * + * @deprecated Use {@code resolvePath} instead. Do not use this method. Will be removed in Quarkus 2.0 + */ public String adjustPath(String path) { - if (!path.startsWith("/")) { - throw new IllegalArgumentException("Path must start with /"); - } - if (rootPath.equals("/")) { - return path; - } - return rootPath + path; + return resolvePath(path.startsWith("/") ? path.substring(1) : path); + } + + /** + * Resolve path into an absolute path. + * If path is relative, it will be resolved against `quarkus.http.root-path`. + * An absolute path will be normalized and returned. + *

+ * Given {@literal quarkus.http.root-path=/} + *

    + *
  • {@code resolvePath("foo")} will return {@literal /foo}
  • + *
  • {@code resolvePath("/foo")} will return {@literal /foo}
  • + *
+ * Given {@literal quarkus.http.root-path=/app} + *
    + *
  • {@code resolvePath("foo")} will return {@literal /app/foo}
  • + *
  • {@code resolvePath("/foo")} will return {@literal /foo}
  • + *
+ *

+ * The returned path will not end with a slash. + * + * @param path Path to be resolved to an absolute path. + * @return An absolute path not ending with a slash + * @see UriNormalizationUtil#normalizeWithBase(URI, String, boolean) + */ + public String resolvePath(String path) { + return UriNormalizationUtil.normalizeWithBase(rootPath, path, false).getPath(); } } diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/NonApplicationRootPathBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/NonApplicationRootPathBuildItem.java index a2895ff8a1068..c6426ae14ebb5 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/NonApplicationRootPathBuildItem.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/NonApplicationRootPathBuildItem.java @@ -1,54 +1,319 @@ package io.quarkus.vertx.http.deployment; +import java.net.URI; +import java.util.function.Consumer; +import java.util.function.Function; + import io.quarkus.builder.item.SimpleBuildItem; +import io.quarkus.deployment.util.UriNormalizationUtil; +import io.quarkus.vertx.http.deployment.devmode.NotFoundPageDisplayableEndpointBuildItem; +import io.quarkus.vertx.http.runtime.HandlerType; +import io.vertx.core.Handler; +import io.vertx.ext.web.Route; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; public final class NonApplicationRootPathBuildItem extends SimpleBuildItem { - private final String httpRootPath; - private final String frameworkRootPath; - private final boolean separateRoot; + /** + * Normalized of quarkus.http.root-path. + * Must end in a slash + */ + final URI httpRootPath; + + /** + * Normalized from quarkus.http.non-application-root-path + */ + final URI nonApplicationRootPath; + + /** + * Non-Application root path is distinct from HTTP root path. + */ + final boolean dedicatedRouterRequired; + + final boolean attachedToMainRouter; + + public NonApplicationRootPathBuildItem(String httpRootPath, String nonApplicationRootPath) { + // Presume value always starts with a slash and is normalized + this.httpRootPath = UriNormalizationUtil.toURI(httpRootPath, true); + + this.nonApplicationRootPath = UriNormalizationUtil.normalizeWithBase(this.httpRootPath, nonApplicationRootPath, + true); + + this.dedicatedRouterRequired = !this.nonApplicationRootPath.getPath().equals(this.httpRootPath.getPath()); + + // Is the non-application root path underneath the http root path. Do we add non-application root to main router or not. + this.attachedToMainRouter = this.nonApplicationRootPath.getPath().startsWith(this.httpRootPath.getPath()); + } + + /** + * Is a dedicated router required for non-application endpoints. + * + * @return boolean + */ + public boolean isDedicatedRouterRequired() { + return dedicatedRouterRequired; + } - public NonApplicationRootPathBuildItem(String frameworkRootPath) { - this(frameworkRootPath, null); + public boolean isAttachedToMainRouter() { + return attachedToMainRouter; } - public NonApplicationRootPathBuildItem(String frameworkRootPath, String httpRootPath) { - this.frameworkRootPath = frameworkRootPath; - this.separateRoot = frameworkRootPath != null - && !frameworkRootPath.equals("") - && !frameworkRootPath.equals("/"); - this.httpRootPath = httpRootPath; + /** + * Path to the Non-application root for use with Vert.x Routers, + * has a leading slash. + *

+ * If it's under the HTTP Root, return a path relative to HTTP Root. + * Otherwise, return an absolute path. + * + * @return String Path suitable for use with Vert.x Router. It has a leading slash + */ + String getVertxRouterPath() { + if (attachedToMainRouter) { + return "/" + relativize(httpRootPath.getPath(), nonApplicationRootPath.getPath()); + } else { + return getNonApplicationRootPath(); + } } - public String getFrameworkRootPath() { - return frameworkRootPath; + String relativize(String rootPath, String leafPath) { + if (leafPath.startsWith(rootPath)) { + return leafPath.substring(rootPath.length()); + } + + return null; } - public boolean isSeparateRoot() { - return separateRoot; + public String getNormalizedHttpRootPath() { + return httpRootPath.getPath(); } /** - * Adjusts a path by including the non-application root path. + * Return normalized root path configured from {@literal quarkus.http.root-path} + * and {quarkus.http.non-application-root-path}. + * This path will always end in a slash. + *

+ * Use {@link #resolvePath(String)} if you need to construct a URI for + * a non-application endpoint. + * + * @return Normalized non-application root path ending with a slash + * @see #resolvePath(String) */ - public String adjustPath(String path) { - if (!path.startsWith("/")) { - throw new IllegalArgumentException("Path must start with /"); + public String getNonApplicationRootPath() { + return nonApplicationRootPath.getPath(); + } + + /** + * Resolve path into an absolute path. + * If path is relative, it will be resolved against `quarkus.http.non-application-root-path`. + * An absolute path will be normalized and returned. + *

+ * Given {@literal quarkus.http.root-path=/} and + * {@literal quarkus.http.non-application-root-path="q"} + *

    + *
  • {@code resolvePath("foo")} will return {@literal /q/foo}
  • + *
  • {@code resolvePath("/foo")} will return {@literal /foo}
  • + *
+ *

+ * Given {@literal quarkus.http.root-path=/} and + * {@literal quarkus.http.non-application-root-path="/q"} + *

    + *
  • {@code resolvePath("foo")} will return {@literal /q/foo}
  • + *
  • {@code resolvePath("/foo")} will return {@literal /foo}
  • + *
+ * Given {@literal quarkus.http.root-path=/app} and + * {@literal quarkus.http.non-application-root-path="q"} + *
    + *
  • {@code resolvePath("foo")} will return {@literal /app/q/foo}
  • + *
  • {@code resolvePath("/foo")} will return {@literal /foo}
  • + *
+ * Given {@literal quarkus.http.root-path=/app} and + * {@literal quarkus.http.non-application-root-path="/q"} + *
    + *
  • {@code resolvePath("foo")} will return {@literal /q/foo}
  • + *
  • {@code resolvePath("/foo")} will return {@literal /foo}
  • + *
+ *

+ * The returned path will not end with a slash. + * + * @param path Path to be resolved to an absolute path. + * @return An absolute path not ending with a slash + * @see UriNormalizationUtil#normalizeWithBase(URI, String, boolean) + * @throws IllegalArgumentException if path is null or empty + */ + public String resolvePath(String path) { + if (path == null || path.trim().isEmpty()) { + throw new IllegalArgumentException("Specified path can not be empty"); } - if (frameworkRootPath.equals("/")) { - return path; + return UriNormalizationUtil.normalizeWithBase(nonApplicationRootPath, path, false).getPath(); + } + + /** + * Resolve a base path and a sub-resource against the non-application root. + * This will call resolvePath on the base path (to establish a fully-resolved, + * absolute path), and then will resolve the subRoute against that resolved path. + * This allows a configured subpath to be configured (consistently) + * as an absolute URI. + * + * Given {@literal quarkus.http.root-path=/} and + * {@literal quarkus.http.non-application-root-path="q"} + *

    + *
  • {@code resolveNestedPath("foo", "a")} will return {@literal /q/foo/a}
  • + *
  • {@code resolveNestedPath("foo", "/a)} will return {@literal /a}
  • + *
+ *

+ * The returned path will not end with a slash. + * + * @param path Path to be resolved to an absolute path. + * @return An absolute path not ending with a slash + * @see UriNormalizationUtil#normalizeWithBase(URI, String, boolean) + * @see #resolvePath(String) + * @throws IllegalArgumentException if path is null or empty + */ + public String resolveNestedPath(String path, String subRoute) { + if (path == null || path.trim().isEmpty()) { + throw new IllegalArgumentException("Specified path can not be empty"); } - return frameworkRootPath + path; + URI base = UriNormalizationUtil.normalizeWithBase(nonApplicationRootPath, path, true); + return UriNormalizationUtil.normalizeWithBase(base, subRoute, false).getPath(); + } + + public Builder routeBuilder() { + return new Builder(this); } /** - * Adjusts a path by including both the non-application root path and - * the HTTP root path. + * Per non-application endpoint instance. */ - public String adjustPathIncludingHttpRootPath(String path) { - String withFrameWorkPath = adjustPath(path); - if (httpRootPath == null || httpRootPath.equals("/")) { - return withFrameWorkPath; + public static class Builder extends RouteBuildItem.Builder { + private final NonApplicationRootPathBuildItem buildItem; + private boolean requiresLegacyRedirect = false; + private RouteBuildItem.RouteType routeType = RouteBuildItem.RouteType.FRAMEWORK_ROUTE; + private String path; + private String absolute; + + Builder(NonApplicationRootPathBuildItem buildItem) { + this.buildItem = buildItem; + } + + @Override + public Builder routeFunction(Function routeFunction) { + throw new RuntimeException("This method is not supported for non-application routes"); + } + + public Builder routeFunction(String route, Consumer routeFunction) { + String temp = route; + route = absolute = buildItem.resolvePath(route); + + boolean isFrameworkRoute = buildItem.dedicatedRouterRequired + && route.startsWith(buildItem.getNonApplicationRootPath()); + + if (isFrameworkRoute) { + // relative non-application root + this.path = "/" + buildItem.relativize(buildItem.getNonApplicationRootPath(), route); + this.routeType = RouteBuildItem.RouteType.FRAMEWORK_ROUTE; + } else if (route.startsWith(buildItem.httpRootPath.getPath())) { + // relative to http root + this.path = "/" + buildItem.relativize(buildItem.httpRootPath.getPath(), route); + this.routeType = RouteBuildItem.RouteType.APPLICATION_ROUTE; + } else if (route.startsWith("/")) { + // absolute path + this.path = route; + this.routeType = RouteBuildItem.RouteType.ABSOLUTE_ROUTE; + } + + super.routeFunction(this.path, routeFunction); + return this; + } + + @Override + public Builder route(String route) { + routeFunction(route, null); + return this; + } + + public Builder nestedRoute(String baseRoute, String subRoute) { + if (subRoute.startsWith("/")) { + routeFunction(subRoute, null); + return this; + } + + baseRoute = baseRoute.endsWith("/") ? baseRoute : baseRoute + "/"; + routeFunction(baseRoute + subRoute, null); + return this; + } + + /** + * @deprecated This will be removed in Quarkus 2.0, don't use unless you have to. + */ + @Deprecated + public Builder requiresLegacyRedirect() { + this.requiresLegacyRedirect = true; + return this; + } + + @Override + public Builder handler(Handler handler) { + super.handler(handler); + return this; + } + + @Override + public Builder handlerType(HandlerType handlerType) { + super.handlerType(handlerType); + return this; + } + + @Override + public Builder blockingRoute() { + super.blockingRoute(); + return this; + } + + @Override + public Builder failureRoute() { + super.failureRoute(); + return this; + } + + @Override + public Builder displayOnNotFoundPage() { + this.displayOnNotFoundPage = true; + return this; + } + + @Override + public Builder displayOnNotFoundPage(String notFoundPageTitle) { + this.displayOnNotFoundPage = true; + this.notFoundPageTitle = notFoundPageTitle; + return this; + } + + @Override + public Builder displayOnNotFoundPage(String notFoundPageTitle, String notFoundPagePath) { + this.displayOnNotFoundPage = true; + this.notFoundPageTitle = notFoundPageTitle; + this.notFoundPagePath = notFoundPagePath; + return this; + } + + public RouteBuildItem build() { + // If path is same as absolute, we don't enable legacy redirect + if (requiresLegacyRedirect && path.equals(absolute)) { + requiresLegacyRedirect = false; + } + return new RouteBuildItem(this, routeType, requiresLegacyRedirect); + } + + @Override + protected NotFoundPageDisplayableEndpointBuildItem getNotFoundEndpoint() { + if (!displayOnNotFoundPage) { + return null; + } + if (notFoundPagePath == null) { + throw new RuntimeException("Cannot display " + routeFunction + + " on not found page as no explicit path was specified and a route function is in use"); + } + return new NotFoundPageDisplayableEndpointBuildItem(absolute, notFoundPageTitle, true); } - return httpRootPath + withFrameWorkPath; } } diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/RouteBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/RouteBuildItem.java index c2074e271ff25..2f732f16c473e 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/RouteBuildItem.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/RouteBuildItem.java @@ -1,8 +1,10 @@ package io.quarkus.vertx.http.deployment; +import java.util.function.Consumer; import java.util.function.Function; import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.vertx.http.deployment.devmode.NotFoundPageDisplayableEndpointBuildItem; import io.quarkus.vertx.http.runtime.BasicRoute; import io.quarkus.vertx.http.runtime.HandlerType; import io.vertx.core.Handler; @@ -19,8 +21,9 @@ public static Builder builder() { private final Function routeFunction; private final Handler handler; private final HandlerType type; - private final boolean frameworkRoute; + private final RouteType routeType; private final boolean requiresLegacyRedirect; + private final NotFoundPageDisplayableEndpointBuildItem notFoundPageDisplayableEndpoint; /** * @deprecated Use the Builder instead. @@ -30,8 +33,9 @@ public RouteBuildItem(Function routeFunction, Handler handler) { this(new BasicRoute(route), handler); } - private RouteBuildItem(Builder builder) { + RouteBuildItem(Builder builder, RouteType routeType, boolean requiresLegacyRedirect) { this.routeFunction = builder.routeFunction; this.handler = builder.handler; this.type = builder.type; - this.frameworkRoute = builder.frameworkRoute; - this.requiresLegacyRedirect = builder.requiresLegacyRedirect; + this.routeType = routeType; + this.requiresLegacyRedirect = requiresLegacyRedirect; + this.notFoundPageDisplayableEndpoint = builder.getNotFoundEndpoint(); } public Handler getHandler() { @@ -95,27 +100,64 @@ public Function getRouteFunction() { } public boolean isFrameworkRoute() { - return frameworkRoute; + return routeType.equals(RouteType.FRAMEWORK_ROUTE); + } + + public boolean isApplicationRoute() { + return routeType.equals(RouteType.APPLICATION_ROUTE); + } + + public boolean isAbsoluteRoute() { + return routeType.equals(RouteType.ABSOLUTE_ROUTE); } public boolean isRequiresLegacyRedirect() { return requiresLegacyRedirect; } - public static class Builder { - private Function routeFunction; - private Handler handler; - private HandlerType type = HandlerType.NORMAL; - private boolean frameworkRoute = false; - private boolean requiresLegacyRedirect = false; + public NotFoundPageDisplayableEndpointBuildItem getNotFoundPageDisplayableEndpoint() { + return notFoundPageDisplayableEndpoint; + } + + public enum RouteType { + FRAMEWORK_ROUTE, + APPLICATION_ROUTE, + ABSOLUTE_ROUTE + } + /** + * NonApplicationRootPathBuildItem.Builder extends this. + * Please verify the extended builders behavior when changing this one. + */ + public static class Builder { + protected Function routeFunction; + protected Handler handler; + protected HandlerType type = HandlerType.NORMAL; + protected boolean displayOnNotFoundPage; + protected String notFoundPageTitle; + protected String notFoundPagePath; + + /** + * {@link #routeFunction(String, Consumer)} should be used instead + * + * @param routeFunction + * @return + */ + @Deprecated public Builder routeFunction(Function routeFunction) { this.routeFunction = routeFunction; return this; } + public Builder routeFunction(String path, Consumer routeFunction) { + this.routeFunction = new BasicRoute(path, null, routeFunction); + this.notFoundPagePath = path; + return this; + } + public Builder route(String route) { this.routeFunction = new BasicRoute(route); + this.notFoundPagePath = route; return this; } @@ -139,20 +181,37 @@ public Builder failureRoute() { return this; } - public Builder nonApplicationRoute() { - this.frameworkRoute = true; - this.requiresLegacyRedirect = true; + public Builder displayOnNotFoundPage() { + this.displayOnNotFoundPage = true; return this; } - public Builder nonApplicationRoute(boolean requiresLegacyRedirect) { - this.frameworkRoute = true; - this.requiresLegacyRedirect = requiresLegacyRedirect; + public Builder displayOnNotFoundPage(String notFoundPageTitle) { + this.displayOnNotFoundPage = true; + this.notFoundPageTitle = notFoundPageTitle; + return this; + } + + public Builder displayOnNotFoundPage(String notFoundPageTitle, String notFoundPagePath) { + this.displayOnNotFoundPage = true; + this.notFoundPageTitle = notFoundPageTitle; + this.notFoundPagePath = notFoundPagePath; return this; } public RouteBuildItem build() { - return new RouteBuildItem(this); + return new RouteBuildItem(this, RouteType.APPLICATION_ROUTE, false); + } + + protected NotFoundPageDisplayableEndpointBuildItem getNotFoundEndpoint() { + if (!displayOnNotFoundPage) { + return null; + } + if (notFoundPagePath == null) { + throw new RuntimeException("Cannot display " + routeFunction + + " on not found page as no explicit path was specified and a route function is in use"); + } + return new NotFoundPageDisplayableEndpointBuildItem(notFoundPagePath, notFoundPageTitle); } } } diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java index 5d8663b8000fe..dd39d2a01b3a2 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java @@ -7,6 +7,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.security.CodeSource; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; @@ -46,6 +47,7 @@ import io.quarkus.vertx.core.deployment.CoreVertxBuildItem; import io.quarkus.vertx.core.deployment.EventLoopCountBuildItem; import io.quarkus.vertx.http.deployment.devmode.HttpRemoteDevClientProvider; +import io.quarkus.vertx.http.deployment.devmode.NotFoundPageDisplayableEndpointBuildItem; import io.quarkus.vertx.http.runtime.CurrentVertxRequest; import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; import io.quarkus.vertx.http.runtime.HttpConfiguration; @@ -80,7 +82,7 @@ HttpRootPathBuildItem httpRoot(HttpBuildTimeConfig httpBuildTimeConfig) { @BuildStep NonApplicationRootPathBuildItem frameworkRoot(HttpBuildTimeConfig httpBuildTimeConfig) { - return new NonApplicationRootPathBuildItem(httpBuildTimeConfig.nonApplicationRootPath, httpBuildTimeConfig.rootPath); + return new NonApplicationRootPathBuildItem(httpBuildTimeConfig.rootPath, httpBuildTimeConfig.nonApplicationRootPath); } @BuildStep @@ -117,6 +119,18 @@ public KubernetesPortBuildItem kubernetes() { return new KubernetesPortBuildItem(port, "http"); } + @BuildStep + void notRoundRoutes( + List routes, + BuildProducer notFound) { + for (RouteBuildItem i : routes) { + if (i.getNotFoundPageDisplayableEndpoint() != null) { + notFound.produce(i.getNotFoundPageDisplayableEndpoint()); + } + + } + } + @BuildStep @Record(ExecutionTime.RUNTIME_INIT) VertxWebRouterBuildItem initializeRouter(VertxHttpRecorder recorder, @@ -124,35 +138,54 @@ VertxWebRouterBuildItem initializeRouter(VertxHttpRecorder recorder, List routes, HttpBuildTimeConfig httpBuildTimeConfig, NonApplicationRootPathBuildItem nonApplicationRootPath, - BuildProducer frameworkRouterBuildProducer, ShutdownContextBuildItem shutdown) { - RuntimeValue router = recorder.initializeRouter(vertx.getVertx()); - RuntimeValue frameworkRouter = recorder.initializeRouter(vertx.getVertx()); - boolean frameworkRouterFound = false; - recorder.setNonApplicationRedirectHandler(nonApplicationRootPath.getFrameworkRootPath(), httpBuildTimeConfig.rootPath); + RuntimeValue httpRouteRouter = recorder.initializeRouter(vertx.getVertx()); + RuntimeValue frameworkRouter = null; + RuntimeValue mainRouter = null; + + List redirectRoutes = new ArrayList<>(); + boolean frameworkRouterCreated = false; + boolean mainRouterCreated = false; for (RouteBuildItem route : routes) { - if (nonApplicationRootPath.isSeparateRoot() && route.isFrameworkRoute()) { - frameworkRouterFound = true; + if (nonApplicationRootPath.isDedicatedRouterRequired() && route.isFrameworkRoute()) { + // Non-application endpoints on a separate path + if (!frameworkRouterCreated) { + frameworkRouter = recorder.initializeRouter(vertx.getVertx()); + frameworkRouterCreated = true; + } + recorder.addRoute(frameworkRouter, route.getRouteFunction(), route.getHandler(), route.getType()); - // Handle redirects from old paths to new non application endpoint root if (httpBuildTimeConfig.redirectToNonApplicationRootPath && route.isRequiresLegacyRedirect()) { - recorder.addRoute(router, route.getRouteFunction(), - recorder.getNonApplicationRedirectHandler(), - route.getType()); + redirectRoutes.add(route); + } + } else if (route.isAbsoluteRoute()) { + // Add Route to "/" + if (!mainRouterCreated) { + mainRouter = recorder.initializeRouter(vertx.getVertx()); + mainRouterCreated = true; } + recorder.addRoute(mainRouter, route.getRouteFunction(), route.getHandler(), route.getType()); } else { - recorder.addRoute(router, route.getRouteFunction(), route.getHandler(), route.getType()); + // Add Route to "/${quarkus.http.root-path}/ + recorder.addRoute(httpRouteRouter, route.getRouteFunction(), route.getHandler(), route.getType()); } } - if (frameworkRouterFound) { - frameworkRouterBuildProducer.produce(new VertxNonApplicationRouterBuildItem(frameworkRouter)); + if (frameworkRouterCreated) { + if (redirectRoutes.size() > 0) { + recorder.setNonApplicationRedirectHandler(nonApplicationRootPath.getNonApplicationRootPath(), + nonApplicationRootPath.getNormalizedHttpRootPath()); + + redirectRoutes.forEach(route -> recorder.addRoute(httpRouteRouter, route.getRouteFunction(), + recorder.getNonApplicationRedirectHandler(), + route.getType())); + } } - return new VertxWebRouterBuildItem(router); + return new VertxWebRouterBuildItem(httpRouteRouter, mainRouter, frameworkRouter); } @BuildStep @@ -167,8 +200,7 @@ ServiceStartBuildItem finalizeRouter( VertxHttpRecorder recorder, BeanContainerBuildItem beanContainer, CoreVertxBuildItem vertx, LaunchModeBuildItem launchMode, List defaultRoutes, List filters, - VertxWebRouterBuildItem router, - Optional frameworkRouter, + VertxWebRouterBuildItem httpRouteRouter, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, HttpBuildTimeConfig httpBuildTimeConfig, HttpConfiguration httpConfiguration, List requireBodyHandlerBuildItems, @@ -204,15 +236,30 @@ ServiceStartBuildItem finalizeRouter( Handler bodyHandler = !requireBodyHandlerBuildItems.isEmpty() ? bodyHandlerBuildItem.getHandler() : null; - if (frameworkRouter.isPresent()) { - recorder.mountFrameworkRouter(router.getRouter(), - frameworkRouter.get().getRouter(), - nonApplicationRootPathBuildItem.getFrameworkRootPath()); + Optional> mainRouter = httpRouteRouter.getMainRouter() != null + ? Optional.of(httpRouteRouter.getMainRouter()) + : Optional.empty(); + + if (httpRouteRouter.getFrameworkRouter() != null) { + if (nonApplicationRootPathBuildItem.isAttachedToMainRouter()) { + // Mount nested framework router + recorder.mountFrameworkRouter(httpRouteRouter.getHttpRouter(), + httpRouteRouter.getFrameworkRouter(), + nonApplicationRootPathBuildItem.getVertxRouterPath()); + } else { + // Create main router, not mounted under application router + if (!mainRouter.isPresent()) { + mainRouter = Optional.of(recorder.initializeRouter(vertx.getVertx())); + } + // Mount independent framework router under new main router + recorder.mountFrameworkRouter(mainRouter.get(), httpRouteRouter.getFrameworkRouter(), + nonApplicationRootPathBuildItem.getVertxRouterPath()); + } } recorder.finalizeRouter(beanContainer.getValue(), defaultRoute.map(DefaultRouteBuildItem::getRoute).orElse(null), - listOfFilters, vertx.getVertx(), lrc, router.getRouter(), httpBuildTimeConfig.rootPath, + listOfFilters, vertx.getVertx(), lrc, mainRouter, httpRouteRouter.getHttpRouter(), httpBuildTimeConfig.rootPath, launchMode.getLaunchMode(), !requireBodyHandlerBuildItems.isEmpty(), bodyHandler, httpConfiguration, gracefulShutdownFilter, shutdownConfig, executorBuildItem.getExecutorProxy()); diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxNonApplicationRouterBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxNonApplicationRouterBuildItem.java deleted file mode 100644 index a1a97109ef9d7..0000000000000 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxNonApplicationRouterBuildItem.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.quarkus.vertx.http.deployment; - -import io.quarkus.builder.item.SimpleBuildItem; -import io.quarkus.runtime.RuntimeValue; -import io.vertx.ext.web.Router; - -public final class VertxNonApplicationRouterBuildItem extends SimpleBuildItem { - - private RuntimeValue router; - - VertxNonApplicationRouterBuildItem(RuntimeValue router) { - this.router = router; - } - - public RuntimeValue getRouter() { - return router; - } -} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxWebRouterBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxWebRouterBuildItem.java index b8f559340854a..b3c9a7a064564 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxWebRouterBuildItem.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxWebRouterBuildItem.java @@ -6,13 +6,37 @@ public final class VertxWebRouterBuildItem extends SimpleBuildItem { - private RuntimeValue router; + private RuntimeValue httpRouter; + private RuntimeValue mainRouter; + private RuntimeValue frameworkRouter; - VertxWebRouterBuildItem(RuntimeValue router) { - this.router = router; + VertxWebRouterBuildItem(RuntimeValue httpRouter, RuntimeValue mainRouter, + RuntimeValue frameworkRouter) { + this.httpRouter = httpRouter; + this.mainRouter = mainRouter; + this.frameworkRouter = frameworkRouter; } - public RuntimeValue getRouter() { - return router; + public RuntimeValue getHttpRouter() { + return httpRouter; + } + + /** + * Will be {@code null} if `${quarkus.http.root-path}` is {@literal /}. + * + * @return RuntimeValue + */ + RuntimeValue getMainRouter() { + return mainRouter; + } + + /** + * Will be {@code null} if {@code ${quarkus.http.root-path}} is the same as + * {@code ${quarkus.http.non-application-root-path}}. + * + * @return RuntimeValue + */ + RuntimeValue getFrameworkRouter() { + return frameworkRouter; } } diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/ArcDevProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/ArcDevProcessor.java index 1de137cccc47e..90a9a0df72eea 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/ArcDevProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/ArcDevProcessor.java @@ -22,19 +22,26 @@ void registerRoutes(ArcConfig arcConfig, ArcDevRecorder recorder, BuildProducer routes, BuildProducer displayableEndpoints, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) { - String basePath = nonApplicationRootPathBuildItem.adjustPath("/arc"); + String basePath = "arc"; String beansPath = basePath + "/beans"; String removedBeansPath = basePath + "/removed-beans"; String observersPath = basePath + "/observers"; - routes.produce(RouteBuildItem.builder().route(basePath) + routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .route(basePath) + .displayOnNotFoundPage("CDI Overview") .handler(recorder.createSummaryHandler(getConfigProperties(arcConfig))).build()); - routes.produce(RouteBuildItem.builder().route(beansPath).handler(recorder.createBeansHandler()).build()); - routes.produce(RouteBuildItem.builder().route(removedBeansPath).handler(recorder.createRemovedBeansHandler()).build()); - routes.produce(RouteBuildItem.builder().route(observersPath).handler(recorder.createObserversHandler()).build()); - displayableEndpoints.produce(new NotFoundPageDisplayableEndpointBuildItem(basePath)); - displayableEndpoints.produce(new NotFoundPageDisplayableEndpointBuildItem(beansPath)); - displayableEndpoints.produce(new NotFoundPageDisplayableEndpointBuildItem(removedBeansPath)); - displayableEndpoints.produce(new NotFoundPageDisplayableEndpointBuildItem(observersPath)); + routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .route(beansPath) + .displayOnNotFoundPage("Active CDI Beans") + .handler(recorder.createBeansHandler()).build()); + routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .route(removedBeansPath) + .displayOnNotFoundPage("Removed CDI Beans") + .handler(recorder.createRemovedBeansHandler()).build()); + routes.produce(nonApplicationRootPathBuildItem.routeBuilder() + .route(observersPath) + .displayOnNotFoundPage("Active CDI Observers") + .handler(recorder.createObserversHandler()).build()); } // Note that we can't turn ArcConfig into BUILD_AND_RUN_TIME_FIXED because it's referencing IndexDependencyConfig diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/NotFoundPageDisplayableEndpointBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/NotFoundPageDisplayableEndpointBuildItem.java index 92b0387ab839a..a0d88468946d6 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/NotFoundPageDisplayableEndpointBuildItem.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/NotFoundPageDisplayableEndpointBuildItem.java @@ -1,14 +1,22 @@ package io.quarkus.vertx.http.deployment.devmode; import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.runtime.TemplateHtmlBuilder; +import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem; final public class NotFoundPageDisplayableEndpointBuildItem extends MultiBuildItem { private final String endpoint; private final String description; + private final boolean absolutePath; public NotFoundPageDisplayableEndpointBuildItem(String endpoint, String description) { + this(endpoint, description, false); + } + + public NotFoundPageDisplayableEndpointBuildItem(String endpoint, String description, boolean absolutePath) { this.endpoint = endpoint; this.description = description; + this.absolutePath = absolutePath; } public NotFoundPageDisplayableEndpointBuildItem(String endpoint) { @@ -22,4 +30,16 @@ public String getEndpoint() { public String getDescription() { return description; } + + public boolean isAbsolutePath() { + return absolutePath; + } + + public String getEndpoint(HttpRootPathBuildItem httpRoot) { + if (absolutePath) { + return endpoint; + } else { + return TemplateHtmlBuilder.adjustRoot(httpRoot.getRootPath(), endpoint); + } + } } diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java index ba8f7787d573c..727246f3ee6e2 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java @@ -76,10 +76,8 @@ import io.quarkus.qute.ValueResolvers; import io.quarkus.qute.Variant; import io.quarkus.runtime.RuntimeValue; -import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem; import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; import io.quarkus.vertx.http.deployment.RouteBuildItem; -import io.quarkus.vertx.http.deployment.devmode.NotFoundPageDisplayableEndpointBuildItem; import io.quarkus.vertx.http.runtime.devmode.DevConsoleFilter; import io.quarkus.vertx.http.runtime.devmode.DevConsoleRecorder; import io.quarkus.vertx.http.runtime.devmode.RedirectHandler; @@ -185,15 +183,12 @@ public void handle(HttpServerRequest event) { } protected static void newRouter(Engine engine, - HttpRootPathBuildItem httpRootPathBuildItem, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) { - // "" or "/myroot" - String httpRootPath = httpRootPathBuildItem.adjustPath("/"); - httpRootPath = httpRootPath.substring(0, httpRootPath.lastIndexOf("/")); - // "" or "/myroot" or "/q" or "/myroot/q" - String frameworkRootPath = httpRootPathBuildItem.adjustPath(nonApplicationRootPathBuildItem.adjustPath("/")); - frameworkRootPath = frameworkRootPath.substring(0, frameworkRootPath.lastIndexOf("/")); + // "/" or "/myroot/" + String httpRootPath = nonApplicationRootPathBuildItem.getNormalizedHttpRootPath(); + // "/" or "/myroot/" or "/q/" or "/myroot/q/" + String frameworkRootPath = nonApplicationRootPathBuildItem.getNonApplicationRootPath(); Handler errorHandler = new Handler() { @Override @@ -211,15 +206,12 @@ public void handle(RoutingContext event) { .handler(new DevConsole(engine, httpRootPath, frameworkRootPath)); mainRouter = Router.router(devConsoleVertx); mainRouter.errorHandler(500, errorHandler); - mainRouter.route(httpRootPathBuildItem.adjustPath(nonApplicationRootPathBuildItem.adjustPath("/dev/*"))) - .subRouter(router); + mainRouter.route(nonApplicationRootPathBuildItem.resolvePath("dev/*")).subRouter(router); } @BuildStep(onlyIf = IsDevelopment.class) public ServiceStartBuildItem buildTimeTemplates(List items, - BuildProducer devTemplatePaths, CurateOutcomeBuildItem curateOutcomeBuildItem) { - collectTemplates(devTemplatePaths); Map> results = new HashMap<>(); for (DevConsoleTemplateInfoBuildItem i : items) { Entry groupAndArtifact = i.groupIdAndArtifactId(curateOutcomeBuildItem); @@ -276,31 +268,30 @@ public HistoryHandlerBuildItem hander(BuildProducer logHand public void setupActions(List routes, BuildProducer routeBuildItemBuildProducer, List devTemplatePaths, - Optional devTemplateVariants, LogStreamRecorder recorder, CurateOutcomeBuildItem curateOutcomeBuildItem, - HttpRootPathBuildItem httpRootPathBuildItem, HistoryHandlerBuildItem historyHandlerBuildItem, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) { initializeVirtual(); - newRouter(buildEngine(devTemplatePaths), httpRootPathBuildItem, nonApplicationRootPathBuildItem); + newRouter(buildEngine(devTemplatePaths), nonApplicationRootPathBuildItem); // Add the log stream - routeBuildItemBuildProducer.produce(new RouteBuildItem.Builder() - .route("/dev/logstream") + routeBuildItemBuildProducer.produce(nonApplicationRootPathBuildItem.routeBuilder() + .route("dev/logstream") .handler(recorder.websocketHandler(historyHandlerBuildItem.value)) - .nonApplicationRoute(false) .build()); for (DevConsoleRouteBuildItem i : routes) { Entry groupAndArtifact = i.groupIdAndArtifactId(curateOutcomeBuildItem); // if the handler is a proxy, then that means it's been produced by a recorder and therefore belongs in the regular runtime Vert.x instance if (i.getHandler() instanceof BytecodeRecorderImpl.ReturnedProxy) { - routeBuildItemBuildProducer.produce(new RouteBuildItem( - new RuntimeDevConsoleRoute(groupAndArtifact.getKey(), groupAndArtifact.getValue(), i.getPath(), - i.getMethod()), - i.getHandler())); + routeBuildItemBuildProducer.produce(nonApplicationRootPathBuildItem.routeBuilder() + .routeFunction( + "dev/" + groupAndArtifact.getKey() + "." + groupAndArtifact.getValue() + "/" + i.getPath(), + new RuntimeDevConsoleRoute(i.getMethod())) + .handler(i.getHandler()) + .build()); } else { router.route(HttpMethod.valueOf(i.getMethod()), "/" + groupAndArtifact.getKey() + "." + groupAndArtifact.getValue() + "/" + i.getPath()) @@ -310,40 +301,32 @@ public void setupActions(List routes, DevConsoleManager.registerHandler(new DevConsoleHttpHandler()); //must be last so the above routes have precedence - routeBuildItemBuildProducer.produce(new RouteBuildItem.Builder() - .route("/dev/*") + routeBuildItemBuildProducer.produce(nonApplicationRootPathBuildItem.routeBuilder() + .route("dev/*") .handler(new DevConsoleFilter()) - .nonApplicationRoute(false) .build()); - routeBuildItemBuildProducer.produce(new RouteBuildItem.Builder() - .route("/dev") + routeBuildItemBuildProducer.produce(nonApplicationRootPathBuildItem.routeBuilder() + .route("dev") + .displayOnNotFoundPage("Dev UI") .handler(new RedirectHandler()) - .nonApplicationRoute(false) .build()); } - @BuildStep(onlyIf = IsDevelopment.class) - public void setupActions(BuildProducer displayableEndpoints, - NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) { - displayableEndpoints.produce(new NotFoundPageDisplayableEndpointBuildItem( - nonApplicationRootPathBuildItem.adjustPath("/dev/"), "Dev UI")); - } - @BuildStep(onlyIf = IsDevelopment.class) @Record(ExecutionTime.RUNTIME_INIT) public void deployStaticResources(DevConsoleRecorder recorder, CurateOutcomeBuildItem curateOutcomeBuildItem, LaunchModeBuildItem launchMode, ShutdownContextBuildItem shutdownContext, - BuildProducer routeBuildItemBuildProducer) throws IOException { + BuildProducer routeBuildItemBuildProducer, + NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) throws IOException { AppArtifact devConsoleResourcesArtifact = WebJarUtil.getAppArtifact(curateOutcomeBuildItem, "io.quarkus", "quarkus-vertx-http-deployment"); Path devConsoleStaticResourcesDeploymentPath = WebJarUtil.copyResourcesForDevOrTest(curateOutcomeBuildItem, launchMode, devConsoleResourcesArtifact, STATIC_RESOURCES_PATH); - routeBuildItemBuildProducer.produce(new RouteBuildItem.Builder() - .route("/dev/resources/*") + routeBuildItemBuildProducer.produce(nonApplicationRootPathBuildItem.routeBuilder() + .route("dev/resources/*") .handler(recorder.devConsoleHandler(devConsoleStaticResourcesDeploymentPath.toString(), shutdownContext)) - .nonApplicationRoute(false) .build()); } @@ -458,7 +441,8 @@ public Optional getVariant() { }); } - private void collectTemplates(BuildProducer devTemplatePaths) { + @BuildStep + void collectTemplates(BuildProducer devTemplatePaths) { try { ClassLoader classLoader = DevConsoleProcessor.class.getClassLoader(); Enumeration devTemplateURLs = classLoader.getResources("/dev-templates"); diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/NonApplicationAndRootPathTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/NonApplicationAndRootPathTest.java index e68a1b5b7af1e..b73a2bf0d103c 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/NonApplicationAndRootPathTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/NonApplicationAndRootPathTest.java @@ -16,6 +16,7 @@ import io.quarkus.builder.BuildContext; import io.quarkus.builder.BuildStep; import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; import io.quarkus.vertx.http.deployment.RouteBuildItem; import io.restassured.RestAssured; import io.vertx.core.Handler; @@ -39,14 +40,16 @@ public void accept(BuildChainBuilder builder) { builder.addBuildStep(new BuildStep() { @Override public void execute(BuildContext context) { - context.produce(new RouteBuildItem.Builder() - .route("/non-app") + NonApplicationRootPathBuildItem buildItem = context.consume(NonApplicationRootPathBuildItem.class); + context.produce(buildItem.routeBuilder() + .route("non-app-relative") .handler(new MyHandler()) .blockingRoute() - .nonApplicationRoute() + .requiresLegacyRedirect() .build()); } }).produces(RouteBuildItem.class) + .consumes(NonApplicationRootPathBuildItem.class) .build(); } }; @@ -64,13 +67,13 @@ public void handle(RoutingContext routingContext) { @Test public void testNonApplicationEndpointOnRootPathWithRedirect() { // Note RestAssured knows the path prefix is /api - RestAssured.given().get("/non-app").then().statusCode(200).body(Matchers.equalTo("/api/q/non-app")); + RestAssured.given().get("/non-app-relative").then().statusCode(200).body(Matchers.equalTo("/api/q/non-app-relative")); } @Test public void testNonApplicationEndpointDirect() { // Note RestAssured knows the path prefix is /api - RestAssured.given().get("/q/non-app").then().statusCode(200).body(Matchers.equalTo("/api/q/non-app")); + RestAssured.given().get("/q/non-app-relative").then().statusCode(200).body(Matchers.equalTo("/api/q/non-app-relative")); } @Singleton diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/NonApplicationEscapeTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/NonApplicationEscapeTest.java new file mode 100644 index 0000000000000..2fdac9dcbcce7 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/NonApplicationEscapeTest.java @@ -0,0 +1,113 @@ +package io.quarkus.vertx.http; + +import java.net.URL; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +import javax.enterprise.event.Observes; +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.awaitility.Awaitility; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.builder.BuildChainBuilder; +import io.quarkus.builder.BuildContext; +import io.quarkus.builder.BuildStep; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; +import io.quarkus.vertx.http.deployment.RouteBuildItem; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.client.HttpResponse; +import io.vertx.ext.web.client.WebClient; +import io.vertx.ext.web.client.predicate.ResponsePredicate; + +public class NonApplicationEscapeTest { + private static final String APP_PROPS = "" + + "quarkus.http.root-path=/api\n" + + "quarkus.http.non-application-root-path=${quarkus.http.root-path}\n"; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsResource(new StringAsset(APP_PROPS), "application.properties") + .addClasses(MyObserver.class)) + .addBuildChainCustomizer(buildCustomizer()); + + static Consumer buildCustomizer() { + return new Consumer() { + @Override + public void accept(BuildChainBuilder builder) { + builder.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + NonApplicationRootPathBuildItem buildItem = context.consume(NonApplicationRootPathBuildItem.class); + context.produce(buildItem.routeBuilder() + .route("/non-app-absolute") + .handler(new MyHandler()) + .blockingRoute() + .requiresLegacyRedirect() + .build()); + } + }).produces(RouteBuildItem.class) + .consumes(NonApplicationRootPathBuildItem.class) + .build(); + } + }; + } + + @TestHTTPResource("/") + URL uri; + + @Inject + Vertx vertx; + + public static class MyHandler implements Handler { + @Override + public void handle(RoutingContext routingContext) { + routingContext.response() + .setStatusCode(200) + .end(routingContext.request().path()); + } + } + + @Test + public void testNonApplicationEndpointEscaped() { + AtomicReference result = new AtomicReference<>(); + + WebClient.create(vertx) + .get(uri.getPort(), uri.getHost(), "/non-app-absolute") + .expect(ResponsePredicate.SC_OK) + .send(ar -> { + if (ar.succeeded()) { + HttpResponse response = ar.result(); + result.set(response.bodyAsString()); + } else { + result.set(ar.cause().getMessage()); + } + }); + + Awaitility.await().atMost(Duration.ofMinutes(2)).until(() -> result.get() != null); + + Assertions.assertEquals("/non-app-absolute", result.get()); + } + + @Singleton + static class MyObserver { + + void test(@Observes String event) { + //Do Nothing + } + + } +} diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/NonApplicationRootPathSiblingTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/NonApplicationRootPathSiblingTest.java new file mode 100644 index 0000000000000..3cbd535370d15 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/NonApplicationRootPathSiblingTest.java @@ -0,0 +1,82 @@ +package io.quarkus.vertx.http; + +import java.util.function.Consumer; + +import javax.enterprise.event.Observes; +import javax.inject.Singleton; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.builder.BuildChainBuilder; +import io.quarkus.builder.BuildContext; +import io.quarkus.builder.BuildStep; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; +import io.quarkus.vertx.http.deployment.RouteBuildItem; +import io.restassured.RestAssured; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +public class NonApplicationRootPathSiblingTest { + private static final String APP_PROPS = "" + + "quarkus.http.root-path=/api\n" + + "quarkus.http.non-application-root-path=${quarkus.http.root-path}"; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsResource(new StringAsset(APP_PROPS), "application.properties") + .addClasses(MyObserver.class)) + .addBuildChainCustomizer(buildCustomizer()); + + static Consumer buildCustomizer() { + return new Consumer() { + @Override + public void accept(BuildChainBuilder builder) { + builder.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + NonApplicationRootPathBuildItem buildItem = context.consume(NonApplicationRootPathBuildItem.class); + context.produce(buildItem.routeBuilder() + .route("non-app-relative") + .handler(new MyHandler()) + .blockingRoute() + .requiresLegacyRedirect() + .build()); + } + }).produces(RouteBuildItem.class) + .consumes(NonApplicationRootPathBuildItem.class) + .build(); + } + }; + } + + public static class MyHandler implements Handler { + @Override + public void handle(RoutingContext routingContext) { + routingContext.response() + .setStatusCode(200) + .end(routingContext.request().path()); + } + } + + @Test + public void testNonApplicationEndpoint() { + // Note RestAssured knows the path prefix is /api + RestAssured.given().get("/non-app-relative").then().statusCode(200).body(Matchers.equalTo("/api/non-app-relative")); + } + + @Singleton + static class MyObserver { + + void test(@Observes String event) { + //Do Nothing + } + + } +} diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/deployment/HttpRootPathBuildItemTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/deployment/HttpRootPathBuildItemTest.java new file mode 100644 index 0000000000000..456675376bf6b --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/deployment/HttpRootPathBuildItemTest.java @@ -0,0 +1,32 @@ +package io.quarkus.vertx.http.deployment; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class HttpRootPathBuildItemTest { + @Test + void testResolvePathWithSlash() { + HttpRootPathBuildItem buildItem = new HttpRootPathBuildItem("/"); + + Assertions.assertEquals("/", buildItem.resolvePath("")); + Assertions.assertEquals("/foo", buildItem.resolvePath("foo")); + Assertions.assertEquals("/foo", buildItem.resolvePath("/foo")); + Assertions.assertThrows(IllegalArgumentException.class, () -> buildItem.resolvePath("../foo")); + } + + @Test + void testResolvePathWithSlashApp() { + HttpRootPathBuildItem buildItem = new HttpRootPathBuildItem("/app"); + + Assertions.assertEquals("/app/", buildItem.resolvePath("")); + Assertions.assertEquals("/app/foo", buildItem.resolvePath("foo")); + Assertions.assertEquals("/foo", buildItem.resolvePath("/foo")); + } + + @Test + void testResolveResolvedPath() { + HttpRootPathBuildItem buildItem = new HttpRootPathBuildItem("/app"); + Assertions.assertEquals("/app", buildItem.resolvePath("/app")); + Assertions.assertEquals("/app/foo", buildItem.resolvePath("/app/foo")); + } +} diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/deployment/NonApplicationRootPathBuildItemTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/deployment/NonApplicationRootPathBuildItemTest.java new file mode 100644 index 0000000000000..4844ba60a7ba8 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/deployment/NonApplicationRootPathBuildItemTest.java @@ -0,0 +1,101 @@ +package io.quarkus.vertx.http.deployment; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class NonApplicationRootPathBuildItemTest { + @Test + void testResolvePathWithSlashRelativeQ() { + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "q"); + Assertions.assertTrue(buildItem.isDedicatedRouterRequired()); + Assertions.assertTrue(buildItem.attachedToMainRouter); + Assertions.assertEquals("/q/", buildItem.getVertxRouterPath()); + Assertions.assertEquals("/", buildItem.httpRootPath.getPath()); + Assertions.assertEquals("/q/", buildItem.nonApplicationRootPath.getPath()); + Assertions.assertNotEquals(buildItem.httpRootPath, buildItem.nonApplicationRootPath); + + Assertions.assertEquals("/q/foo", buildItem.resolvePath("foo")); + Assertions.assertEquals("/q/foo/sub/path", buildItem.resolvePath("foo/sub/path")); + Assertions.assertEquals("/foo", buildItem.resolvePath("/foo")); + Assertions.assertEquals("/foo/sub/path", buildItem.resolvePath("/foo/sub/path")); + Assertions.assertThrows(IllegalArgumentException.class, () -> buildItem.resolvePath("../foo")); + Assertions.assertThrows(IllegalArgumentException.class, () -> buildItem.resolvePath("")); + } + + @Test + void testResolvePathWithSlashAbsoluteQ() { + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "/q"); + Assertions.assertTrue(buildItem.isDedicatedRouterRequired()); + Assertions.assertTrue(buildItem.attachedToMainRouter); + Assertions.assertEquals("/q/", buildItem.getVertxRouterPath()); + Assertions.assertEquals("/", buildItem.httpRootPath.getPath()); + Assertions.assertEquals("/q/", buildItem.nonApplicationRootPath.getPath()); + Assertions.assertNotEquals(buildItem.httpRootPath, buildItem.nonApplicationRootPath); + + Assertions.assertEquals("/q/foo", buildItem.resolvePath("foo")); + Assertions.assertEquals("/foo", buildItem.resolvePath("/foo")); + } + + @Test + void testResolvePathWithSlashAppWithRelativeQ() { + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/app", "q"); + Assertions.assertTrue(buildItem.isDedicatedRouterRequired()); + Assertions.assertTrue(buildItem.attachedToMainRouter); + Assertions.assertEquals("/q/", buildItem.getVertxRouterPath()); + Assertions.assertEquals("/app/", buildItem.httpRootPath.getPath()); + Assertions.assertEquals("/app/q/", buildItem.nonApplicationRootPath.getPath()); + Assertions.assertNotEquals(buildItem.httpRootPath, buildItem.nonApplicationRootPath); + + Assertions.assertEquals("/app/q/foo", buildItem.resolvePath("foo")); + Assertions.assertEquals("/foo", buildItem.resolvePath("/foo")); + } + + @Test + void testResolvePathWithSlashAppWithAbsoluteQ() { + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/app", "/q"); + Assertions.assertTrue(buildItem.isDedicatedRouterRequired()); + Assertions.assertFalse(buildItem.attachedToMainRouter); + Assertions.assertEquals("/q/", buildItem.getVertxRouterPath()); + Assertions.assertEquals("/app/", buildItem.httpRootPath.getPath()); + Assertions.assertEquals("/q/", buildItem.nonApplicationRootPath.getPath()); + Assertions.assertNotEquals(buildItem.httpRootPath, buildItem.nonApplicationRootPath); + + Assertions.assertEquals("/q/foo", buildItem.resolvePath("foo")); + Assertions.assertEquals("/foo", buildItem.resolvePath("/foo")); + } + + @Test + void testResolvePathWithSlashEmpty() { + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", ""); + Assertions.assertFalse(buildItem.isDedicatedRouterRequired()); + Assertions.assertTrue(buildItem.attachedToMainRouter); + Assertions.assertEquals("/", buildItem.getVertxRouterPath()); + Assertions.assertEquals("/", buildItem.httpRootPath.getPath()); + Assertions.assertEquals("/", buildItem.nonApplicationRootPath.getPath()); + Assertions.assertEquals(buildItem.httpRootPath, buildItem.nonApplicationRootPath); + + Assertions.assertEquals("/foo", buildItem.resolvePath("foo")); + Assertions.assertEquals("/foo", buildItem.resolvePath("/foo")); + } + + @Test + void testResolvePathWithSlashWithSlash() { + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "/"); + Assertions.assertFalse(buildItem.isDedicatedRouterRequired()); + Assertions.assertTrue(buildItem.attachedToMainRouter); + Assertions.assertEquals("/", buildItem.getVertxRouterPath()); + Assertions.assertEquals("/", buildItem.httpRootPath.getPath()); + Assertions.assertEquals("/", buildItem.nonApplicationRootPath.getPath()); + Assertions.assertEquals(buildItem.httpRootPath, buildItem.nonApplicationRootPath); + + Assertions.assertEquals("/foo", buildItem.resolvePath("foo")); + Assertions.assertEquals("/foo", buildItem.resolvePath("/foo")); + } + + @Test + void testResolvePathWithSlashWithSlashQWithWildcards() { + NonApplicationRootPathBuildItem buildItem = new NonApplicationRootPathBuildItem("/", "/q"); + Assertions.assertEquals("/q/foo/*", buildItem.resolvePath("foo/*")); + Assertions.assertEquals("/foo/*", buildItem.resolvePath("/foo/*")); + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/BasicRoute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/BasicRoute.java index 181e5cbd8d761..064145e894d45 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/BasicRoute.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/BasicRoute.java @@ -1,5 +1,6 @@ package io.quarkus.vertx.http.runtime; +import java.util.function.Consumer; import java.util.function.Function; import io.vertx.ext.web.Route; @@ -11,6 +12,8 @@ public class BasicRoute implements Function { private Integer order; + private Consumer customizer; + public BasicRoute(String path) { this(path, null); } @@ -20,6 +23,12 @@ public BasicRoute(String path, Integer order) { this.order = order; } + public BasicRoute(String path, Integer order, Consumer customizer) { + this.path = path; + this.order = order; + this.customizer = customizer; + } + public BasicRoute() { } @@ -39,12 +48,24 @@ public void setOrder(Integer order) { this.order = order; } + public Consumer getCustomizer() { + return customizer; + } + + public BasicRoute setCustomizer(Consumer customizer) { + this.customizer = customizer; + return this; + } + @Override public Route apply(Router router) { Route route = router.route(path); if (order != null) { route.order(order); } + if (customizer != null) { + customizer.accept(route); + } return route; } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java index fc97336e4b4de..172a24288d0ad 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java @@ -31,29 +31,41 @@ public class HttpBuildTimeConfig { public boolean virtual; /** - * The HTTP root path for non application endpoints. Various endpoints such as metrics, health, - * and open api are deployed under this path. - * Setting the value to "/" disables the separate non application root, - * resulting in all non application endpoints being served from "/" along with the application. + * A common root path for non-application endpoints. Various extension-provided endpoints such as metrics, health, + * and openapi are deployed under this path by default. + * + * * Relative path (Default, `q`) -> + * Non-application endpoints will be served from + * `${quarkus.http.root-path}/${quarkus.http.non-application-root-path}`. + * * Absolute path (`/q`) -> + * Non-application endpoints will be served from the specified path. + * * `${quarkus.http.root-path}` -> Setting this path to the same value as HTTP root path disables + * this root path. All extension-provided endpoints will be served from `${quarkus.http.root-path}`. + * + * @asciidoclet */ - @ConfigItem(defaultValue = "/q") + @ConfigItem(defaultValue = "q") public String nonApplicationRootPath; /** - * Whether to redirect non application endpoints from previous location off the root to the - * new non application root path. - * Enabled by default. + * Provide redirect endpoints for extension-provided endpoints existing prior to Quarkus 1.11. + * This will trigger HTTP 301 Redirects for the following: + * + * * `/graphql-ui` + * * `/health` + * * `/health-ui` + * * `/metrics` + * * `/openapi` + * * `/swagger-ui` + * + * Default is `true` for Quarkus 1.11.x to facilitate transition to name-spaced URIs using + * `${quarkus.http.non-application-root-path}`. + * + * Quarkus 1.13 will change the default to `false`, + * and the config item will be removed in Quarkus 2.0. + * + * @asciidoclet */ @ConfigItem(defaultValue = "true") public boolean redirectToNonApplicationRootPath; - - public String adjustPath(String path) { - if (!path.startsWith("/")) { - throw new IllegalArgumentException("Path must start with /"); - } - if (rootPath.equals("/")) { - return path; - } - return rootPath + path; - } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java index cbe271a5158e3..91c2ee7eb7a72 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java @@ -259,13 +259,13 @@ public void mountFrameworkRouter(RuntimeValue mainRouter, RuntimeValue defaultRouteHandler, List filterList, Supplier vertx, - LiveReloadConfig liveReloadConfig, - RuntimeValue runtimeValue, String rootPath, LaunchMode launchMode, boolean requireBodyHandler, + LiveReloadConfig liveReloadConfig, Optional> mainRouterRuntimeValue, + RuntimeValue httpRouterRuntimeValue, String rootPath, LaunchMode launchMode, boolean requireBodyHandler, Handler bodyHandler, HttpConfiguration httpConfiguration, GracefulShutdownFilter gracefulShutdownFilter, ShutdownConfig shutdownConfig, Executor executor) { // install the default route at the end - Router router = runtimeValue.getValue(); + Router httpRouteRouter = httpRouterRuntimeValue.getValue(); //allow the router to be modified programmatically Event event = Arc.container().beanManager().getEvent(); @@ -277,26 +277,26 @@ public void finalizeRouter(BeanContainer container, Consumer defaultRoute filterList.addAll(filters.getFilters()); // Then, fire the resuming router - event.select(Router.class).fire(router); + event.select(Router.class).fire(httpRouteRouter); for (Filter filter : filterList) { if (filter.getHandler() != null) { // Filters with high priority gets called first. - router.route().order(-1 * filter.getPriority()).handler(filter.getHandler()); + httpRouteRouter.route().order(-1 * filter.getPriority()).handler(filter.getHandler()); } } if (defaultRouteHandler != null) { - defaultRouteHandler.accept(router.route().order(DEFAULT_ROUTE_ORDER)); + defaultRouteHandler.accept(httpRouteRouter.route().order(DEFAULT_ROUTE_ORDER)); } - container.instance(RouterProducer.class).initialize(router); - router.route().last().failureHandler(new QuarkusErrorHandler(launchMode.isDevOrTest())); + container.instance(RouterProducer.class).initialize(httpRouteRouter); + httpRouteRouter.route().last().failureHandler(new QuarkusErrorHandler(launchMode.isDevOrTest())); if (requireBodyHandler) { //if this is set then everything needs the body handler installed //TODO: config etc - router.route().order(Integer.MIN_VALUE + 1).handler(new Handler() { + httpRouteRouter.route().order(Integer.MIN_VALUE + 1).handler(new Handler() { @Override public void handle(RoutingContext routingContext) { routingContext.request().resume(); @@ -308,7 +308,7 @@ public void handle(RoutingContext routingContext) { if (httpConfiguration.limits.maxBodySize.isPresent()) { long limit = httpConfiguration.limits.maxBodySize.get().asLongValue(); Long limitObj = limit; - router.route().order(-2).handler(new Handler() { + httpRouteRouter.route().order(-2).handler(new Handler() { @Override public void handle(RoutingContext event) { String lengthString = event.request().headers().get(HttpHeaderNames.CONTENT_LENGTH); @@ -340,7 +340,7 @@ public void handle(Void e) { if (hotReplacementHandler != null) { //recorders are always executed in the current CL ClassLoader currentCl = Thread.currentThread().getContextClassLoader(); - router.route().order(Integer.MIN_VALUE).handler(new Handler() { + httpRouteRouter.route().order(Integer.MIN_VALUE).handler(new Handler() { @Override public void handle(RoutingContext event) { Thread.currentThread().setContextClassLoader(currentCl); @@ -348,10 +348,11 @@ public void handle(RoutingContext event) { } }); } - root = router; + root = httpRouteRouter; } else { - Router mainRouter = Router.router(vertx.get()); - mainRouter.mountSubRouter(rootPath, router); + Router mainRouter = mainRouterRuntimeValue.isPresent() ? mainRouterRuntimeValue.get().getValue() + : Router.router(vertx.get()); + mainRouter.mountSubRouter(rootPath, httpRouteRouter); if (hotReplacementHandler != null) { ClassLoader currentCl = Thread.currentThread().getContextClassLoader(); mainRouter.route().order(Integer.MIN_VALUE).handler(new Handler() { @@ -395,7 +396,7 @@ public void handle(HttpServerRequest event) { receiver = new JBossLoggingAccessLogReceiver(accessLog.category); } AccessLogHandler handler = new AccessLogHandler(receiver, accessLog.pattern, getClass().getClassLoader()); - router.route().order(Integer.MIN_VALUE).handler(handler); + httpRouteRouter.route().order(Integer.MIN_VALUE).handler(handler); quarkusWrapperNeeded = true; } @@ -424,7 +425,7 @@ public void handle(HttpServerRequest event) { } }; if (httpConfiguration.recordRequestStartTime) { - router.route().order(Integer.MIN_VALUE).handler(new Handler() { + httpRouteRouter.route().order(Integer.MIN_VALUE).handler(new Handler() { @Override public void handle(RoutingContext event) { event.put(REQUEST_START_TIME, System.nanoTime()); @@ -778,14 +779,9 @@ public void setNonApplicationRedirectHandler(String nonApplicationPath, String r nonApplicationRedirectHandler = new Handler() { @Override public void handle(RoutingContext context) { - String absoluteURI = context.request().absoluteURI(); - int pathStart = absoluteURI.indexOf(context.request().path()); - if (rootPath.length() > 1 && absoluteURI.contains(rootPath)) { - // Only do this when rootPath is not '/' - pathStart = pathStart + rootPath.length(); - } - String redirectTo = absoluteURI.substring(0, pathStart) + nonApplicationPath - + absoluteURI.substring(pathStart); + String absoluteURI = context.request().path(); + String target = absoluteURI.substring(rootPath.length()); + String redirectTo = nonApplicationPath + target; context.response() .setStatusCode(HttpResponseStatus.MOVED_PERMANENTLY.code()) diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/RuntimeDevConsoleRoute.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/RuntimeDevConsoleRoute.java index 583153ae48757..ff5a05a705691 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/RuntimeDevConsoleRoute.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/RuntimeDevConsoleRoute.java @@ -1,53 +1,19 @@ package io.quarkus.vertx.http.runtime.devmode; -import java.util.function.Function; +import java.util.function.Consumer; import io.vertx.core.http.HttpMethod; import io.vertx.ext.web.Route; -import io.vertx.ext.web.Router; -public class RuntimeDevConsoleRoute implements Function { +public class RuntimeDevConsoleRoute implements Consumer { - private String groupId; - private String artifactId; - private String path; private String method; - public RuntimeDevConsoleRoute(String groupId, String artifactId, String path, String method) { - this.groupId = groupId; - this.artifactId = artifactId; - this.path = path; - this.method = method; - } - public RuntimeDevConsoleRoute() { } - public String getGroupId() { - return groupId; - } - - public RuntimeDevConsoleRoute setGroupId(String groupId) { - this.groupId = groupId; - return this; - } - - public String getArtifactId() { - return artifactId; - } - - public RuntimeDevConsoleRoute setArtifactId(String artifactId) { - this.artifactId = artifactId; - return this; - } - - public String getPath() { - return path; - } - - public RuntimeDevConsoleRoute setPath(String path) { - this.path = path; - return this; + public RuntimeDevConsoleRoute(String method) { + this.method = method; } public String getMethod() { @@ -60,8 +26,8 @@ public RuntimeDevConsoleRoute setMethod(String method) { } @Override - public Route apply(Router route) { - return route.route(HttpMethod.valueOf(method), "/q/dev/" + groupId + "." + artifactId + "/" + path) + public void accept(Route route) { + route.method(HttpMethod.valueOf(method)) .order(-100); } } diff --git a/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/VertxWebProcessor.java b/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/VertxWebProcessor.java index 737ac8e2c892b..0ed4646d84768 100644 --- a/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/VertxWebProcessor.java +++ b/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/VertxWebProcessor.java @@ -85,6 +85,7 @@ import io.quarkus.gizmo.WhileLoop; import io.quarkus.hibernate.validator.spi.BeanValidationAnnotationsBuildItem; import io.quarkus.runtime.LaunchMode; +import io.quarkus.runtime.TemplateHtmlBuilder; import io.quarkus.runtime.util.HashUtil; import io.quarkus.vertx.http.deployment.FilterBuildItem; import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem; @@ -419,9 +420,11 @@ void routeNotFound(Capabilities capabilities, ResourceNotFoundRecorder recorder, List additionalEndpoints) { if (capabilities.isMissing(Capability.RESTEASY)) { // Register a special error handler if JAX-RS not available - recorder.registerNotFoundHandler(router.getRouter(), httpRoot.getRootPath(), + recorder.registerNotFoundHandler(router.getHttpRouter(), httpRoot.getRootPath(), descriptions.stream().map(RouteDescriptionBuildItem::getDescription).collect(Collectors.toList()), - additionalEndpoints.stream().map(NotFoundPageDisplayableEndpointBuildItem::getEndpoint) + additionalEndpoints.stream() + .map(s -> s.isAbsolutePath() ? s.getEndpoint() + : TemplateHtmlBuilder.adjustRoot(httpRoot.getRootPath(), s.getEndpoint())) .collect(Collectors.toList())); } } diff --git a/extensions/vertx-web/runtime/src/main/java/io/quarkus/vertx/web/runtime/devmode/ResourceNotFoundHandler.java b/extensions/vertx-web/runtime/src/main/java/io/quarkus/vertx/web/runtime/devmode/ResourceNotFoundHandler.java index 36acac172cde5..e465c96531a73 100644 --- a/extensions/vertx-web/runtime/src/main/java/io/quarkus/vertx/web/runtime/devmode/ResourceNotFoundHandler.java +++ b/extensions/vertx-web/runtime/src/main/java/io/quarkus/vertx/web/runtime/devmode/ResourceNotFoundHandler.java @@ -48,7 +48,7 @@ public void handle(RoutingContext routingContext) { if (!additionalEndpoints.isEmpty()) { builder.resourcesStart("Additional endpoints"); for (String additionalEndpoint : additionalEndpoints) { - builder.staticResourcePath(adjustRoot(httpRoot, additionalEndpoint)); + builder.staticResourcePath(additionalEndpoint); } builder.resourcesEnd(); } diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithHealthTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithHealthTest.java index af0b2a0d66a8e..74b39ab42ff36 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithHealthTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithHealthTest.java @@ -84,7 +84,7 @@ public void assertGeneratedResources() throws IOException { }); assertThat(container.getLivenessProbe()).isNotNull().satisfies(p -> { assertThat(p.getInitialDelaySeconds()).isEqualTo(20); - assertProbePath(p, "/q/health/live"); + assertProbePath(p, "/liveness"); assertNotNull(p.getHttpGet()); assertEquals(p.getHttpGet().getPort().getIntVal(), 9090); diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsCustomAbsoluteTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsCustomAbsoluteTest.java new file mode 100644 index 0000000000000..bbba410693d54 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsCustomAbsoluteTest.java @@ -0,0 +1,86 @@ +package io.quarkus.it.kubernetes; + +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.hamcrest.Matchers.is; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.quarkus.bootstrap.model.AppArtifact; +import io.quarkus.builder.Version; +import io.quarkus.test.LogFile; +import io.quarkus.test.ProdBuildResults; +import io.quarkus.test.ProdModeTestResults; +import io.quarkus.test.QuarkusProdModeTest; + +public class KubernetesWithMetricsCustomAbsoluteTest { + + @RegisterExtension + static final QuarkusProdModeTest config = new QuarkusProdModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClasses(GreetingResource.class)) + .setApplicationName("metrics") + .setApplicationVersion("0.1-SNAPSHOT") + .setRun(true) + .setLogFileName("k8s.log") + .withConfigurationResource("kubernetes-with-metrics-custom-absolute.properties") + .setForcedDependencies( + Collections.singletonList( + new AppArtifact("io.quarkus", "quarkus-micrometer-registry-prometheus", Version.getVersion()))); + + @ProdBuildResults + private ProdModeTestResults prodModeTestResults; + + @LogFile + private Path logfile; + + @Test + public void assertApplicationRuns() { + assertThat(logfile).isRegularFile().hasFileName("k8s.log"); + TestUtil.assertLogFileContents(logfile, "kubernetes", "metrics"); + + given() + .when().get("/greeting") + .then() + .statusCode(200) + .body(is("hello")); + } + + @Test + public void assertGeneratedResources() throws IOException { + final Path kubernetesDir = prodModeTestResults.getBuildDir().resolve("kubernetes"); + assertThat(kubernetesDir) + .isDirectoryContaining(p -> p.getFileName().endsWith("kubernetes.json")) + .isDirectoryContaining(p -> p.getFileName().endsWith("kubernetes.yml")); + List kubernetesList = DeserializationUtil + .deserializeAsList(kubernetesDir.resolve("kubernetes.yml")); + assertThat(kubernetesList.get(0)).isInstanceOfSatisfying(Deployment.class, d -> { + assertThat(d.getMetadata()).satisfies(m -> { + assertThat(m.getName()).isEqualTo("metrics"); + }); + + assertThat(d.getSpec()).satisfies(deploymentSpec -> { + assertThat(deploymentSpec.getTemplate()).satisfies(t -> { + assertThat(t.getMetadata()).satisfies(meta -> { + // Annotations will have a different default prefix, + // and the scrape annotation was specifically configured + assertThat(meta.getAnnotations()).contains(entry("example.io/should_be_scraped", "true"), + entry("example.io/path", "/absolute-metrics"), entry("example.io/port", "9090"), + entry("example.io/scheme", "http")); + }); + }); + }); + }); + } + +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsCustomPrefixTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsCustomRelativeTest.java similarity index 94% rename from integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsCustomPrefixTest.java rename to integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsCustomRelativeTest.java index 60bb0ba62749c..a681d25ffe2f9 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsCustomPrefixTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsCustomRelativeTest.java @@ -24,16 +24,16 @@ import io.quarkus.test.ProdModeTestResults; import io.quarkus.test.QuarkusProdModeTest; -public class KubernetesWithMetricsCustomPrefixTest { +public class KubernetesWithMetricsCustomRelativeTest { @RegisterExtension static final QuarkusProdModeTest config = new QuarkusProdModeTest() .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClasses(GreetingResource.class)) - .setApplicationName("health") + .setApplicationName("metrics") .setApplicationVersion("0.1-SNAPSHOT") .setRun(true) .setLogFileName("k8s.log") - .withConfigurationResource("kubernetes-with-metrics-custom-prefix.properties") + .withConfigurationResource("kubernetes-with-metrics-custom-relative.properties") .setForcedDependencies( Collections.singletonList( new AppArtifact("io.quarkus", "quarkus-smallrye-metrics", Version.getVersion()))); @@ -66,7 +66,7 @@ public void assertGeneratedResources() throws IOException { .deserializeAsList(kubernetesDir.resolve("kubernetes.yml")); assertThat(kubernetesList.get(0)).isInstanceOfSatisfying(Deployment.class, d -> { assertThat(d.getMetadata()).satisfies(m -> { - assertThat(m.getName()).isEqualTo("health"); + assertThat(m.getName()).isEqualTo("metrics"); }); assertThat(d.getSpec()).satisfies(deploymentSpec -> { diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsNoAnnotationsTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsNoAnnotationsTest.java index f49f3be602bc6..e0f24a7a0c2f9 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsNoAnnotationsTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsNoAnnotationsTest.java @@ -29,14 +29,14 @@ public class KubernetesWithMetricsNoAnnotationsTest { @RegisterExtension static final QuarkusProdModeTest config = new QuarkusProdModeTest() .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClasses(GreetingResource.class)) - .setApplicationName("health") + .setApplicationName("metrics") .setApplicationVersion("0.1-SNAPSHOT") .setRun(true) .setLogFileName("k8s.log") .withConfigurationResource("kubernetes-with-metrics-no-annotations.properties") .setForcedDependencies( Collections.singletonList( - new AppArtifact("io.quarkus", "quarkus-smallrye-metrics", Version.getVersion()))); + new AppArtifact("io.quarkus", "quarkus-micrometer-registry-prometheus", Version.getVersion()))); @ProdBuildResults private ProdModeTestResults prodModeTestResults; @@ -66,7 +66,7 @@ public void assertGeneratedResources() throws IOException { .deserializeAsList(kubernetesDir.resolve("kubernetes.yml")); assertThat(kubernetesList.get(0)).isInstanceOfSatisfying(Deployment.class, d -> { assertThat(d.getMetadata()).satisfies(m -> { - assertThat(m.getName()).isEqualTo("health"); + assertThat(m.getName()).isEqualTo("metrics"); }); assertThat(d.getSpec()).satisfies(deploymentSpec -> { @@ -74,7 +74,7 @@ public void assertGeneratedResources() throws IOException { assertThat(t.getMetadata()).satisfies(meta -> { // Annotations should not have been created in this configuration. assertThat(meta.getAnnotations()).doesNotContain(entry("prometheus.io/scrape", "true"), - entry("prometheus.io/path", "/q/met"), entry("prometheus.io/port", "9090"), + entry("prometheus.io/path", "/met"), entry("prometheus.io/port", "9090"), entry("prometheus.io/scheme", "http")); }); }); diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsTest.java index cc623dd327aec..30d04ba17ad71 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMetricsTest.java @@ -29,7 +29,7 @@ public class KubernetesWithMetricsTest { @RegisterExtension static final QuarkusProdModeTest config = new QuarkusProdModeTest() .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClasses(GreetingResource.class)) - .setApplicationName("health") + .setApplicationName("metrics") .setApplicationVersion("0.1-SNAPSHOT") .setRun(true) .setLogFileName("k8s.log") @@ -66,7 +66,7 @@ public void assertGeneratedResources() throws IOException { .deserializeAsList(kubernetesDir.resolve("kubernetes.yml")); assertThat(kubernetesList.get(0)).isInstanceOfSatisfying(Deployment.class, d -> { assertThat(d.getMetadata()).satisfies(m -> { - assertThat(m.getName()).isEqualTo("health"); + assertThat(m.getName()).isEqualTo("metrics"); }); assertThat(d.getSpec()).satisfies(deploymentSpec -> { diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMicrometerTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMicrometerTest.java index 5c9d7c441ab11..306f597bd598f 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMicrometerTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithMicrometerTest.java @@ -29,7 +29,7 @@ public class KubernetesWithMicrometerTest { @RegisterExtension static final QuarkusProdModeTest config = new QuarkusProdModeTest() .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClasses(GreetingResource.class)) - .setApplicationName("health") + .setApplicationName("metrics") .setApplicationVersion("0.1-SNAPSHOT") .setRun(true) .setLogFileName("k8s.log") @@ -65,7 +65,7 @@ public void assertGeneratedResources() throws IOException { .deserializeAsList(kubernetesDir.resolve("kubernetes.yml")); assertThat(kubernetesList.get(0)).isInstanceOfSatisfying(Deployment.class, d -> { assertThat(d.getMetadata()).satisfies(m -> { - assertThat(m.getName()).isEqualTo("health"); + assertThat(m.getName()).isEqualTo("metrics"); }); assertThat(d.getSpec()).satisfies(deploymentSpec -> { diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithRootAndHealthTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithRootAndHealthTest.java index c08256dddf51d..cd9b9b5d0dfaf 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithRootAndHealthTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithRootAndHealthTest.java @@ -57,7 +57,7 @@ public void assertGeneratedResources() throws IOException { assertProbePath(p, "/api/q/health/ready"); }); assertThat(container.getLivenessProbe()).satisfies(p -> { - assertProbePath(p, "/api/q/health/liveness"); + assertProbePath(p, "/liveness"); }); }); }); diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-health.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-health.properties index 4410bb400544b..b7337fe50a68f 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-health.properties +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-health.properties @@ -1,2 +1,3 @@ quarkus.http.port=9090 quarkus.kubernetes.liveness-probe.initial-delay=20s +quarkus.smallrye-health.liveness-path=/liveness diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-metrics-custom-absolute.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-metrics-custom-absolute.properties new file mode 100644 index 0000000000000..befe3e634b923 --- /dev/null +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-metrics-custom-absolute.properties @@ -0,0 +1,4 @@ +quarkus.http.port=9090 +quarkus.micrometer.export.prometheus.path=/absolute-metrics +quarkus.kubernetes.prometheus.prefix=example.io +quarkus.kubernetes.prometheus.scrape=example.io/should_be_scraped \ No newline at end of file diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-metrics-custom-prefix.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-metrics-custom-relative.properties similarity index 79% rename from integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-metrics-custom-prefix.properties rename to integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-metrics-custom-relative.properties index 209d328df81da..ce7853e673614 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-metrics-custom-prefix.properties +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-metrics-custom-relative.properties @@ -1,4 +1,4 @@ quarkus.http.port=9090 -quarkus.smallrye-metrics.path=/met +quarkus.smallrye-metrics.path=met quarkus.kubernetes.prometheus.prefix=example.io quarkus.kubernetes.prometheus.scrape=example.io/should_be_scraped \ No newline at end of file diff --git a/integration-tests/legacy-redirect/pom.xml b/integration-tests/legacy-redirect/pom.xml new file mode 100644 index 0000000000000..c38d191a4bb96 --- /dev/null +++ b/integration-tests/legacy-redirect/pom.xml @@ -0,0 +1,194 @@ + + + 4.0.0 + + + io.quarkus + quarkus-integration-tests-parent + 999-SNAPSHOT + + + quarkus-integration-test-legacy-redirect + Quarkus - Integration Tests - Legacy redirect + + + + io.quarkus + quarkus-smallrye-graphql + + + io.quarkus + quarkus-smallrye-health + + + io.quarkus + quarkus-smallrye-metrics + + + io.quarkus + quarkus-smallrye-openapi + + + io.quarkus + quarkus-swagger-ui + + + + + io.quarkus + quarkus-resteasy + + + io.quarkus + quarkus-vertx-http + + + + + io.quarkus + quarkus-kubernetes + + + + + io.quarkus + quarkus-junit5 + test + + + io.vertx + vertx-web-client + test + + + org.jboss.logging + commons-logging-jboss-logging + test + + + + + io.quarkus + quarkus-smallrye-graphql-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-smallrye-health-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-smallrye-metrics-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-smallrye-openapi-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-swagger-ui-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-kubernetes-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-resteasy-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-vertx-http-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + diff --git a/integration-tests/legacy-redirect/src/main/java/io/quarkus/it/legacy/redirect/Farewell.java b/integration-tests/legacy-redirect/src/main/java/io/quarkus/it/legacy/redirect/Farewell.java new file mode 100644 index 0000000000000..215a3bbdefc07 --- /dev/null +++ b/integration-tests/legacy-redirect/src/main/java/io/quarkus/it/legacy/redirect/Farewell.java @@ -0,0 +1,10 @@ +package io.quarkus.it.legacy.redirect; + +public class Farewell extends Salutation { + + @Override + public String getType() { + return "Farewell"; + } + +} diff --git a/integration-tests/legacy-redirect/src/main/java/io/quarkus/it/legacy/redirect/Greeting.java b/integration-tests/legacy-redirect/src/main/java/io/quarkus/it/legacy/redirect/Greeting.java new file mode 100644 index 0000000000000..fb8d9f95f9355 --- /dev/null +++ b/integration-tests/legacy-redirect/src/main/java/io/quarkus/it/legacy/redirect/Greeting.java @@ -0,0 +1,39 @@ +package io.quarkus.it.legacy.redirect; + +import java.time.LocalTime; + +public class Greeting extends Salutation { + + private String message; + private LocalTime time; + + public Greeting() { + } + + public Greeting(String message, LocalTime time) { + this.message = message; + this.time = time; + } + + public String getMessage() { + return message; + } + + public LocalTime getTime() { + return time; + } + + public void setMessage(String message) { + this.message = message; + } + + public void setTime(LocalTime time) { + this.time = time; + } + + @Override + public String getType() { + return "Greet"; + } + +} diff --git a/integration-tests/legacy-redirect/src/main/java/io/quarkus/it/legacy/redirect/GreetingResource.java b/integration-tests/legacy-redirect/src/main/java/io/quarkus/it/legacy/redirect/GreetingResource.java new file mode 100644 index 0000000000000..dc834e4207880 --- /dev/null +++ b/integration-tests/legacy-redirect/src/main/java/io/quarkus/it/legacy/redirect/GreetingResource.java @@ -0,0 +1,47 @@ +package io.quarkus.it.legacy.redirect; + +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Mutation; +import org.eclipse.microprofile.graphql.Name; +import org.eclipse.microprofile.graphql.Query; +import org.eclipse.microprofile.graphql.Source; + +@GraphQLApi +public class GreetingResource { + + @ConfigProperty(name = "message") + String message; + + @Query + public String message() { + return message; + } + + @Query + public Greeting hello() { + return new Greeting("hello", LocalTime.of(11, 34)); + } + + @Mutation + public Greetings load(Greetings greetings) { + return greetings; + } + + @Name("options") + public List buildInOptions(@Source Greeting greeting) { + List options = new ArrayList<>(); + options.add(new Hello()); + options.add(new Morning()); + return options; + } + + @Query + public Farewell farewell() { + return new Farewell(); + } +} diff --git a/integration-tests/legacy-redirect/src/main/java/io/quarkus/it/legacy/redirect/Greetings.java b/integration-tests/legacy-redirect/src/main/java/io/quarkus/it/legacy/redirect/Greetings.java new file mode 100644 index 0000000000000..3c8841ff3c0d4 --- /dev/null +++ b/integration-tests/legacy-redirect/src/main/java/io/quarkus/it/legacy/redirect/Greetings.java @@ -0,0 +1,24 @@ +package io.quarkus.it.legacy.redirect; + +import java.util.List; + +public class Greetings { + private String language; + private List hellos; + + public String getLanguage() { + return language; + } + + public void setLanguage(String language) { + this.language = language; + } + + public List getHellos() { + return hellos; + } + + public void setHellos(List hellos) { + this.hellos = hellos; + } +} diff --git a/integration-tests/legacy-redirect/src/main/java/io/quarkus/it/legacy/redirect/Hello.java b/integration-tests/legacy-redirect/src/main/java/io/quarkus/it/legacy/redirect/Hello.java new file mode 100644 index 0000000000000..0e1586a5a18a3 --- /dev/null +++ b/integration-tests/legacy-redirect/src/main/java/io/quarkus/it/legacy/redirect/Hello.java @@ -0,0 +1,20 @@ +package io.quarkus.it.legacy.redirect; + +import java.time.LocalTime; + +public class Hello extends Greeting { + + private TimeOfDay timeOfDay = TimeOfDay.ANY; + + public Hello() { + super("Hello", LocalTime.now()); + } + + public TimeOfDay getTimeOfDay() { + return timeOfDay; + } + + public void setTimeOfDay(TimeOfDay timeOfDay) { + this.timeOfDay = timeOfDay; + } +} diff --git a/integration-tests/legacy-redirect/src/main/java/io/quarkus/it/legacy/redirect/MessageResource.java b/integration-tests/legacy-redirect/src/main/java/io/quarkus/it/legacy/redirect/MessageResource.java new file mode 100644 index 0000000000000..62eab08d02e7b --- /dev/null +++ b/integration-tests/legacy-redirect/src/main/java/io/quarkus/it/legacy/redirect/MessageResource.java @@ -0,0 +1,14 @@ +package io.quarkus.it.legacy.redirect; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +@Path("/message") +public class MessageResource { + + @GET + public String message() { + return "hello world"; + } + +} diff --git a/integration-tests/legacy-redirect/src/main/java/io/quarkus/it/legacy/redirect/Morning.java b/integration-tests/legacy-redirect/src/main/java/io/quarkus/it/legacy/redirect/Morning.java new file mode 100644 index 0000000000000..07e45d1f5e07c --- /dev/null +++ b/integration-tests/legacy-redirect/src/main/java/io/quarkus/it/legacy/redirect/Morning.java @@ -0,0 +1,20 @@ +package io.quarkus.it.legacy.redirect; + +import java.time.LocalTime; + +public class Morning extends Greeting { + + private TimeOfDay timeOfDay = TimeOfDay.MORNING; + + public Morning() { + super("Good Morning", LocalTime.now()); + } + + public TimeOfDay getTimeOfDay() { + return timeOfDay; + } + + public void setTimeOfDay(TimeOfDay timeOfDay) { + this.timeOfDay = timeOfDay; + } +} diff --git a/integration-tests/legacy-redirect/src/main/java/io/quarkus/it/legacy/redirect/Salutation.java b/integration-tests/legacy-redirect/src/main/java/io/quarkus/it/legacy/redirect/Salutation.java new file mode 100644 index 0000000000000..2f6d65e8b4602 --- /dev/null +++ b/integration-tests/legacy-redirect/src/main/java/io/quarkus/it/legacy/redirect/Salutation.java @@ -0,0 +1,6 @@ +package io.quarkus.it.legacy.redirect; + +public abstract class Salutation { + + public abstract String getType(); +} diff --git a/integration-tests/legacy-redirect/src/main/java/io/quarkus/it/legacy/redirect/TimeOfDay.java b/integration-tests/legacy-redirect/src/main/java/io/quarkus/it/legacy/redirect/TimeOfDay.java new file mode 100644 index 0000000000000..b8eff96f3cf86 --- /dev/null +++ b/integration-tests/legacy-redirect/src/main/java/io/quarkus/it/legacy/redirect/TimeOfDay.java @@ -0,0 +1,10 @@ +package io.quarkus.it.legacy.redirect; + +public enum TimeOfDay { + + ANY, + MORNING, + AFTERNOON, + EVENING, + NIGHT +} diff --git a/integration-tests/legacy-redirect/src/main/java/io/quarkus/it/legacy/redirect/Welcome.java b/integration-tests/legacy-redirect/src/main/java/io/quarkus/it/legacy/redirect/Welcome.java new file mode 100644 index 0000000000000..044412e210726 --- /dev/null +++ b/integration-tests/legacy-redirect/src/main/java/io/quarkus/it/legacy/redirect/Welcome.java @@ -0,0 +1,10 @@ +package io.quarkus.it.legacy.redirect; + +public class Welcome extends Salutation { + + @Override + public String getType() { + return "Welcome"; + } + +} diff --git a/integration-tests/legacy-redirect/src/main/resources/application.properties b/integration-tests/legacy-redirect/src/main/resources/application.properties new file mode 100644 index 0000000000000..2eb12f844bf76 --- /dev/null +++ b/integration-tests/legacy-redirect/src/main/resources/application.properties @@ -0,0 +1,2 @@ +quarkus.http.redirect-to-non-application-root-path=true +message=Production diff --git a/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/LegacyRedirectAppRootDuplicateFrameworkIT.java b/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/LegacyRedirectAppRootDuplicateFrameworkIT.java new file mode 100644 index 0000000000000..9271a7e5b16de --- /dev/null +++ b/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/LegacyRedirectAppRootDuplicateFrameworkIT.java @@ -0,0 +1,7 @@ +package io.quarkus.it.legacy.redirect; + +import io.quarkus.test.junit.NativeImageTest; + +@NativeImageTest +class LegacyRedirectAppRootDuplicateFrameworkIT extends LegacyRedirectAppRootTest { +} diff --git a/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/LegacyRedirectAppRootDuplicateFrameworkTest.java b/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/LegacyRedirectAppRootDuplicateFrameworkTest.java new file mode 100644 index 0000000000000..44e98e85ce219 --- /dev/null +++ b/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/LegacyRedirectAppRootDuplicateFrameworkTest.java @@ -0,0 +1,94 @@ +package io.quarkus.it.legacy.redirect; + +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@TestProfile(LegacyRedirectAppRootDuplicateFrameworkTest.SetApplicationSameFrameworkRoot.class) +class LegacyRedirectAppRootDuplicateFrameworkTest { + + // When the root path and the framework path are the same, there are + // no redirects at all. + + public static class SetApplicationSameFrameworkRoot implements QuarkusTestProfile { + @Override + public Map getConfigOverrides() { + Map overrides = new HashMap<>(); + overrides.put("quarkus.http.root-path", "/app"); + overrides.put("quarkus.http.non-application-root-path", "/app"); + return overrides; + } + } + + @TestHTTPResource(value = "/") + URL hostPortUrl; + + WebClientUtil clientUtil = new WebClientUtil(); + + @BeforeEach + void setUrl() { + clientUtil.setHostPortUrl(hostPortUrl); + } + + @Test + public void testGraphQlUnchangedNeverRedirect() { + // Not found: moved with application endpoint + clientUtil.validate("/graphql", 404); + // Not the same as 404.. graphql is found in the right place + clientUtil.validate("/app/graphql", 405); + } + + @Test + public void testGraphQlUiWithRedirect() { + // Not found: moved with application endpoint + clientUtil.validate("/graphql-ui", 404); + + clientUtil.validate("/app/graphql-ui", 302); + } + + @Test + public void testHealthWithRedirect() { + // Not found: moved with application endpoint + clientUtil.validate("/health", 404); + + clientUtil.validate("/app/health", 200); + clientUtil.validate("/app/health/group", 200); + clientUtil.validate("/app/health/live", 200); + clientUtil.validate("/app/health/ready", 200); + clientUtil.validate("/app/health/well", 200); + clientUtil.validate("/app/health-ui", 302); + } + + @Test + public void testMetricsWithRedirect() { + // Not found: moved with application endpoint + clientUtil.validate("/metrics", 404); + + clientUtil.validateText("/app/metrics", 200); + } + + @Test + public void testOpenApiWithRedirect() { + // Not found: moved with application endpoint + clientUtil.validate("/openapi", 404); + + clientUtil.validate("/app/openapi", 200); + } + + @Test + public void testSwaggerUiWithRedirect() { + // Not found: moved with application endpoint + clientUtil.validate("/swagger-ui", 404); + + clientUtil.validate("/app/swagger-ui", 302); + } +} diff --git a/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/LegacyRedirectAppRootIT.java b/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/LegacyRedirectAppRootIT.java new file mode 100644 index 0000000000000..b3c3d70ff0486 --- /dev/null +++ b/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/LegacyRedirectAppRootIT.java @@ -0,0 +1,7 @@ +package io.quarkus.it.legacy.redirect; + +import io.quarkus.test.junit.NativeImageTest; + +@NativeImageTest +class LegacyRedirectAppRootIT extends LegacyRedirectAppRootTest { +} diff --git a/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/LegacyRedirectAppRootTest.java b/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/LegacyRedirectAppRootTest.java new file mode 100644 index 0000000000000..2c340ba8bf0b2 --- /dev/null +++ b/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/LegacyRedirectAppRootTest.java @@ -0,0 +1,106 @@ +package io.quarkus.it.legacy.redirect; + +import java.net.URL; +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@TestProfile(LegacyRedirectAppRootTest.SetApplicationRoot.class) +class LegacyRedirectAppRootTest { + + // Legacy redirect behavior is implicitly scoped to the configured + // http root (i.e. /app/health will be redirected) + + public static class SetApplicationRoot implements QuarkusTestProfile { + @Override + public Map getConfigOverrides() { + return Collections.singletonMap("quarkus.http.root-path", "/app"); + } + } + + @TestHTTPResource(value = "/") + URL hostPortUrl; + + WebClientUtil clientUtil = new WebClientUtil(); + + @BeforeEach + void setUrl() { + clientUtil.setHostPortUrl(hostPortUrl); + } + + @Test + public void testGraphQlUnchangedNeverRedirect() { + // Not found: moved with application endpoint + clientUtil.validate("/graphql", 404); + // Not the same as 404.. graphql is found in the right place + clientUtil.validate("/app/graphql", 405); + } + + @Test + public void testGraphQlUiWithRedirect() { + // Not found: moved with application endpoint + clientUtil.validate("/graphql-ui", 404); + + clientUtil.validate("/app/graphql-ui", 301, "/app/q/graphql-ui"); + clientUtil.validate("/app/q/graphql-ui", 302); + } + + @Test + public void testHealthWithRedirect() { + // Not found: moved with application endpoint + clientUtil.validate("/health", 404); + + clientUtil.validate("/app/health", 301, "/app/q/health"); + clientUtil.validate("/app/q/health", 200); + + clientUtil.validate("/app/health/group", 301, "/app/q/health/group"); + clientUtil.validate("/app/q/health/group", 200); + + clientUtil.validate("/app/health/live", 301, "/app/q/health/live"); + clientUtil.validate("/app/q/health/live", 200); + + clientUtil.validate("/app/health/ready", 301, "/app/q/health/ready"); + clientUtil.validate("/app/q/health/ready", 200); + + clientUtil.validate("/app/health/well", 301, "/app/q/health/well"); + clientUtil.validate("/app/q/health/well", 200); + + clientUtil.validate("/app/health-ui", 301, "/app/q/health-ui"); + clientUtil.validate("/app/q/health-ui", 302); + } + + @Test + public void testMetricsWithRedirect() { + // Not found: moved with application endpoint + clientUtil.validate("/metrics", 404); + + clientUtil.validate("/app/metrics", 301, "/app/q/metrics"); + clientUtil.validateText("/app/q/metrics", 200); + } + + @Test + public void testOpenApiWithRedirect() { + // Not found: moved with application endpoint + clientUtil.validate("/openapi", 404); + + clientUtil.validate("/app/openapi", 301, "/app/q/openapi"); + clientUtil.validate("/app/q/openapi", 200); + } + + @Test + public void testSwaggerUiWithRedirect() { + // Not found: moved with application endpoint + clientUtil.validate("/swagger-ui", 404); + + clientUtil.validate("/app/swagger-ui", 301, "/app/q/swagger-ui"); + clientUtil.validate("/app/q/swagger-ui", 302); + } +} diff --git a/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/LegacyRedirectExplicitTargetIT.java b/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/LegacyRedirectExplicitTargetIT.java new file mode 100644 index 0000000000000..d748459ffc07a --- /dev/null +++ b/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/LegacyRedirectExplicitTargetIT.java @@ -0,0 +1,7 @@ +package io.quarkus.it.legacy.redirect; + +import io.quarkus.test.junit.NativeImageTest; + +@NativeImageTest +class LegacyRedirectExplicitTargetIT extends LegacyRedirectAppRootTest { +} diff --git a/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/LegacyRedirectExplicitTargetTest.java b/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/LegacyRedirectExplicitTargetTest.java new file mode 100644 index 0000000000000..90744532fa25a --- /dev/null +++ b/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/LegacyRedirectExplicitTargetTest.java @@ -0,0 +1,69 @@ +package io.quarkus.it.legacy.redirect; + +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@TestProfile(LegacyRedirectExplicitTargetTest.SetExplicitTarget.class) +class LegacyRedirectExplicitTargetTest { + + // When explicit endpoints are specified, those endpoints should be used + // directly, which should disable the compatibility redirect for that + // endpoint (404) + + public static class SetExplicitTarget implements QuarkusTestProfile { + @Override + public Map getConfigOverrides() { + Map overrides = new HashMap<>(); + overrides.put("quarkus.http.root-path", "/app"); + overrides.put("quarkus.http.non-application-root-path", "/framework"); + overrides.put("quarkus.smallrye-metrics.path", "/my-metrics"); + overrides.put("quarkus.smallrye-health.liveness-path", "/liveness"); + return overrides; + } + } + + @TestHTTPResource(value = "/") + URL hostPortUrl; + + WebClientUtil clientUtil = new WebClientUtil(); + + @BeforeEach + void setUrl() { + clientUtil.setHostPortUrl(hostPortUrl); + } + + @Test + public void testHealthWithRedirect() { + // Not found: moved with application endpoint + clientUtil.validate("/health", 404); + + clientUtil.validate("/app/health", 301, "/framework/health"); + clientUtil.validate("/framework/health", 200); + + clientUtil.validate("/app/health/liveness", 404); + clientUtil.validate("/framework/health/liveness", 404); + clientUtil.validate("/liveness", 200); + } + + @Test + public void testMetricsWithRedirect() { + // Not found: moved with application endpoint + clientUtil.validate("/metrics", 404); + clientUtil.validate("/app/metrics", 404); + clientUtil.validate("/framework/metrics", 404); + + clientUtil.validateText("/my-metrics", 200); + clientUtil.validate("/app/my-metrics", 404); + clientUtil.validate("/framework/my-metrics", 404); + } +} diff --git a/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/LegacyRedirectFrameworkRootIT.java b/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/LegacyRedirectFrameworkRootIT.java new file mode 100644 index 0000000000000..03a1e12ce8820 --- /dev/null +++ b/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/LegacyRedirectFrameworkRootIT.java @@ -0,0 +1,7 @@ +package io.quarkus.it.legacy.redirect; + +import io.quarkus.test.junit.NativeImageTest; + +@NativeImageTest +class LegacyRedirectFrameworkRootIT extends LegacyRedirectFrameworkRootTest { +} diff --git a/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/LegacyRedirectFrameworkRootTest.java b/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/LegacyRedirectFrameworkRootTest.java new file mode 100644 index 0000000000000..9b1d008accfeb --- /dev/null +++ b/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/LegacyRedirectFrameworkRootTest.java @@ -0,0 +1,109 @@ +package io.quarkus.it.legacy.redirect; + +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@TestProfile(LegacyRedirectFrameworkRootTest.SetFrameworkRoot.class) +class LegacyRedirectFrameworkRootTest { + + // Legacy redirect behavior is implicitly scoped to the configured + // http root (i.e. /app/health will be redirected) + + public static class SetFrameworkRoot implements QuarkusTestProfile { + @Override + public Map getConfigOverrides() { + Map overrides = new HashMap<>(); + overrides.put("quarkus.http.root-path", "/app"); + overrides.put("quarkus.http.non-application-root-path", "/framework"); + return overrides; + } + } + + @TestHTTPResource(value = "/") + URL hostPortUrl; + + WebClientUtil clientUtil = new WebClientUtil(); + + @BeforeEach + void setUrl() { + clientUtil.setHostPortUrl(hostPortUrl); + } + + @Test + public void testGraphQlUnchangedNeverRedirect() { + // Not found: moved with application endpoint + clientUtil.validate("/graphql", 404); + // Not the same as 404.. graphql is found in the right place + clientUtil.validate("/app/graphql", 405); + } + + @Test + public void testGraphQlUiWithRedirect() { + // Not found: moved with application endpoint + clientUtil.validate("/graphql-ui", 404); + + clientUtil.validate("/app/graphql-ui", 301, "/framework/graphql-ui"); + clientUtil.validate("/framework/graphql-ui", 302); + } + + @Test + public void testHealthWithRedirect() { + // Not found: moved with application endpoint + clientUtil.validate("/health", 404); + + clientUtil.validate("/app/health", 301, "/framework/health"); + clientUtil.validate("/framework/health", 200); + + clientUtil.validate("/app/health/group", 301, "/framework/health/group"); + clientUtil.validate("/framework/health/group", 200); + + clientUtil.validate("/app/health/live", 301, "/framework/health/live"); + clientUtil.validate("/framework/health/live", 200); + + clientUtil.validate("/app/health/ready", 301, "/framework/health/ready"); + clientUtil.validate("/framework/health/ready", 200); + + clientUtil.validate("/app/health/well", 301, "/framework/health/well"); + clientUtil.validate("/framework/health/well", 200); + + clientUtil.validate("/app/health-ui", 301, "/framework/health-ui"); + clientUtil.validate("/framework/health-ui", 302); + } + + @Test + public void testMetricsWithRedirect() { + // Not found: moved with application endpoint + clientUtil.validate("/metrics", 404); + + clientUtil.validate("/app/metrics", 301, "/framework/metrics"); + clientUtil.validateText("/framework/metrics", 200); + } + + @Test + public void testOpenApiWithRedirect() { + // Not found: moved with application endpoint + clientUtil.validate("/openapi", 404); + + clientUtil.validate("/app/openapi", 301, "/framework/openapi"); + clientUtil.validate("/framework/openapi", 200); + } + + @Test + public void testSwaggerUiWithRedirect() { + // Not found: moved with application endpoint + clientUtil.validate("/swagger-ui", 404); + + clientUtil.validate("/app/swagger-ui", 301, "/framework/swagger-ui"); + clientUtil.validate("/framework/swagger-ui", 302); + } +} diff --git a/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/LegacyRedirectIT.java b/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/LegacyRedirectIT.java new file mode 100644 index 0000000000000..1edd1a26f4535 --- /dev/null +++ b/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/LegacyRedirectIT.java @@ -0,0 +1,7 @@ +package io.quarkus.it.legacy.redirect; + +import io.quarkus.test.junit.NativeImageTest; + +@NativeImageTest +class LegacyRedirectIT extends LegacyRedirectTest { +} diff --git a/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/LegacyRedirectTest.java b/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/LegacyRedirectTest.java new file mode 100644 index 0000000000000..f513e2e065422 --- /dev/null +++ b/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/LegacyRedirectTest.java @@ -0,0 +1,76 @@ +package io.quarkus.it.legacy.redirect; + +import java.net.URL; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +class LegacyRedirectTest { + + // Default configuration + + @TestHTTPResource(value = "/") + URL hostPortUrl; + + WebClientUtil clientUtil = new WebClientUtil(); + + @BeforeEach + void setUrl() { + clientUtil.setHostPortUrl(hostPortUrl); + } + + @Test + public void testGraphQlUnchangedNeverRedirect() { + // Not the same as 404.. graphql is found in the right place + clientUtil.validate("/graphql", 405); + } + + @Test + public void testGraphQlUiWithRedirect() { + clientUtil.validate("/graphql-ui", 301, "/q/graphql-ui"); + clientUtil.validate("/q/graphql-ui", 302); + } + + @Test + public void testHealthWithRedirect() { + clientUtil.validate("/health", 301, "/q/health"); + clientUtil.validate("/q/health", 200); + + clientUtil.validate("/health/group", 301, "/q/health/group"); + clientUtil.validate("/q/health/group", 200); + + clientUtil.validate("/health/live", 301, "/q/health/live"); + clientUtil.validate("/q/health/live", 200); + + clientUtil.validate("/health/ready", 301, "/q/health/ready"); + clientUtil.validate("/q/health/ready", 200); + + clientUtil.validate("/health/well", 301, "/q/health/well"); + clientUtil.validate("/q/health/well", 200); + + clientUtil.validate("/health-ui", 301, "/q/health-ui"); + clientUtil.validate("/q/health-ui", 302); + } + + @Test + public void testMetricsWithRedirect() { + clientUtil.validate("/metrics", 301, "/q/metrics"); + clientUtil.validateText("/q/metrics", 200); + } + + @Test + public void testOpenApiWithRedirect() { + clientUtil.validate("/openapi", 301, "/q/openapi"); + clientUtil.validate("/q/openapi", 200); + } + + @Test + public void testSwaggerUiWithRedirect() { + clientUtil.validate("/swagger-ui", 301, "/q/swagger-ui"); + clientUtil.validate("/q/swagger-ui", 302); + } +} diff --git a/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/WebClientUtil.java b/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/WebClientUtil.java new file mode 100644 index 0000000000000..c179169b67ee3 --- /dev/null +++ b/integration-tests/legacy-redirect/src/test/java/io/quarkus/it/legacy/redirect/WebClientUtil.java @@ -0,0 +1,96 @@ +package io.quarkus.it.legacy.redirect; + +import java.net.URL; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import org.junit.jupiter.api.Assertions; + +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.ext.web.client.HttpRequest; +import io.vertx.ext.web.client.HttpResponse; +import io.vertx.ext.web.client.WebClient; +import io.vertx.ext.web.client.WebClientOptions; + +public class WebClientUtil { + + static class Result { + String requestedPath; + int statusCode; + String redirectPath; + } + + URL hostPortUrl; + + void setHostPortUrl(URL hostPortUrl) { + this.hostPortUrl = hostPortUrl; + } + + public void validate(String requestPath, int expectedStatusCode) { + internalValidate(requestPath, expectedStatusCode, null, false); + } + + public void validateText(String requestPath, int expectedStatusCode) { + internalValidate(requestPath, expectedStatusCode, null, true); + } + + public void validate(String requestPath, int expectedStatusCode, String expectedRedirect) { + internalValidate(requestPath, expectedStatusCode, expectedRedirect, false); + } + + public void validateText(String requestPath, int expectedStatusCode, String expectedRedirect) { + internalValidate(requestPath, expectedStatusCode, expectedRedirect, false); + } + + void internalValidate(String requestPath, int expectedStatusCode, String expectedRedirect, boolean text) { + WebClientUtil.Result result = get(hostPortUrl, requestPath, text); + Assertions.assertEquals(expectedStatusCode, result.statusCode, + "Expected status code " + expectedStatusCode + " for request to path " + requestPath); + if (expectedRedirect != null) { + Assertions.assertEquals(expectedRedirect, result.redirectPath, + "Expected a Location header value of " + expectedRedirect + " for request to path " + requestPath); + } + } + + Result get(URL hostPortUrl, final String requestPath, boolean text) { + Vertx vertx = Vertx.vertx(); + + try { + CompletableFuture resultFuture = new CompletableFuture<>(); + + WebClientOptions options = new WebClientOptions() + .setFollowRedirects(false); + + HttpRequest request = WebClient.create(vertx, options) + .get(hostPortUrl.getPort(), hostPortUrl.getHost(), requestPath); + + if (text) { + request.putHeader("Accept", "text/plain"); + } + + request.send(ar -> { + if (ar.succeeded()) { + HttpResponse response = ar.result(); + Result result = new Result(); + result.requestedPath = requestPath; + result.statusCode = response.statusCode(); + if (result.statusCode == 301) { + result.redirectPath = response.getHeader("Location"); + } + resultFuture.complete(result); + } else { + resultFuture.completeExceptionally(ar.cause()); + } + }); + + return resultFuture.get(); + } catch (InterruptedException | ExecutionException e) { + Assertions.fail(e); + return null; + } finally { + vertx.close(); + } + } + +} diff --git a/integration-tests/main/src/main/resources/application.properties b/integration-tests/main/src/main/resources/application.properties index 75ce3fb94adb2..6ad399d7456fa 100644 --- a/integration-tests/main/src/main/resources/application.properties +++ b/integration-tests/main/src/main/resources/application.properties @@ -44,6 +44,8 @@ configproperties.instant=2010-10-10T10:10:10Z quarkus.swagger-ui.always-include=true +quarkus.health.openapi.included=true + quarkus.native.resources.includes = test-resources/**.txt quarkus.native.resources.excludes = **/unwanted.* diff --git a/integration-tests/main/src/test/java/io/quarkus/it/main/OpenApiTestCase.java b/integration-tests/main/src/test/java/io/quarkus/it/main/OpenApiTestCase.java index 18e328346355a..c0c69a749e805 100644 --- a/integration-tests/main/src/test/java/io/quarkus/it/main/OpenApiTestCase.java +++ b/integration-tests/main/src/test/java/io/quarkus/it/main/OpenApiTestCase.java @@ -5,6 +5,7 @@ import java.io.InputStream; import java.net.URL; import java.net.URLConnection; +import java.util.Iterator; import java.util.Set; import javax.json.Json; @@ -137,6 +138,34 @@ public void testOpenAPIJSON() throws Exception { Assertions.assertEquals(ctSchemaType, mutliTypedObjectSchema, "Normal and Mutiny Multi have same schema"); + + // Verify presence of Health API + JsonObject healthPath = paths.getJsonObject("/q/health"); + Assertions.assertNotNull(healthPath); + Set healthKeys = healthPath.keySet(); + Iterator healthKeysIterator = healthKeys.iterator(); + Assertions.assertEquals(3, healthPath.size()); + Assertions.assertEquals("summary", healthKeysIterator.next()); + Assertions.assertEquals("description", healthKeysIterator.next()); + Assertions.assertEquals("get", healthKeysIterator.next()); + + JsonObject livenessPath = paths.getJsonObject("/q/health/live"); + Assertions.assertNotNull(livenessPath); + Set livenessKeys = livenessPath.keySet(); + Iterator livenessKeysIterator = livenessKeys.iterator(); + Assertions.assertEquals(3, livenessKeys.size()); + Assertions.assertEquals("summary", livenessKeysIterator.next()); + Assertions.assertEquals("description", livenessKeysIterator.next()); + Assertions.assertEquals("get", livenessKeysIterator.next()); + + JsonObject readinessPath = paths.getJsonObject("/q/health/ready"); + Assertions.assertNotNull(readinessPath); + Set readinessKeys = readinessPath.keySet(); + Iterator readinessKeysIterator = readinessKeys.iterator(); + Assertions.assertEquals(3, readinessKeys.size()); + Assertions.assertEquals("summary", readinessKeysIterator.next()); + Assertions.assertEquals("description", readinessKeysIterator.next()); + Assertions.assertEquals("get", readinessKeysIterator.next()); } protected static String schemaType(String responseCode, String mediaType, JsonObject responses, JsonObject schemas) { diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 3593b1885f747..dac3303bdc51a 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -62,6 +62,7 @@ hibernate-search-orm-elasticsearch hibernate-tenancy hibernate-orm-envers + legacy-redirect vertx-http vertx-web vertx