diff --git a/core/deployment/src/main/java/io/quarkus/deployment/util/WebJarUtil.java b/core/deployment/src/main/java/io/quarkus/deployment/util/WebJarUtil.java index 43a98436f25c4..71b62733e55aa 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/util/WebJarUtil.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/util/WebJarUtil.java @@ -44,7 +44,10 @@ /** * Utility for Web resource related operations + * + * @deprecated Use WebJarBuildItem and WebJarResultBuildItem instead. */ +@Deprecated(forRemoval = true) public class WebJarUtil { private static final Logger LOG = Logger.getLogger(WebJarUtil.class); diff --git a/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java b/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java index 8c059f1dfd94e..55c4f9741bf4b 100644 --- a/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java +++ b/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java @@ -2,6 +2,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.Collection; @@ -11,6 +12,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Scanner; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -33,22 +35,17 @@ import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; -import io.quarkus.deployment.builditem.GeneratedResourceBuildItem; import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem; import io.quarkus.deployment.builditem.IndexDependencyBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; -import io.quarkus.deployment.builditem.LiveReloadBuildItem; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; import io.quarkus.deployment.builditem.SystemPropertyBuildItem; import io.quarkus.deployment.builditem.TransformedClassesBuildItem; -import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyBuildItem; import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; import io.quarkus.deployment.metrics.MetricsCapabilityBuildItem; -import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; -import io.quarkus.deployment.util.WebJarUtil; -import io.quarkus.maven.dependency.ResolvedDependency; +import io.quarkus.maven.dependency.GACT; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.configuration.ConfigurationException; @@ -60,7 +57,9 @@ import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem; import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; import io.quarkus.vertx.http.deployment.RouteBuildItem; -import io.quarkus.vertx.http.deployment.devmode.NotFoundPageDisplayableEndpointBuildItem; +import io.quarkus.vertx.http.deployment.webjar.WebJarBuildItem; +import io.quarkus.vertx.http.deployment.webjar.WebJarResourcesFilter; +import io.quarkus.vertx.http.deployment.webjar.WebJarResultBuildItem; import io.smallrye.graphql.api.Entry; import io.smallrye.graphql.cdi.config.ConfigKey; import io.smallrye.graphql.cdi.config.MicroProfileConfig; @@ -95,9 +94,9 @@ public class SmallRyeGraphQLProcessor { private static final String FALSE = "false"; // For the UI - private static final String GRAPHQL_UI_WEBJAR_GROUP_ID = "io.smallrye"; - private static final String GRAPHQL_UI_WEBJAR_ARTIFACT_ID = "smallrye-graphql-ui-graphiql"; - private static final String GRAPHQL_UI_WEBJAR_PREFIX = "META-INF/resources/graphql-ui/"; + private static final GACT GRAPHQL_UI_WEBJAR_ARTIFACT_KEY = new GACT("io.smallrye", "smallrye-graphql-ui-graphiql", null, + "jar"); + private static final String GRAPHQL_UI_WEBJAR_STATIC_RESOURCES_PATH = "META-INF/resources/graphql-ui/"; private static final String GRAPHQL_UI_FINAL_DESTINATION = "META-INF/graphql-ui-files"; private static final String FILE_TO_UPDATE = "render.js"; private static final String LINE_TO_UPDATE = "const api = '"; @@ -520,16 +519,11 @@ private boolean shouldActivateService(Optional serviceEnabled, @BuildStep void getGraphqlUiFinalDestination( - BuildProducer generatedResourceProducer, - BuildProducer nativeImageResourceProducer, - BuildProducer notFoundPageDisplayableEndpointProducer, - BuildProducer smallRyeGraphQLBuildProducer, HttpRootPathBuildItem httpRootPath, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, - CurateOutcomeBuildItem curateOutcomeBuildItem, LaunchModeBuildItem launchMode, SmallRyeGraphQLConfig graphQLConfig, - LiveReloadBuildItem liveReloadBuildItem) throws Exception { + BuildProducer webJarBuildProducer) { if (shouldInclude(launchMode, graphQLConfig)) { @@ -542,58 +536,28 @@ void getGraphqlUiFinalDestination( String graphQLPath = httpRootPath.resolvePath(graphQLConfig.rootPath); String graphQLUiPath = nonApplicationRootPathBuildItem.resolvePath(graphQLConfig.ui.rootPath); - ResolvedDependency artifact = WebJarUtil.getAppArtifact(curateOutcomeBuildItem, GRAPHQL_UI_WEBJAR_GROUP_ID, - GRAPHQL_UI_WEBJAR_ARTIFACT_ID); - if (launchMode.getLaunchMode().isDevOrTest()) { - Path tempPath = WebJarUtil.copyResourcesForDevOrTest(liveReloadBuildItem, curateOutcomeBuildItem, launchMode, - artifact, - GRAPHQL_UI_WEBJAR_PREFIX); - WebJarUtil.updateUrl(tempPath.resolve(FILE_TO_UPDATE), graphQLPath, LINE_TO_UPDATE, LINE_FORMAT); - WebJarUtil.updateUrl(tempPath.resolve(FILE_TO_UPDATE), graphQLUiPath, - UI_LINE_TO_UPDATE, UI_LINE_FORMAT); - WebJarUtil.updateUrl(tempPath.resolve(FILE_TO_UPDATE), nonApplicationRootPathBuildItem.resolvePath("dev"), - LOGO_LINE_TO_UPDATE, LOGO_LINE_FORMAT); - - smallRyeGraphQLBuildProducer - .produce(new SmallRyeGraphQLBuildItem(tempPath.toAbsolutePath().toString(), graphQLUiPath)); - - // Handle live reload of branding files - if (liveReloadBuildItem.isLiveReload() && !liveReloadBuildItem.getChangedResources().isEmpty()) { - WebJarUtil.hotReloadBrandingChanges(curateOutcomeBuildItem, launchMode, artifact, - liveReloadBuildItem.getChangedResources()); - } - } else { - Map files = WebJarUtil.copyResourcesForProduction(curateOutcomeBuildItem, artifact, - GRAPHQL_UI_WEBJAR_PREFIX); - - for (Map.Entry file : files.entrySet()) { - - String fileName = file.getKey(); - byte[] content = file.getValue(); - if (fileName.endsWith(FILE_TO_UPDATE)) { - content = WebJarUtil - .updateUrl(new String(content, StandardCharsets.UTF_8), graphQLPath, LINE_TO_UPDATE, - LINE_FORMAT) - .getBytes(StandardCharsets.UTF_8); - content = WebJarUtil - .updateUrl(new String(content, StandardCharsets.UTF_8), graphQLUiPath, - UI_LINE_TO_UPDATE, - UI_LINE_FORMAT) - .getBytes(StandardCharsets.UTF_8); - content = WebJarUtil - .updateUrl(new String(content, StandardCharsets.UTF_8), graphQLUiPath, - LOGO_LINE_TO_UPDATE, - LOGO_LINE_FORMAT) - .getBytes(StandardCharsets.UTF_8); - } - fileName = GRAPHQL_UI_FINAL_DESTINATION + "/" + fileName; - - generatedResourceProducer.produce(new GeneratedResourceBuildItem(fileName, content)); - nativeImageResourceProducer.produce(new NativeImageResourceBuildItem(fileName)); - } - - smallRyeGraphQLBuildProducer.produce(new SmallRyeGraphQLBuildItem(GRAPHQL_UI_FINAL_DESTINATION, graphQLUiPath)); - } + webJarBuildProducer + .produce(new WebJarBuildItem(GRAPHQL_UI_WEBJAR_ARTIFACT_KEY, GRAPHQL_UI_WEBJAR_STATIC_RESOURCES_PATH, + GRAPHQL_UI_FINAL_DESTINATION, new WebJarResourcesFilter() { + @Override + public InputStream apply(String fileName, InputStream file) throws IOException { + if (fileName.endsWith(FILE_TO_UPDATE)) { + String content = new String(file.readAllBytes(), StandardCharsets.UTF_8); + content = updateUrl(content, graphQLPath, LINE_TO_UPDATE, + LINE_FORMAT); + content = updateUrl(content, graphQLUiPath, + UI_LINE_TO_UPDATE, + UI_LINE_FORMAT); + content = updateUrl(content, graphQLUiPath, + LOGO_LINE_TO_UPDATE, + LOGO_LINE_FORMAT); + + return new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); + } + + return file; + } + })); } } @@ -603,14 +567,24 @@ void registerGraphQLUiHandler( BuildProducer routeProducer, SmallRyeGraphQLRecorder recorder, SmallRyeGraphQLRuntimeConfig runtimeConfig, - SmallRyeGraphQLBuildItem smallRyeGraphQLBuildItem, LaunchModeBuildItem launchMode, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, - SmallRyeGraphQLConfig graphQLConfig) throws Exception { + SmallRyeGraphQLConfig graphQLConfig, + WebJarResultBuildItem webJarResultBuildItem, + BuildProducer smallRyeGraphQLBuildProducer) { + + WebJarResultBuildItem.WebJarResult result = webJarResultBuildItem.byArtifactKey(GRAPHQL_UI_WEBJAR_ARTIFACT_KEY); + if (result == null) { + return; + } if (shouldInclude(launchMode, graphQLConfig)) { - Handler handler = recorder.uiHandler(smallRyeGraphQLBuildItem.getGraphqlUiFinalDestination(), - smallRyeGraphQLBuildItem.getGraphqlUiPath(), runtimeConfig); + String graphQLUiPath = nonApplicationRootPathBuildItem.resolvePath(graphQLConfig.ui.rootPath); + smallRyeGraphQLBuildProducer + .produce(new SmallRyeGraphQLBuildItem(result.getFinalDestination(), graphQLUiPath)); + + Handler handler = recorder.uiHandler(result.getFinalDestination(), + graphQLUiPath, runtimeConfig); routeProducer.produce(nonApplicationRootPathBuildItem.routeBuilder() .route(graphQLConfig.ui.rootPath) .displayOnNotFoundPage("GraphQL UI") @@ -629,4 +603,18 @@ void registerGraphQLUiHandler( private static boolean shouldInclude(LaunchModeBuildItem launchMode, SmallRyeGraphQLConfig graphQLConfig) { return launchMode.getLaunchMode().isDevOrTest() || graphQLConfig.ui.alwaysInclude; } + + private String updateUrl(String original, String path, String lineStartsWith, String format) { + try (Scanner scanner = new Scanner(original)) { + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + if (line.trim().startsWith(lineStartsWith)) { + String newLine = String.format(format, path); + return original.replace(line.trim(), newLine); + } + } + } + + return original; + } } 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 92845bea66f9b..49c29c405811d 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 @@ -3,15 +3,14 @@ import static io.quarkus.arc.processor.Annotations.containsAny; import static io.quarkus.arc.processor.Annotations.getAnnotations; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.function.BooleanSupplier; import java.util.stream.Collectors; @@ -46,21 +45,15 @@ import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.FeatureBuildItem; -import io.quarkus.deployment.builditem.GeneratedResourceBuildItem; import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; -import io.quarkus.deployment.builditem.LiveReloadBuildItem; import io.quarkus.deployment.builditem.ShutdownListenerBuildItem; import io.quarkus.deployment.builditem.SystemPropertyBuildItem; -import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; -import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; import io.quarkus.deployment.util.ServiceUtil; -import io.quarkus.deployment.util.WebJarUtil; import io.quarkus.kubernetes.spi.KubernetesHealthLivenessPathBuildItem; import io.quarkus.kubernetes.spi.KubernetesHealthReadinessPathBuildItem; import io.quarkus.kubernetes.spi.KubernetesHealthStartupPathBuildItem; -import io.quarkus.maven.dependency.ResolvedDependency; -import io.quarkus.runtime.LaunchMode; +import io.quarkus.maven.dependency.GACT; import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.smallrye.health.deployment.spi.HealthBuildItem; import io.quarkus.smallrye.health.runtime.QuarkusAsyncHealthCheckFactory; @@ -77,6 +70,9 @@ import io.quarkus.smallrye.openapi.deployment.spi.AddToOpenAPIDefinitionBuildItem; import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; import io.quarkus.vertx.http.deployment.RouteBuildItem; +import io.quarkus.vertx.http.deployment.webjar.WebJarBuildItem; +import io.quarkus.vertx.http.deployment.webjar.WebJarResourcesFilter; +import io.quarkus.vertx.http.deployment.webjar.WebJarResultBuildItem; import io.smallrye.health.SmallRyeHealthReporter; import io.smallrye.health.api.HealthGroup; import io.smallrye.health.api.HealthGroups; @@ -96,9 +92,8 @@ class SmallRyeHealthProcessor { private static final DotName JAX_RS_PATH = DotName.createSimple("javax.ws.rs.Path"); // For the UI - private static final String HEALTH_UI_WEBJAR_GROUP_ID = "io.smallrye"; - private static final String HEALTH_UI_WEBJAR_ARTIFACT_ID = "smallrye-health-ui"; - private static final String HEALTH_UI_WEBJAR_PREFIX = "META-INF/resources/health-ui/"; + private static final GACT HEALTH_UI_WEBJAR_ARTIFACT_KEY = new GACT("io.smallrye", "smallrye-health-ui", null, "jar"); + private static final String HEALTH_UI_WEBJAR_STATIC_RESOURCES_PATH = "META-INF/resources/health-ui/"; private static final String HEALTH_UI_FINAL_DESTINATION = "META-INF/health-ui-files"; private static final String JS_FILE_TO_UPDATE = "healthui.js"; private static final String INDEX_FILE_TO_UPDATE = "index.html"; @@ -408,14 +403,10 @@ public void transform(TransformationContext ctx) { // UI @BuildStep void registerUiExtension( - BuildProducer generatedResourceProducer, - BuildProducer nativeImageResourceProducer, - BuildProducer smallRyeHealthBuildProducer, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, SmallRyeHealthConfig healthConfig, - CurateOutcomeBuildItem curateOutcomeBuildItem, LaunchModeBuildItem launchModeBuildItem, - LiveReloadBuildItem liveReloadBuildItem) throws Exception { + BuildProducer webJarBuildProducer) { if (shouldInclude(launchModeBuildItem, healthConfig)) { @@ -426,49 +417,24 @@ void registerUiExtension( } String healthPath = nonApplicationRootPathBuildItem.resolvePath(healthConfig.rootPath); - String healthUiPath = nonApplicationRootPathBuildItem.resolvePath(healthConfig.ui.rootPath); - - ResolvedDependency artifact = WebJarUtil.getAppArtifact(curateOutcomeBuildItem, HEALTH_UI_WEBJAR_GROUP_ID, - HEALTH_UI_WEBJAR_ARTIFACT_ID); - - if (launchModeBuildItem.getLaunchMode().isDevOrTest()) { - Path tempPath = WebJarUtil.copyResourcesForDevOrTest(liveReloadBuildItem, curateOutcomeBuildItem, - launchModeBuildItem, - artifact, - HEALTH_UI_WEBJAR_PREFIX); - if (launchModeBuildItem.getLaunchMode().equals(LaunchMode.DEVELOPMENT)) { - updateApiUrl(tempPath.resolve(JS_FILE_TO_UPDATE), healthPath); - updateApiUrl(tempPath.resolve(INDEX_FILE_TO_UPDATE), healthPath); - } - - smallRyeHealthBuildProducer - .produce(new SmallRyeHealthBuildItem(tempPath.toAbsolutePath().toString(), healthUiPath)); - - // Handle live reload of branding files - if (liveReloadBuildItem.isLiveReload() && !liveReloadBuildItem.getChangedResources().isEmpty()) { - WebJarUtil.hotReloadBrandingChanges(curateOutcomeBuildItem, launchModeBuildItem, artifact, - liveReloadBuildItem.getChangedResources()); - } - } else { - Map files = WebJarUtil.copyResourcesForProduction(curateOutcomeBuildItem, artifact, - HEALTH_UI_WEBJAR_PREFIX); - - for (Map.Entry file : files.entrySet()) { - - String fileName = file.getKey(); - byte[] content = file.getValue(); - if (fileName.endsWith(JS_FILE_TO_UPDATE) || fileName.endsWith(INDEX_FILE_TO_UPDATE)) { - content = updateApiUrl(new String(content, StandardCharsets.UTF_8), healthPath) - .getBytes(StandardCharsets.UTF_8); - } - fileName = HEALTH_UI_FINAL_DESTINATION + "/" + fileName; - - generatedResourceProducer.produce(new GeneratedResourceBuildItem(fileName, content)); - nativeImageResourceProducer.produce(new NativeImageResourceBuildItem(fileName)); - } - smallRyeHealthBuildProducer.produce(new SmallRyeHealthBuildItem(HEALTH_UI_FINAL_DESTINATION, healthUiPath)); - } + webJarBuildProducer + .produce(new WebJarBuildItem(HEALTH_UI_WEBJAR_ARTIFACT_KEY, HEALTH_UI_WEBJAR_STATIC_RESOURCES_PATH, + HEALTH_UI_FINAL_DESTINATION, new WebJarResourcesFilter() { + @Override + public InputStream apply(String fileName, InputStream file) throws IOException { + if (fileName.endsWith(JS_FILE_TO_UPDATE) || fileName.endsWith(INDEX_FILE_TO_UPDATE)) { + byte[] content = SmallRyeHealthProcessor.this + .updateApiUrl(new String(file.readAllBytes(), StandardCharsets.UTF_8), + healthPath) + .getBytes(StandardCharsets.UTF_8); + + return new ByteArrayInputStream(content); + } + + return file; + } + })); } } @@ -478,14 +444,24 @@ void registerHealthUiHandler( BuildProducer routeProducer, SmallRyeHealthRecorder recorder, SmallRyeHealthRuntimeConfig runtimeConfig, - SmallRyeHealthBuildItem smallRyeHealthBuildItem, + WebJarResultBuildItem webJarResultBuildItem, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, LaunchModeBuildItem launchMode, - SmallRyeHealthConfig healthConfig) { + SmallRyeHealthConfig healthConfig, + BuildProducer smallryeHealthBuildProducer) { + + WebJarResultBuildItem.WebJarResult result = webJarResultBuildItem.byArtifactKey(HEALTH_UI_WEBJAR_ARTIFACT_KEY); + if (result == null) { + return; + } if (shouldInclude(launchMode, healthConfig)) { - Handler handler = recorder.uiHandler(smallRyeHealthBuildItem.getHealthUiFinalDestination(), - smallRyeHealthBuildItem.getHealthUiPath(), runtimeConfig); + String healthUiPath = nonApplicationRootPathBuildItem.resolvePath(healthConfig.ui.rootPath); + smallryeHealthBuildProducer + .produce(new SmallRyeHealthBuildItem(result.getFinalDestination(), healthUiPath)); + + Handler handler = recorder.uiHandler(result.getFinalDestination(), + healthUiPath, runtimeConfig); routeProducer.produce(nonApplicationRootPathBuildItem.routeBuilder() .route(healthConfig.ui.rootPath) .displayOnNotFoundPage("Health UI") @@ -497,15 +473,6 @@ void registerHealthUiHandler( .route(healthConfig.ui.rootPath + "*") .handler(handler) .build()); - - } - } - - private void updateApiUrl(Path fileToUpdate, String healthPath) throws IOException { - String content = Files.readString(fileToUpdate); - String result = updateApiUrl(content, healthPath); - if (result != null) { - Files.write(fileToUpdate, result.getBytes(StandardCharsets.UTF_8)); } } 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 e666e8a611c25..5a1fd6928d041 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 @@ -1,7 +1,8 @@ package io.quarkus.swaggerui.deployment; +import java.io.ByteArrayInputStream; import java.io.IOException; -import java.nio.file.Path; +import java.io.InputStream; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -23,20 +24,18 @@ import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.DevServicesLauncherConfigResultBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; -import io.quarkus.deployment.builditem.GeneratedResourceBuildItem; import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; -import io.quarkus.deployment.builditem.LiveReloadBuildItem; -import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; -import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; -import io.quarkus.deployment.util.WebJarUtil; -import io.quarkus.maven.dependency.ResolvedDependency; +import io.quarkus.maven.dependency.GACT; import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.smallrye.openapi.common.deployment.SmallRyeOpenApiConfig; import io.quarkus.swaggerui.runtime.SwaggerUiRecorder; import io.quarkus.swaggerui.runtime.SwaggerUiRuntimeConfig; import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; import io.quarkus.vertx.http.deployment.RouteBuildItem; +import io.quarkus.vertx.http.deployment.webjar.WebJarBuildItem; +import io.quarkus.vertx.http.deployment.webjar.WebJarResourcesFilter; +import io.quarkus.vertx.http.deployment.webjar.WebJarResultBuildItem; import io.smallrye.openapi.ui.IndexHtmlCreator; import io.smallrye.openapi.ui.Option; import io.smallrye.openapi.ui.ThemeHref; @@ -46,9 +45,8 @@ public class SwaggerUiProcessor { private static final Logger LOG = Logger.getLogger(SwaggerUiProcessor.class); - private static final String SWAGGER_UI_WEBJAR_GROUP_ID = "io.smallrye"; - private static final String SWAGGER_UI_WEBJAR_ARTIFACT_ID = "smallrye-open-api-ui"; - private static final String SWAGGER_UI_WEBJAR_PREFIX = "META-INF/resources/openapi-ui/"; + private static final GACT SWAGGER_UI_WEBJAR_ARTIFACT_KEY = new GACT("io.smallrye", "smallrye-open-api-ui", null, "jar"); + private static final String SWAGGER_UI_WEBJAR_STATIC_RESOURCES_PATH = "META-INF/resources/openapi-ui/"; private static final String SWAGGER_UI_FINAL_DESTINATION = "META-INF/swagger-ui-files"; // Branding files to monitor for changes @@ -87,16 +85,12 @@ List brandingFiles() { @BuildStep public void getSwaggerUiFinalDestination( - BuildProducer generatedResources, - BuildProducer nativeImageResourceBuildItemBuildProducer, - BuildProducer swaggerUiBuildProducer, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, - CurateOutcomeBuildItem curateOutcomeBuildItem, LaunchModeBuildItem launchMode, SwaggerUiConfig swaggerUiConfig, SmallRyeOpenApiConfig openapi, - LiveReloadBuildItem liveReloadBuildItem, - Optional devServicesLauncherConfig) throws Exception { + Optional devServicesLauncherConfig, + BuildProducer webJarBuildProducer) throws Exception { if (shouldInclude(launchMode, swaggerUiConfig)) { if ("/".equals(swaggerUiConfig.path)) { @@ -127,52 +121,34 @@ public void getSwaggerUiFinalDestination( String openApiPath = nonApplicationRootPathBuildItem.resolvePath(openapi.path); String swaggerUiPath = nonApplicationRootPathBuildItem.resolvePath(swaggerUiConfig.path); + ThemeHref theme = swaggerUiConfig.theme.orElse(ThemeHref.feeling_blue); - ResolvedDependency artifact = WebJarUtil.getAppArtifact(curateOutcomeBuildItem, SWAGGER_UI_WEBJAR_GROUP_ID, - SWAGGER_UI_WEBJAR_ARTIFACT_ID); + NonApplicationRootPathBuildItem indexRootPathBuildItem = null; if (launchMode.getLaunchMode().isDevOrTest()) { + indexRootPathBuildItem = nonApplicationRootPathBuildItem; // In dev mode, default to persist Authorization true if (!swaggerUiConfig.persistAuthorization.isPresent()) { swaggerUiConfig.persistAuthorization = Optional.of(true); } - - Path tempPath = WebJarUtil.copyResourcesForDevOrTest(liveReloadBuildItem, curateOutcomeBuildItem, launchMode, - artifact, - SWAGGER_UI_WEBJAR_PREFIX); - // Update index.html - WebJarUtil.updateFile(tempPath.resolve("index.html"), - generateIndexHtml(openApiPath, swaggerUiPath, swaggerUiConfig, nonApplicationRootPathBuildItem)); - - swaggerUiBuildProducer.produce(new SwaggerUiBuildItem(tempPath.toAbsolutePath().toString(), swaggerUiPath)); - - // Handle live reload of branding files - if (liveReloadBuildItem.isLiveReload() && !liveReloadBuildItem.getChangedResources().isEmpty()) { - WebJarUtil.hotReloadBrandingChanges(curateOutcomeBuildItem, launchMode, artifact, - liveReloadBuildItem.getChangedResources()); - } - } else { - Map files = WebJarUtil.copyResourcesForProduction(curateOutcomeBuildItem, artifact, - SWAGGER_UI_WEBJAR_PREFIX); - ThemeHref theme = swaggerUiConfig.theme.orElse(ThemeHref.feeling_blue); - for (Map.Entry file : files.entrySet()) { - String fileName = file.getKey(); - // Make sure to only include the selected theme - if (fileName.equals(theme.toString()) || !fileName.startsWith("theme-")) { - byte[] content; - if (fileName.endsWith("index.html")) { - content = generateIndexHtml(openApiPath, swaggerUiPath, swaggerUiConfig, null); - } else { - content = file.getValue(); - } - fileName = SWAGGER_UI_FINAL_DESTINATION + "/" + fileName; - generatedResources.produce(new GeneratedResourceBuildItem(fileName, content)); - nativeImageResourceBuildItemBuildProducer.produce(new NativeImageResourceBuildItem(fileName)); - } - } - swaggerUiBuildProducer.produce(new SwaggerUiBuildItem(SWAGGER_UI_FINAL_DESTINATION, swaggerUiPath)); } + + byte[] indexHtmlContent = generateIndexHtml(openApiPath, swaggerUiPath, swaggerUiConfig, indexRootPathBuildItem); + webJarBuildProducer + .produce(new WebJarBuildItem(SWAGGER_UI_WEBJAR_ARTIFACT_KEY, SWAGGER_UI_WEBJAR_STATIC_RESOURCES_PATH, + SWAGGER_UI_FINAL_DESTINATION, new WebJarResourcesFilter() { + @Override + public InputStream apply(String fileName, InputStream file) throws IOException { + if (!fileName.equals(theme.toString()) && fileName.startsWith("theme-")) { + return null; + } + if (fileName.endsWith("index.html")) { + return new ByteArrayInputStream(indexHtmlContent); + } + return file; + } + })); } } @@ -181,14 +157,23 @@ public void getSwaggerUiFinalDestination( public void registerSwaggerUiHandler(SwaggerUiRecorder recorder, BuildProducer routes, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, - SwaggerUiBuildItem finalDestinationBuildItem, + WebJarResultBuildItem webJarResultBuildItem, SwaggerUiRuntimeConfig runtimeConfig, LaunchModeBuildItem launchMode, - SwaggerUiConfig swaggerUiConfig) throws Exception { + SwaggerUiConfig swaggerUiConfig, + BuildProducer swaggerUiBuildProducer) { + + WebJarResultBuildItem.WebJarResult result = webJarResultBuildItem.byArtifactKey(SWAGGER_UI_WEBJAR_ARTIFACT_KEY); + if (result == null) { + return; + } if (shouldInclude(launchMode, swaggerUiConfig)) { - Handler handler = recorder.handler(finalDestinationBuildItem.getSwaggerUiFinalDestination(), - finalDestinationBuildItem.getSwaggerUiPath(), + String swaggerUiPath = nonApplicationRootPathBuildItem.resolvePath(swaggerUiConfig.path); + swaggerUiBuildProducer.produce(new SwaggerUiBuildItem(result.getFinalDestination(), swaggerUiPath)); + + Handler handler = recorder.handler(result.getFinalDestination(), + swaggerUiPath, runtimeConfig); routes.produce(nonApplicationRootPathBuildItem.routeBuilder() diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java index c967fc6ab043c..23e34b3541f7e 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java @@ -48,7 +48,6 @@ import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.ConfigDescriptionBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; -import io.quarkus.deployment.builditem.LiveReloadBuildItem; import io.quarkus.deployment.builditem.ServiceStartBuildItem; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; import io.quarkus.deployment.builditem.WebSocketLogHandlerBuildItem; @@ -60,13 +59,12 @@ import io.quarkus.deployment.pkg.builditem.BuildSystemTargetBuildItem; import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; import io.quarkus.deployment.util.ArtifactInfoUtil; -import io.quarkus.deployment.util.WebJarUtil; import io.quarkus.dev.console.DevConsoleManager; import io.quarkus.dev.spi.DevModeType; import io.quarkus.devconsole.spi.DevConsoleRouteBuildItem; import io.quarkus.devconsole.spi.DevConsoleRuntimeTemplateInfoBuildItem; import io.quarkus.devconsole.spi.DevConsoleTemplateInfoBuildItem; -import io.quarkus.maven.dependency.ResolvedDependency; +import io.quarkus.maven.dependency.GACT; import io.quarkus.netty.runtime.virtual.VirtualChannel; import io.quarkus.netty.runtime.virtual.VirtualServerChannel; import io.quarkus.qute.Engine; @@ -93,6 +91,8 @@ import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem; import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; import io.quarkus.vertx.http.deployment.RouteBuildItem; +import io.quarkus.vertx.http.deployment.webjar.WebJarBuildItem; +import io.quarkus.vertx.http.deployment.webjar.WebJarResultBuildItem; import io.quarkus.vertx.http.runtime.devmode.DevConsoleFilter; import io.quarkus.vertx.http.runtime.devmode.DevConsoleRecorder; import io.quarkus.vertx.http.runtime.devmode.FileSystemStaticHandler; @@ -123,7 +123,10 @@ public class DevConsoleProcessor { private static final Logger log = Logger.getLogger(DevConsoleProcessor.class); - private static final String STATIC_RESOURCES_PATH = "dev-static/"; + private static final GACT DEVCONSOLE_WEBJAR_ARTIFACT_KEY = new GACT("io.quarkus", "quarkus-vertx-http-deployment", null, + "jar"); + private static final String DEVCONSOLE_WEBJAR_STATIC_RESOURCES_PATH = "dev-static/"; + private static final String DEVCONSOLE_FINAL_DESTINATION = "META-INF/dev-ui-files"; private static final Object EMPTY = new Object(); // FIXME: config, take from Qute? @@ -379,6 +382,16 @@ public ServiceStartBuildItem setupDeploymentSideHandling(List routes, - CurateOutcomeBuildItem curateOutcomeBuildItem, HistoryHandlerBuildItem historyHandlerBuildItem, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, - LaunchModeBuildItem launchModeBuildItem, ShutdownContextBuildItem shutdownContext, BuildProducer routeBuildItemBuildProducer, - LiveReloadBuildItem liveReloadBuildItem) throws IOException { - if (launchModeBuildItem.getDevModeType().orElse(null) != DevModeType.LOCAL) { - return; - } + WebJarResultBuildItem webJarResultBuildItem, + CurateOutcomeBuildItem curateOutcomeBuildItem) { - // Add the static resources - ResolvedDependency devConsoleResourcesArtifact = WebJarUtil.getAppArtifact(curateOutcomeBuildItem, "io.quarkus", - "quarkus-vertx-http-deployment"); + WebJarResultBuildItem.WebJarResult result = webJarResultBuildItem.byArtifactKey(DEVCONSOLE_WEBJAR_ARTIFACT_KEY); - Path devConsoleStaticResourcesDeploymentPath = WebJarUtil.copyResourcesForDevOrTest(liveReloadBuildItem, - curateOutcomeBuildItem, - launchModeBuildItem, - devConsoleResourcesArtifact, STATIC_RESOURCES_PATH, true, true); + if (result == null) { + return; + } List webRootConfigurations = new ArrayList<>(); webRootConfigurations.add( - new FileSystemStaticHandler.StaticWebRootConfiguration(devConsoleStaticResourcesDeploymentPath.toString(), "")); - for (Path resolvedPath : devConsoleResourcesArtifact.getResolvedPaths()) { + new FileSystemStaticHandler.StaticWebRootConfiguration(result.getFinalDestination(), + "")); + for (Path resolvedPath : result.getDependency().getResolvedPaths()) { webRootConfigurations .add(new FileSystemStaticHandler.StaticWebRootConfiguration(resolvedPath.toString(), - STATIC_RESOURCES_PATH)); + DEVCONSOLE_WEBJAR_STATIC_RESOURCES_PATH)); } routeBuildItemBuildProducer.produce(nonApplicationRootPathBuildItem.routeBuilder() .route("dev/resources/*") .handler(recorder.fileSystemStaticHandler( - webRootConfigurations, devConsoleStaticResourcesDeploymentPath.toString(), shutdownContext)) + webRootConfigurations, shutdownContext)) .build()); // Add the log stream diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/webjar/InMemoryTargetVisitor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/webjar/InMemoryTargetVisitor.java new file mode 100644 index 0000000000000..777a702460073 --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/webjar/InMemoryTargetVisitor.java @@ -0,0 +1,22 @@ +package io.quarkus.vertx.http.deployment.webjar; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +/** + * Visitor which holds all web jar resources in memory. + */ +public class InMemoryTargetVisitor implements WebJarResourcesTargetVisitor { + private Map content = new HashMap<>(); + + public Map getContent() { + return content; + } + + @Override + public void visitFile(String path, InputStream stream) throws IOException { + content.put(path, stream.readAllBytes()); + } +} \ No newline at end of file diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/webjar/PathTargetVisitor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/webjar/PathTargetVisitor.java new file mode 100644 index 0000000000000..0b70e4196426a --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/webjar/PathTargetVisitor.java @@ -0,0 +1,42 @@ +package io.quarkus.vertx.http.deployment.webjar; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Visitor which copies resources of the web jar to a given path. + */ +public class PathTargetVisitor implements WebJarResourcesTargetVisitor { + private final Path deploymentPath; + + public PathTargetVisitor(Path deploymentPath) { + this.deploymentPath = deploymentPath; + } + + @Override + public void visitDirectory(String path) throws IOException { + Files.createDirectories(deploymentPath.resolve(path)); + } + + @Override + public void visitFile(String path, InputStream stream) throws IOException { + Path targetFilePath = deploymentPath.resolve(path); + createFile(stream, targetFilePath); + } + + private static void createFile(InputStream source, Path targetFile) throws IOException { + try (FileOutputStream fos = new FileOutputStream(targetFile.toString())) { + FileChannel channel = fos.getChannel(); + try (FileLock lock = channel.tryLock()) { + if (lock != null) { + source.transferTo(fos); + } + } + } + } +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/webjar/WebJarBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/webjar/WebJarBuildItem.java new file mode 100644 index 0000000000000..05bd59b59d30f --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/webjar/WebJarBuildItem.java @@ -0,0 +1,76 @@ +package io.quarkus.vertx.http.deployment.webjar; + +import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.maven.dependency.ArtifactKey; +import io.quarkus.maven.dependency.GACT; + +/** + * BuildItem for deploying a webjar. + */ +public final class WebJarBuildItem extends MultiBuildItem { + /** + * ArtifactKey pointing to the web jar. Has to be one of the applications dependencies. + */ + private final GACT artifactKey; + + /** + * Root inside the webJar starting from which resources are unpacked. + */ + private final String root; + + /** + * Only copy resources of the webjar which are either user overridden, or contain variables. + */ + private final boolean onlyCopyNonArtifactFiles; + + /** + * Defines whether quarkus can override resources of the webjar with quarkus internal files. + */ + private final boolean useDefaultQuarkusBranding; + + /** + * Path where the webjar content should be unpacked to. For dev and test mode, the files while be unpacked to a temp + * directory, with this path as parent. In Prod Mode, the files will be available as generated resources inside this path. + */ + private final String finalDestination; + + private final WebJarResourcesFilter filter; + + public WebJarBuildItem(GACT artifactKey, String webjarRoot, String finalDestination, WebJarResourcesFilter filter) { + this(artifactKey, webjarRoot, true, false, finalDestination, filter); + } + + public WebJarBuildItem(GACT webJarKey, String webjarRoot, boolean useDefaultQuarkusBranding, + boolean onlyCopyNonArtifactFiles, String finalDestination, WebJarResourcesFilter filter) { + this.artifactKey = webJarKey; + this.root = webjarRoot; + this.useDefaultQuarkusBranding = useDefaultQuarkusBranding; + this.onlyCopyNonArtifactFiles = onlyCopyNonArtifactFiles; + this.finalDestination = finalDestination; + this.filter = filter; + } + + public GACT getArtifactKey() { + return artifactKey; + } + + public String getRoot() { + return root; + } + + public boolean getUseDefaultQuarkusBranding() { + return useDefaultQuarkusBranding; + } + + public boolean getOnlyCopyNonArtifactFiles() { + return onlyCopyNonArtifactFiles; + } + + public String getFinalDestination() { + return finalDestination; + } + + public WebJarResourcesFilter getFilter() { + return filter; + } +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/webjar/WebJarProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/webjar/WebJarProcessor.java new file mode 100644 index 0000000000000..003be1d3fe53b --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/webjar/WebJarProcessor.java @@ -0,0 +1,82 @@ +package io.quarkus.vertx.http.deployment.webjar; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.quarkus.deployment.IsNormal; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.GeneratedResourceBuildItem; +import io.quarkus.deployment.builditem.ShutdownContextBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; +import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; +import io.quarkus.maven.dependency.ArtifactKey; +import io.quarkus.maven.dependency.ResolvedDependency; +import io.quarkus.runtime.ApplicationConfig; +import io.quarkus.vertx.http.runtime.webjar.WebJarRecorder; + +public class WebJarProcessor { + @Record(ExecutionTime.RUNTIME_INIT) + @BuildStep(onlyIfNot = IsNormal.class) + WebJarResultBuildItem processWebJarDevMode(WebJarRecorder recorder, List webJars, + CurateOutcomeBuildItem curateOutcomeBuildItem, + ShutdownContextBuildItem shutdownContext, + ApplicationConfig applicationConfig) throws IOException { + + Map results = new HashMap<>(); + + Path deploymentBasePath = Files.createTempDirectory("quarkus-webjar"); + recorder.shutdownTask(shutdownContext, deploymentBasePath.toString()); + + for (WebJarBuildItem webJar : webJars) { + Path resourcesDirectory = deploymentBasePath.resolve(webJar.getFinalDestination()); + ResolvedDependency dependency = WebJarUtil.getAppArtifact(curateOutcomeBuildItem, webJar.getArtifactKey()); + + Path staticResourcesPath = WebJarUtil.copyResourcesForDevOrTest(curateOutcomeBuildItem, applicationConfig, webJar, + dependency, resourcesDirectory); + + results.put(webJar.getArtifactKey(), + new WebJarResultBuildItem.WebJarResult(dependency, staticResourcesPath.toAbsolutePath().toString())); + } + + return new WebJarResultBuildItem(results); + } + + @BuildStep(onlyIf = IsNormal.class) + WebJarResultBuildItem processWebJarProdMode(List webJars, + CurateOutcomeBuildItem curateOutcomeBuildItem, + BuildProducer generatedResources, + BuildProducer nativeImageResourceBuildItemBuildProducer, + ApplicationConfig applicationConfig) throws IOException { + + Map results = new HashMap<>(); + + for (WebJarBuildItem webJar : webJars) { + + ResolvedDependency dependency = WebJarUtil.getAppArtifact(curateOutcomeBuildItem, webJar.getArtifactKey()); + + Map files = WebJarUtil.copyResourcesForProduction( + curateOutcomeBuildItem, applicationConfig, webJar, dependency); + + for (Map.Entry file : files.entrySet()) { + String fileName = webJar.getFinalDestination() + "/" + file.getKey(); + byte[] fileContent = file.getValue(); + + generatedResources + .produce(new GeneratedResourceBuildItem(fileName, fileContent)); + nativeImageResourceBuildItemBuildProducer.produce(new NativeImageResourceBuildItem(fileName)); + } + + results.put(webJar.getArtifactKey(), + new WebJarResultBuildItem.WebJarResult(dependency, webJar.getFinalDestination())); + } + + return new WebJarResultBuildItem(results); + } +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/webjar/WebJarResourcesFilter.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/webjar/WebJarResourcesFilter.java new file mode 100644 index 0000000000000..4b0e02310ed89 --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/webjar/WebJarResourcesFilter.java @@ -0,0 +1,18 @@ +package io.quarkus.vertx.http.deployment.webjar; + +import java.io.IOException; +import java.io.InputStream; + +@FunctionalInterface +public interface WebJarResourcesFilter { + + /** + * Filter web jar resources. Can either update or not update the content of a web jars resource, or not include a resource + * at all. + * + * @param fileName path and name of the resource inside the webjar, starting from the webJarRoot. + * @param stream current resource content + * @return stream with the resource content, or null if this resource should be ignored. + */ + InputStream apply(String fileName, InputStream stream) throws IOException; +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/webjar/WebJarResourcesTargetVisitor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/webjar/WebJarResourcesTargetVisitor.java new file mode 100644 index 0000000000000..0076f0a659262 --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/webjar/WebJarResourcesTargetVisitor.java @@ -0,0 +1,26 @@ +package io.quarkus.vertx.http.deployment.webjar; + +import java.io.IOException; +import java.io.InputStream; + +public interface WebJarResourcesTargetVisitor { + default void visitDirectory(String path) throws IOException { + } + + default void visitFile(String path, InputStream stream) throws IOException { + + } + + default void filterAndVisitFile(String path, InputStream stream, WebJarResourcesFilter filter) + throws IOException { + if (filter == null) { + visitFile(path, stream); + return; + } + + stream = filter.apply(path, stream); + if (stream != null) { + visitFile(path, stream); + } + } +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/webjar/WebJarResultBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/webjar/WebJarResultBuildItem.java new file mode 100644 index 0000000000000..fa746107d9d28 --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/webjar/WebJarResultBuildItem.java @@ -0,0 +1,46 @@ +package io.quarkus.vertx.http.deployment.webjar; + +import java.nio.file.Path; +import java.util.Map; + +import io.quarkus.builder.item.SimpleBuildItem; +import io.quarkus.maven.dependency.ArtifactKey; +import io.quarkus.maven.dependency.ResolvedDependency; + +public final class WebJarResultBuildItem extends SimpleBuildItem { + private final Map results; + + public WebJarResultBuildItem(Map results) { + this.results = results; + } + + public WebJarResult byArtifactKey(ArtifactKey artifactKey) { + return results.get(artifactKey); + } + + public static class WebJarResult { + /** + * Resolved dependency of the webjar + */ + private ResolvedDependency dependency; + + /** + * Path to either the created directory on disk (dev and test), or the same value as + * {@link WebJarBuildItem#getFinalDestination()} (prod mode) + */ + private String finalDestination; + + public WebJarResult(ResolvedDependency dependency, String finalDestination) { + this.dependency = dependency; + this.finalDestination = finalDestination; + } + + public ResolvedDependency getDependency() { + return dependency; + } + + public String getFinalDestination() { + return finalDestination; + } + } +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/webjar/WebJarUtil.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/webjar/WebJarUtil.java new file mode 100644 index 0000000000000..0f0a3d7314974 --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/webjar/WebJarUtil.java @@ -0,0 +1,283 @@ +package io.quarkus.vertx.http.deployment.webjar; + +import java.io.ByteArrayInputStream; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import org.jboss.logging.Logger; + +import io.quarkus.builder.Version; +import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; +import io.quarkus.maven.dependency.GACT; +import io.quarkus.maven.dependency.ResolvedDependency; +import io.quarkus.paths.PathTree; +import io.quarkus.paths.PathVisit; +import io.quarkus.runtime.ApplicationConfig; + +/** + * Utility for Web resource related operations + */ +public class WebJarUtil { + + private static final Logger LOG = Logger.getLogger(WebJarUtil.class); + + private static final String CUSTOM_MEDIA_FOLDER = "META-INF/branding/"; + private static final List OVERRIDABLE_RESOURCES = Arrays.asList("logo.png", "favicon.ico", "style.css"); + private static final String CSS = ".css"; + + private WebJarUtil() { + } + + static Path copyResourcesForDevOrTest(CurateOutcomeBuildItem curateOutcomeBuildItem, ApplicationConfig config, + WebJarBuildItem webJar, + ResolvedDependency resourcesArtifact, + Path deploymentBasePath) + throws IOException { + + Path deploymentPath = Files.createDirectories(deploymentBasePath); + + PathTargetVisitor visitor = new PathTargetVisitor(deploymentPath); + copyResources(curateOutcomeBuildItem, config, webJar, resourcesArtifact, visitor); + + return deploymentPath; + } + + static Map copyResourcesForProduction(CurateOutcomeBuildItem curateOutcomeBuildItem, + ApplicationConfig config, WebJarBuildItem webJar, + ResolvedDependency resourcesArtifact) + throws IOException { + + InMemoryTargetVisitor visitor = new InMemoryTargetVisitor(); + copyResources(curateOutcomeBuildItem, config, webJar, resourcesArtifact, visitor); + + return visitor.getContent(); + } + + private static void copyResources(CurateOutcomeBuildItem curateOutcomeBuildItem, ApplicationConfig config, + WebJarBuildItem webJar, + ResolvedDependency resourcesArtifact, WebJarResourcesTargetVisitor visitor) + throws IOException { + final ResolvedDependency userApplication = curateOutcomeBuildItem.getApplicationModel().getAppArtifact(); + + ClassLoader classLoader = WebJarUtil.class.getClassLoader(); + + resourcesArtifact.getContentTree().accept(webJar.getRoot(), new Consumer() { + @Override + public void accept(PathVisit pathVisit) { + if (pathVisit == null || !Files.isDirectory(pathVisit.getPath())) { + return; + } + + try { + Files.walkFileTree(pathVisit.getPath(), + new ResourcesFileVisitor(visitor, pathVisit.getPath(), resourcesArtifact, + userApplication, config, classLoader, webJar)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + }); + } + + static ResolvedDependency getAppArtifact(CurateOutcomeBuildItem curateOutcomeBuildItem, GACT artifactKey) { + for (ResolvedDependency dep : curateOutcomeBuildItem.getApplicationModel().getDependencies()) { + if (dep.getKey().equals(artifactKey)) { + return dep; + } + } + throw new RuntimeException("Could not find artifact " + artifactKey + + " among the application dependencies"); + } + + private static String getModuleOverrideName(ResolvedDependency artifact, String filename) { + String type = filename.substring(filename.lastIndexOf(".")); + return artifact.getArtifactId() + type; + } + + private static InputStream getOverride(ResolvedDependency userApplication, ClassLoader classLoader, String filename, + String moduleName, + boolean useDefaultQuarkusBranding) { + + // First check if the developer supplied the files + InputStream overrideStream = getCustomOverride(userApplication, filename, moduleName); + if (overrideStream == null && useDefaultQuarkusBranding) { + // Else check if Quarkus has a default branding + overrideStream = getQuarkusOverride(classLoader, filename, moduleName); + } + return overrideStream; + } + + private static InsertVariableResult insertVariablesWithResult(ResolvedDependency appArtifact, ApplicationConfig config, + InputStream is, + String filename) + throws IOException { + if (is == null) { + return new InsertVariableResult(null, false); + } + + // Allow replacement of certain values in css + if (filename.endsWith(CSS)) { + String applicationName = config.name + .orElse(appArtifact.getArtifactId()); + + String applicationVersion = config.version + .orElse(appArtifact.getVersion()); + + byte[] oldContentBytes = is.readAllBytes(); + String oldContents = new String(oldContentBytes); + String contents = replaceHeaderVars(oldContents, applicationName, applicationVersion); + + String header = replaceHeaderVars(config.uiHeader.orElse(""), applicationName, applicationVersion); + contents = contents.replace("{applicationHeader}", header); + + boolean changed = contents.length() != oldContents.length() || !contents.equals(oldContents); + if (changed) { + return new InsertVariableResult(new ByteArrayInputStream(contents.getBytes()), true); + } else { + return new InsertVariableResult(new ByteArrayInputStream(oldContentBytes), false); + } + } + + return new InsertVariableResult(is, false); + } + + private static String replaceHeaderVars(String contents, String applicationName, String applicationVersion) { + contents = contents.replace("{applicationName}", applicationName); + contents = contents.replace("{applicationVersion}", applicationVersion); + contents = contents.replace("{quarkusVersion}", Version.getVersion()); + return contents; + } + + private static InputStream getCustomOverride(ResolvedDependency userApplication, String filename, String moduleName) { + // Check if the developer supplied the files + byte[] content = readFromPathTree(userApplication.getContentTree(), CUSTOM_MEDIA_FOLDER + moduleName); + if (content != null) { + return new ByteArrayInputStream(content); + } + + content = readFromPathTree(userApplication.getContentTree(), CUSTOM_MEDIA_FOLDER + filename); + if (content != null) { + return new ByteArrayInputStream(content); + } + + return null; + } + + private static byte[] readFromPathTree(PathTree tree, String relativePath) { + return tree.apply(relativePath, (visit) -> { + if (visit == null) { + return null; + } + + try { + return Files.readAllBytes(visit.getPath()); + } catch (IOException e) { + LOG.error("Could not read file content " + visit.getPath(), e); + } + return null; + }); + } + + private static InputStream getQuarkusOverride(ClassLoader classLoader, String filename, String moduleName) { + // Allow quarkus per module override + InputStream stream = classLoader.getResourceAsStream(CUSTOM_MEDIA_FOLDER + moduleName); + if (stream != null) { + return stream; + } + + return classLoader.getResourceAsStream(CUSTOM_MEDIA_FOLDER + filename); + } + + private static class InsertVariableResult implements Closeable { + final InputStream inputStream; + final boolean changed; + + InsertVariableResult(InputStream inputStream, boolean changed) { + this.inputStream = inputStream; + this.changed = changed; + } + + @Override + public void close() throws IOException { + if (inputStream != null) { + inputStream.close(); + } + } + } + + private static class ResourcesFileVisitor extends SimpleFileVisitor { + private final WebJarResourcesTargetVisitor visitor; + private final Path rootFolderToCopy; + private final ResolvedDependency resourcesArtifact; + private final ResolvedDependency userApplication; + private final ApplicationConfig config; + private final ClassLoader classLoader; + private final WebJarBuildItem webJar; + + public ResourcesFileVisitor(WebJarResourcesTargetVisitor visitor, Path rootFolderToCopy, + ResolvedDependency resourcesArtifact, ResolvedDependency userApplication, ApplicationConfig config, + ClassLoader classLoader, WebJarBuildItem webJar) { + this.visitor = visitor; + this.rootFolderToCopy = rootFolderToCopy; + this.resourcesArtifact = resourcesArtifact; + this.userApplication = userApplication; + this.config = config; + this.classLoader = classLoader; + this.webJar = webJar; + } + + @Override + public FileVisitResult preVisitDirectory(final Path dir, + final BasicFileAttributes attrs) throws IOException { + visitor.visitDirectory(rootFolderToCopy.relativize(dir).toString()); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(final Path file, + final BasicFileAttributes attrs) throws IOException { + String fileName = rootFolderToCopy.relativize(file).toString(); + + String moduleName = getModuleOverrideName(resourcesArtifact, fileName); + boolean overrideFileCreated = false; + if (OVERRIDABLE_RESOURCES.contains(fileName)) { + try (InsertVariableResult insertVariableResult = insertVariablesWithResult(userApplication, + config, + getOverride(userApplication, classLoader, + fileName, moduleName, webJar.getUseDefaultQuarkusBranding()), + fileName)) { + if (insertVariableResult.inputStream != null) { + overrideFileCreated = true; + // Override (either developer supplied or Quarkus) + visitor.filterAndVisitFile(fileName, insertVariableResult.inputStream, + webJar.getFilter()); + } + } + } + + if (!overrideFileCreated) { + try (InsertVariableResult insertVariableResult = insertVariablesWithResult(userApplication, + config, + Files.newInputStream(file), fileName)) { + if (!webJar.getOnlyCopyNonArtifactFiles() || insertVariableResult.changed) { + visitor.filterAndVisitFile(fileName, insertVariableResult.inputStream, + webJar.getFilter()); + } + } + } + + return FileVisitResult.CONTINUE; + } + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/DevConsoleRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/DevConsoleRecorder.java index c4605e54e81b1..8d89b99540370 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/DevConsoleRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/DevConsoleRecorder.java @@ -1,12 +1,5 @@ package io.quarkus.vertx.http.runtime.devmode; -import java.io.IOException; -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.HashMap; import java.util.List; @@ -16,7 +9,6 @@ import java.util.function.Supplier; import org.eclipse.microprofile.config.ConfigProvider; -import org.jboss.logging.Logger; import io.quarkus.dev.console.DevConsoleManager; import io.quarkus.dev.testing.ContinuousTestingSharedStateManager; @@ -29,8 +21,6 @@ @Recorder public class DevConsoleRecorder { - private static final Logger LOG = Logger.getLogger(DevConsoleRecorder.class); - public void addInfo(String groupId, String artifactId, String name, Supplier supplier) { Map> info = DevConsoleManager.getTemplateInfo(); Map data = info.computeIfAbsent(groupId + "." + artifactId, @@ -58,7 +48,7 @@ public Optional apply(String name) { * @param devConsoleFinalDestination * @param shutdownContext * @return - * @deprecated use {@link #fileSystemStaticHandler(List, String, ShutdownContext)} + * @deprecated use {@link #fileSystemStaticHandler(List, ShutdownContext)} */ @Deprecated public Handler devConsoleHandler(String devConsoleFinalDestination, @@ -67,17 +57,15 @@ public Handler devConsoleHandler(String devConsoleFinalDestinati webRootConfigurations.add( new FileSystemStaticHandler.StaticWebRootConfiguration(devConsoleFinalDestination, "")); - return fileSystemStaticHandler(webRootConfigurations, devConsoleFinalDestination, shutdownContext); + return fileSystemStaticHandler(webRootConfigurations, shutdownContext); } public Handler fileSystemStaticHandler( List webRootConfigurations, - String devConsoleFinalDestination, ShutdownContext shutdownContext) { FileSystemStaticHandler fileSystemStaticHandler = new FileSystemStaticHandler(webRootConfigurations); - shutdownContext.addShutdownTask(new CleanupDevConsoleTempDirectory(devConsoleFinalDestination)); shutdownContext.addShutdownTask(new ShutdownContext.CloseRunnable(fileSystemStaticHandler)); return fileSystemStaticHandler; @@ -96,35 +84,4 @@ public void run() { }); return handler; } - - private static final class CleanupDevConsoleTempDirectory implements Runnable { - - private final Path devConsoleFinalDestination; - - private CleanupDevConsoleTempDirectory(String devConsoleFinalDestination) { - this.devConsoleFinalDestination = Paths.get(devConsoleFinalDestination); - } - - @Override - public void run() { - try { - Files.walkFileTree(devConsoleFinalDestination, - new SimpleFileVisitor() { - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { - Files.delete(dir); - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - Files.delete(file); - return FileVisitResult.CONTINUE; - } - }); - } catch (IOException e) { - LOG.error("Error cleaning up DEV Console temporary directory: " + devConsoleFinalDestination, e); - } - } - } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/webjar/WebJarRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/webjar/WebJarRecorder.java new file mode 100644 index 0000000000000..4c25971ac0c43 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/webjar/WebJarRecorder.java @@ -0,0 +1,56 @@ +package io.quarkus.vertx.http.runtime.webjar; + +import java.io.IOException; +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 org.jboss.logging.Logger; + +import io.quarkus.runtime.ShutdownContext; +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class WebJarRecorder { + + private static final Logger LOG = Logger.getLogger(WebJarRecorder.class); + + public void shutdownTask(ShutdownContext shutdownContext, String deploymentBasePath) { + + shutdownContext.addShutdownTask(new DeleteDirectoryRunnable(deploymentBasePath)); + } + + private static final class DeleteDirectoryRunnable implements Runnable { + + private final Path directory; + + private DeleteDirectoryRunnable(String directory) { + this.directory = Paths.get(directory); + } + + @Override + public void run() { + try { + Files.walkFileTree(directory, + new SimpleFileVisitor() { + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + LOG.error("Error cleaning up webjar temporary directory: " + directory, e); + } + } + } +}