From 8500a2559cd1a70245c1fa01ce4934bcd7e8e4dc Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 17 Oct 2024 12:28:43 -0500 Subject: [PATCH] CE-1887 Initial checkin --- .../javalin/QApplicationJavalinServer.java | 510 ++++++++++++++++++ .../QJavalinRouteProviderInterface.java | 47 ++ .../javalin/QMiddlewareApiSpecHandler.java | 269 +++++++++ 3 files changed, 826 insertions(+) create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServer.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QJavalinRouteProviderInterface.java create mode 100644 qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QMiddlewareApiSpecHandler.java diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServer.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServer.java new file mode 100644 index 00000000..b8f20c63 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServer.java @@ -0,0 +1,510 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin; + + +import java.util.List; +import java.util.function.Consumer; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; +import com.kingsrook.qqq.backend.core.instances.AbstractQQQApplication; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.utils.ClassPathUtils; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; +import com.kingsrook.qqq.backend.javalin.QJavalinImplementation; +import com.kingsrook.qqq.middleware.javalin.specs.AbstractMiddlewareVersion; +import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1; +import io.javalin.Javalin; +import io.javalin.http.Context; +import org.apache.commons.lang.BooleanUtils; + + +/******************************************************************************* + ** Second-generation qqq javalin server. + ** + ** An evolution over the original QJavalinImplementation, which both managed + ** the javalin instance itself, but also provided all of the endpoint handlers... + ** This class instead just configures & starts the server. + ** + ** Makes several setters available, to let application-developer choose what + ** standard qqq endpoints are served (e.g., frontend-material-dashboard, the + ** legacy-unversioned middleware, newer versioned-middleware, and additional qqq + ** modules or application-defined services (both provided as instances of + ** QJavalinRouteProviderInterface). + ** + ** System property `qqq.javalin.hotSwapInstance` (defaults to false), causes the + ** QInstance to be re-loaded every X millis, to avoid some server restarts while + ** doing dev. + *******************************************************************************/ +public class QApplicationJavalinServer +{ + private static final QLogger LOG = QLogger.getLogger(QApplicationJavalinServer.class); + + private final AbstractQQQApplication application; + + private Integer port = 8000; + private boolean serveFrontendMaterialDashboard = true; + private boolean serveLegacyUnversionedMiddlewareAPI = true; + private List middlewareVersionList = List.of(new MiddlewareVersionV1()); + private List additionalRouteProviders = null; + private Consumer javalinConfigurationCustomizer = null; + + private long lastQInstanceHotSwapMillis; + private long millisBetweenHotSwaps = 2500; + private Consumer hotSwapCustomizer = null; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public QApplicationJavalinServer(AbstractQQQApplication application) + { + this.application = application; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public void start() throws QException + { + QInstance qInstance = application.defineValidatedQInstance(); + + Javalin service = Javalin.create(config -> + { + if(serveFrontendMaterialDashboard) + { + //////////////////////////////////////////////////////////////////////////////////////// + // If you have any assets to add to the web server (e.g., logos, icons) place them at // + // src/main/resources/material-dashboard-overlay (or a directory of your choice // + // under src/main/resources) and use this line of code to tell javalin about it. // + // Make sure to add your app-specific directory to the javalin config before the core // + // material-dashboard directory, so in case the same file exists in both (e.g., // + // favicon.png), the app-specific one will be used. // + //////////////////////////////////////////////////////////////////////////////////////// + config.staticFiles.add("/material-dashboard-overlay"); + + ///////////////////////////////////////////////////////////////////// + // tell javalin where to find material-dashboard static web assets // + ///////////////////////////////////////////////////////////////////// + config.staticFiles.add("/material-dashboard"); + + //////////////////////////////////////////////////////////// + // set the index page for the SPA from material dashboard // + //////////////////////////////////////////////////////////// + config.spaRoot.addFile("/", "material-dashboard/index.html"); + } + + /////////////////////////////////////////// + // add qqq routes to the javalin service // + /////////////////////////////////////////// + if(serveLegacyUnversionedMiddlewareAPI) + { + try + { + QJavalinImplementation qJavalinImplementation = new QJavalinImplementation(qInstance); + config.router.apiBuilder(qJavalinImplementation.getRoutes()); + } + catch(QInstanceValidationException e) + { + /////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // we should be pretty comfortable that this won't happen, because we've pre-validated the instance above... // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////// + throw new RuntimeException(e); + } + } + + ///////////////////////////////////// + // versioned qqq middleware routes // + ///////////////////////////////////// + if(CollectionUtils.nullSafeHasContents(middlewareVersionList)) + { + config.router.apiBuilder(new QMiddlewareApiSpecHandler(middlewareVersionList).defineJavalinEndpointGroup()); + for(AbstractMiddlewareVersion version : middlewareVersionList) + { + version.setQInstance(qInstance); + config.router.apiBuilder(version.getJavalinEndpointGroup(qInstance)); + } + } + + //////////////////////////////////////////////////////////////////////////// + // additional route providers (e.g., application-apis, other middlewares) // + //////////////////////////////////////////////////////////////////////////// + for(QJavalinRouteProviderInterface routeProvider : CollectionUtils.nonNullList(additionalRouteProviders)) + { + routeProvider.setQInstance(qInstance); + config.router.apiBuilder(routeProvider.getJavalinEndpointGroup()); + } + }); + + ////////////////////////////////////////////////////////////////////////////////////// + // per system property, set the server to hot-swap the q instance before all routes // + ////////////////////////////////////////////////////////////////////////////////////// + String hotSwapPropertyValue = System.getProperty("qqq.javalin.hotSwapInstance", "false"); + if(BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(hotSwapPropertyValue))) + { + LOG.info("Server will hotSwap QInstance before requests every [" + millisBetweenHotSwaps + "] millis."); + service.before(context -> hotSwapQInstance()); + } + + service.before((Context context) -> context.header("Content-Type", "application/json")); + service.after(QJavalinImplementation::clearQContext); + + //////////////////////////////////////////////// + // allow a configuration-customizer to be run // + //////////////////////////////////////////////// + if(javalinConfigurationCustomizer != null) + { + javalinConfigurationCustomizer.accept(service); + } + + service.start(port); + } + + + + /******************************************************************************* + ** If there's a qInstanceHotSwapSupplier, and its been a little while, replace + ** the qInstance with a new one from the supplier. Meant to be used while doing + ** development. + *******************************************************************************/ + public void hotSwapQInstance() + { + long now = System.currentTimeMillis(); + if(now - lastQInstanceHotSwapMillis < millisBetweenHotSwaps) + { + return; + } + + lastQInstanceHotSwapMillis = now; + + try + { + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // clear the cache of classes in this class, so that new classes can be found if a meta-data-producer is being used // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ClassPathUtils.clearTopLevelClassCache(); + + /////////////////////////////////////////////////////////////// + // try to get a new, validated instance from the application // + /////////////////////////////////////////////////////////////// + QInstance newQInstance = application.defineValidatedQInstance(); + if(newQInstance == null) + { + LOG.warn("Got a null qInstance from the application.defineQInstance(). Not hot-swapping."); + return; + } + + //////////////////////////////////////// + // allow a hot-swap customizer to run // + //////////////////////////////////////// + if(hotSwapCustomizer != null) + { + hotSwapCustomizer.accept(newQInstance); + } + + /////////////////////////////////////////////////////////////////////// + // pass the new qInstance into all of the objects serving qqq routes // + /////////////////////////////////////////////////////////////////////// + if(serveLegacyUnversionedMiddlewareAPI) + { + QJavalinImplementation.setQInstance(newQInstance); + } + + if(CollectionUtils.nullSafeHasContents(middlewareVersionList)) + { + for(AbstractMiddlewareVersion spec : CollectionUtils.nonNullList(middlewareVersionList)) + { + spec.setQInstance(newQInstance); + } + } + + for(QJavalinRouteProviderInterface routeProvider : CollectionUtils.nonNullList(additionalRouteProviders)) + { + routeProvider.setQInstance(newQInstance); + } + + LOG.info("Swapped qInstance"); + } + catch(QInstanceValidationException e) + { + LOG.error("Validation Error while hot-swapping QInstance", e); + } + catch(Exception e) + { + LOG.error("Error hot-swapping QInstance", e); + } + } + + + + /******************************************************************************* + ** Getter for port + *******************************************************************************/ + public Integer getPort() + { + return (this.port); + } + + + + /******************************************************************************* + ** Setter for port + *******************************************************************************/ + public void setPort(Integer port) + { + this.port = port; + } + + + + /******************************************************************************* + ** Fluent setter for port + *******************************************************************************/ + public QApplicationJavalinServer withPort(Integer port) + { + this.port = port; + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void setMillisBetweenHotSwaps(long millisBetweenHotSwaps) + { + this.millisBetweenHotSwaps = millisBetweenHotSwaps; + } + + + + /******************************************************************************* + ** Getter for serveFrontendMaterialDashboard + *******************************************************************************/ + public boolean getServeFrontendMaterialDashboard() + { + return (this.serveFrontendMaterialDashboard); + } + + + + /******************************************************************************* + ** Setter for serveFrontendMaterialDashboard + *******************************************************************************/ + public void setServeFrontendMaterialDashboard(boolean serveFrontendMaterialDashboard) + { + this.serveFrontendMaterialDashboard = serveFrontendMaterialDashboard; + } + + + + /******************************************************************************* + ** Fluent setter for serveFrontendMaterialDashboard + *******************************************************************************/ + public QApplicationJavalinServer withServeFrontendMaterialDashboard(boolean serveFrontendMaterialDashboard) + { + this.serveFrontendMaterialDashboard = serveFrontendMaterialDashboard; + return (this); + } + + + + /******************************************************************************* + ** Getter for serveLegacyUnversionedMiddlewareAPI + *******************************************************************************/ + public boolean getServeLegacyUnversionedMiddlewareAPI() + { + return (this.serveLegacyUnversionedMiddlewareAPI); + } + + + + /******************************************************************************* + ** Setter for serveLegacyUnversionedMiddlewareAPI + *******************************************************************************/ + public void setServeLegacyUnversionedMiddlewareAPI(boolean serveLegacyUnversionedMiddlewareAPI) + { + this.serveLegacyUnversionedMiddlewareAPI = serveLegacyUnversionedMiddlewareAPI; + } + + + + /******************************************************************************* + ** Fluent setter for serveLegacyUnversionedMiddlewareAPI + *******************************************************************************/ + public QApplicationJavalinServer withServeLegacyUnversionedMiddlewareAPI(boolean serveLegacyUnversionedMiddlewareAPI) + { + this.serveLegacyUnversionedMiddlewareAPI = serveLegacyUnversionedMiddlewareAPI; + return (this); + } + + + + /******************************************************************************* + ** Getter for middlewareVersionList + *******************************************************************************/ + public List getMiddlewareVersionList() + { + return (this.middlewareVersionList); + } + + + + /******************************************************************************* + ** Setter for middlewareVersionList + *******************************************************************************/ + public void setMiddlewareVersionList(List middlewareVersionList) + { + this.middlewareVersionList = middlewareVersionList; + } + + + + /******************************************************************************* + ** Fluent setter for middlewareVersionList + *******************************************************************************/ + public QApplicationJavalinServer withMiddlewareVersionList(List middlewareVersionList) + { + this.middlewareVersionList = middlewareVersionList; + return (this); + } + + + + /******************************************************************************* + ** Getter for additionalRouteProviders + *******************************************************************************/ + public List getAdditionalRouteProviders() + { + return (this.additionalRouteProviders); + } + + + + /******************************************************************************* + ** Setter for additionalRouteProviders + *******************************************************************************/ + public void setAdditionalRouteProviders(List additionalRouteProviders) + { + this.additionalRouteProviders = additionalRouteProviders; + } + + + + /******************************************************************************* + ** Fluent setter for additionalRouteProviders + *******************************************************************************/ + public QApplicationJavalinServer withAdditionalRouteProviders(List additionalRouteProviders) + { + this.additionalRouteProviders = additionalRouteProviders; + return (this); + } + + + + /******************************************************************************* + ** Getter for MILLIS_BETWEEN_HOT_SWAPS + *******************************************************************************/ + public long getMillisBetweenHotSwaps() + { + return (millisBetweenHotSwaps); + } + + + + /******************************************************************************* + ** Fluent setter for MILLIS_BETWEEN_HOT_SWAPS + *******************************************************************************/ + public void withMillisBetweenHotSwaps(long millisBetweenHotSwaps) + { + this.millisBetweenHotSwaps = millisBetweenHotSwaps; + } + + + /******************************************************************************* + ** Getter for hotSwapCustomizer + *******************************************************************************/ + public Consumer getHotSwapCustomizer() + { + return (this.hotSwapCustomizer); + } + + + + /******************************************************************************* + ** Setter for hotSwapCustomizer + *******************************************************************************/ + public void setHotSwapCustomizer(Consumer hotSwapCustomizer) + { + this.hotSwapCustomizer = hotSwapCustomizer; + } + + + + /******************************************************************************* + ** Fluent setter for hotSwapCustomizer + *******************************************************************************/ + public QApplicationJavalinServer withHotSwapCustomizer(Consumer hotSwapCustomizer) + { + this.hotSwapCustomizer = hotSwapCustomizer; + return (this); + } + + + + /******************************************************************************* + ** Getter for javalinConfigurationCustomizer + *******************************************************************************/ + public Consumer getJavalinConfigurationCustomizer() + { + return (this.javalinConfigurationCustomizer); + } + + + + /******************************************************************************* + ** Setter for javalinConfigurationCustomizer + *******************************************************************************/ + public void setJavalinConfigurationCustomizer(Consumer javalinConfigurationCustomizer) + { + this.javalinConfigurationCustomizer = javalinConfigurationCustomizer; + } + + + + /******************************************************************************* + ** Fluent setter for javalinConfigurationCustomizer + *******************************************************************************/ + public QApplicationJavalinServer withJavalinConfigurationCustomizer(Consumer javalinConfigurationCustomizer) + { + this.javalinConfigurationCustomizer = javalinConfigurationCustomizer; + return (this); + } + + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QJavalinRouteProviderInterface.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QJavalinRouteProviderInterface.java new file mode 100644 index 00000000..dabe521c --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QJavalinRouteProviderInterface.java @@ -0,0 +1,47 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin; + + +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import io.javalin.apibuilder.EndpointGroup; + + +/******************************************************************************* + ** Interface for classes that can provide a list of endpoints to a javalin + ** server. + *******************************************************************************/ +public interface QJavalinRouteProviderInterface +{ + + /*************************************************************************** + ** For initial setup when server boots, set the qInstance - but also, + ** e.g., for development, to do a hot-swap. + ***************************************************************************/ + void setQInstance(QInstance qInstance); + + /*************************************************************************** + ** + ***************************************************************************/ + EndpointGroup getJavalinEndpointGroup(); + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QMiddlewareApiSpecHandler.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QMiddlewareApiSpecHandler.java new file mode 100644 index 00000000..cee9441a --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QMiddlewareApiSpecHandler.java @@ -0,0 +1,269 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin; + + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.core.utils.YamlUtils; +import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; +import com.kingsrook.qqq.backend.javalin.QJavalinImplementation; +import com.kingsrook.qqq.middleware.javalin.specs.AbstractMiddlewareVersion; +import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1; +import com.kingsrook.qqq.openapi.model.OpenAPI; +import io.javalin.apibuilder.ApiBuilder; +import io.javalin.apibuilder.EndpointGroup; +import io.javalin.http.ContentType; +import io.javalin.http.Context; +import org.apache.commons.io.IOUtils; + + +/******************************************************************************* + ** javalin-handler that serves both rapidoc static html/css/js files, and + ** dynamically generated openapi json/yaml, for a given list of qqq middleware + ** versions + *******************************************************************************/ +public class QMiddlewareApiSpecHandler +{ + private final List middlewareVersionList; + private final String basePath; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public QMiddlewareApiSpecHandler(List middlewareVersionList) + { + this(middlewareVersionList, "qqq"); + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public QMiddlewareApiSpecHandler(List middlewareVersionList, String basePath) + { + this.middlewareVersionList = middlewareVersionList; + this.basePath = basePath.replaceFirst("^/+", "").replaceFirst("/+$", "");; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public EndpointGroup defineJavalinEndpointGroup() + { + return (() -> + { + ApiBuilder.get("/api/docs/js/rapidoc.min.js", (context) -> serveResource(context, "rapidoc/rapidoc-9.3.8.min.js", MapBuilder.of("Content-Type", ContentType.JAVASCRIPT))); + ApiBuilder.get("/api/docs/css/qqq-api-styles.css", (context) -> serveResource(context, "rapidoc/rapidoc-overrides.css", MapBuilder.of("Content-Type", ContentType.CSS))); + ApiBuilder.get("/images/qqq-api-logo.png", (context) -> serveResource(context, "images/qqq-on-crown-trans-160x80.png", MapBuilder.of("Content-Type", ContentType.IMAGE_PNG.getMimeType()))); + + ////////////////////////////////////////////// + // default page is the current version spec // + ////////////////////////////////////////////// + ApiBuilder.get("/" + basePath + "/", context -> doSpecHtml(context)); + ApiBuilder.get("/" + basePath + "/versions.json", context -> doVersions(context)); + + //////////////////////////////////////////// + // default page for a version is its spec // + //////////////////////////////////////////// + for(AbstractMiddlewareVersion middlewareSpec : middlewareVersionList) + { + String version = middlewareSpec.getVersion(); + String versionPath = "/" + basePath + "/" + version; + ApiBuilder.get(versionPath + "/", context -> doSpecHtml(context, version)); + + /////////////////////////////////////////// + // add known paths for specs & docs page // + /////////////////////////////////////////// + ApiBuilder.get(versionPath + "/openapi.yaml", context -> doSpecYaml(context, version)); + ApiBuilder.get(versionPath + "/openapi.json", context -> doSpecJson(context, version)); + ApiBuilder.get(versionPath + "/openapi.html", context -> doSpecHtml(context, version)); + } + }); + } + + + + /******************************************************************************* + ** list the versions in this api + *******************************************************************************/ + private void doVersions(Context context) + { + Map rs = new HashMap<>(); + + List supportedVersions = middlewareVersionList.stream().map(msi -> msi.getVersion()).toList(); + String currentVersion = supportedVersions.get(supportedVersions.size() - 1); + + rs.put("supportedVersions", supportedVersions); + rs.put("currentVersion", currentVersion); + + context.contentType(ContentType.APPLICATION_JSON); + context.result(JsonUtils.toJson(rs)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void serveResource(Context context, String resourcePath, Map headers) + { + InputStream resourceAsStream = QJavalinImplementation.class.getClassLoader().getResourceAsStream(resourcePath); + for(Map.Entry entry : CollectionUtils.nonNullMap(headers).entrySet()) + { + context.header(entry.getKey(), entry.getValue()); + } + context.result(resourceAsStream); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void doSpecYaml(Context context, String version) + { + try + { + OpenAPI openAPI = new MiddlewareVersionV1().generate(basePath); + context.contentType(ContentType.APPLICATION_YAML); + context.result(YamlUtils.toYaml(openAPI)); + } + catch(Exception e) + { + QJavalinImplementation.handleException(context, e); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void doSpecJson(Context context, String version) + { + try + { + OpenAPI openAPI = new MiddlewareVersionV1().generate(basePath); + context.contentType(ContentType.APPLICATION_JSON); + context.result(JsonUtils.toJson(openAPI)); + } + catch(Exception e) + { + QJavalinImplementation.handleException(context, e); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void doSpecHtml(Context context) + { + String version = null; + + try + { + version = context.pathParam("version"); + } + catch(Exception e) + { + //////////////// + // leave null // + //////////////// + } + + if(!StringUtils.hasContent(version)) + { + List supportedVersions = middlewareVersionList.stream().map(msi -> msi.getVersion()).toList(); + version = supportedVersions.get(supportedVersions.size() - 1); + } + + doSpecHtml(context, version); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void doSpecHtml(Context context, String version) + { + try + { + ////////////////////////////////// + // read html from resource file // + ////////////////////////////////// + InputStream resourceAsStream = QMiddlewareApiSpecHandler.class.getClassLoader().getResourceAsStream("rapidoc/rapidoc-container.html"); + String html = IOUtils.toString(resourceAsStream, StandardCharsets.UTF_8); + + ///////////////////////////////// + // do replacements in the html // + ///////////////////////////////// + html = html.replace("{spec-url}", "/" + basePath + "/" + version + "/openapi.json"); + html = html.replace("{version}", version); + html = html.replace("{primaryColor}", "#444444"); + html = html.replace("{navLogoImg}", ""); + + Optional middlewareSpec = middlewareVersionList.stream().filter(msi -> msi.getVersion().equals(version)).findFirst(); + if(middlewareSpec.isEmpty()) + { + throw (new QUserFacingException("Unrecognized version: " + version)); + } + + OpenAPI openAPI = middlewareSpec.get().generate(basePath); + html = html.replace("{title}", openAPI.getInfo().getTitle() + " - " + version); + + StringBuilder otherVersionOptions = new StringBuilder(); + for(AbstractMiddlewareVersion otherVersionSpec : middlewareVersionList) + { + otherVersionOptions.append(""); + } + + html = html.replace("{otherVersionOptions}", otherVersionOptions.toString()); + + context.contentType(ContentType.HTML); + context.result(html); + } + catch(Exception e) + { + QJavalinImplementation.handleException(context, e); + } + } + +}