summaryrefslogtreecommitdiff
path: root/opendc-web/opendc-web-ui-quarkus
diff options
context:
space:
mode:
authorFabian Mastenbroek <mail.fabianm@gmail.com>2022-03-18 13:36:32 +0100
committerFabian Mastenbroek <mail.fabianm@gmail.com>2022-04-05 18:54:52 +0200
commitc69035732ef073257cb56f7c9edfdbe25a47aa44 (patch)
tree0e1533944f8e48712772bd518d3ffb187db9a309 /opendc-web/opendc-web-ui-quarkus
parent6cf097b60366ecb2116f742be4374fcec841a950 (diff)
feat(web/ui): Add extension for serving OpenDC web UI
This change adds a new Quarkus extension that is able to serve the OpenDC web interface via the Quarkus deployment of OpenDC.
Diffstat (limited to 'opendc-web/opendc-web-ui-quarkus')
-rw-r--r--opendc-web/opendc-web-ui-quarkus/build.gradle.kts23
-rw-r--r--opendc-web/opendc-web-ui-quarkus/deployment/build.gradle.kts39
-rw-r--r--opendc-web/opendc-web-ui-quarkus/deployment/src/main/java/org/opendc/web/ui/deployment/AuthConfiguration.java52
-rw-r--r--opendc-web/opendc-web-ui-quarkus/deployment/src/main/java/org/opendc/web/ui/deployment/OpenDCUiConfig.java63
-rw-r--r--opendc-web/opendc-web-ui-quarkus/deployment/src/main/java/org/opendc/web/ui/deployment/OpenDCUiProcessor.java314
-rw-r--r--opendc-web/opendc-web-ui-quarkus/deployment/src/main/java/org/opendc/web/ui/deployment/OpenDCUiRoutingBuildItem.java119
-rw-r--r--opendc-web/opendc-web-ui-quarkus/runtime/build.gradle.kts36
-rw-r--r--opendc-web/opendc-web-ui-quarkus/runtime/src/main/java/org/opendc/web/ui/runtime/OpenDCUiRecorder.java107
-rw-r--r--opendc-web/opendc-web-ui-quarkus/runtime/src/main/java/org/opendc/web/ui/runtime/OpenDCUiRuntimeConfig.java39
-rw-r--r--opendc-web/opendc-web-ui-quarkus/runtime/src/main/resources/META-INF/quarkus-extension.yaml5
10 files changed, 797 insertions, 0 deletions
diff --git a/opendc-web/opendc-web-ui-quarkus/build.gradle.kts b/opendc-web/opendc-web-ui-quarkus/build.gradle.kts
new file mode 100644
index 00000000..cbec021c
--- /dev/null
+++ b/opendc-web/opendc-web-ui-quarkus/build.gradle.kts
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+description = "Quarkus extension for serving OpenDC web interface"
diff --git a/opendc-web/opendc-web-ui-quarkus/deployment/build.gradle.kts b/opendc-web/opendc-web-ui-quarkus/deployment/build.gradle.kts
new file mode 100644
index 00000000..0f3ae8ce
--- /dev/null
+++ b/opendc-web/opendc-web-ui-quarkus/deployment/build.gradle.kts
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+description = "Quarkus extension for serving OpenDC web interface"
+
+/* Build configuration */
+plugins {
+ `java-library-conventions`
+}
+
+dependencies {
+ implementation(enforcedPlatform(libs.quarkus.bom))
+
+ implementation(projects.opendcWeb.opendcWebUi)
+ implementation(projects.opendcWeb.opendcWebUiQuarkus.runtime)
+
+ implementation(libs.quarkus.core.deployment)
+ implementation(libs.quarkus.vertx.http.deployment)
+ implementation(libs.quarkus.arc.deployment)
+}
diff --git a/opendc-web/opendc-web-ui-quarkus/deployment/src/main/java/org/opendc/web/ui/deployment/AuthConfiguration.java b/opendc-web/opendc-web-ui-quarkus/deployment/src/main/java/org/opendc/web/ui/deployment/AuthConfiguration.java
new file mode 100644
index 00000000..2e4d9198
--- /dev/null
+++ b/opendc-web/opendc-web-ui-quarkus/deployment/src/main/java/org/opendc/web/ui/deployment/AuthConfiguration.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2022 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.ui.deployment;
+
+import io.quarkus.runtime.annotations.ConfigGroup;
+import io.quarkus.runtime.annotations.ConfigItem;
+
+import java.util.Optional;
+
+/**
+ * Auth configuration for the OpenDC UI extension.
+ */
+@ConfigGroup
+public class AuthConfiguration {
+ /**
+ * The authentication domain.
+ */
+ @ConfigItem
+ Optional<String> domain;
+
+ /**
+ * The client identifier used by the OpenDC web ui.
+ */
+ @ConfigItem
+ Optional<String> clientId;
+
+ /**
+ * The audience of the OpenDC API.
+ */
+ @ConfigItem
+ Optional<String> audience;
+}
diff --git a/opendc-web/opendc-web-ui-quarkus/deployment/src/main/java/org/opendc/web/ui/deployment/OpenDCUiConfig.java b/opendc-web/opendc-web-ui-quarkus/deployment/src/main/java/org/opendc/web/ui/deployment/OpenDCUiConfig.java
new file mode 100644
index 00000000..50c1fbe3
--- /dev/null
+++ b/opendc-web/opendc-web-ui-quarkus/deployment/src/main/java/org/opendc/web/ui/deployment/OpenDCUiConfig.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2022 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.ui.deployment;
+
+import io.quarkus.runtime.annotations.ConfigItem;
+import io.quarkus.runtime.annotations.ConfigRoot;
+
+import java.util.Optional;
+
+/**
+ * Build-time configuration for the OpenDC UI extension.
+ */
+@ConfigRoot(name = "opendc-ui")
+public class OpenDCUiConfig {
+ /**
+ * A flag to include the OpenDC UI extension into the build.
+ */
+ @ConfigItem(defaultValue = "true")
+ boolean include;
+
+ /**
+ * The path where the OpenDC UI is available.
+ */
+ @ConfigItem(defaultValue = "/")
+ String path;
+
+ /**
+ * The base URL of the OpenDC API.
+ */
+ @ConfigItem(defaultValue = "/api")
+ String apiBaseUrl;
+
+ /**
+ * Configuration properties for web UI authentication.
+ */
+ AuthConfiguration auth;
+
+ /**
+ * Sentry DSN.
+ */
+ @ConfigItem
+ Optional<String> sentryDsn;
+}
diff --git a/opendc-web/opendc-web-ui-quarkus/deployment/src/main/java/org/opendc/web/ui/deployment/OpenDCUiProcessor.java b/opendc-web/opendc-web-ui-quarkus/deployment/src/main/java/org/opendc/web/ui/deployment/OpenDCUiProcessor.java
new file mode 100644
index 00000000..983bb852
--- /dev/null
+++ b/opendc-web/opendc-web-ui-quarkus/deployment/src/main/java/org/opendc/web/ui/deployment/OpenDCUiProcessor.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright (c) 2022 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.ui.deployment;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+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.FeatureBuildItem;
+import io.quarkus.deployment.builditem.ShutdownContextBuildItem;
+import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem;
+import io.quarkus.maven.dependency.GACT;
+import io.quarkus.maven.dependency.ResolvedDependency;
+import io.quarkus.paths.PathVisit;
+import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem;
+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.WebJarResultsBuildItem;
+import io.vertx.core.Handler;
+import io.vertx.ext.web.RoutingContext;
+import org.opendc.web.ui.runtime.OpenDCUiRecorder;
+import org.opendc.web.ui.runtime.OpenDCUiRuntimeConfig;
+
+import java.io.*;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.function.BooleanSupplier;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Build processor for the OpenDC web UI Quarkus extension.
+ */
+public class OpenDCUiProcessor {
+
+ private static final String FEATURE = "opendc-ui";
+ private static final GACT OPENDC_UI_WEBJAR_ARTIFACT_KEY = new GACT("org.opendc", "opendc-web-ui", null, "jar");
+ private static final String OPENDC_UI_WEBJAR_STATIC_RESOURCES_PATH = "META-INF/resources/opendc-web-ui";
+ private static final Pattern PATH_PARAM_PATTERN = Pattern.compile("\\[(\\w+)]");
+
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ /**
+ * Provide the {@link FeatureBuildItem} for this Quarkus extension.
+ */
+ @BuildStep(onlyIf = IsIncluded.class)
+ public FeatureBuildItem feature() {
+ return new FeatureBuildItem(FEATURE);
+ }
+
+ /**
+ * Build the WebJar that is used to serve the Next.js resources.
+ */
+ @BuildStep(onlyIf = IsIncluded.class)
+ public WebJarBuildItem buildWebJar(OpenDCUiConfig config,
+ HttpRootPathBuildItem httpRootPathBuildItem) {
+ return WebJarBuildItem.builder()
+ .artifactKey(OPENDC_UI_WEBJAR_ARTIFACT_KEY)
+ .root(OPENDC_UI_WEBJAR_STATIC_RESOURCES_PATH)
+ .onlyCopyNonArtifactFiles(false)
+ .useDefaultQuarkusBranding(false)
+ .filter(new InsertVariablesResourcesFilter(config, httpRootPathBuildItem))
+ .build();
+ }
+
+
+ /**
+ * Build the Next.js routes based on the route manifest generated by it.
+ */
+ @BuildStep(onlyIf = IsIncluded.class)
+ public OpenDCUiRoutingBuildItem buildRoutes(CurateOutcomeBuildItem curateOutcomeBuildItem) throws IOException {
+ ResolvedDependency dependency = getAppArtifact(curateOutcomeBuildItem, OPENDC_UI_WEBJAR_ARTIFACT_KEY);
+ PathVisit visit = dependency.getContentTree().apply(OPENDC_UI_WEBJAR_STATIC_RESOURCES_PATH + "/routes-manifest.json", v -> v);
+
+ if (visit == null) {
+ throw new FileNotFoundException("Cannot find routes-manifest.json");
+ }
+
+ JsonNode routeManifest = objectMapper.readTree(visit.getUrl());
+
+ var pages = new ArrayList<OpenDCUiRoutingBuildItem.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 OpenDCUiRoutingBuildItem.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 OpenDCUiRoutingBuildItem.Page(path, page));
+ }
+
+ var redirects = new ArrayList<OpenDCUiRoutingBuildItem.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().replaceAll("/%%NEXT_BASE_PATH%%", "");
+ String destination = redirect.get("destination").asText().replaceAll("/%%NEXT_BASE_PATH%%", "");
+
+ if (path.isEmpty()) {
+ path = "/";
+ }
+
+ redirects.add(new OpenDCUiRoutingBuildItem.Redirect(path, destination, statusCode));
+ }
+
+ var custom404 = routeManifest.get("pages404").asBoolean();
+ return new OpenDCUiRoutingBuildItem(pages, redirects, custom404);
+ }
+
+ /**
+ * Register the HTTP handles for the OpenDC web UI.
+ */
+ @BuildStep(onlyIf = IsIncluded.class)
+ @Record(ExecutionTime.RUNTIME_INIT)
+ public void registerOpenDCUiHandler(OpenDCUiRecorder recorder,
+ BuildProducer<RouteBuildItem> routes,
+ HttpRootPathBuildItem httpRootPathBuildItem,
+ WebJarResultsBuildItem webJarResultsBuildItem,
+ OpenDCUiRoutingBuildItem openDCUiBuildItem,
+ OpenDCUiRuntimeConfig runtimeConfig,
+ OpenDCUiConfig buildConfig,
+ ShutdownContextBuildItem shutdownContext) {
+
+ WebJarResultsBuildItem.WebJarResult result = webJarResultsBuildItem.byArtifactKey(OPENDC_UI_WEBJAR_ARTIFACT_KEY);
+ if (result == null) {
+ return;
+ }
+
+ String basePath = httpRootPathBuildItem.resolvePath(buildConfig.path);
+ String finalDestination = result.getFinalDestination();
+
+ /* Construct dynamic routes */
+ for (var redirect : openDCUiBuildItem.getRedirects()) {
+ String destination = basePath.equals("/") ? redirect.getDestination() : basePath + redirect.getDestination();
+
+ routes.produce(httpRootPathBuildItem.routeBuilder()
+ .route(basePath + redirect.getPath())
+ .handler(recorder.redirectHandler(destination, redirect.getStatusCode(), runtimeConfig))
+ .build());
+ }
+
+ for (var page : openDCUiBuildItem.getPages()) {
+ routes.produce(httpRootPathBuildItem.routeBuilder()
+ .route(basePath + page.getPath())
+ .handler(recorder.pageHandler(finalDestination, page.getName(), runtimeConfig))
+ .build());
+ }
+
+ /* Construct static routes */
+ Handler<RoutingContext> staticHandler = recorder.staticHandler(
+ finalDestination,
+ basePath,
+ result.getWebRootConfigurations(),
+ runtimeConfig,
+ shutdownContext
+ );
+
+ routes.produce(httpRootPathBuildItem.routeBuilder()
+ .route(buildConfig.path)
+ .displayOnNotFoundPage("OpenDC UI")
+ .routeConfigKey("quarkus.opendc-ui.path")
+ .handler(staticHandler)
+ .build());
+
+ routes.produce(httpRootPathBuildItem.routeBuilder()
+ .route(buildConfig.path + "*")
+ .handler(staticHandler)
+ .build());
+ }
+
+ /**
+ * A {@link WebJarResourcesFilter} that instantiates the variables in the web jar resource.
+ */
+ private static class InsertVariablesResourcesFilter implements WebJarResourcesFilter {
+
+ private static final String HTML = ".html";
+ private static final String CSS = ".css";
+ private static final String JS = ".js";
+
+ private final OpenDCUiConfig config;
+ private final HttpRootPathBuildItem httpRootPathBuildItem;
+
+
+ public InsertVariablesResourcesFilter(OpenDCUiConfig config, HttpRootPathBuildItem httpRootPathBuildItem) {
+ this.config = config;
+ this.httpRootPathBuildItem = httpRootPathBuildItem;
+ }
+
+ @Override
+ public FilterResult apply(String fileName, InputStream stream) throws IOException {
+ if (stream == null) {
+ return new FilterResult(null, false);
+ }
+
+ // Allow replacement of variables in HTML, CSS, and JavaScript files
+ if (fileName.endsWith(HTML) || fileName.endsWith(CSS) || fileName.endsWith(JS)) {
+ byte[] oldContentBytes = stream.readAllBytes();
+ String oldContents = new String(oldContentBytes);
+ String contents = substituteVariables(oldContents, this::substitute);
+
+ boolean changed = contents.length() != oldContents.length() || !contents.equals(oldContents);
+ if (changed) {
+ return new FilterResult(new ByteArrayInputStream(contents.getBytes()), true);
+ } else {
+ return new FilterResult(new ByteArrayInputStream(oldContentBytes), false);
+ }
+ }
+
+ return new FilterResult(stream, false);
+ }
+
+ private String substitute(String var) {
+ switch (var) {
+ case "NEXT_BASE_PATH":
+ String basePath = httpRootPathBuildItem.resolvePath(config.path);
+ return basePath.equals("/") ? "" : basePath; // Base path must not end with trailing slash
+ case "NEXT_PUBLIC_API_BASE_URL":
+ return config.apiBaseUrl;
+ case "NEXT_PUBLIC_SENTRY_DSN":
+ return config.sentryDsn.orElse("");
+ case "NEXT_PUBLIC_AUTH0_DOMAIN":
+ return config.auth.domain.orElse("");
+ case "NEXT_PUBLIC_AUTH0_CLIENT_ID":
+ return config.auth.clientId.orElse("");
+ case "NEXT_PUBLIC_AUTH0_AUDIENCE":
+ return config.auth.audience.orElse("");
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Pattern to match variables in the OpenDC web UI sources specified using the following format: "%%VAR_NAME%%".
+ * <p>
+ * Be aware that to properly handle Next.js base path, we need to also match a possible forward slash in front
+ * of the variable.
+ */
+ private static final Pattern PATTERN = Pattern.compile("/?%%(\\w+)%%");
+
+ /**
+ * Helper method to substitute variables in the OpenDC web UI.
+ */
+ private static String substituteVariables(String contents, Function<String, String> substitute) {
+ Matcher m = PATTERN.matcher(contents);
+ StringBuilder sb = new StringBuilder();
+
+ while (m.find()) {
+ String group = m.group(1);
+ String val = substitute.apply(group);
+ m.appendReplacement(sb, val != null ? val : group);
+ }
+
+ m.appendTail(sb);
+ return sb.toString();
+ }
+ }
+
+ /**
+ * A {@link BooleanSupplier} to determine if the OpenDC web UI extension should be included.
+ */
+ private static class IsIncluded implements BooleanSupplier {
+ OpenDCUiConfig config;
+
+ @Override
+ public boolean getAsBoolean() {
+ return config.include;
+ }
+ }
+
+ private 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");
+ }
+}
diff --git a/opendc-web/opendc-web-ui-quarkus/deployment/src/main/java/org/opendc/web/ui/deployment/OpenDCUiRoutingBuildItem.java b/opendc-web/opendc-web-ui-quarkus/deployment/src/main/java/org/opendc/web/ui/deployment/OpenDCUiRoutingBuildItem.java
new file mode 100644
index 00000000..7e0f9408
--- /dev/null
+++ b/opendc-web/opendc-web-ui-quarkus/deployment/src/main/java/org/opendc/web/ui/deployment/OpenDCUiRoutingBuildItem.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (c) 2022 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.ui.deployment;
+
+import io.quarkus.builder.item.SimpleBuildItem;
+
+import java.util.List;
+
+/**
+ * Build item containing the routes for the OpenDC web UI.
+ */
+public final class OpenDCUiRoutingBuildItem extends SimpleBuildItem {
+
+ private final boolean custom404;
+ private final List<Page> pages;
+ private final List<Redirect> redirects;
+
+ /**
+ * Construct a {@link OpenDCUiRoutingBuildItem} instance.
+ *
+ * @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 OpenDCUiRoutingBuildItem(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.
+ */
+ public static final class Redirect {
+
+ private final String path;
+ private final String destination;
+ private final int statusCode;
+
+ /**
+ * Construct a {@link Redirect} route.
+ *
+ * @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 Redirect(String path, String destination, int statusCode) {
+ this.statusCode = statusCode;
+ this.path = path;
+ this.destination = destination;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public String getDestination() {
+ return destination;
+ }
+
+ public int getStatusCode() {
+ return statusCode;
+ }
+ }
+
+ /**
+ * A page defined by the Next.js routes manifest.
+ */
+ public static final class Page {
+
+ private final String path;
+ private final String name;
+
+ public Page(String path, String page) {
+ this.path = path;
+ this.name = page;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public String getName() {
+ return name;
+ }
+ }
+}
diff --git a/opendc-web/opendc-web-ui-quarkus/runtime/build.gradle.kts b/opendc-web/opendc-web-ui-quarkus/runtime/build.gradle.kts
new file mode 100644
index 00000000..f4131f0b
--- /dev/null
+++ b/opendc-web/opendc-web-ui-quarkus/runtime/build.gradle.kts
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+description = "Quarkus extension for serving OpenDC web interface"
+
+plugins {
+ `java-library-conventions`
+ id("io.quarkus.extension")
+}
+
+dependencies {
+ implementation(enforcedPlatform(libs.quarkus.bom))
+
+ implementation(libs.quarkus.core.runtime)
+ implementation(libs.quarkus.vertx.http.runtime)
+ implementation(libs.quarkus.arc.runtime)
+}
diff --git a/opendc-web/opendc-web-ui-quarkus/runtime/src/main/java/org/opendc/web/ui/runtime/OpenDCUiRecorder.java b/opendc-web/opendc-web-ui-quarkus/runtime/src/main/java/org/opendc/web/ui/runtime/OpenDCUiRecorder.java
new file mode 100644
index 00000000..026a9039
--- /dev/null
+++ b/opendc-web/opendc-web-ui-quarkus/runtime/src/main/java/org/opendc/web/ui/runtime/OpenDCUiRecorder.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2022 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.ui.runtime;
+
+import io.quarkus.runtime.ShutdownContext;
+import io.quarkus.runtime.annotations.Recorder;
+import io.quarkus.vertx.http.runtime.devmode.FileSystemStaticHandler;
+import io.quarkus.vertx.http.runtime.webjar.WebJarNotFoundHandler;
+import io.quarkus.vertx.http.runtime.webjar.WebJarStaticHandler;
+import io.vertx.core.Handler;
+import io.vertx.ext.web.RoutingContext;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Helper class for serving the OpenDC web interface.
+ */
+@Recorder
+public class OpenDCUiRecorder {
+ /**
+ * Construct a {@link Handler} for serving a page of the OpenDC web interface.
+ */
+ public Handler<RoutingContext> pageHandler(
+ String finalDestination,
+ String page,
+ OpenDCUiRuntimeConfig runtimeConfig
+ ) {
+ if (runtimeConfig.enable) {
+ String pageDirectory = finalDestination + "/pages";
+ return (event) -> {
+ event.response()
+ .setStatusCode(200)
+ .sendFile(pageDirectory + page + ".html");
+ };
+ }
+
+ return new WebJarNotFoundHandler();
+ }
+
+ /**
+ * Construct a {@link Handler} for handling redirects in the OpenDC web interface.
+ */
+ public Handler<RoutingContext> redirectHandler(
+ String destination,
+ int statusCode,
+ OpenDCUiRuntimeConfig runtimeConfig
+ ) {
+ if (runtimeConfig.enable) {
+ return (event) -> {
+ String query = event.request().query();
+ String fullDestination = query != null ? destination + "?" + query : destination;
+
+ event.response()
+ .setStatusCode(statusCode)
+ .putHeader("Location", fullDestination)
+ .end();
+ };
+ }
+
+ return new WebJarNotFoundHandler();
+ }
+
+ /**
+ * Construct a {@link Handler} for serving the static files of the OpenDC web interface.
+ */
+ public Handler<RoutingContext> staticHandler(
+ String finalDestination,
+ String path,
+ List<FileSystemStaticHandler.StaticWebRootConfiguration> webRootConfigurations,
+ OpenDCUiRuntimeConfig runtimeConfig,
+ ShutdownContext shutdownContext
+ ) {
+ if (runtimeConfig.enable) {
+ var augmentedWebRootConfigurations = webRootConfigurations
+ .stream()
+ .map(c -> new FileSystemStaticHandler.StaticWebRootConfiguration(c.getFileSystem(), c.getWebRoot().isEmpty() ? "static" : c.getWebRoot() + "/static"))
+ .collect(Collectors.toList());
+
+ WebJarStaticHandler handler = new WebJarStaticHandler(finalDestination + "/static", path, augmentedWebRootConfigurations);
+ shutdownContext.addShutdownTask(new ShutdownContext.CloseRunnable(handler));
+ return handler;
+ }
+
+ return new WebJarNotFoundHandler();
+ }
+}
diff --git a/opendc-web/opendc-web-ui-quarkus/runtime/src/main/java/org/opendc/web/ui/runtime/OpenDCUiRuntimeConfig.java b/opendc-web/opendc-web-ui-quarkus/runtime/src/main/java/org/opendc/web/ui/runtime/OpenDCUiRuntimeConfig.java
new file mode 100644
index 00000000..8ae3b6a2
--- /dev/null
+++ b/opendc-web/opendc-web-ui-quarkus/runtime/src/main/java/org/opendc/web/ui/runtime/OpenDCUiRuntimeConfig.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2022 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.ui.runtime;
+
+import io.quarkus.runtime.annotations.ConfigItem;
+import io.quarkus.runtime.annotations.ConfigPhase;
+import io.quarkus.runtime.annotations.ConfigRoot;
+
+/**
+ * Configuration for the OpenDC web UI.
+ */
+@ConfigRoot(phase = ConfigPhase.RUN_TIME, name = "opendc-ui")
+public class OpenDCUiRuntimeConfig {
+ /**
+ * Flag to indicate whether the web interface should be served by the OpenDC API server.
+ */
+ @ConfigItem(defaultValue = "true")
+ public boolean enable;
+}
diff --git a/opendc-web/opendc-web-ui-quarkus/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/opendc-web/opendc-web-ui-quarkus/runtime/src/main/resources/META-INF/quarkus-extension.yaml
new file mode 100644
index 00000000..581a1779
--- /dev/null
+++ b/opendc-web/opendc-web-ui-quarkus/runtime/src/main/resources/META-INF/quarkus-extension.yaml
@@ -0,0 +1,5 @@
+---
+name: "OpenDC Web UI"
+metadata:
+ status: "preview"
+ unlisted: true