feat(distro-wildfly): serve portal from config directory so users can modify files easily
MODIFY
.gitmodules
.gitmodules
-[submodule "developer-portal"]
- path = developer-portal
+[submodule "portal/ui"]
+ path = portal/ui
url = https://github.com/apiman/apiman-developer-portal
branch = various-work-for-release

DELETE
developer-portal
-Subproject commit da5a336de0d874a517a8d1fc76831587b254c25c

MODIFY
distro/wildfly/pom.xml
distro/wildfly/pom.xml
<!-- Apiman WildFly WAR deps -->
<dependency>
<groupId>io.apiman</groupId>
- <artifactId>apiman-developer-portal</artifactId>
+ <artifactId>apiman-portal-war-wildfly</artifactId>
<type>war</type>
</dependency>
<dependency>
<version>${project.version}</version>
<type>zip</type>
</dependency>
+ <dependency>
+ <groupId>io.apiman</groupId>
+ <artifactId>apiman-portal-ui</artifactId>
+ </dependency>
<!-- Keycloak adapter (this will be deprecated soon) -->
<dependency>
<groupId>org.keycloak</groupId>

MODIFY
distro/wildfly/src/main/assembly/overlay.xml
distro/wildfly/src/main/assembly/overlay.xml
<directoryMode>0755</directoryMode>
<fileMode>0755</fileMode>
</dependencySet>
- <!-- apiman portal -->
- <dependencySet>
- <outputDirectory>standalone/deployments</outputDirectory>
- <includes>
- <include>*:apiman-developer-portal:war</include>
- </includes>
- <outputFileNameMapping>portal.war</outputFileNameMapping>
- <useProjectArtifact>false</useProjectArtifact>
- <useProjectAttachments>false</useProjectAttachments>
- <useTransitiveDependencies>false</useTransitiveDependencies>
- <useTransitiveFiltering>false</useTransitiveFiltering>
- <directoryMode>0755</directoryMode>
- <fileMode>0755</fileMode>
- </dependencySet>
<!-- apiman -->
<dependencySet>
<outputDirectory>standalone/deployments</outputDirectory>
</excludes>
</unpackOptions>
</dependencySet>
+ <!-- Apiman portal -->
+ <dependencySet>
+ <outputDirectory>standalone/deployments</outputDirectory>
+ <includes>
+ <include>*:apiman-portal-war-wildfly:war</include>
+ </includes>
+ <outputFileNameMapping>apiman-developer-portal-wildfly.war</outputFileNameMapping>
+ <useProjectArtifact>false</useProjectArtifact>
+ <useProjectAttachments>false</useProjectAttachments>
+ <useTransitiveDependencies>false</useTransitiveDependencies>
+ <useTransitiveFiltering>false</useTransitiveFiltering>
+ <directoryMode>0755</directoryMode>
+ <fileMode>0755</fileMode>
+ </dependencySet>
+ <dependencySet>
+ <outputDirectory>standalone/configuration/portal</outputDirectory>
+ <includes>
+ <include>*:apiman-portal-ui:jar</include>
+ </includes>
+ <unpack>true</unpack>
+ <useProjectArtifact>false</useProjectArtifact>
+ <useProjectAttachments>false</useProjectAttachments>
+ <useTransitiveDependencies>false</useTransitiveDependencies>
+ <useTransitiveFiltering>false</useTransitiveFiltering>
+ <directoryMode>0755</directoryMode>
+ <fileMode>0755</fileMode>
+ </dependencySet>
</dependencySets>
<fileSets>
<!-- Filter module defs -->

MODIFY
distro/wildfly/src/main/resources/overlay/standalone/configuration/standalone-apiman.xml
distro/wildfly/src/main/resources/overlay/standalone/configuration/standalone-apiman.xml
<host name="default-host" alias="localhost">
<location name="/" handler="welcome-content"/>
<http-invoker security-realm="ApplicationRealm"/>
+<!-- <location name="/portal-assets" handler="portal-handler"/>-->
</host>
</server>
<servlet-container name="default">
<websockets/>
</servlet-container>
<handlers>
+<!-- <file name="portal-handler" path="${jboss.home.dir}/portal-assets"/>-->
<file name="welcome-content" path="${jboss.home.dir}/welcome-content"/>
</handlers>
</subsystem>

MODIFY
pom.xml
pom.xml
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
- <artifactId>apiman-developer-portal</artifactId>
+ <artifactId>apiman-distro-wildfly8-portal</artifactId>
+ <version>${project.version}</version>
+ <type>war</type>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>apiman-portal-ui</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>apiman-portal-war-wildfly</artifactId>
<version>${project.version}</version>
<type>war</type>
</dependency>
<module>test</module>
<module>migration-assistant</module>
<module>sso</module>
- <module>developer-portal</module>
+ <module>portal</module>
</modules>
<profiles>

ADD
portal/pom.xml
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>io.apiman</groupId>
+ <artifactId>apiman</artifactId>
+ <version>3.0.0-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+ <artifactId>apiman-portal</artifactId>
+ <packaging>pom</packaging>
+
+ <modules>
+ <module>ui</module>
+ <module>war</module>
+ </modules>
+</project>

ADD
portal/ui
+Subproject commit 7694c3f03bf1a2ec1a02c395f9f2040364e334e9

ADD
portal/war/pom.xml
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>io.apiman</groupId>
+ <artifactId>apiman-portal</artifactId>
+ <version>3.0.0-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+ <artifactId>apiman-portal-war</artifactId>
+ <packaging>pom</packaging>
+
+ <modules>
+ <module>wildfly</module>
+ </modules>
+</project>

ADD
portal/war/wildfly/pom.xml
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>io.apiman</groupId>
+ <artifactId>apiman-portal-war</artifactId>
+ <version>3.0.0-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+ <artifactId>apiman-portal-war-wildfly</artifactId>
+ <packaging>war</packaging>
+ <name>apiman-portal-war-wildfly</name>
+
+ <dependencies>
+ <dependency>
+ <groupId>jakarta.servlet</groupId>
+ <artifactId>jakarta.servlet-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.apiman</groupId>
+ <artifactId>apiman-common-config</artifactId>
+ </dependency>
+ <!-- Include this in your overlay -->
+<!-- <dependency>-->
+<!-- <groupId>io.apiman</groupId>-->
+<!-- <artifactId>apiman-portal-ui</artifactId>-->
+<!-- </dependency>-->
+ <dependency>
+ <groupId>io.undertow</groupId>
+ <artifactId>undertow-servlet</artifactId>
+ <version>2.2.5.Final</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <finalName>apiman-developer-portal</finalName>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-war-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </build>
+</project>

ADD
portal/war/wildfly/src/main/java/io/apiman/portal/AngularServlet.java
+package io.apiman.portal;
+
+import java.io.IOException;
+import javax.servlet.GenericServlet;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+/**
+ * SPA Servlet
+ *
+ * @author msavy
+ */
+public class AngularServlet extends GenericServlet {
+
+ private static final long serialVersionUID = 8545062359275971153L;
+
+ /**
+ * @see javax.servlet.GenericServlet#service(javax.servlet.ServletRequest, javax.servlet.ServletResponse)
+ */
+ @Override
+ public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
+ try (var stream = getServletContext().getResourceAsStream("/index.html")) {
+ long len = stream.transferTo(res.getOutputStream());
+ res.setContentLength(Math.toIntExact(len));
+ }
+ res.setContentType("text/html; charset=UTF-8");
+ res.flushBuffer();
+ }
+
+}

ADD
portal/war/wildfly/src/main/java/io/apiman/portal/SplitterFilter.java
+/*
+ * Copyright 2022. Black Parrot Labs Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.apiman.portal;
+
+import io.apiman.common.config.ConfigDirectoryFinder;
+import io.apiman.common.config.ConfigFactory;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+
+import io.undertow.server.handlers.resource.FileResourceManager;
+import io.undertow.servlet.spec.ServletContextImpl;
+import org.apache.commons.configuration.CompositeConfiguration;
+import org.apache.commons.io.FileUtils;
+
+/**
+ * @author Marc Savy {@literal <marc@blackparrotlabs.io>}
+ */
+public class SplitterFilter implements Filter {
+ private final Set<String> resourceExistsSet = new HashSet<>();
+ private final Map<String, String> configCache = new HashMap<>();
+ private CompositeConfiguration config;
+ private Path resourceRoot;
+ private FilterConfig filterConfig;
+
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ this.filterConfig = filterConfig;
+ this.resourceRoot = ConfigDirectoryFinder.getConfigDirectory().resolve("portal");
+ this.config = ConfigFactory.createConfig();
+ // Some code sleuthing in Undertow let me find out how to provide a custom resource manager, hence letting the default HTTP servlet do its work in a different
+ // directory. Unfortunately, this will not be portable, so need to think of a more vendor-neutral way to achieve this, potentially.
+ ServletContextImpl sc = (ServletContextImpl) filterConfig.getServletContext();
+ FileResourceManager resManager = new FileResourceManager(resourceRoot.toFile());
+ sc.getDeployment().getDeploymentInfo().setResourceManager(resManager);
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
+ HttpServletRequest req = (HttpServletRequest) request;
+ String path = trim(req.getRequestURI().substring(req.getContextPath().length()));
+ // Would be better if we can put all the assets (other than index) under dist, so we can return real 404s
+ // Currently this breaks the loading of the config file, though...
+ // if (!path.startsWith("/dist")) {
+ // chain.doFilter(request, response);
+ // } else {
+ // request.getRequestDispatcher("/spa/index.html").forward(request, response);
+ // }
+ if (resourceExists(path) && !path.isBlank()) {
+ // If file exists, serve it up.
+ if (path.endsWith(".json5") || path.endsWith(".json")) {
+ String newResponse = configCache.computeIfAbsent(path, this::parseAndSubstitute);
+ response.setContentType("application/json");
+ response.setContentLength(newResponse.length());
+ response.getWriter().write(newResponse);
+ response.flushBuffer();
+ } else {
+ chain.doFilter(request, response);
+ }
+ } else {
+ // Else, send the spa index.
+ // String index = FileUtils.readFileToString(resourceRoot.resolve("index.html").toFile(), StandardCharsets.UTF_8);
+ // response.setContentLength(index.length());
+ // response.setContentType("text/html; charset=UTF-8");
+ // response.getWriter().write(index);
+ // response.flushBuffer();
+ req.getRequestDispatcher("/index.html").forward(req, response);
+ }
+ }
+
+ private String trim(String path) {
+ return path.startsWith("/") ? path.substring(1) : path;
+ }
+
+ private String parseAndSubstitute(String path) {
+ try {
+ String str = FileUtils.readFileToString(resourceRoot.resolve(path).toFile(), StandardCharsets.UTF_8);
+ return config.getSubstitutor().replace(str);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ private boolean resourceExists(String path) {
+ if (resourceExistsSet.contains(path)) {
+ return true;
+ }
+ if (Files.exists(resourceRoot.resolve(path))) {
+ resourceExistsSet.add(path);
+ return true;
+ } else {
+ return false;
+ }
+ }
+}

ADD
portal/war/wildfly/src/main/webapp/WEB-INF/jboss-web.xml
+<?xml version="1.0" encoding="UTF-8"?>
+<jboss-web>
+ <context-root>portal</context-root>
+</jboss-web>

ADD
portal/war/wildfly/src/main/webapp/WEB-INF/web.xml
+<?xml version="1.0" encoding="UTF-8"?>
+<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
+ http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
+ version="4.0">
+
+ <filter>
+ <filter-name>splitter</filter-name>
+ <filter-class>io.apiman.portal.SplitterFilter</filter-class>
+ </filter>
+ <filter-mapping>
+ <filter-name>splitter</filter-name>
+ <url-pattern>/*</url-pattern>
+ </filter-mapping>
+
+ <servlet>
+ <servlet-name>AngularServletMainUi</servlet-name>
+ <servlet-class>io.apiman.portal.AngularServlet</servlet-class>
+ </servlet>
+ <servlet-mapping>
+ <servlet-name>AngularServletMainUi</servlet-name>
+ <url-pattern>/spa/*</url-pattern>
+ </servlet-mapping>
+
+ <servlet>
+ <servlet-name>default</servlet-name>
+ <servlet-class>io.undertow.servlet.handlers.DefaultServlet</servlet-class>
+ </servlet>
+ <servlet-mapping>
+ <servlet-name>default</servlet-name>
+ <url-pattern>/*</url-pattern>
+ </servlet-mapping>
+
+ <security-constraint>
+ <web-resource-collection>
+ <web-resource-name>portal</web-resource-name>
+ <url-pattern>/*</url-pattern>
+ </web-resource-collection>
+ <web-resource-collection>
+ <web-resource-name>dist</web-resource-name>
+ <url-pattern>/dist/*</url-pattern>
+ </web-resource-collection>
+ </security-constraint>
+</web-app>

feat(distro-wildfly): serve portal from config directory so users can modify files easily