Skip to content

Commit

Permalink
Merge pull request #41066 from ia3andy/add-generated-file-support
Browse files Browse the repository at this point in the history
Add support for Path in GeneratedStaticResourceBuildItem
  • Loading branch information
ia3andy authored Jun 10, 2024
2 parents 37942d4 + 4da6a17 commit 797b3ef
Show file tree
Hide file tree
Showing 10 changed files with 317 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <br>
* 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.
* <br>
* 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.
* <br>
* 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -25,77 +26,110 @@
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<GeneratedStaticResourceBuildItem> generatedStaticResources,
BuildProducer<GeneratedResourceBuildItem> generatedResourceBuildItem,
BuildProducer<NativeImageResourceBuildItem> nativeImageResourcesProducer,
LaunchModeBuildItem launchModeBuildItem,
BuildProducer<AdditionalStaticResourceBuildItem> 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<GeneratedStaticResourceBuildItem> generatedStaticResources,
LaunchModeBuildItem launchModeBuildItem,
BuildProducer<RouteBuildItem> routes, GeneratedStaticResourcesRecorder generatedStaticResourcesRecorder,
BuildProducer<NotFoundPageDisplayableEndpointBuildItem> notFoundPageProducer) {
if (!launchModeBuildItem.getLaunchMode().isDevOrTest() || generatedStaticResources.isEmpty()) {
if (generatedStaticResources.isEmpty()) {
return;
}
Set<String> paths = generatedStaticResources.stream().map(GeneratedStaticResourceBuildItem::getPath)
.peek(path -> notFoundPageProducer.produce(new NotFoundPageDisplayableEndpointBuildItem(path)))
Map<String, String> generatedFilesResources = generatedStaticResources.stream()
.peek(path -> notFoundPageProducer.produce(new NotFoundPageDisplayableEndpointBuildItem(path.getEndpoint())))
.filter(GeneratedStaticResourceBuildItem::isFile)
.collect(Collectors.toMap(GeneratedStaticResourceBuildItem::getEndpoint,
GeneratedStaticResourceBuildItem::getFileAbsolutePath));
Set<String> 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<HotDeploymentWatchedFileBuildItem> hotDeployment,
BuildProducer<GeneratedStaticResourceBuildItem> generatedStaticResourceProducer) throws IOException {
BuildProducer<GeneratedStaticResourceBuildItem> 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<String, Path> classpathResources = getClasspathResources();
for (Map.Entry<String, Path> entries : classpathResources.entrySet()) {
byte[] bytes = Files.readAllBytes(entries.getValue());
generatedStaticResourceProducer.produce(new GeneratedStaticResourceBuildItem(
"/" + entries.getKey(), bytes));
for (Map.Entry<String, Path> 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);
}
}

Expand All @@ -104,20 +138,20 @@ private Map<String, Path> 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());
}
});
return knownPaths;
}

private static void visitRuntimeMetaInfResources(PathVisitor visitor) {
final List<ClassPathElement> elements = QuarkusClassLoader.getElements(META_INF_GENERATED_RESOURCES,
final List<ClassPathElement> 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 -> {
Expand All @@ -128,10 +162,4 @@ private static void visitRuntimeMetaInfResources(PathVisitor visitor) {
}
}
}

private static String buildGeneratedStaticResourceLocation(
GeneratedStaticResourceBuildItem generatedStaticResourceBuildItem) {
return META_INF_RESOURCES +
generatedStaticResourceBuildItem.getPath();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,9 @@ void collectStaticResources(Capabilities capabilities,
return;
}
Set<StaticResourcesBuildItem.Entry> 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()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading

0 comments on commit 797b3ef

Please sign in to comment.