diff --git a/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java b/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java index 1d080ab8be9f8..4d8b4b9315749 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java @@ -240,7 +240,7 @@ public TemplateHtmlBuilder staticResourcePath(String title, String description) } public TemplateHtmlBuilder servletMapping(String title) { - return resourcePath(title, false, false, null); + return resourcePath(title, false, true, null); } private TemplateHtmlBuilder resourcePath(String title, boolean withListStart, boolean withAnchor, String description) { diff --git a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/ResteasyStandaloneRecorder.java b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/ResteasyStandaloneRecorder.java index 245f60035d1a2..9de7cb651a3b1 100644 --- a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/ResteasyStandaloneRecorder.java +++ b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/ResteasyStandaloneRecorder.java @@ -40,7 +40,7 @@ import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; import io.quarkus.vertx.http.runtime.HttpCompressionHandler; import io.quarkus.vertx.http.runtime.HttpConfiguration; -import io.quarkus.vertx.http.runtime.devmode.ResourceNotFoundHandler; +import io.quarkus.vertx.http.runtime.devmode.ResourceNotFoundData; import io.quarkus.vertx.http.runtime.devmode.RouteDescription; import io.quarkus.vertx.http.runtime.devmode.RouteMethodDescription; import io.quarkus.vertx.http.runtime.security.HttpSecurityRecorder.DefaultAuthFailureHandler; @@ -107,7 +107,7 @@ public Handler vertxRequestHandler(Supplier vertx, Execut if (LaunchMode.current() == LaunchMode.DEVELOPMENT) { // For Not Found Screen Registry registry = deployment.getRegistry(); - ResourceNotFoundHandler.runtimeRoutes = fromBoundResourceInvokers(registry, nonJaxRsClassNameToMethodPaths); + ResourceNotFoundData.setRuntimeRoutes(fromBoundResourceInvokers(registry, nonJaxRsClassNameToMethodPaths)); } return handler; diff --git a/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java b/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java index 7d0a6f1231ae1..fc52b7a35c0e6 100644 --- a/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java +++ b/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java @@ -69,7 +69,7 @@ import io.quarkus.security.identity.CurrentIdentityAssociation; import io.quarkus.vertx.http.runtime.CurrentVertxRequest; import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; -import io.quarkus.vertx.http.runtime.devmode.ResourceNotFoundHandler; +import io.quarkus.vertx.http.runtime.devmode.ResourceNotFoundData; import io.quarkus.vertx.http.runtime.devmode.RouteDescription; import io.quarkus.vertx.http.runtime.devmode.RouteMethodDescription; import io.quarkus.vertx.http.runtime.security.HttpSecurityRecorder.DefaultAuthFailureHandler; @@ -158,7 +158,7 @@ closeTaskHandler, contextFactory, new ArcThreadSetupAction(beanContainer.request if (LaunchMode.current() == LaunchMode.DEVELOPMENT) { // For Not Found Screen - ResourceNotFoundHandler.runtimeRoutes = fromClassMappers(deployment.getClassMappers()); + ResourceNotFoundData.setRuntimeRoutes(fromClassMappers(deployment.getClassMappers())); // For Dev UI Screen RuntimeResourceVisitor.visitRuntimeResources(deployment.getClassMappers(), ScoreSystem.ScoreVisitor); } diff --git a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/QuarkusNotFoundServlet.java b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/QuarkusNotFoundServlet.java new file mode 100644 index 0000000000000..b3160e9acf59c --- /dev/null +++ b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/QuarkusNotFoundServlet.java @@ -0,0 +1,33 @@ +package io.quarkus.undertow.runtime; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import jakarta.enterprise.inject.spi.CDI; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import io.quarkus.vertx.http.runtime.devmode.ResourceNotFoundData; +import io.vertx.core.json.Json; + +public class QuarkusNotFoundServlet extends HttpServlet { + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + ResourceNotFoundData resourceNotFoundData = CDI.current().select(ResourceNotFoundData.class).get(); + String accept = req.getHeader("Accept"); + if (accept != null && accept.contains("application/json")) { + resp.setContentType("application/json"); + resp.setCharacterEncoding(StandardCharsets.UTF_8.name()); + resp.getWriter().write(Json.encodePrettily(resourceNotFoundData.getJsonContent())); + } else { + //We default to HTML representation + resp.setContentType("text/html"); + resp.setCharacterEncoding(StandardCharsets.UTF_8.name()); + resp.getWriter().write(resourceNotFoundData.getHTMLContent()); + } + } + +} diff --git a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java index a0ccfe2a2d83d..fb00e1a9bd6cb 100644 --- a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java +++ b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java @@ -54,7 +54,7 @@ import io.quarkus.vertx.http.runtime.HttpCompressionHandler; import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.quarkus.vertx.http.runtime.VertxHttpRecorder; -import io.quarkus.vertx.http.runtime.devmode.ResourceNotFoundHandler; +import io.quarkus.vertx.http.runtime.devmode.ResourceNotFoundData; import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; import io.undertow.httpcore.BufferAllocator; import io.undertow.httpcore.StatusCodes; @@ -278,7 +278,7 @@ public void addServletMapping(RuntimeValue info, String name, St if (sv != null) { sv.addMapping(mapping); if (LaunchMode.current() == LaunchMode.DEVELOPMENT) { - ResourceNotFoundHandler.addServlet(mapping); + ResourceNotFoundData.addServlet(mapping); } } } @@ -463,20 +463,27 @@ public DeploymentManager bootServletContainer(RuntimeValue info, if (info.getValue().getExceptionHandler() == null) { //if a 500 error page has not been mapped we change the default to our more modern one, with a UID in the //log. If this is not production we also include the stack trace - boolean alreadyMapped = false; + boolean alreadyMapped500 = false; + boolean alreadyMapped404 = false; for (ErrorPage i : info.getValue().getErrorPages()) { if (i.getErrorCode() != null && i.getErrorCode() == StatusCodes.INTERNAL_SERVER_ERROR) { - alreadyMapped = true; - break; + alreadyMapped500 = true; + } else if (i.getErrorCode() != null && i.getErrorCode() == StatusCodes.NOT_FOUND) { + alreadyMapped404 = true; } } - if (!alreadyMapped || launchMode.isDevOrTest()) { + if (!alreadyMapped500 || launchMode.isDevOrTest()) { info.getValue().setExceptionHandler(new QuarkusExceptionHandler()); info.getValue().addErrorPage(new ErrorPage("/@QuarkusError", StatusCodes.INTERNAL_SERVER_ERROR)); info.getValue().addServlet(new ServletInfo("@QuarkusError", QuarkusErrorServlet.class) .addMapping("/@QuarkusError").setAsyncSupported(true) .addInitParam(QuarkusErrorServlet.SHOW_STACK, Boolean.toString(launchMode.isDevOrTest()))); } + if (!alreadyMapped404 && launchMode.equals(LaunchMode.DEVELOPMENT)) { + info.getValue().addErrorPage(new ErrorPage("/@QuarkusNotFound", StatusCodes.NOT_FOUND)); + info.getValue().addServlet(new ServletInfo("@QuarkusNotFound", QuarkusNotFoundServlet.class) + .addMapping("/@QuarkusNotFound").setAsyncSupported(true)); + } } setupRequestScope(info.getValue(), beanContainer); diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/menu/EndpointsProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/menu/EndpointsProcessor.java index 50ac913d76a67..84b9b46399985 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/menu/EndpointsProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/menu/EndpointsProcessor.java @@ -3,13 +3,16 @@ import io.quarkus.deployment.IsDevelopment; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.devui.deployment.InternalPageBuildItem; +import io.quarkus.devui.spi.JsonRPCProvidersBuildItem; import io.quarkus.devui.spi.page.Page; import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; +import io.quarkus.vertx.http.runtime.devmode.ResourceNotFoundData; /** * This creates Endpoints Page */ public class EndpointsProcessor { + private static final String NAMESPACE = "devui-endpoints"; private static final String DEVUI = "dev-ui"; @BuildStep(onlyIf = IsDevelopment.class) @@ -23,17 +26,22 @@ InternalPageBuildItem createEndpointsPage(NonApplicationRootPathBuildItem nonApp // Page endpointsPage.addPage(Page.webComponentPageBuilder() - .namespace("devui-endpoints") + .namespace(NAMESPACE) .title("Endpoints") .icon("font-awesome-solid:plug") .componentLink("qwc-endpoints.js")); endpointsPage.addPage(Page.webComponentPageBuilder() - .namespace("devui-endpoints") + .namespace(NAMESPACE) .title("Routes") .icon("font-awesome-solid:route") .componentLink("qwc-routes.js")); return endpointsPage; } + + @BuildStep(onlyIf = IsDevelopment.class) + JsonRPCProvidersBuildItem createJsonRPCService() { + return new JsonRPCProvidersBuildItem(NAMESPACE, ResourceNotFoundData.class); + } } diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/NotFoundProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/NotFoundProcessor.java index 4fb738557cb9d..787b570fa123d 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/NotFoundProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/NotFoundProcessor.java @@ -11,6 +11,8 @@ import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.BeanContainerBuildItem; import io.quarkus.deployment.IsDevelopment; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.Record; @@ -20,6 +22,7 @@ import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem; import io.quarkus.vertx.http.deployment.VertxWebRouterBuildItem; import io.quarkus.vertx.http.runtime.devmode.AdditionalRouteDescription; +import io.quarkus.vertx.http.runtime.devmode.ResourceNotFoundData; import io.quarkus.vertx.http.runtime.devmode.ResourceNotFoundRecorder; import io.quarkus.vertx.http.runtime.devmode.RouteDescription; import io.vertx.core.Handler; @@ -29,11 +32,19 @@ public class NotFoundProcessor { private static final String META_INF_RESOURCES = "META-INF/resources"; + @BuildStep(onlyIf = IsDevelopment.class) + AdditionalBeanBuildItem resourceNotFoundDataAvailable() { + return AdditionalBeanBuildItem.builder() + .addBeanClass(ResourceNotFoundData.class) + .setUnremovable().build(); + } + @BuildStep(onlyIf = IsDevelopment.class) @Record(RUNTIME_INIT) void routeNotFound(ResourceNotFoundRecorder recorder, VertxWebRouterBuildItem router, HttpRootPathBuildItem httpRoot, + BeanContainerBuildItem beanContainer, LaunchModeBuildItem launchMode, ApplicationArchivesBuildItem applicationArchivesBuildItem, List routeDescriptions, @@ -66,6 +77,7 @@ void routeNotFound(ResourceNotFoundRecorder recorder, router.getHttpRouter(), router.getMainRouter(), router.getManagementRouter(), + beanContainer.getValue(), getBaseUrl(launchMode), httpRoot.getRootPath(), routes, diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-endpoints.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-endpoints.js index efc01c57d7364..efe3b43c30a22 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-endpoints.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-endpoints.js @@ -1,14 +1,15 @@ import { LitElement, html, css} from 'lit'; -import { basepath } from 'devui-data'; import '@vaadin/progress-bar'; import '@vaadin/grid'; import { columnBodyRenderer } from '@vaadin/grid/lit.js'; import '@vaadin/grid/vaadin-grid-sort-column.js'; +import { JsonRpc } from 'jsonrpc'; /** * This component show all available endpoints */ export class QwcEndpoints extends LitElement { + jsonRpc = new JsonRpc(this); static styles = css` .infogrid { @@ -44,25 +45,15 @@ export class QwcEndpoints extends LitElement { this._info = null; } - async connectedCallback() { + connectedCallback() { super.connectedCallback(); - await this.load(); + this.jsonRpc.getJsonContent().then(jsonRpcResponse => { + this._info = jsonRpcResponse.result; + }); } - - async load() { - const response = await fetch("/quarkus404", { - method: 'GET', - headers: { - 'Accept': 'application/json' - } - }); - const data = await response.json(); - this._info = data; - } - + render() { if (this._info) { - const typeTemplates = []; for (const [type, list] of Object.entries(this._info)) { typeTemplates.push(html`${this._renderType(type,list)}`); diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundData.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundData.java new file mode 100644 index 0000000000000..1469c5c6c8289 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundData.java @@ -0,0 +1,287 @@ +package io.quarkus.vertx.http.runtime.devmode; + +import static io.quarkus.runtime.TemplateHtmlBuilder.adjustRoot; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.jboss.logging.Logger; + +import io.quarkus.runtime.TemplateHtmlBuilder; +import io.quarkus.runtime.util.ClassPathUtils; +import io.smallrye.common.annotation.NonBlocking; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; + +@ApplicationScoped +public class ResourceNotFoundData { + private static final Logger LOG = Logger.getLogger(ResourceNotFoundData.class); + private static volatile List runtimeRoutes = null; + private static volatile List servletMappings = new ArrayList<>(); + private static final String META_INF_RESOURCES = "META-INF/resources"; + + private String baseUrl; + private String httpRoot; + private List endpointRoutes; + private Set staticRoots; + private List additionalEndpoints; + + public void setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + } + + public void setHttpRoot(String httpRoot) { + this.httpRoot = httpRoot; + } + + public void setEndpointRoutes(List endpointRoutes) { + this.endpointRoutes = endpointRoutes; + } + + public void setStaticRoots(Set staticRoots) { + this.staticRoots = staticRoots; + } + + public void setAdditionalEndpoints(List additionalEndpoints) { + this.additionalEndpoints = additionalEndpoints; + } + + public static void addServlet(String mapping) { + servletMappings.add(mapping); + } + + public static void setRuntimeRoutes(List routeDescriptions) { + runtimeRoutes = routeDescriptions; + } + + public String getHTMLContent() { + + List combinedRoutes = getCombinedRoutes(); + TemplateHtmlBuilder builder = new TemplateHtmlBuilder(this.baseUrl, + "404 - Resource Not Found", "", "Resources overview"); + + builder.resourcesStart(RESOURCE_ENDPOINTS); + + for (RouteDescription resource : combinedRoutes) { + builder.resourcePath(adjustRoot(this.httpRoot, resource.getBasePath())); + for (RouteMethodDescription method : resource.getCalls()) { + builder.method(method.getHttpMethod(), + adjustRoot(this.httpRoot, method.getFullPath())); + if (method.getJavaMethod() != null) { + builder.listItem(method.getJavaMethod()); + } + if (method.getConsumes() != null) { + builder.consumes(method.getConsumes()); + } + if (method.getProduces() != null) { + builder.produces(method.getProduces()); + } + builder.methodEnd(); + } + builder.resourceEnd(); + } + if (combinedRoutes.isEmpty()) { + builder.noResourcesFound(); + } + builder.resourcesEnd(); + + if (!servletMappings.isEmpty()) { + builder.resourcesStart(SERVLET_MAPPINGS); + for (String servletMapping : servletMappings) { + builder.servletMapping(adjustRoot(this.httpRoot, servletMapping)); + } + builder.resourcesEnd(); + } + + // Static Resources + if (!this.staticRoots.isEmpty()) { + List resources = findRealResources(); + if (!resources.isEmpty()) { + builder.resourcesStart(STATIC_RESOURCES); + for (String staticResource : resources) { + builder.staticResourcePath(adjustRoot(this.httpRoot, staticResource)); + } + builder.resourcesEnd(); + } + } + + // Additional Endpoints + if (!this.additionalEndpoints.isEmpty()) { + builder.resourcesStart(ADDITIONAL_ENDPOINTS); + for (AdditionalRouteDescription additionalEndpoint : this.additionalEndpoints) { + builder.staticResourcePath(additionalEndpoint.getUri(), additionalEndpoint.getDescription()); + } + builder.resourcesEnd(); + } + + return builder.toString(); + } + + @NonBlocking + public JsonObject getJsonContent() { + List combinedRoutes = getCombinedRoutes(); + JsonObject infoMap = new JsonObject(); + + // REST Endpoints + if (!combinedRoutes.isEmpty()) { + JsonArray r = new JsonArray(); + for (RouteDescription resource : combinedRoutes) { + String path = adjustRoot(this.httpRoot, resource.getBasePath()); + + for (RouteMethodDescription method : resource.getCalls()) { + String description = method.getHttpMethod(); + if (method.getConsumes() != null) { + description = description + " (consumes: " + method.getConsumes() + ")"; + } + if (method.getProduces() != null) { + description = description + " (produces:" + method.getProduces() + ")"; + } + if (method.getJavaMethod() != null) { + description = description + " (java:" + method.getJavaMethod() + ")"; + } + r.add(JsonObject.of(URI, adjustRoot(this.httpRoot, method.getFullPath()), + DESCRIPTION, description)); + } + } + infoMap.put(RESOURCE_ENDPOINTS, r); + } + + // Servlets + if (!servletMappings.isEmpty()) { + JsonArray sm = new JsonArray(); + for (String servletMapping : servletMappings) { + sm.add(JsonObject.of(URI, adjustRoot(this.httpRoot, servletMapping), DESCRIPTION, + EMPTY)); + } + infoMap.put(SERVLET_MAPPINGS, sm); + } + + // Static Resources + if (!this.staticRoots.isEmpty()) { + List resources = findRealResources(); + if (!resources.isEmpty()) { + JsonArray sr = new JsonArray(); + for (String staticResource : resources) { + sr.add(JsonObject.of(URI, adjustRoot(this.httpRoot, staticResource), DESCRIPTION, + EMPTY)); + } + infoMap.put(STATIC_RESOURCES, sr); + } + } + + // Additional Endpoints + if (!this.additionalEndpoints.isEmpty()) { + JsonArray ae = new JsonArray(); + for (AdditionalRouteDescription additionalEndpoint : this.additionalEndpoints) { + ae.add(JsonObject.of(URI, additionalEndpoint.getUri(), DESCRIPTION, additionalEndpoint.getDescription())); + } + infoMap.put(ADDITIONAL_ENDPOINTS, ae); + } + + return infoMap; + + } + + private List getCombinedRoutes() { + // Endpoints + List combinedRoutes = new ArrayList<>(); + if (this.runtimeRoutes != null) { + combinedRoutes.addAll(this.runtimeRoutes); + } + if (endpointRoutes != null) { + combinedRoutes.addAll(this.endpointRoutes); + } + return combinedRoutes; + } + + private List findRealResources() { + + //we need to check for web resources in order to get welcome files to work + //this kinda sucks + Set knownFiles = new HashSet<>(); + for (String staticResourceRoot : this.staticRoots) { + if (staticResourceRoot != null) { + Path resource = Paths.get(staticResourceRoot); + if (Files.exists(resource)) { + try (Stream fileTreeElements = Files.walk(resource)) { + fileTreeElements.forEach(new Consumer() { + @Override + public void accept(java.nio.file.Path path) { + // Skip META-INF/resources entry + if (resource.equals(path)) { + return; + } + java.nio.file.Path rel = resource.relativize(path); + if (!Files.isDirectory(path)) { + knownFiles.add("/" + rel.toString()); + } + } + }); + } catch (IOException e) { + LOG.error("Failed to read static resources", e); + } + } + } + } + try { + ClassPathUtils.consumeAsPaths(META_INF_RESOURCES, p -> { + collectKnownPaths(p, knownFiles); + }); + } catch (IOException e) { + LOG.error("Failed to read static resources", e); + } + + //limit to 1000 to not have to many files to display + return knownFiles.stream().filter(this::isHtmlFileName).limit(1000).distinct().sorted(Comparator.naturalOrder()) + .collect(Collectors.toList()); + } + + private void collectKnownPaths(java.nio.file.Path resource, Set knownPaths) { + try { + Files.walkFileTree(resource, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(java.nio.file.Path p, BasicFileAttributes attrs) + throws IOException { + String file = resource.relativize(p).toString(); + // Windows has a backslash + file = file.replace('\\', '/'); + if (!file.startsWith("_static/") && !file.startsWith("webjars/")) { + knownPaths.add("/" + file); + } + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private boolean isHtmlFileName(String fileName) { + return fileName.endsWith(".html") || fileName.endsWith(".htm") || fileName.endsWith(".xhtml"); + } + + private static final String RESOURCE_ENDPOINTS = "Resource Endpoints"; + private static final String SERVLET_MAPPINGS = "Servlet mappings"; + private static final String STATIC_RESOURCES = "Static resources"; + private static final String ADDITIONAL_ENDPOINTS = "Additional endpoints"; + private static final String URI = "uri"; + private static final String DESCRIPTION = "description"; + private static final String EMPTY = ""; + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundHandler.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundHandler.java index 46fec0b2f11c8..8dc6e6cf029e1 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundHandler.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundHandler.java @@ -1,291 +1,43 @@ package io.quarkus.vertx.http.runtime.devmode; -import static io.quarkus.runtime.TemplateHtmlBuilder.adjustRoot; +import jakarta.enterprise.inject.spi.CDI; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.function.Consumer; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.jboss.logging.Logger; - -import io.quarkus.runtime.TemplateHtmlBuilder; -import io.quarkus.runtime.util.ClassPathUtils; import io.vertx.core.Handler; import io.vertx.core.json.Json; -import io.vertx.core.json.JsonArray; -import io.vertx.core.json.JsonObject; import io.vertx.ext.web.RoutingContext; /** * Lists all routes when no route matches the path in the dev mode. */ public class ResourceNotFoundHandler implements Handler { - private static final Logger LOG = Logger.getLogger(ResourceNotFoundHandler.class); - protected static final String META_INF_RESOURCES = "META-INF/resources"; - - public static volatile List runtimeRoutes; - private static volatile List servletMappings = new ArrayList<>(); + private final ResourceNotFoundData resourceNotFoundData; - private final String baseUrl; - private final String httpRoot; - private final List routes; - private final Set staticResourceRoots; - private final List additionalEndpoints; - - public ResourceNotFoundHandler(String baseUrl, - String httpRoot, - List routes, - Set staticResourceRoots, - List additionalEndpoints) { - this.baseUrl = baseUrl; - this.httpRoot = httpRoot; - this.routes = routes; - this.staticResourceRoots = staticResourceRoots; - this.additionalEndpoints = additionalEndpoints; + public ResourceNotFoundHandler() { + this.resourceNotFoundData = CDI.current().select(ResourceNotFoundData.class).get(); } @Override public void handle(RoutingContext routingContext) { - // Endpoints - List combinedRoutes = new ArrayList<>(); - if (runtimeRoutes != null) { - combinedRoutes.addAll(runtimeRoutes); - } - if (routes != null) { - combinedRoutes.addAll(routes); - } - String header = routingContext.request().getHeader("Accept"); if (header != null && header.startsWith("application/json")) { - handleJson(routingContext, combinedRoutes); + handleJson(routingContext); } else { - handleHTML(routingContext, combinedRoutes); + handleHTML(routingContext); } } - private void handleJson(RoutingContext routingContext, List combinedRoutes) { + private void handleJson(RoutingContext routingContext) { routingContext.response() .setStatusCode(404) .putHeader("content-type", "application/json; charset=utf-8") - .end(Json.encodePrettily(getJsonContent(combinedRoutes))); + .end(Json.encodePrettily(resourceNotFoundData.getJsonContent())); } - private void handleHTML(RoutingContext routingContext, List combinedRoutes) { + private void handleHTML(RoutingContext routingContext) { routingContext.response() .setStatusCode(404) .putHeader("content-type", "text/html; charset=utf-8") - .end(getHTMLContent(combinedRoutes)); - } - - private List findRealResources() { - - //we need to check for web resources in order to get welcome files to work - //this kinda sucks - Set knownFiles = new HashSet<>(); - for (String staticResourceRoot : staticResourceRoots) { - if (staticResourceRoot != null) { - Path resource = Paths.get(staticResourceRoot); - if (Files.exists(resource)) { - try (Stream fileTreeElements = Files.walk(resource)) { - fileTreeElements.forEach(new Consumer() { - @Override - public void accept(java.nio.file.Path path) { - // Skip META-INF/resources entry - if (resource.equals(path)) { - return; - } - java.nio.file.Path rel = resource.relativize(path); - if (!Files.isDirectory(path)) { - knownFiles.add("/" + rel.toString()); - } - } - }); - } catch (IOException e) { - LOG.error("Failed to read static resources", e); - } - } - } - } - try { - ClassPathUtils.consumeAsPaths(META_INF_RESOURCES, p -> { - collectKnownPaths(p, knownFiles); - }); - } catch (IOException e) { - LOG.error("Failed to read static resources", e); - } - - //limit to 1000 to not have to many files to display - return knownFiles.stream().filter(this::isHtmlFileName).limit(1000).distinct().sorted(Comparator.naturalOrder()) - .collect(Collectors.toList()); - } - - private void collectKnownPaths(java.nio.file.Path resource, Set knownPaths) { - try { - Files.walkFileTree(resource, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(java.nio.file.Path p, BasicFileAttributes attrs) - throws IOException { - String file = resource.relativize(p).toString(); - // Windows has a backslash - file = file.replace('\\', '/'); - if (!file.startsWith("_static/") && !file.startsWith("webjars/")) { - knownPaths.add("/" + file); - } - return FileVisitResult.CONTINUE; - } - }); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - private boolean isHtmlFileName(String fileName) { - return fileName.endsWith(".html") || fileName.endsWith(".htm") || fileName.endsWith(".xhtml"); - } - - public static void addServlet(String mapping) { - servletMappings.add(mapping); - } - - private String getHTMLContent(List combinedRoutes) { - TemplateHtmlBuilder builder = new TemplateHtmlBuilder(baseUrl, "404 - Resource Not Found", "", "Resources overview"); - - builder.resourcesStart(RESOURCE_ENDPOINTS); - - for (RouteDescription resource : combinedRoutes) { - builder.resourcePath(adjustRoot(httpRoot, resource.getBasePath())); - for (RouteMethodDescription method : resource.getCalls()) { - builder.method(method.getHttpMethod(), adjustRoot(httpRoot, method.getFullPath())); - if (method.getJavaMethod() != null) { - builder.listItem(method.getJavaMethod()); - } - if (method.getConsumes() != null) { - builder.consumes(method.getConsumes()); - } - if (method.getProduces() != null) { - builder.produces(method.getProduces()); - } - builder.methodEnd(); - } - builder.resourceEnd(); - } - if (combinedRoutes.isEmpty()) { - builder.noResourcesFound(); - } - builder.resourcesEnd(); - - if (!servletMappings.isEmpty()) { - builder.resourcesStart(SERVLET_MAPPINGS); - for (String servletMapping : servletMappings) { - builder.servletMapping(adjustRoot(httpRoot, servletMapping)); - } - builder.resourcesEnd(); - } - - // Static Resources - if (!staticResourceRoots.isEmpty()) { - List resources = findRealResources(); - if (!resources.isEmpty()) { - builder.resourcesStart(STATIC_RESOURCES); - for (String staticResource : resources) { - builder.staticResourcePath(adjustRoot(httpRoot, staticResource)); - } - builder.resourcesEnd(); - } - } - - // Additional Endpoints - if (!additionalEndpoints.isEmpty()) { - builder.resourcesStart(ADDITIONAL_ENDPOINTS); - for (AdditionalRouteDescription additionalEndpoint : additionalEndpoints) { - builder.staticResourcePath(additionalEndpoint.getUri(), additionalEndpoint.getDescription()); - } - builder.resourcesEnd(); - } - - return builder.toString(); - } - - private JsonObject getJsonContent(List combinedRoutes) { - - JsonObject infoMap = new JsonObject(); - - // REST Endpoints - if (!combinedRoutes.isEmpty()) { - JsonArray r = new JsonArray(); - for (RouteDescription resource : combinedRoutes) { - String path = adjustRoot(httpRoot, resource.getBasePath()); - - for (RouteMethodDescription method : resource.getCalls()) { - String description = method.getHttpMethod(); - if (method.getConsumes() != null) { - description = description + " (consumes: " + method.getConsumes() + ")"; - } - if (method.getProduces() != null) { - description = description + " (produces:" + method.getProduces() + ")"; - } - if (method.getJavaMethod() != null) { - description = description + " (java:" + method.getJavaMethod() + ")"; - } - r.add(JsonObject.of(URI, adjustRoot(httpRoot, method.getFullPath()), DESCRIPTION, description)); - } - } - infoMap.put(RESOURCE_ENDPOINTS, r); - } - - // Servlets - if (!servletMappings.isEmpty()) { - JsonArray sm = new JsonArray(); - for (String servletMapping : servletMappings) { - sm.add(JsonObject.of(URI, adjustRoot(httpRoot, servletMapping), DESCRIPTION, EMPTY)); - } - infoMap.put(SERVLET_MAPPINGS, sm); - } - - // Static Resources - if (!staticResourceRoots.isEmpty()) { - List resources = findRealResources(); - if (!resources.isEmpty()) { - JsonArray sr = new JsonArray(); - for (String staticResource : resources) { - sr.add(JsonObject.of(URI, adjustRoot(httpRoot, staticResource), DESCRIPTION, EMPTY)); - } - infoMap.put(STATIC_RESOURCES, sr); - } - } - - // Additional Endpoints - if (!additionalEndpoints.isEmpty()) { - JsonArray ae = new JsonArray(); - for (AdditionalRouteDescription additionalEndpoint : additionalEndpoints) { - ae.add(JsonObject.of(URI, additionalEndpoint.getUri(), DESCRIPTION, additionalEndpoint.getDescription())); - } - infoMap.put(ADDITIONAL_ENDPOINTS, ae); - } - - return infoMap; - + .end(resourceNotFoundData.getHTMLContent()); } - - private static final String RESOURCE_ENDPOINTS = "Resource Endpoints"; - private static final String SERVLET_MAPPINGS = "Servlet mappings"; - private static final String STATIC_RESOURCES = "Static resources"; - private static final String ADDITIONAL_ENDPOINTS = "Additional endpoints"; - private static final String URI = "uri"; - private static final String DESCRIPTION = "description"; - private static final String EMPTY = ""; } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundRecorder.java index 6e848021a7f5f..ef5b60f70e65f 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundRecorder.java @@ -3,6 +3,7 @@ import java.util.List; import java.util.Set; +import io.quarkus.arc.runtime.BeanContainer; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; import io.vertx.core.Handler; @@ -15,14 +16,21 @@ public class ResourceNotFoundRecorder { public Handler registerNotFoundHandler(RuntimeValue httpRouter, RuntimeValue mainRouter, RuntimeValue managementRouter, + BeanContainer beanContainer, String baseUrl, String httpRoot, List endpointRoutes, Set staticRoots, List additionalEndpoints) { - ResourceNotFoundHandler rbfh = new ResourceNotFoundHandler(baseUrl, httpRoot, endpointRoutes, staticRoots, - additionalEndpoints); + ResourceNotFoundData resourceNotFoundData = beanContainer.beanInstance(ResourceNotFoundData.class); + resourceNotFoundData.setBaseUrl(baseUrl); + resourceNotFoundData.setHttpRoot(httpRoot); + resourceNotFoundData.setEndpointRoutes(endpointRoutes); + resourceNotFoundData.setStaticRoots(staticRoots); + resourceNotFoundData.setAdditionalEndpoints(additionalEndpoints); + + ResourceNotFoundHandler rbfh = new ResourceNotFoundHandler(); addErrorHandler(mainRouter, rbfh); addErrorHandler(httpRouter, rbfh);