diff options
Diffstat (limited to 'opendc-web/opendc-web-quarkus-deployment/src/main/java/org')
6 files changed, 489 insertions, 0 deletions
diff --git a/opendc-web/opendc-web-quarkus-deployment/src/main/java/org/opendc/web/quarkus/deployment/runner/OpenDCRunnerBuildItem.java b/opendc-web/opendc-web-quarkus-deployment/src/main/java/org/opendc/web/quarkus/deployment/runner/OpenDCRunnerBuildItem.java new file mode 100644 index 00000000..544365c8 --- /dev/null +++ b/opendc-web/opendc-web-quarkus-deployment/src/main/java/org/opendc/web/quarkus/deployment/runner/OpenDCRunnerBuildItem.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.quarkus.deployment.runner; + +import io.quarkus.builder.item.SimpleBuildItem; +import io.quarkus.runtime.RuntimeValue; +import org.opendc.web.runner.OpenDCRunner; + +/** + * A {@link SimpleBuildItem} that produces an {@link OpenDCRunner} instance. + */ +public final class OpenDCRunnerBuildItem extends SimpleBuildItem { + private final RuntimeValue<OpenDCRunner> runner; + + public OpenDCRunnerBuildItem(RuntimeValue<OpenDCRunner> runner) { + this.runner = runner; + } + + public RuntimeValue<OpenDCRunner> getRunner() { + return runner; + } +} diff --git a/opendc-web/opendc-web-quarkus-deployment/src/main/java/org/opendc/web/quarkus/deployment/runner/OpenDCRunnerConfig.java b/opendc-web/opendc-web-quarkus-deployment/src/main/java/org/opendc/web/quarkus/deployment/runner/OpenDCRunnerConfig.java new file mode 100644 index 00000000..1bc201b2 --- /dev/null +++ b/opendc-web/opendc-web-quarkus-deployment/src/main/java/org/opendc/web/quarkus/deployment/runner/OpenDCRunnerConfig.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.quarkus.deployment.runner; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigRoot; + +/** + * Build-time configuration for the OpenDC web runner extension. + */ +@ConfigRoot(name = "opendc-runner") +public class OpenDCRunnerConfig { + /** + * A flag to include the OpenDC web runner extension into the build. + */ + @ConfigItem(defaultValue = "true") + boolean include; +} diff --git a/opendc-web/opendc-web-quarkus-deployment/src/main/java/org/opendc/web/quarkus/deployment/runner/OpenDCRunnerProcessor.java b/opendc-web/opendc-web-quarkus-deployment/src/main/java/org/opendc/web/quarkus/deployment/runner/OpenDCRunnerProcessor.java new file mode 100644 index 00000000..a796c01d --- /dev/null +++ b/opendc-web/opendc-web-quarkus-deployment/src/main/java/org/opendc/web/quarkus/deployment/runner/OpenDCRunnerProcessor.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2023 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.quarkus.deployment.runner; + +import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT; + +import io.quarkus.arc.deployment.UnremovableBeanBuildItem; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.*; +import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; +import io.quarkus.deployment.util.ServiceUtil; +import io.quarkus.runtime.RuntimeValue; +import java.io.IOException; +import java.util.Set; +import java.util.function.BooleanSupplier; +import org.opendc.trace.spi.TraceFormat; +import org.opendc.web.quarkus.runtime.runner.OpenDCRunnerRecorder; +import org.opendc.web.quarkus.runtime.runner.OpenDCRunnerRuntimeConfig; +import org.opendc.web.runner.JobManager; +import org.opendc.web.runner.OpenDCRunner; + +/** + * Build processor for the OpenDC web runner Quarkus extension. + */ +public class OpenDCRunnerProcessor { + + private static final String FEATURE = "opendc-runner"; + + /** + * Provide the {@link FeatureBuildItem} for this Quarkus extension. + */ + @BuildStep(onlyIf = IsIncluded.class) + public FeatureBuildItem feature() { + return new FeatureBuildItem(FEATURE); + } + + /** + * Build step to register the trace formats used by OpenDC. + */ + @BuildStep + void registerTraceFormats(BuildProducer<ServiceProviderBuildItem> services) throws IOException { + String service = "META-INF/services/" + TraceFormat.class.getName(); + + Set<String> implementations = + ServiceUtil.classNamesNamedIn(Thread.currentThread().getContextClassLoader(), service); + + services.produce( + new ServiceProviderBuildItem(TraceFormat.class.getName(), implementations.toArray(new String[0]))); + } + + /** + * Mark {@link JobManager} as unremoveable, since we look up this service dynamically in {@link OpenDCRunnerRecorder}. + */ + @BuildStep + UnremovableBeanBuildItem unremovableBeans() { + return UnremovableBeanBuildItem.beanTypes(JobManager.class); + } + + /** + * Build step to create the runner service. + */ + @BuildStep(onlyIf = IsIncluded.class) + @Record(RUNTIME_INIT) + ServiceStartBuildItem createRunnerService( + OpenDCRunnerRecorder recorder, + OpenDCRunnerRuntimeConfig config, + BuildProducer<OpenDCRunnerBuildItem> runnerBuildItem) { + RuntimeValue<OpenDCRunner> runner = recorder.createRunner(config); + runnerBuildItem.produce(new OpenDCRunnerBuildItem(runner)); + return new ServiceStartBuildItem("OpenDCRunnerService"); + } + + /** + * Build step to start the runner service. + */ + @BuildStep(onlyIf = IsIncluded.class) + @Record(RUNTIME_INIT) + void startRunnerService( + ApplicationStartBuildItem start, + OpenDCRunnerBuildItem runnerBuildItem, + OpenDCRunnerRecorder recorder, + OpenDCRunnerRuntimeConfig config, + ShutdownContextBuildItem shutdownContextBuildItem) { + recorder.startRunner(runnerBuildItem.getRunner(), config, shutdownContextBuildItem); + } + + /** + * A {@link BooleanSupplier} to determine if the OpenDC web runner extension should be included. + */ + private static class IsIncluded implements BooleanSupplier { + OpenDCRunnerConfig config; + + @Override + public boolean getAsBoolean() { + return config.include; + } + } +} diff --git a/opendc-web/opendc-web-quarkus-deployment/src/main/java/org/opendc/web/quarkus/deployment/ui/NextRouteManifestBuildItem.java b/opendc-web/opendc-web-quarkus-deployment/src/main/java/org/opendc/web/quarkus/deployment/ui/NextRouteManifestBuildItem.java new file mode 100644 index 00000000..4640b04b --- /dev/null +++ b/opendc-web/opendc-web-quarkus-deployment/src/main/java/org/opendc/web/quarkus/deployment/ui/NextRouteManifestBuildItem.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2023 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.quarkus.deployment.ui; + +import io.quarkus.builder.item.SimpleBuildItem; +import java.util.List; + +/** + * Build item containing the route manifest of the Next.js application. + */ +public final class NextRouteManifestBuildItem extends SimpleBuildItem { + + private final boolean custom404; + private final List<Page> pages; + private final List<Redirect> redirects; + + /** + * Construct a {@link NextRouteManifestBuildItem} object. + * + * @param routes The routes defined by Next.js. + * @param redirects The redirects that have been defined by Next.js. + * @param custom404 A flag to indicate that custom 404 pages are enabled. + */ + public NextRouteManifestBuildItem(List<Page> routes, List<Redirect> redirects, boolean custom404) { + this.custom404 = custom404; + this.pages = routes; + this.redirects = redirects; + } + + public List<Page> getPages() { + return pages; + } + + public List<Redirect> getRedirects() { + return redirects; + } + + public boolean hasCustom404() { + return this.custom404; + } + + /** + * A redirect defined by the Next.js routes manifest. + * + * @param path The path that should result in a redirect. + * @param destination The destination of the redirect. + * @param statusCode The status code of the redirect. + */ + public record Redirect(String path, String destination, int statusCode) {} + + /** + * A page defined by the Next.js routes manifest. + * + * @param path The path that to the page. + * @param name The name of the page. + */ + public record Page(String path, String name) {} +} diff --git a/opendc-web/opendc-web-quarkus-deployment/src/main/java/org/opendc/web/quarkus/deployment/ui/OpenDCUiProcessor.java b/opendc-web/opendc-web-quarkus-deployment/src/main/java/org/opendc/web/quarkus/deployment/ui/OpenDCUiProcessor.java new file mode 100644 index 00000000..8b2c0244 --- /dev/null +++ b/opendc-web/opendc-web-quarkus-deployment/src/main/java/org/opendc/web/quarkus/deployment/ui/OpenDCUiProcessor.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.quarkus.deployment.ui; + +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.vertx.http.deployment.HttpRootPathBuildItem; +import io.quarkus.vertx.http.deployment.RouteBuildItem; +import org.opendc.web.quarkus.runtime.ui.OpenDCUiConfig; +import org.opendc.web.quarkus.runtime.ui.OpenDCUiRecorder; + +/** + * Quarkus build processor for the OpenDC web UI. + */ +public class OpenDCUiProcessor { + /** + * Register a route handler for serving the config of the OpenDC web UI. + */ + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + public RouteBuildItem registerConfigHandler( + OpenDCUiRecorder openDCUiRecorder, + BuildProducer<RouteBuildItem> routes, + HttpRootPathBuildItem httpRootPathBuildItem, + OpenDCUiConfig openDCUiConfig) { + + String basePath = httpRootPathBuildItem.getRootPath(); + + return httpRootPathBuildItem + .routeBuilder() + .route(basePath + "/__ENV.js") + .handler(openDCUiRecorder.configHandler(openDCUiConfig)) + .build(); + } +} diff --git a/opendc-web/opendc-web-quarkus-deployment/src/main/java/org/opendc/web/quarkus/deployment/ui/QuinoaNextRoutingProcessor.java b/opendc-web/opendc-web-quarkus-deployment/src/main/java/org/opendc/web/quarkus/deployment/ui/QuinoaNextRoutingProcessor.java new file mode 100644 index 00000000..ddaa8809 --- /dev/null +++ b/opendc-web/opendc-web-quarkus-deployment/src/main/java/org/opendc/web/quarkus/deployment/ui/QuinoaNextRoutingProcessor.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2023 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.quarkus.deployment.ui; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.quarkiverse.quinoa.deployment.items.BuiltResourcesBuildItem; +import io.quarkiverse.quinoa.deployment.items.ConfiguredQuinoaBuildItem; +import io.quarkiverse.quinoa.deployment.items.TargetDirBuildItem; +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.vertx.http.deployment.HttpRootPathBuildItem; +import io.quarkus.vertx.http.deployment.RouteBuildItem; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.regex.Pattern; +import org.opendc.web.quarkus.runtime.ui.QuinoaNextRoutingRecorder; + +/** + * Quarkus build processor for enabling dynamic routes and redirects in Quinoa for Next.js applications. + */ +public class QuinoaNextRoutingProcessor { + private static final Pattern PATH_PARAM_PATTERN = Pattern.compile("\\[(\\w+)]"); + private final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * Build the dynamic routes of the application based on the route manifest generated by Next.js. + * + * @param configuredQuinoa Quinoa configuration pointing to the UI directory. + * @param targetDirBuildItem Dependency on the build directory to ensure the Next.js is built before this build step + * is run. + * @return Routing manifest generated by the Next.js application. + */ + @BuildStep + public NextRouteManifestBuildItem buildRoutes( + ConfiguredQuinoaBuildItem configuredQuinoa, TargetDirBuildItem targetDirBuildItem) throws IOException { + if (configuredQuinoa == null) { + return null; + } + + Path routeManifestPath = configuredQuinoa.uiDir().resolve(".next/routes-manifest.json"); + if (Files.notExists(routeManifestPath)) { + throw new FileNotFoundException("Cannot find " + routeManifestPath + " for creating route map"); + } + + JsonNode routeManifest = objectMapper.readTree(routeManifestPath.toFile()); + + var pages = new ArrayList<NextRouteManifestBuildItem.Page>(); + for (Iterator<JsonNode> it = routeManifest.get("staticRoutes").elements(); it.hasNext(); ) { + JsonNode route = it.next(); + + String page = route.get("page").asText(); + + // Static routes do not have any path parameters + pages.add(new NextRouteManifestBuildItem.Page(page, page)); + } + + for (Iterator<JsonNode> it = routeManifest.get("dynamicRoutes").elements(); it.hasNext(); ) { + JsonNode route = it.next(); + + String page = route.get("page").asText(); + String path = PATH_PARAM_PATTERN.matcher(page).replaceAll(r -> ":" + r.group(1)); + + pages.add(new NextRouteManifestBuildItem.Page(path, page)); + } + + var redirects = new ArrayList<NextRouteManifestBuildItem.Redirect>(); + for (Iterator<JsonNode> it = routeManifest.get("redirects").elements(); it.hasNext(); ) { + JsonNode redirect = it.next(); + if (redirect.has("internal")) { + continue; + } + + int statusCode = redirect.get("statusCode").asInt(); + String path = redirect.get("source").asText(); + String destination = redirect.get("destination").asText(); + + if (path.isEmpty()) { + path = "/"; + } + + redirects.add(new NextRouteManifestBuildItem.Redirect(path, destination, statusCode)); + } + + var custom404 = routeManifest.get("pages404").asBoolean(); + return new NextRouteManifestBuildItem(pages, redirects, custom404); + } + + /** + * Register the dynamic routes and redirects of the Next.js application. + */ + @Record(ExecutionTime.RUNTIME_INIT) + @BuildStep(onlyIf = IsNormal.class) + public void registerNextRoutes( + QuinoaNextRoutingRecorder recorder, + BuildProducer<RouteBuildItem> routes, + HttpRootPathBuildItem httpRootPathBuildItem, + BuiltResourcesBuildItem uiResources, + NextRouteManifestBuildItem routeManifestBuildItem) { + + if (uiResources.getNames().isEmpty()) { + return; + } + + String basePath = httpRootPathBuildItem.getRootPath(); + + /* Construct redirects */ + for (var redirect : routeManifestBuildItem.getRedirects()) { + String destination = basePath.equals("/") ? redirect.destination() : basePath + redirect.destination(); + + routes.produce(httpRootPathBuildItem + .routeBuilder() + .route(basePath + redirect.path()) + .handler(recorder.redirectHandler(destination, redirect.statusCode())) + .build()); + } + + /* Construct dynamic routes */ + for (var page : routeManifestBuildItem.getPages()) { + routes.produce(httpRootPathBuildItem + .routeBuilder() + .route(basePath + page.path()) + .handler(recorder.pageHandler(basePath, page.name())) + .build()); + } + } +} |
