diff options
| author | Fabian Mastenbroek <mail.fabianm@gmail.com> | 2022-03-18 13:36:32 +0100 |
|---|---|---|
| committer | Fabian Mastenbroek <mail.fabianm@gmail.com> | 2022-04-05 18:54:52 +0200 |
| commit | c69035732ef073257cb56f7c9edfdbe25a47aa44 (patch) | |
| tree | 0e1533944f8e48712772bd518d3ffb187db9a309 | |
| parent | 6cf097b60366ecb2116f742be4374fcec841a950 (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.
18 files changed, 850 insertions, 15 deletions
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index dfe89d2e..e975bcbe 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -40,7 +40,8 @@ dependencies { implementation(libs.shadow) implementation(libs.jandex.gradle) - implementation(libs.quarkus.gradle) + implementation(libs.quarkus.gradle.application) + implementation(libs.quarkus.gradle.extension) implementation(libs.gradle.node) } diff --git a/buildSrc/src/main/kotlin/java-conventions.gradle.kts b/buildSrc/src/main/kotlin/java-conventions.gradle.kts new file mode 100644 index 00000000..d4cc667a --- /dev/null +++ b/buildSrc/src/main/kotlin/java-conventions.gradle.kts @@ -0,0 +1,35 @@ +/* + * 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. + */ + +plugins { + `java-library` +} + +/* Project configuration */ +repositories { + mavenCentral() +} + +java { + sourceCompatibility = Libs.jvmTarget + targetCompatibility = Libs.jvmTarget +} diff --git a/buildSrc/src/main/kotlin/java-library-conventions.gradle.kts b/buildSrc/src/main/kotlin/java-library-conventions.gradle.kts index d4cc667a..6a091a37 100644 --- a/buildSrc/src/main/kotlin/java-library-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/java-library-conventions.gradle.kts @@ -21,15 +21,10 @@ */ plugins { - `java-library` -} - -/* Project configuration */ -repositories { - mavenCentral() + id("java-conventions") + id("publishing-conventions") } java { - sourceCompatibility = Libs.jvmTarget - targetCompatibility = Libs.jvmTarget + withSourcesJar() } diff --git a/buildSrc/src/main/kotlin/kotlin-conventions.gradle.kts b/buildSrc/src/main/kotlin/kotlin-conventions.gradle.kts index 5d9ae56e..6b9109f7 100644 --- a/buildSrc/src/main/kotlin/kotlin-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/kotlin-conventions.gradle.kts @@ -23,7 +23,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - id("java-library-conventions") + id("java-conventions") kotlin("jvm") id("org.jlleitschuh.gradle.ktlint") } diff --git a/buildSrc/src/main/kotlin/kotlin-library-conventions.gradle.kts b/buildSrc/src/main/kotlin/kotlin-library-conventions.gradle.kts index 8fd45a41..86156476 100644 --- a/buildSrc/src/main/kotlin/kotlin-library-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/kotlin-library-conventions.gradle.kts @@ -22,15 +22,12 @@ plugins { id("kotlin-conventions") + id("java-library-conventions") id("publishing-conventions") id("dokka-conventions") } /* Project configuration */ -java { - withSourcesJar() -} - kotlin { explicitApi() } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5aee60fd..8f2e64f7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -72,9 +72,16 @@ parquet = { module = "org.apache.parquet:parquet-avro", version.ref = "parquet" config = { module = "com.typesafe:config", version.ref = "config" } # Quarkus -quarkus-gradle = { module = "io.quarkus:gradle-application-plugin", version.ref = "quarkus" } +quarkus-gradle-application = { module = "io.quarkus:gradle-application-plugin", version.ref = "quarkus" } +quarkus-gradle-extension = { module = "io.quarkus.extension:gradle-extension-plugin", version.ref = "quarkus" } quarkus-bom = { module = "io.quarkus:quarkus-bom", version.ref = "quarkus" } quarkus-kotlin = { module = "io.quarkus:quarkus-kotlin" } +quarkus-core-runtime = { module = "io.quarkus:quarkus-core", version.ref = "quarkus" } +quarkus-core-deployment = { module = "io.quarkus:quarkus-core-deployment" } +quarkus-vertx-http-runtime = { module = "io.quarkus:quarkus-vertx-http" } +quarkus-vertx-http-deployment = { module = "io.quarkus:quarkus-vertx-http-deployment" } +quarkus-arc-runtime = { module = "io.quarkus:quarkus-arc" } +quarkus-arc-deployment = { module = "io.quarkus:quarkus-arc-deployment" } quarkus-resteasy-core = { module = "io.quarkus:quarkus-resteasy" } quarkus-resteasy-jackson = { module = "io.quarkus:quarkus-resteasy-jackson" } quarkus-smallrye-openapi = { module = "io.quarkus:quarkus-smallrye-openapi" } diff --git a/opendc-web/opendc-web-api/build.gradle.kts b/opendc-web/opendc-web-api/build.gradle.kts index 853632a7..497ac52a 100644 --- a/opendc-web/opendc-web-api/build.gradle.kts +++ b/opendc-web/opendc-web-api/build.gradle.kts @@ -36,6 +36,7 @@ dependencies { implementation(enforcedPlatform(libs.quarkus.bom)) implementation(projects.opendcWeb.opendcWebProto) + implementation(projects.opendcWeb.opendcWebUiQuarkus.runtime) implementation(libs.quarkus.kotlin) implementation(libs.quarkus.resteasy.core) 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 diff --git a/settings.gradle.kts b/settings.gradle.kts index 267c8edd..cc454b19 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -39,6 +39,8 @@ include(":opendc-web:opendc-web-proto") include(":opendc-web:opendc-web-api") include(":opendc-web:opendc-web-client") include(":opendc-web:opendc-web-ui") +include(":opendc-web:opendc-web-ui-quarkus:deployment") +include(":opendc-web:opendc-web-ui-quarkus:runtime") include(":opendc-web:opendc-web-runner") include(":opendc-simulator:opendc-simulator-core") include(":opendc-simulator:opendc-simulator-flow") |
