diff --git a/extensions/vertx-http/deployment-spi/src/main/java/io/quarkus/vertx/http/deployment/spi/AdditionalStaticResourceBuildItem.java b/extensions/vertx-http/deployment-spi/src/main/java/io/quarkus/vertx/http/deployment/spi/AdditionalStaticResourceBuildItem.java index 94ec70a8809f3..2fdbe78cae532 100644 --- a/extensions/vertx-http/deployment-spi/src/main/java/io/quarkus/vertx/http/deployment/spi/AdditionalStaticResourceBuildItem.java +++ b/extensions/vertx-http/deployment-spi/src/main/java/io/quarkus/vertx/http/deployment/spi/AdditionalStaticResourceBuildItem.java @@ -9,7 +9,7 @@ * * The value of {@code path} should be prefixed with {@code '/'} and is assumed to be a path under {@code 'META-INF/resources'}. * - * @deprecated Use {@link GeneratedStaticResourceBuildItem} instead. + * @deprecated Use {@link GeneratedStaticResourceBuildItem} instead (the goal is to make this BuildItem internal). */ @Deprecated public final class AdditionalStaticResourceBuildItem extends MultiBuildItem { diff --git a/extensions/vertx-http/deployment-spi/src/main/java/io/quarkus/vertx/http/deployment/spi/GeneratedStaticResourceBuildItem.java b/extensions/vertx-http/deployment-spi/src/main/java/io/quarkus/vertx/http/deployment/spi/GeneratedStaticResourceBuildItem.java index 21580b8bf8d90..ffa37294d0314 100644 --- a/extensions/vertx-http/deployment-spi/src/main/java/io/quarkus/vertx/http/deployment/spi/GeneratedStaticResourceBuildItem.java +++ b/extensions/vertx-http/deployment-spi/src/main/java/io/quarkus/vertx/http/deployment/spi/GeneratedStaticResourceBuildItem.java @@ -2,40 +2,79 @@ import static java.util.Objects.requireNonNull; +import java.nio.file.Path; + import io.quarkus.builder.item.MultiBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; /** * This build item aims to be used by extensions to generate static resources. *
- * Those resources will be served on the given {@link GeneratedStaticResourceBuildItem#path}. It is NOT necessary to create the + * Those resources will be served on the given {@link GeneratedStaticResourceBuildItem#endpoint}. It is NOT necessary to create + * the * file on disk. *
* Behind the scenes the build step will take care of add those resources to the final build, through * {@link AdditionalStaticResourceBuildItem}, {@link NativeImageResourceBuildItem} * and {@link io.quarkus.deployment.builditem.GeneratedResourceBuildItem} build items. *
- * The value of {@code path} should be prefixed with {@code '/'}. + * The value of {@code endpoint} should be prefixed with {@code '/'}. */ public final class GeneratedStaticResourceBuildItem extends MultiBuildItem { - private final String path; + private final String endpoint; + + private final Path file; private final byte[] content; - public GeneratedStaticResourceBuildItem(final String path, final byte[] content) { - if (!requireNonNull(path, "path is required").startsWith("/")) { - throw new IllegalArgumentException("path must start with '/'"); + private GeneratedStaticResourceBuildItem(final String endpoint, final byte[] content, final Path file) { + if (!requireNonNull(endpoint, "endpoint is required").startsWith("/")) { + throw new IllegalArgumentException("endpoint must start with '/'"); } - this.path = path; + this.endpoint = endpoint; + this.file = file; this.content = content; } - public String getPath() { - return this.path; + /** + * The resource will be served at {@code '{quarkus.http.root-path}{endpoint}'} + * + * @param endpoint the endpoint from the {@code '{quarkus.http.root-path}'} for this generated static resource. It should be + * prefixed with {@code '/'} + * @param content the content of this generated static resource + */ + public GeneratedStaticResourceBuildItem(final String endpoint, final byte[] content) { + this(endpoint, content, null); + } + + /** + * The resource will be served at {root-path}{path} + * + * @param endpoint the endpoint from the {@code '{quarkus.http.root-path}'} for this generated static resource. It should be + * prefixed with {@code '/'} + * @param file the file Path on the local filesystem + */ + public GeneratedStaticResourceBuildItem(final String endpoint, final Path file) { + this(endpoint, null, file); + } + + public String getEndpoint() { + return this.endpoint; } + public boolean isFile() { + return file != null; + }; + public byte[] getContent() { return this.content; } + public Path getFile() { + return file; + } + + public String getFileAbsolutePath() { + return file.toAbsolutePath().toString(); + } } diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/GeneratedStaticResourcesProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/GeneratedStaticResourcesProcessor.java index bcd7cd7089c44..d6316b11e86ed 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/GeneratedStaticResourcesProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/GeneratedStaticResourcesProcessor.java @@ -14,6 +14,7 @@ import io.quarkus.bootstrap.classloading.ClassPathElement; import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.IsNormal; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; @@ -25,22 +26,22 @@ import io.quarkus.paths.FilteredPathTree; import io.quarkus.paths.PathFilter; import io.quarkus.paths.PathVisitor; +import io.quarkus.runtime.LaunchMode; import io.quarkus.vertx.http.deployment.devmode.NotFoundPageDisplayableEndpointBuildItem; import io.quarkus.vertx.http.deployment.spi.AdditionalStaticResourceBuildItem; import io.quarkus.vertx.http.deployment.spi.GeneratedStaticResourceBuildItem; import io.quarkus.vertx.http.runtime.GeneratedStaticResourcesRecorder; import io.quarkus.vertx.http.runtime.RouteConstants; -import io.quarkus.vertx.http.runtime.handlers.DevClasspathStaticHandler; +import io.quarkus.vertx.http.runtime.handlers.DevStaticHandler; /** * {@link GeneratedStaticResourcesProcessor} is responsible for dealing {@link GeneratedStaticResourceBuildItem} - * creating a {@link DevClasspathStaticHandler} to handle all static resources + * creating a {@link DevStaticHandler} to handle all static resources * generated from extensions through {@link GeneratedStaticResourceBuildItem} build item. */ public class GeneratedStaticResourcesProcessor { private static final int ROUTE_ORDER = RouteConstants.ROUTE_ORDER_BEFORE_DEFAULT + 60; - private static final String META_INF_GENERATED_RESOURCES = "META-INF/generated-resources"; @BuildStep public void produceResources(List generatedStaticResources, @@ -48,54 +49,87 @@ public void produceResources(List generatedSta BuildProducer nativeImageResourcesProducer, LaunchModeBuildItem launchModeBuildItem, BuildProducer additionalStaticResourcesProducer) { - for (GeneratedStaticResourceBuildItem generatedStaticResource : generatedStaticResources) { String generatedStaticResourceLocation = buildGeneratedStaticResourceLocation(generatedStaticResource); - - generatedResourceBuildItem.produce( - new GeneratedResourceBuildItem(generatedStaticResourceLocation, - generatedStaticResource.getContent(), false)); + if (!generatedStaticResource.isFile()) { + generatedResourceBuildItem.produce( + new GeneratedResourceBuildItem(generatedStaticResourceLocation, + generatedStaticResource.getContent(), false)); + } else if (launchModeBuildItem.getLaunchMode() != LaunchMode.DEVELOPMENT) { + // For files, we need to read it and add it in the classpath for normal and test mode + try { + final byte[] content = Files.readAllBytes(generatedStaticResource.getFile()); + generatedResourceBuildItem.produce( + new GeneratedResourceBuildItem(generatedStaticResourceLocation, + content, false)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } // We can't use the vert.x StaticHandler for tests as it doesn't support 'quarkus' protocol - if (!launchModeBuildItem.getLaunchMode().isDevOrTest()) { + if (launchModeBuildItem.getLaunchMode() == LaunchMode.NORMAL) { additionalStaticResourcesProducer.produce( - new AdditionalStaticResourceBuildItem(generatedStaticResource.getPath(), false)); + new AdditionalStaticResourceBuildItem(generatedStaticResource.getEndpoint(), false)); nativeImageResourcesProducer.produce(new NativeImageResourceBuildItem(generatedStaticResourceLocation)); } } } - @BuildStep + @BuildStep(onlyIfNot = IsNormal.class) @Record(ExecutionTime.RUNTIME_INIT) public void process(List generatedStaticResources, LaunchModeBuildItem launchModeBuildItem, BuildProducer routes, GeneratedStaticResourcesRecorder generatedStaticResourcesRecorder, BuildProducer notFoundPageProducer) { - if (!launchModeBuildItem.getLaunchMode().isDevOrTest() || generatedStaticResources.isEmpty()) { + if (generatedStaticResources.isEmpty()) { return; } - Set paths = generatedStaticResources.stream().map(GeneratedStaticResourceBuildItem::getPath) - .peek(path -> notFoundPageProducer.produce(new NotFoundPageDisplayableEndpointBuildItem(path))) + Map generatedFilesResources = generatedStaticResources.stream() + .peek(path -> notFoundPageProducer.produce(new NotFoundPageDisplayableEndpointBuildItem(path.getEndpoint()))) + .filter(GeneratedStaticResourceBuildItem::isFile) + .collect(Collectors.toMap(GeneratedStaticResourceBuildItem::getEndpoint, + GeneratedStaticResourceBuildItem::getFileAbsolutePath)); + Set generatedClassPathResources = generatedStaticResources.stream() + .map(GeneratedStaticResourceBuildItem::getEndpoint) .collect(Collectors.toSet()); routes.produce(RouteBuildItem.builder() .orderedRoute("/*", ROUTE_ORDER, generatedStaticResourcesRecorder.createRouteCustomizer()) - .handler(generatedStaticResourcesRecorder.createHandler(paths)) + .handler(generatedStaticResourcesRecorder.createHandler(generatedClassPathResources, generatedFilesResources)) .build()); } + private static String buildGeneratedStaticResourceLocation( + GeneratedStaticResourceBuildItem generatedStaticResourceBuildItem) { + return META_INF_RESOURCES + + generatedStaticResourceBuildItem.getEndpoint(); + } + + // THIS IS TO TEST DEV MODE + + private static final String META_INF_GENERATED_RESOURCES_TEST = "META-INF/generated-resources-test"; + @BuildStep(onlyIf = IsDevelopment.class) public void devMode( BuildProducer hotDeployment, - BuildProducer generatedStaticResourceProducer) throws IOException { + BuildProducer generatedStaticResourceProducer, + LaunchModeBuildItem launchMode) throws IOException { + // this is only for dev-mode tests + if (!launchMode.isTest()) { + return; + } + hotDeployment.produce(HotDeploymentWatchedFileBuildItem.builder() .setRestartNeeded(true) - .setLocationPredicate(l -> l.startsWith(META_INF_GENERATED_RESOURCES)) + .setLocationPredicate(l -> l.startsWith(META_INF_GENERATED_RESOURCES_TEST + "/bytes")) .build()); Map classpathResources = getClasspathResources(); - for (Map.Entry entries : classpathResources.entrySet()) { - byte[] bytes = Files.readAllBytes(entries.getValue()); - generatedStaticResourceProducer.produce(new GeneratedStaticResourceBuildItem( - "/" + entries.getKey(), bytes)); + for (Map.Entry entry : classpathResources.entrySet()) { + final String key = "/" + entry.getKey(); + final GeneratedStaticResourceBuildItem item = key.startsWith("/bytes") + ? new GeneratedStaticResourceBuildItem(key, Files.readAllBytes(entry.getValue())) + : new GeneratedStaticResourceBuildItem(key, entry.getValue()); + generatedStaticResourceProducer.produce(item); } } @@ -104,7 +138,7 @@ private Map getClasspathResources() { visitRuntimeMetaInfResources(visit -> { if (!Files.isDirectory(visit.getPath())) { String resourcePath = visit.getRelativePath("/") - .substring("/".concat(META_INF_GENERATED_RESOURCES).length()); + .substring("/".concat(META_INF_GENERATED_RESOURCES_TEST).length()); knownPaths.put(resourcePath, visit.getPath()); } }); @@ -112,12 +146,12 @@ private Map getClasspathResources() { } private static void visitRuntimeMetaInfResources(PathVisitor visitor) { - final List elements = QuarkusClassLoader.getElements(META_INF_GENERATED_RESOURCES, + final List elements = QuarkusClassLoader.getElements(META_INF_GENERATED_RESOURCES_TEST, false); if (!elements.isEmpty()) { final PathFilter filter = PathFilter.forIncludes(List.of( - META_INF_GENERATED_RESOURCES + "/**", - META_INF_GENERATED_RESOURCES)); + META_INF_GENERATED_RESOURCES_TEST + "/**", + META_INF_GENERATED_RESOURCES_TEST)); for (var element : elements) { if (element.isRuntime()) { element.apply(tree -> { @@ -128,10 +162,4 @@ private static void visitRuntimeMetaInfResources(PathVisitor visitor) { } } } - - private static String buildGeneratedStaticResourceLocation( - GeneratedStaticResourceBuildItem generatedStaticResourceBuildItem) { - return META_INF_RESOURCES + - generatedStaticResourceBuildItem.getPath(); - } } diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/StaticResourcesProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/StaticResourcesProcessor.java index 7c6a362204199..3b64ea89e1c7c 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/StaticResourcesProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/StaticResourcesProcessor.java @@ -43,10 +43,9 @@ void collectStaticResources(Capabilities capabilities, return; } Set paths = getClasspathResources(); - if (!launchModeBuildItem.getLaunchMode().isDevOrTest()) { - for (AdditionalStaticResourceBuildItem bi : additionalStaticResources) { - paths.add(new StaticResourcesBuildItem.Entry(bi.getPath(), bi.isDirectory())); - } + // We shouldn't add them in test and dev-mode (as they are handled by the GeneratedStaticResourcesProcessor), but for backward compatibility we keep it for now + for (AdditionalStaticResourceBuildItem bi : additionalStaticResources) { + paths.add(new StaticResourcesBuildItem.Entry(bi.getPath(), bi.isDirectory())); } if (!paths.isEmpty()) { diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/GeneratedStaticResourcesTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/GeneratedStaticClasspathResourcesTest.java similarity index 98% rename from extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/GeneratedStaticResourcesTest.java rename to extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/GeneratedStaticClasspathResourcesTest.java index 324565a2cf177..612927e4bf484 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/GeneratedStaticResourcesTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/GeneratedStaticClasspathResourcesTest.java @@ -16,7 +16,7 @@ import io.quarkus.vertx.http.deployment.spi.GeneratedStaticResourceBuildItem; import io.restassured.RestAssured; -public class GeneratedStaticResourcesTest { +public class GeneratedStaticClasspathResourcesTest { @RegisterExtension final static QuarkusUnitTest test = new QuarkusUnitTest().withApplicationRoot( diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/GeneratedStaticFileResourcesTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/GeneratedStaticFileResourcesTest.java new file mode 100644 index 0000000000000..d4e6ddea85ebc --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/GeneratedStaticFileResourcesTest.java @@ -0,0 +1,125 @@ +package io.quarkus.vertx.http; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.Consumer; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.asset.StringAsset; +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.deployment.builditem.GeneratedResourceBuildItem; +import io.quarkus.runtime.util.ClassPathUtils; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.vertx.http.deployment.spi.GeneratedStaticResourceBuildItem; +import io.restassured.RestAssured; + +public class GeneratedStaticFileResourcesTest { + + @RegisterExtension + final static QuarkusUnitTest test = new QuarkusUnitTest().withApplicationRoot( + (jar) -> jar + .addAsResource("static-file.html", "static-file.html") + .add(new StringAsset( + "quarkus.http.enable-compression=true\nquarkus.http.static-resources.index-page=default.html"), + "application.properties")) + .addBuildChainCustomizer(new Consumer() { + @Override + public void accept(BuildChainBuilder buildChainBuilder) { + + buildChainBuilder.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + final Path file = resolveResource("/static-file.html"); + context.produce(new GeneratedStaticResourceBuildItem( + "/default.html", file)); + context.produce(new GeneratedStaticResourceBuildItem("/hello-from-generated-static-resource.html", + file)); + + context.produce(new GeneratedStaticResourceBuildItem("/static-file.html", + file)); + context.produce(new GeneratedStaticResourceBuildItem( + "/.nojekyll", resolveResource("/.nojekyll"))); + context.produce(new GeneratedStaticResourceBuildItem("/quarkus-openapi-generator/default.html", + file)); + } + }).produces(GeneratedStaticResourceBuildItem.class).produces(GeneratedResourceBuildItem.class).build(); + } + }); + + private static Path resolveResource(String name) { + return ClassPathUtils.toLocalPath(GeneratedStaticFileResourcesTest.class.getResource(name)); + } + + @Test + public void shouldGetStaticFileHtmlPageWhenThereIsAGeneratedStaticResource() throws IOException { + final String result = Files.readString(resolveResource("/static-file.html")); + RestAssured.get("/static-file.html").then() + .body(Matchers.is(result)) + .statusCode(Matchers.is(200)); + + RestAssured.get("hello-from-generated-static-resource.html").then() + .body(Matchers.is(result)) + .statusCode(Matchers.is(200)); + + RestAssured.get("/").then() + .body(Matchers.is(result)) + .statusCode(200); + } + + @Test + public void shouldCompress() throws IOException { + final String result = Files.readString(resolveResource("/static-file.html")); + RestAssured.get("/static-file.html").then() + .header("Content-Encoding", "gzip") + .body(Matchers.is(result)) + .statusCode(Matchers.is(200)); + } + + @Test + public void shouldGetHiddenFiles() { + RestAssured.get("/.nojekyll") + .then() + .body(Matchers.containsString("{empty}")) + .statusCode(200); + } + + @Test + public void shouldGetTheIndexPageCorrectly() throws IOException { + final String result = Files.readString(resolveResource("/static-file.html")); + RestAssured.get("/quarkus-openapi-generator/") + .then() + .body(Matchers.is(result)) + .statusCode(200); + } + + @Test + public void shouldNotGetWhenPathEndsWithoutSlash() { + RestAssured.get("/quarkus-openapi-generator") + .then() + .statusCode(404); // We are using next() + } + + @Test + public void shouldGetAllowHeaderWhenUsingOptions() { + RestAssured.options("/quarkus-openapi-generator/") + .then() + .header("Allow", Matchers.is("HEAD,GET,OPTIONS")) + .statusCode(204); + } + + @Test + public void shouldGetHeadersFromHeadRequest() throws IOException { + final byte[] result = Files.readAllBytes(resolveResource("/static-file.html")); + RestAssured.head("/static-file.html") + .then() + .header("Content-Length", Integer::parseInt, Matchers.is(result.length)) + .header("Content-Type", Matchers.is("text/html;charset=UTF-8")) + .statusCode(200); + } +} diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/devmode/GeneratedStaticResourcesDevModeTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/devmode/GeneratedStaticResourcesDevModeTest.java index 89c88ee71dd84..77b51020560ec 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/devmode/GeneratedStaticResourcesDevModeTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/devmode/GeneratedStaticResourcesDevModeTest.java @@ -15,20 +15,57 @@ public class GeneratedStaticResourcesDevModeTest { .withApplicationRoot((jar) -> jar .add(new StringAsset("quarkus.http.enable-compression=true\n"), "application.properties") - .addAsResource("static-file.html", "META-INF/generated-resources/static-file.html") - .addAsResource("static-file.html", "META-INF/generated-resources/.hidden-file.html") - .addAsResource("static-file.html", "META-INF/generated-resources/index.html") - .addAsResource("static-file.html", "META-INF/generated-resources/image.svg")); + .addAsResource("static-file.html", "META-INF/generated-resources-test/bytes/static-file.html") + .addAsResource("static-file.html", "META-INF/generated-resources-test/bytes/.hidden-file.html") + .addAsResource("static-file.html", "META-INF/generated-resources-test/bytes/index.html") + .addAsResource("static-file.html", "META-INF/generated-resources-test/bytes/image.svg") + .addAsResource("static-file.html", "META-INF/generated-resources-test/static-file.html") + .addAsResource("static-file.html", "META-INF/generated-resources-test/.hidden-file.html") + .addAsResource("static-file.html", "META-INF/generated-resources-test/index.html") + .addAsResource("static-file.html", "META-INF/generated-resources-test/image.svg")); @Test void shouldUpdateResourceIndexHtmlOnUserChange() { + RestAssured.given() + .get("/bytes/") + .then() + .statusCode(200) + .body(Matchers.containsString("This is the title of the webpage!")); + + devMode.modifyResourceFile("META-INF/generated-resources-test/bytes/index.html", s -> s.replace("webpage", "Matheus")); + + RestAssured.given() + .get("/bytes/") + .then() + .statusCode(200) + .body(Matchers.containsString("This is the title of the Matheus!")); + } + + @Test + void shouldUpdateHiddenResourceOnUserChange() { + RestAssured.given() + .get("/bytes/.hidden-file.html") + .then() + .statusCode(200) + .body(Matchers.containsString("This is the title of the webpage!")); + devMode.modifyResourceFile("META-INF/generated-resources-test/bytes/.hidden-file.html", + s -> s.replace("webpage", "Matheus")); + RestAssured.given() + .get("/bytes/.hidden-file.html") + .then() + .statusCode(200) + .body(Matchers.containsString("This is the title of the Matheus!")); + } + + @Test + void shouldUpdateFileResourceIndexHtmlOnUserChange() { RestAssured.given() .get("/") .then() .statusCode(200) .body(Matchers.containsString("This is the title of the webpage!")); - devMode.modifyResourceFile("META-INF/generated-resources/index.html", s -> s.replace("webpage", "Matheus")); + devMode.modifyResourceFile("META-INF/generated-resources-test/index.html", s -> s.replace("webpage", "Matheus")); RestAssured.given() .get("/") @@ -38,15 +75,16 @@ void shouldUpdateResourceIndexHtmlOnUserChange() { } @Test - void shouldUpdateHiddenResourceOnUserChange() { + void shouldUpdateHiddenFileResourceOnUserChange() { RestAssured.given() .get("/.hidden-file.html") .then() .statusCode(200) .body(Matchers.containsString("This is the title of the webpage!")); - devMode.modifyResourceFile("META-INF/generated-resources/.hidden-file.html", s -> s.replace("webpage", "Matheus")); + devMode.modifyResourceFile("META-INF/generated-resources-test/.hidden-file.html", + s -> s.replace("webpage", "Matheus")); RestAssured.given() - .get(".hidden-file.html") + .get("/.hidden-file.html") .then() .statusCode(200) .body(Matchers.containsString("This is the title of the Matheus!")); diff --git a/extensions/vertx-http/deployment/src/test/resources/.nojekyll b/extensions/vertx-http/deployment/src/test/resources/.nojekyll new file mode 100644 index 0000000000000..06130e8de130d --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/resources/.nojekyll @@ -0,0 +1 @@ +{empty} \ No newline at end of file diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/GeneratedStaticResourcesRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/GeneratedStaticResourcesRecorder.java index 87df8dd36b0e8..394657aa62211 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/GeneratedStaticResourcesRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/GeneratedStaticResourcesRecorder.java @@ -1,12 +1,13 @@ package io.quarkus.vertx.http.runtime; +import java.util.Map; import java.util.Set; import java.util.function.Consumer; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; -import io.quarkus.vertx.http.runtime.handlers.DevClasspathStaticHandler; import io.quarkus.vertx.http.runtime.handlers.DevClasspathStaticHandlerOptions; +import io.quarkus.vertx.http.runtime.handlers.DevStaticHandler; import io.vertx.core.Handler; import io.vertx.core.http.HttpMethod; import io.vertx.ext.web.Route; @@ -26,7 +27,8 @@ public GeneratedStaticResourcesRecorder(RuntimeValue httpConf this.httpBuildTimeConfig = httpBuildTimeConfig; } - public Handler createHandler(Set generatedResources) { + public Handler createHandler(Set generatedClasspathResources, + Map generatedFilesResources) { if (httpBuildTimeConfig.enableCompression && httpBuildTimeConfig.compressMediaTypes.isPresent()) { this.compressMediaTypes = Set.copyOf(httpBuildTimeConfig.compressMediaTypes.get()); @@ -38,7 +40,8 @@ public Handler createHandler(Set generatedResources) { .enableCompression(httpBuildTimeConfig.enableCompression) .compressMediaTypes(compressMediaTypes) .defaultEncoding(config.contentEncoding).build(); - return new DevClasspathStaticHandler(generatedResources, + return new DevStaticHandler(generatedClasspathResources, + generatedFilesResources, options); } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/handlers/DevClasspathStaticHandler.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/handlers/DevStaticHandler.java similarity index 81% rename from extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/handlers/DevClasspathStaticHandler.java rename to extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/handlers/DevStaticHandler.java index fda217224b8d8..08e0a8826a3ed 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/handlers/DevClasspathStaticHandler.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/handlers/DevStaticHandler.java @@ -4,6 +4,7 @@ import java.io.InputStream; import java.net.URL; import java.nio.charset.Charset; +import java.util.Map; import java.util.Set; import org.jboss.logging.Logger; @@ -21,22 +22,25 @@ * {@link StaticHandler} implementation to handle static resources using the Classpath. * This is meant to be used on {@link io.quarkus.runtime.LaunchMode#DEVELOPMENT} mode. */ -public class DevClasspathStaticHandler implements Handler { +public class DevStaticHandler implements Handler { - private static final Logger LOG = Logger.getLogger(DevClasspathStaticHandler.class); + private static final Logger LOG = Logger.getLogger(DevStaticHandler.class); private static final int HTTP_STATUS_OK = 200; private static final int HTTP_STATUS_NO_CONTENT = 204; private static final String ALLOW_HEADER = "Allow"; private static final String ALLOW_HEADER_VALUE = "HEAD,GET,OPTIONS"; - private final Set generatedResources; + private final Set generatedClasspathResources; + private final Map generatedFilesResources; private final Set compressedMediaTypes; private final ClassLoader currentClassLoader; private final boolean enableCompression; private final String indexPage; private final Charset defaultEncoding; - public DevClasspathStaticHandler(Set generatedResources, DevClasspathStaticHandlerOptions options) { - this.generatedResources = generatedResources; + public DevStaticHandler(Set generatedClasspathResources, Map generatedFilesResources, + DevClasspathStaticHandlerOptions options) { + this.generatedClasspathResources = generatedClasspathResources; + this.generatedFilesResources = generatedFilesResources; this.compressedMediaTypes = options.getCompressMediaTypes(); this.currentClassLoader = Thread.currentThread().getContextClassLoader(); this.enableCompression = options.isEnableCompression(); @@ -54,7 +58,8 @@ public void handle(RoutingContext context) { LOG.debugf("Handling request for path '%s'", path); } - boolean containsGeneratedResource = this.generatedResources.contains(path); + boolean containsGeneratedResource = this.generatedClasspathResources.contains(path) + || this.generatedFilesResources.containsKey(path); if (!containsGeneratedResource) { beforeNextHandler(this.currentClassLoader, context); @@ -69,6 +74,17 @@ public void handle(RoutingContext context) { compressIfNeeded(context, path); + if (generatedFilesResources.containsKey(path)) { + context.vertx().fileSystem().readFile(generatedFilesResources.get(path), r -> { + if (r.succeeded()) { + handleAsyncResultSucceeded(context, r.result(), path); + } else { + context.fail(r.cause()); + } + }); + return; + } + context.vertx().executeBlocking(future -> { try { byte[] content = getClasspathResourceContent(path); @@ -79,14 +95,14 @@ public void handle(RoutingContext context) { }, asyncResult -> { if (asyncResult.succeeded()) { byte[] result = (byte[]) asyncResult.result(); - handleAsyncResultSucceeded(context, result, path); + handleAsyncResultSucceeded(context, result == null ? null : Buffer.buffer(result), path); } else { context.fail(asyncResult.cause()); } }); } - private void handleAsyncResultSucceeded(RoutingContext context, byte[] result, String path) { + private void handleAsyncResultSucceeded(RoutingContext context, Buffer result, String path) { if (result == null) { LOG.warnf("The '%s' file does not contain any content. Proceeding to the next handler if it exists", path); @@ -104,12 +120,12 @@ private void handleAsyncResultSucceeded(RoutingContext context, byte[] result, S if (context.request().method().equals(HttpMethod.HEAD)) { handleHeadMethod(context, result); } else { - context.response().send(Buffer.buffer(result)); + context.response().send(result); } } - private void handleHeadMethod(RoutingContext context, byte[] content) { - context.response().putHeader(HttpHeaders.CONTENT_LENGTH, Long.toString(content.length)); + private void handleHeadMethod(RoutingContext context, Buffer content) { + context.response().putHeader(HttpHeaders.CONTENT_LENGTH, Long.toString(content.length())); context.response().setStatusCode(HTTP_STATUS_OK).end(); }