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 df448e18e8aac..6327a478d3197 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 @@ -2,6 +2,7 @@ import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT; +import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -21,6 +22,8 @@ import io.quarkus.resteasy.runtime.UnauthorizedExceptionMapper; import io.quarkus.resteasy.server.common.deployment.ResteasyDeploymentBuildItem; import io.quarkus.undertow.deployment.StaticResourceFilesBuildItem; +import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem; +import io.quarkus.vertx.http.deployment.devmode.NotFoundPageDisplayableEndpointBuildItem; public class ResteasyBuiltinsProcessor { @@ -54,8 +57,8 @@ void setupExceptionMapper(BuildProducer provider providers.produce(new ResteasyJaxrsProviderBuildItem(NotFoundExceptionMapper.class.getName())); } - @BuildStep(onlyIf = IsDevelopment.class) @Record(STATIC_INIT) + @BuildStep(onlyIf = IsDevelopment.class) void addStaticResourcesExceptionMapper(StaticResourceFilesBuildItem paths, ExceptionMapperRecorder recorder) { //limit to 1000 to not have to many files to display Set staticResources = paths.files.stream().filter(this::isHtmlFileName).limit(1000).collect(Collectors.toSet()); @@ -69,4 +72,17 @@ private boolean isHtmlFileName(String fileName) { return fileName.endsWith(".html") || fileName.endsWith(".htm"); } + @Record(STATIC_INIT) + @BuildStep(onlyIf = IsDevelopment.class) + void addAdditionalEndpointsExceptionMapper(List displayableEndpoints, + ExceptionMapperRecorder recorder, HttpRootPathBuildItem httpRoot) { + List endpoints = displayableEndpoints + .stream() + .map(displayableAdditionalBuildItem -> httpRoot.adjustPath(displayableAdditionalBuildItem.getEndpoint()) + .substring(1)) + .sorted() + .collect(Collectors.toList()); + + recorder.setAdditionalEndpoints(endpoints); + } } diff --git a/extensions/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyStandaloneBuildStep.java b/extensions/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyStandaloneBuildStep.java index 4677f4b13cea9..e61d4c9f0db57 100644 --- a/extensions/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyStandaloneBuildStep.java +++ b/extensions/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyStandaloneBuildStep.java @@ -79,7 +79,7 @@ public void staticInit(ResteasyStandaloneRecorder recorder, deploymentRootPath = deployment.getRootPath(); if (rootPath.endsWith("/")) { if (deploymentRootPath.startsWith("/")) { - rootPath += deploymentRootPath.substring(1, deploymentRootPath.length()); + rootPath += deploymentRootPath.substring(1); } else { rootPath += deploymentRootPath; } diff --git a/extensions/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/ExceptionMapperRecorder.java b/extensions/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/ExceptionMapperRecorder.java index f6ac76dc75fef..191a9617e4c36 100644 --- a/extensions/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/ExceptionMapperRecorder.java +++ b/extensions/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/ExceptionMapperRecorder.java @@ -13,6 +13,10 @@ public void setStaticResource(Set resources) { NotFoundExceptionMapper.staticResources(resources); } + public void setAdditionalEndpoints(List additionalEndpoints) { + NotFoundExceptionMapper.setAdditionalEndpoints(additionalEndpoints); + } + public void setServlets(Map> servletToMapping) { NotFoundExceptionMapper.servlets(servletToMapping); } @@ -20,7 +24,7 @@ public void setServlets(Map> servletToMapping) { /** * Uses to register the paths of classes that are not annotated with JAX-RS annotations (like Spring Controllers for * example) - * + * * @param nonJaxRsClassNameToMethodPaths A map that contains the class name as a key and a map that * contains the method name to path as a value */ 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 2b07c45952a1a..f71dd40b1ed5c 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 @@ -45,6 +45,7 @@ public class NotFoundExceptionMapper implements ExceptionMapper servletMappings = Collections.EMPTY_LIST; private volatile static List staticResources = Collections.EMPTY_LIST; + private volatile static List additionalEndpoints = Collections.EMPTY_LIST; private volatile static Map nonJaxRsClassNameToMethodPaths = Collections.EMPTY_MAP; @Context @@ -241,6 +242,14 @@ private Response respond(List descriptions) { sb.resourcesEnd(); } + if (!additionalEndpoints.isEmpty()) { + sb.resourcesStart("Additional endpoints"); + for (String additionalEndpoint : additionalEndpoints) { + sb.staticResourcePath(additionalEndpoint); + } + sb.resourcesEnd(); + } + return Response.status(Status.NOT_FOUND).entity(sb.toString()).type(MediaType.TEXT_HTML_TYPE).build(); } return Response.status(Status.NOT_FOUND).build(); @@ -266,4 +275,8 @@ public static void staticResources(Set knownFiles) { public static void nonJaxRsClassNameToMethodPaths(Map nonJaxRsPaths) { NotFoundExceptionMapper.nonJaxRsClassNameToMethodPaths = nonJaxRsPaths; } + + public static void setAdditionalEndpoints(List additionalEndpoints) { + NotFoundExceptionMapper.additionalEndpoints = additionalEndpoints; + } } 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 f0d47eaceec7e..4d87c6a80a969 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 @@ -17,6 +17,7 @@ import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.recording.RecorderContext; import io.quarkus.deployment.util.ServiceUtil; import io.quarkus.kubernetes.spi.KubernetesHealthLivenessPathBuildItem; @@ -29,6 +30,7 @@ import io.quarkus.smallrye.health.runtime.SmallRyeLivenessHandler; import io.quarkus.smallrye.health.runtime.SmallRyeReadinessHandler; import io.quarkus.vertx.http.deployment.RouteBuildItem; +import io.quarkus.vertx.http.deployment.devmode.NotFoundPageDisplayableEndpointBuildItem; import io.quarkus.vertx.http.runtime.HandlerType; import io.smallrye.health.SmallRyeHealthReporter; @@ -87,7 +89,9 @@ void build(SmallRyeHealthRecorder recorder, RecorderContext recorderContext, BuildProducer feature, BuildProducer routes, BuildProducer additionalBean, - BuildProducer beanDefiningAnnotation) throws IOException { + BuildProducer beanDefiningAnnotation, + BuildProducer displayableEndpoints, + LaunchModeBuildItem launchModeBuildItem) throws IOException { feature.produce(new FeatureBuildItem(FeatureBuildItem.SMALLRYE_HEALTH)); @@ -100,6 +104,14 @@ void build(SmallRyeHealthRecorder recorder, RecorderContext recorderContext, new RouteBuildItem(health.rootPath + health.readinessPath, new SmallRyeReadinessHandler(), HandlerType.BLOCKING)); + // add health endpoints to not found page + if (launchModeBuildItem.getLaunchMode().isDevOrTest()) { + displayableEndpoints.produce(new NotFoundPageDisplayableEndpointBuildItem(health.rootPath)); + displayableEndpoints.produce(new NotFoundPageDisplayableEndpointBuildItem(health.rootPath + health.livenessPath)); + displayableEndpoints + .produce(new NotFoundPageDisplayableEndpointBuildItem(health.rootPath + health.readinessPath)); + } + // Make ArC discover the beans marked with the @Health qualifier beanDefiningAnnotation.produce(new BeanDefiningAnnotationBuildItem(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 e1dd81cf4ab38..dba58d534edbe 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 @@ -53,6 +53,7 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.logging.LogCleanupFilterBuildItem; @@ -63,6 +64,7 @@ import io.quarkus.smallrye.metrics.runtime.SmallRyeMetricsRecorder; import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem; import io.quarkus.vertx.http.deployment.RouteBuildItem; +import io.quarkus.vertx.http.deployment.devmode.NotFoundPageDisplayableEndpointBuildItem; import io.quarkus.vertx.http.runtime.HandlerType; import io.smallrye.metrics.MetricProducer; import io.smallrye.metrics.MetricRegistries; @@ -96,9 +98,16 @@ static final class SmallRyeMetricsConfig { @Record(STATIC_INIT) void createRoute(BuildProducer routes, SmallRyeMetricsRecorder recorder, - HttpRootPathBuildItem httpRoot) { + HttpRootPathBuildItem httpRoot, + BuildProducer displayableEndpoints, + LaunchModeBuildItem launchModeBuildItem) { 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(route, recorder.handler(httpRoot.adjustPath(metrics.path)), HandlerType.BLOCKING)); routes.produce(new RouteBuildItem(slash, recorder.handler(httpRoot.adjustPath(metrics.path)), HandlerType.BLOCKING)); } 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 d0d63b7d0f124..07d81b71b7fb7 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 @@ -56,6 +56,7 @@ import io.quarkus.smallrye.openapi.runtime.OpenApiDocumentProducer; import io.quarkus.smallrye.openapi.runtime.OpenApiHandler; import io.quarkus.vertx.http.deployment.RouteBuildItem; +import io.quarkus.vertx.http.deployment.devmode.NotFoundPageDisplayableEndpointBuildItem; import io.quarkus.vertx.http.runtime.HandlerType; import io.smallrye.openapi.api.OpenApiConfig; import io.smallrye.openapi.api.OpenApiConfigImpl; @@ -101,7 +102,8 @@ List configFiles() { } @BuildStep - RouteBuildItem handler(DeploymentClassLoaderBuildItem deploymentClassLoaderBuildItem, LaunchModeBuildItem launch) { + RouteBuildItem handler(DeploymentClassLoaderBuildItem deploymentClassLoaderBuildItem, LaunchModeBuildItem launch, + BuildProducer displayableEndpoints) { /* * Ugly Hack * In dev mode, we pass a classloader to load the up to date OpenAPI document. @@ -115,6 +117,7 @@ RouteBuildItem handler(DeploymentClassLoaderBuildItem deploymentClassLoaderBuild */ if (launch.getLaunchMode() == LaunchMode.DEVELOPMENT) { OpenApiHandler.classLoader = deploymentClassLoaderBuildItem.getClassLoader(); + displayableEndpoints.produce(new NotFoundPageDisplayableEndpointBuildItem(openapi.path)); } else { OpenApiHandler.classLoader = null; } diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/hotreload/DisplayOpenAPiEndpointInNotFoundExceptionPageTest.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/hotreload/DisplayOpenAPiEndpointInNotFoundExceptionPageTest.java new file mode 100644 index 0000000000000..067805560288a --- /dev/null +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/hotreload/DisplayOpenAPiEndpointInNotFoundExceptionPageTest.java @@ -0,0 +1,39 @@ +package io.quarkus.smallrye.openapi.test.hotreload; + +import static org.hamcrest.Matchers.containsString; + +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.test.QuarkusDevModeTest; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; + +public class DisplayOpenAPiEndpointInNotFoundExceptionPageTest { + private static final String OPEN_API_PATH = "/openapi-path"; + private static final String SWAGGER_UI_PATH = "/swagger-path"; + + @RegisterExtension + static final QuarkusDevModeTest test = new QuarkusDevModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(MyResource.class) + .addAsResource(new StringAsset( + "quarkus.smallrye-openapi.path=" + OPEN_API_PATH + "\nquarkus.swagger-ui.path=" + SWAGGER_UI_PATH), + "application.properties")); + + @Test + public void shouldDisplayOpenApiAndSwaggerUiEndpointsInNotFoundPage() { + RestAssured + .given() + .accept(ContentType.HTML) + .when() + .get("/open") + .then() + .statusCode(404) + .body(containsString(OPEN_API_PATH)) + .body(containsString(SWAGGER_UI_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 464231b859e90..bd61d30540ba8 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 @@ -39,6 +39,7 @@ import io.quarkus.swaggerui.runtime.SwaggerUiRecorder; import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem; import io.quarkus.vertx.http.deployment.RouteBuildItem; +import io.quarkus.vertx.http.deployment.devmode.NotFoundPageDisplayableEndpointBuildItem; import io.vertx.core.Handler; import io.vertx.ext.web.RoutingContext; @@ -79,7 +80,8 @@ public void registerSwaggerUiServletExtension(SwaggerUiRecorder recorder, BuildProducer generatedResources, BuildProducer nativeImageResourceBuildItemBuildProducer, LiveReloadBuildItem liveReloadBuildItem, - HttpRootPathBuildItem httpRootPathBuildItem) throws Exception { + HttpRootPathBuildItem httpRootPathBuildItem, + BuildProducer displayableEndpoints) throws Exception { if ("/".equals(swaggerUiConfig.path)) { throw new ConfigurationError( @@ -119,6 +121,7 @@ public void registerSwaggerUiServletExtension(SwaggerUiRecorder recorder, Handler handler = recorder.handler(cached.cachedDirectory, swaggerUiConfig.path); routes.produce(new RouteBuildItem(swaggerUiConfig.path, handler)); routes.produce(new RouteBuildItem(swaggerUiConfig.path + "/*", handler)); + displayableEndpoints.produce(new NotFoundPageDisplayableEndpointBuildItem(swaggerUiConfig.path + "/")); } else if (swaggerUiConfig.alwaysInclude) { ResolvedArtifact artifact = getSwaggerUiArtifact(); //we are including in a production artifact 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 new file mode 100644 index 0000000000000..d97756c578cb7 --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/NotFoundPageDisplayableEndpointBuildItem.java @@ -0,0 +1,15 @@ +package io.quarkus.vertx.http.deployment.devmode; + +import io.quarkus.builder.item.MultiBuildItem; + +final public class NotFoundPageDisplayableEndpointBuildItem extends MultiBuildItem { + private final String endpoint; + + public NotFoundPageDisplayableEndpointBuildItem(String endpoint) { + this.endpoint = endpoint; + } + + public String getEndpoint() { + return endpoint; + } +}