diff --git a/dist/platform/pom.xml b/dist/platform/pom.xml
index cd9ffec360..6c03c6374d 100644
--- a/dist/platform/pom.xml
+++ b/dist/platform/pom.xml
@@ -72,7 +72,11 @@
httpPorthttpPort2
- httpsPort2
+ httpPort2b
+ httpPort3
+ httpPort3b
+ httpPort4
+ httpPort4b
@@ -96,16 +100,6 @@
-
- org.apache.maven.plugins
- maven-surefire-plugin
-
-
- ${httpPort2}
- ${httpsPort2}
-
-
- org.apache.maven.pluginsmaven-failsafe-plugin
@@ -125,7 +119,11 @@
${httpPort}${httpPort2}
- ${httpsPort2}
+ ${httpPort2b}
+ ${httpPort2}
+ ${httpPort2b}
+ ${httpPort2}
+ ${httpPort2b}
diff --git a/dist/platform/src/test/java/cloud/piranha/dist/platform/PlatformPiranhaBuilderTest.java b/dist/platform/src/test/java/cloud/piranha/dist/platform/PlatformPiranhaBuilderTest.java
index 1931a93fd6..f409b6a627 100644
--- a/dist/platform/src/test/java/cloud/piranha/dist/platform/PlatformPiranhaBuilderTest.java
+++ b/dist/platform/src/test/java/cloud/piranha/dist/platform/PlatformPiranhaBuilderTest.java
@@ -75,7 +75,7 @@ void testHttpPort2() throws Exception {
PlatformPiranha piranha = new PlatformPiranhaBuilder()
.extensionClass(PlatformExtension.class)
.httpPort(-1)
- .httpsPort(Integer.parseInt(System.getProperty("httpsPort2")))
+ .httpsPort(Integer.parseInt(System.getProperty("httpPort2b")))
.build();
piranha.start();
Thread.sleep(5000);
@@ -99,13 +99,13 @@ void testHttpsPort2() throws Exception {
.extensionClass(PlatformExtension.class)
.httpsKeystoreFile("src/main/zip/etc/keystore.jks")
.httpsKeystorePassword("password")
- .httpPort(8228)
- .httpsPort(8338)
+ .httpPort(Integer.parseInt(System.getProperty("httpPort3")))
+ .httpsPort(Integer.parseInt(System.getProperty("httpPort3b")))
.build();
piranha.start();
Thread.sleep(5000);
SocketFactory factory = SSLSocketFactory.getDefault();
- try (SSLSocket socket = (SSLSocket) factory.createSocket("localhost", 8338)) {
+ try (SSLSocket socket = (SSLSocket) factory.createSocket("localhost", Integer.parseInt(System.getProperty("httpPort3b")))) {
assertNotNull(socket.getOutputStream());
assertNotNull(socket.getSSLParameters());
assertEquals("TLSv1.3", socket.getSSLParameters().getProtocols()[0]);
@@ -123,12 +123,12 @@ void testHttpsPort2() throws Exception {
void testDefaultExtensionClass() throws Exception {
PlatformPiranha piranha = new PlatformPiranhaBuilder()
.extensionClass(PlatformExtension.class.getName())
- .httpPort(8080)
+ .httpPort(Integer.parseInt(System.getProperty("httpPort4")))
.verbose(true)
.build();
piranha.start();
Thread.sleep(5000);
- try (Socket socket = new Socket("localhost", 8080)) {
+ try (Socket socket = new Socket("localhost", Integer.parseInt(System.getProperty("httpPort4")))) {
assertNotNull(socket.getOutputStream());
} catch (ConnectException e) {
}
diff --git a/multi/pom.xml b/multi/pom.xml
new file mode 100644
index 0000000000..98a5d80bbd
--- /dev/null
+++ b/multi/pom.xml
@@ -0,0 +1,146 @@
+
+
+
+ 4.0.0
+
+
+ cloud.piranha
+ project
+ 24.8.0-SNAPSHOT
+
+
+ cloud.piranha
+ piranha-multi
+ jar
+
+ Piranha - Multi
+
+
+
+
+ cloud.piranha.feature
+ piranha-feature-exitonstop
+ ${project.version}
+ compile
+
+
+ cloud.piranha.feature
+ piranha-feature-http
+ ${project.version}
+ compile
+
+
+ cloud.piranha.feature
+ piranha-feature-https
+ ${project.version}
+ compile
+
+
+ cloud.piranha.feature
+ piranha-feature-logging
+ ${project.version}
+ compile
+
+
+ cloud.piranha.feature
+ piranha-feature-webapps
+ ${project.version}
+ compile
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-params
+ test
+
+
+
+
+ piranha-multi
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+
+
+ reserve-network-port
+
+ reserve-network-port
+
+ validate
+
+
+ httpPort
+ httpPort2
+ httpsPort2
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+
+
+ package
+
+ single
+
+
+ false
+
+ src/main/assembly/zip.xml
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+
+
+
+ integration-test
+ verify
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+
+ true
+ cloud.piranha.multi.MultiPiranhaMain
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ ${httpPort}
+ ${httpPort2}
+ ${httpsPort2}
+
+
+
+
+
+
diff --git a/multi/src/main/assembly/zip.xml b/multi/src/main/assembly/zip.xml
new file mode 100644
index 0000000000..e618f8f7c6
--- /dev/null
+++ b/multi/src/main/assembly/zip.xml
@@ -0,0 +1,35 @@
+
+ zip
+ piranha
+
+ zip
+ tar.gz
+
+
+
+ ${project.basedir}/src/main/zip
+
+
+
+ ${project.build.directory}/webapps
+ webapps
+
+
+
+
+
+ lib
+
+
+
+
+
+ ${project.groupId}:${project.artifactId}:jar:*
+
+ lib
+ true
+
+
+
diff --git a/multi/src/main/docker/Dockerfile b/multi/src/main/docker/Dockerfile
new file mode 100644
index 0000000000..bc7cd0a79e
--- /dev/null
+++ b/multi/src/main/docker/Dockerfile
@@ -0,0 +1,7 @@
+FROM eclipse-temurin:21
+RUN useradd -m piranha
+ADD target/piranha-multi.tar.gz /home/piranha/
+RUN chown -R piranha:piranha /home/piranha
+WORKDIR /home/piranha/piranha/bin
+USER piranha
+CMD ["/bin/bash", "./run.sh"]
diff --git a/multi/src/main/java/cloud/piranha/multi/MultiPiranha.java b/multi/src/main/java/cloud/piranha/multi/MultiPiranha.java
new file mode 100644
index 0000000000..e9f9a9d421
--- /dev/null
+++ b/multi/src/main/java/cloud/piranha/multi/MultiPiranha.java
@@ -0,0 +1,346 @@
+/*
+ * Copyright (c) 2002-2024 Manorrock.com. All Rights Reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+package cloud.piranha.multi;
+
+import cloud.piranha.core.api.Piranha;
+import cloud.piranha.core.api.PiranhaConfiguration;
+import cloud.piranha.core.api.WebApplicationExtension;
+import cloud.piranha.core.impl.DefaultPiranhaConfiguration;
+import cloud.piranha.feature.api.FeatureManager;
+import cloud.piranha.feature.exitonstop.ExitOnStopFeature;
+import cloud.piranha.feature.http.HttpFeature;
+import cloud.piranha.feature.https.HttpsFeature;
+import cloud.piranha.feature.impl.DefaultFeatureManager;
+import cloud.piranha.feature.logging.LoggingFeature;
+import cloud.piranha.feature.webapps.WebAppsFeature;
+import cloud.piranha.http.api.HttpServer;
+import java.io.File;
+import java.io.IOException;
+import java.lang.System.Logger;
+import static java.lang.System.Logger.Level.INFO;
+import static java.lang.System.Logger.Level.WARNING;
+import java.nio.file.Files;
+
+/**
+ * The Multi version of Piranha.
+ *
+ *
+ * This version of Piranha supports the following:
+ *
+ *
+ *
Running with Java modules
+ *
Exiting on stop
+ *
Exposing a HTTP endpoint
+ *
Exposing a HTTPS endpoint
+ *
Setting the logging level
+ *
Hosting multiple web application
+ *
+ *
+ * @author Manfred Riem (mriem@manorrock.com)
+ */
+public class MultiPiranha implements Piranha, Runnable {
+
+ /**
+ * Stores the logger.
+ */
+ private static final Logger LOGGER = System.getLogger(MultiPiranha.class.getName());
+
+ /**
+ * Stores the 'tmp/piranha.pid' file constant.
+ */
+ private static final String PID_FILE = "tmp/piranha.pid";
+
+ /**
+ * Stores the configuration.
+ */
+ private final PiranhaConfiguration configuration;
+
+ /**
+ * Stores the feature manager.
+ */
+ private final FeatureManager featureManager;
+
+ /**
+ * Stores the HTTP feature.
+ */
+ private HttpFeature httpFeature;
+
+ /**
+ * Stores the HTTP server.
+ */
+ private HttpServer httpServer;
+
+ /**
+ * Stores the HTTP feature.
+ */
+ private HttpsFeature httpsFeature;
+
+ /**
+ * Stores the HTTP server.
+ */
+ private HttpServer httpsServer;
+
+ /**
+ * Stores the started flag.
+ */
+ private boolean started = false;
+
+ /**
+ * Stores the thread we use.
+ */
+ private Thread thread;
+
+ /**
+ * Stores the WebAppsFeature.
+ */
+ private WebAppsFeature webAppsFeature;
+
+ /**
+ * Constructor.
+ */
+ public MultiPiranha() {
+ configuration = new DefaultPiranhaConfiguration();
+ configuration.setBoolean("exitOnStop", false);
+ configuration.setInteger("httpPort", 8080);
+ configuration.setInteger("httpsPort", -1);
+ configuration.setBoolean("jpmsEnabled", false);
+ configuration.setFile("webAppsDir", new File("webapps"));
+ featureManager = new DefaultFeatureManager();
+ }
+
+ @Override
+ public PiranhaConfiguration getConfiguration() {
+ return configuration;
+ }
+
+ /**
+ * Are we running?
+ *
+ * @return true if we are, false otherwise.
+ */
+ private boolean isRunning() {
+ boolean result = false;
+ if (httpServer != null) {
+ result = httpServer.isRunning();
+ } else if (httpsServer != null) {
+ result = httpsServer.isRunning();
+ }
+ return result;
+ }
+
+ /**
+ * Have we started?
+ *
+ * @return true if we have, false otherwise.
+ */
+ private boolean isStarted() {
+ return started;
+ }
+
+ /**
+ * Run method.
+ */
+ @Override
+ public void run() {
+ long startTime = System.currentTimeMillis();
+
+ LoggingFeature loggingFeature = new LoggingFeature();
+ featureManager.addFeature(loggingFeature);
+ loggingFeature.setLevel(configuration.getString("loggingLevel"));
+ loggingFeature.init();
+ loggingFeature.start();
+
+ LOGGER.log(INFO, () -> "Starting Piranha");
+
+ webAppsFeature = new WebAppsFeature();
+ featureManager.addFeature(webAppsFeature);
+ webAppsFeature.setExtensionClass((Class extends WebApplicationExtension>) configuration.getClass("extensionClass"));
+ webAppsFeature.setJpmsEnabled(configuration.getBoolean("jpmsEnabled", false));
+ webAppsFeature.setWebAppsDir(configuration.getFile("webAppsDir"));
+ webAppsFeature.init();
+ webAppsFeature.start();
+
+ /*
+ * Construct, initialize and start HTTP endpoint (if applicable).
+ */
+ if (configuration.getInteger("httpPort") > 0) {
+ httpFeature = new HttpFeature();
+ httpFeature.setHttpServerClass(configuration.getString("httpServerClass"));
+ httpFeature.setPort(configuration.getInteger("httpPort"));
+ httpFeature.init();
+ httpFeature.getHttpServer().setHttpServerProcessor(webAppsFeature.getHttpServerProcessor());
+ httpFeature.start();
+ httpServer = httpFeature.getHttpServer();
+ }
+
+ /*
+ * Construct, initialize and start HTTPS endpoint (if applicable).
+ */
+ if (configuration.getInteger("httpsPort") > 0) {
+ httpsFeature = new HttpsFeature();
+ httpsFeature.setHttpsKeystoreFile(configuration.getString("httpsKeystoreFile"));
+ httpsFeature.setHttpsKeystorePassword(configuration.getString("httpsKeystorePassword"));
+ httpsFeature.setHttpsServerClass(configuration.getString("httpsServerClass"));
+ httpsFeature.setHttpsTruststoreFile(configuration.getString("httpsTruststoreFile"));
+ httpsFeature.setHttpsTruststorePassword(configuration.getString("httpsTruststorePassword"));
+ httpsFeature.setPort(configuration.getInteger("httpsPort"));
+ httpsFeature.init();
+ httpsFeature.getHttpsServer().setHttpServerProcessor(webAppsFeature.getHttpServerProcessor());
+ httpsFeature.start();
+ httpServer = httpsFeature.getHttpsServer();
+ }
+
+ if (configuration.getBoolean("exitOnStop", false)) {
+ ExitOnStopFeature exitOnStopFeature = new ExitOnStopFeature();
+ featureManager.addFeature(exitOnStopFeature);
+ }
+
+ long finishTime = System.currentTimeMillis();
+ LOGGER.log(INFO, "Started Piranha");
+ LOGGER.log(INFO, "It took {0} milliseconds", finishTime - startTime);
+
+ started = true;
+
+ File startedFile = new File("tmp/piranha.started");
+ File stoppedFile = new File("tmp/piranha.stopped");
+
+ if (stoppedFile.exists()) {
+ try {
+ Files.delete(stoppedFile.toPath());
+ } catch (IOException ioe) {
+ LOGGER.log(WARNING, "Error while deleting existing piranha.stopped file", ioe);
+ }
+ }
+
+ if (!startedFile.exists()) {
+ try {
+ startedFile.createNewFile();
+ } catch (IOException ioe) {
+ LOGGER.log(WARNING, "Unable to create piranha.started file", ioe);
+ }
+ }
+
+ File pidFile = new File(PID_FILE);
+ while (isRunning()) {
+ try {
+ Thread.sleep(2000);
+ } catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ }
+
+ if (!pidFile.exists()) {
+ if (httpServer != null) {
+ httpServer.stop();
+ }
+ if (httpsServer != null) {
+ httpsServer.stop();
+ }
+ }
+ }
+
+ finishTime = System.currentTimeMillis();
+ LOGGER.log(INFO, "Stopped Piranha");
+ LOGGER.log(INFO, "We ran for {0} milliseconds", finishTime - startTime);
+
+ if (startedFile.exists()) {
+ try {
+ Files.delete(startedFile.toPath());
+ } catch (IOException ioe) {
+ LOGGER.log(WARNING, "Error while deleting existing piranha.started file", ioe);
+ }
+ }
+ if (!stoppedFile.exists()) {
+ try {
+ stoppedFile.createNewFile();
+ } catch (IOException ioe) {
+ LOGGER.log(WARNING, "Unable to create piranha.stopped file", ioe);
+ }
+ }
+
+ featureManager.stop();
+ }
+
+ /**
+ * Set the web applications directory.
+ *
+ * @param webAppsDir the web applications directory.
+ */
+ public void setWebAppsDir(File webAppsDir) {
+ this.configuration.setFile("webAppsDir", webAppsDir);
+ }
+
+ /**
+ * Start the server.
+ */
+ public void start() {
+ File pidFile = new File(PID_FILE);
+
+ if (!pidFile.exists()) {
+ try {
+ if (!pidFile.getParentFile().exists()) {
+ pidFile.getParentFile().mkdirs();
+ }
+ pidFile.createNewFile();
+ } catch (IOException ex) {
+ LOGGER.log(WARNING, "Unable to create PID file");
+ }
+ } else {
+ LOGGER.log(WARNING, "PID file already exists");
+ }
+
+ thread = new Thread(this);
+ thread.setDaemon(false);
+ thread.start();
+
+ while (!isStarted()) {
+ try {
+ Thread.sleep(10);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
+ /**
+ * Stop the server.
+ */
+ public void stop() {
+ File pidFile = new File(PID_FILE);
+
+ if (pidFile.exists()) {
+ try {
+ Files.delete(pidFile.toPath());
+ } catch (IOException ioe) {
+ LOGGER.log(WARNING, "Error occurred while deleting PID file", ioe);
+ }
+ }
+
+ started = false;
+ thread = null;
+ }
+}
diff --git a/multi/src/main/java/cloud/piranha/multi/MultiPiranhaBuilder.java b/multi/src/main/java/cloud/piranha/multi/MultiPiranhaBuilder.java
new file mode 100644
index 0000000000..88f88a0f9e
--- /dev/null
+++ b/multi/src/main/java/cloud/piranha/multi/MultiPiranhaBuilder.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (c) 2002-2024 Manorrock.com. All Rights Reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+package cloud.piranha.multi;
+
+import cloud.piranha.core.api.PiranhaBuilder;
+import java.lang.System.Logger.Level;
+
+import static javax.naming.Context.INITIAL_CONTEXT_FACTORY;
+import cloud.piranha.core.api.WebApplicationExtension;
+import java.io.File;
+import static java.lang.System.Logger.Level.WARNING;
+
+/**
+ * The builder so you can easily build instances of
+ * {@link cloud.piranha.multi.MultiPiranha}.
+ *
+ * @author Manfred Riem (mriem@manorrock.com)
+ * @see cloud.piranha.multi.MultiPiranha
+ */
+public class MultiPiranhaBuilder implements PiranhaBuilder {
+
+ /**
+ * Stores the logger.
+ */
+ private static final System.Logger LOGGER = System.getLogger(MultiPiranhaBuilder.class.getName());
+
+ /**
+ * Stores the Piranha Server instance.
+ */
+ private final MultiPiranha piranha = new MultiPiranha();
+
+ /**
+ * Stores the InitialContext factory.
+ */
+ private String initialContextFactory = "com.manorrock.herring.thread.ThreadInitialContextFactory";
+
+ /**
+ * Stores the verbose flag.
+ */
+ private boolean verbose = false;
+
+ /**
+ * Build the server.
+ *
+ * @return the server.
+ */
+ @Override
+ public MultiPiranha build() {
+ if (verbose) {
+ showArguments();
+ }
+ System.setProperty(INITIAL_CONTEXT_FACTORY, initialContextFactory);
+ return piranha;
+ }
+
+ /**
+ * Set the exit on stop flag.
+ *
+ * @param exitOnStop the exit on stop flag.
+ * @return the builder.
+ */
+ public MultiPiranhaBuilder exitOnStop(boolean exitOnStop) {
+ piranha.getConfiguration().setBoolean("exitOnStop", exitOnStop);
+ return this;
+ }
+
+ /**
+ * Set the extension class.
+ *
+ * @param extensionClass the extension class.
+ * @return the builder.
+ */
+ public MultiPiranhaBuilder extensionClass(Class extends WebApplicationExtension> extensionClass) {
+ piranha.getConfiguration().setClass("extensionClass", extensionClass);
+ return this;
+ }
+
+ /**
+ * Set the extension class.
+ *
+ * @param extensionClassName the extension class name.
+ * @return the builder.
+ */
+ public MultiPiranhaBuilder extensionClass(String extensionClassName) {
+ try {
+ extensionClass(Class.forName(extensionClassName)
+ .asSubclass(WebApplicationExtension.class));
+ } catch (ClassNotFoundException cnfe) {
+ LOGGER.log(WARNING, "Unable to load default extension class", cnfe);
+ }
+ return this;
+ }
+
+ /**
+ * Set the HTTP server port.
+ *
+ * @param httpPort the HTTP server port.
+ * @return the builder.
+ */
+ public MultiPiranhaBuilder httpPort(int httpPort) {
+ piranha.getConfiguration().setInteger("httpPort", httpPort);
+ return this;
+ }
+
+ /**
+ * Set the HTTP server class.
+ *
+ * @param httpServerClass the HTTP server class.
+ * @return the builder.
+ */
+ public MultiPiranhaBuilder httpServerClass(String httpServerClass) {
+ piranha.getConfiguration().setString("httpServerClass", httpServerClass);
+ return this;
+ }
+
+ /**
+ * Set the HTTPS keystore file.
+ *
+ * @param httpsKeystoreFile the HTTPS keystore file.
+ * @return the builder.
+ */
+ public MultiPiranhaBuilder httpsKeystoreFile(String httpsKeystoreFile) {
+ piranha.getConfiguration().setString("httpsKeystoreFile", httpsKeystoreFile);
+ return this;
+ }
+
+ /**
+ * Set the HTTPS keystore password.
+ *
+ * @param httpsKeystorePassword the HTTPS keystore password.
+ * @return the builder.
+ */
+ public MultiPiranhaBuilder httpsKeystorePassword(String httpsKeystorePassword) {
+ piranha.getConfiguration().setString("httpsKeystorePassword", httpsKeystorePassword);
+ return this;
+ }
+
+ /**
+ * Set the HTTPS server port.
+ *
+ * @param httpsPort the HTTPS server port.
+ * @return the builder.
+ */
+ public MultiPiranhaBuilder httpsPort(int httpsPort) {
+ piranha.getConfiguration().setInteger("httpsPort", httpsPort);
+ return this;
+ }
+
+ /**
+ * Set the HTTPS server class.
+ *
+ * @param httpsServerClass the HTTPS server class.
+ * @return the builder.
+ */
+ public MultiPiranhaBuilder httpsServerClass(String httpsServerClass) {
+ piranha.getConfiguration().setString("httpsServerClass", httpsServerClass);
+ return this;
+ }
+
+ /**
+ * Set the HTTPS truststore file.
+ *
+ * @param httpsTruststoreFile the HTTPS truststore file.
+ * @return the builder.
+ */
+ public MultiPiranhaBuilder httpsTruststoreFile(String httpsTruststoreFile) {
+ piranha.getConfiguration().setString("httpsTruststoreFile", httpsTruststoreFile);
+ return this;
+ }
+
+ /**
+ * Set the HTTPS truststore password.
+ *
+ * @param httpsTruststorePassword the HTTPS truststore password.
+ * @return the builder.
+ */
+ public MultiPiranhaBuilder httpsTruststorePassword(String httpsTruststorePassword) {
+ piranha.getConfiguration().setString("httpsTruststorePassword", httpsTruststorePassword);
+ return this;
+ }
+
+ /**
+ * Enable/disable JPMS.
+ *
+ * @param jpms the JPMS flag.
+ * @return the builder.
+ */
+ public MultiPiranhaBuilder jpms(boolean jpms) {
+ piranha.getConfiguration().setBoolean("jpmsEnabled", jpms);
+ return this;
+ }
+
+ /**
+ * Set the logging level.
+ *
+ * @param loggingLevel the logging level.
+ * @return the builder.
+ */
+ public MultiPiranhaBuilder loggingLevel(String loggingLevel) {
+ piranha.getConfiguration().setString("loggingLevel", loggingLevel);
+ return this;
+ }
+
+ /**
+ * Show the arguments used.
+ */
+ private void showArguments() {
+ LOGGER.log(Level.INFO,
+ """
+
+ PIRANHA
+
+ Arguments
+ =========
+
+ Default extension class : %s
+ Exit on stop : %s
+ HTTP port : %s
+ HTTP server class : %s
+ HTTPS keystore file : %s
+ HTTPS keystore password : ****
+ HTTPS port : %s
+ HTTPS server class : %s
+ HTTPS truststore file : %s
+ HTTPS truststore password : ****
+ JPMS enabled : %s
+ Logging level : %s
+ Web applications dir : %s
+
+ """.formatted(
+ piranha.getConfiguration().getClass("extensionClass"),
+ piranha.getConfiguration().getBoolean("exitOnStop", false),
+ piranha.getConfiguration().getInteger("httpPort"),
+ piranha.getConfiguration().getString("httpServerClass"),
+ piranha.getConfiguration().getString("httpsKeystoreFile"),
+ piranha.getConfiguration().getInteger("httpsPort"),
+ piranha.getConfiguration().getString("httpsServerClass"),
+ piranha.getConfiguration().getString("httpsTruststoreFile"),
+ piranha.getConfiguration().getBoolean("jpmsEnabled", false),
+ piranha.getConfiguration().getString("loggingLevel"),
+ piranha.getConfiguration().getFile("webAppsDir")
+ ));
+ }
+
+ /**
+ * Set the verbose flag.
+ *
+ * @param verbose the verbose flag.
+ * @return the builder.
+ */
+ public MultiPiranhaBuilder verbose(boolean verbose) {
+ this.verbose = verbose;
+ return this;
+ }
+
+ /**
+ * Set the web applications directory.
+ *
+ * @param webAppsDir the web applications directory.
+ * @return the builder.
+ */
+ public MultiPiranhaBuilder webAppsDir(String webAppsDir) {
+ if (webAppsDir != null) {
+ piranha.getConfiguration().setFile("webAppsDir", new File(webAppsDir));
+ }
+ return this;
+ }
+}
diff --git a/multi/src/main/java/cloud/piranha/multi/MultiPiranhaMain.java b/multi/src/main/java/cloud/piranha/multi/MultiPiranhaMain.java
new file mode 100644
index 0000000000..0476e04dc5
--- /dev/null
+++ b/multi/src/main/java/cloud/piranha/multi/MultiPiranhaMain.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (c) 2002-2024 Manorrock.com. All Rights Reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+package cloud.piranha.multi;
+
+import java.lang.System.Logger.Level;
+
+/**
+ * The Main for Piranha Multi.
+ *
+ * @author Manfred Riem (mriem@manorrock.com)
+ */
+public class MultiPiranhaMain {
+
+ /**
+ * Stores the logger
+ */
+ private static final System.Logger LOGGER = System.getLogger(MultiPiranhaMain.class.getName());
+
+ /**
+ * Main method.
+ *
+ * @param arguments the arguments.
+ */
+ public static void main(String[] arguments) {
+ MultiPiranhaBuilder builder = new MultiPiranhaMain().processArguments(arguments);
+ if (builder != null) {
+ builder.build().start();
+ } else {
+ showHelp();
+ }
+ }
+
+ /**
+ * Process the arguments.
+ *
+ * @param arguments the arguments.
+ * @return the builder.
+ */
+ protected MultiPiranhaBuilder processArguments(String[] arguments) {
+ MultiPiranhaBuilder builder = new MultiPiranhaBuilder()
+ .exitOnStop(true);
+
+ if (arguments != null) {
+ for (int i = 0; i < arguments.length; i++) {
+ if (arguments[i].equals("--extension-class")) {
+ builder = builder.extensionClass(arguments[i + 1]);
+ }
+ if (arguments[i].equals("--help")) {
+ return null;
+ }
+ if (arguments[i].equals("--http-port")) {
+ builder = builder.httpPort(Integer.parseInt(arguments[i + 1]));
+ }
+ if (arguments[i].equals("--http-server-class")) {
+ builder = builder.httpServerClass(arguments[i + 1]);
+ }
+ if (arguments[i].equals("--https-keystore-file")) {
+ builder = builder.httpsKeystoreFile(arguments[i + 1]);
+ }
+ if (arguments[i].equals("--https-keystore-password")) {
+ builder = builder.httpsKeystorePassword(arguments[i + 1]);
+ }
+ if (arguments[i].equals("--https-port")) {
+ builder = builder.httpsPort(Integer.parseInt(arguments[i + 1]));
+ }
+ if (arguments[i].equals("--https-server-class")) {
+ builder = builder.httpsServerClass(arguments[i + 1]);
+ }
+ if (arguments[i].equals("--https-truststore-file")) {
+ builder = builder.httpsTruststoreFile(arguments[i + 1]);
+ }
+ if (arguments[i].equals("--https-truststore-password")) {
+ builder = builder.httpsTruststorePassword(arguments[i + 1]);
+ }
+ if (arguments[i].equals("--jpms")) {
+ builder = builder.jpms(true);
+ }
+ if (arguments[i].equals("--logging-level")) {
+ builder = builder.loggingLevel(arguments[i + 1]);
+ }
+ if (arguments[i].equals("--verbose")) {
+ builder = builder.verbose(true);
+ }
+ if (arguments[i].equals("--webapps-dir")) {
+ builder = builder.webAppsDir(arguments[i + 1]);
+ }
+ }
+ }
+ return builder;
+ }
+
+ /**
+ * Show help.
+ */
+ protected static void showHelp() {
+ LOGGER.log(Level.INFO, "");
+ LOGGER.log(Level.INFO,
+ """
+ --extension-class - Set the extension to use
+ --help - Show this help
+ --http-port - Set the HTTP port (use -1 to disable)
+ --http-server-class - Set the HTTP server class to use
+ --https-keystore-file - Set the HTTPS keystore file (applies to
+ the whole JVM)
+ --https-keystore-password - Set the HTTPS keystore password
+ (applies to the whole JVM)
+ --https-port - Set the HTTPS port (disabled by
+ default)
+ --https-server-class - Set the HTTPS server class to use
+ --https-truststore-file - Set the HTTPS keystore file (applies to
+ the whole JVM)
+ --https-truststore-password - Set the HTTPS keystore password
+ (applies to the whole JVM)
+ --jpms - Enable Java Platform Module System
+ --logging-level - Set the logging level
+ --verbose - Shows the runtime parameters
+ --webapps-dir - Set the web applications directory
+ """);
+ }
+}
diff --git a/multi/src/main/java/cloud/piranha/multi/MultiWebApplication.java b/multi/src/main/java/cloud/piranha/multi/MultiWebApplication.java
new file mode 100644
index 0000000000..ffb293246f
--- /dev/null
+++ b/multi/src/main/java/cloud/piranha/multi/MultiWebApplication.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2002-2024 Manorrock.com. All Rights Reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+package cloud.piranha.multi;
+
+import cloud.piranha.core.api.WebApplicationServerRequestMapper;
+import cloud.piranha.core.impl.DefaultWebApplication;
+import jakarta.servlet.ServletContext;
+import java.util.Objects;
+
+/**
+ * This web application supports finding other contexts using
+ * {@link ServletContext#getContext(String)}.
+ */
+public class MultiWebApplication extends DefaultWebApplication {
+
+ /**
+ * Stores the request mapper.
+ */
+ private final WebApplicationServerRequestMapper requestMapper;
+
+ /**
+ * Constructor.
+ *
+ * @param requestMapper the request mapper.
+ */
+ public MultiWebApplication(WebApplicationServerRequestMapper requestMapper) {
+ this.requestMapper = Objects.requireNonNull(requestMapper);
+ }
+
+ @Override
+ public ServletContext getContext(String uripath) {
+ return requestMapper.findMapping(uripath);
+ }
+}
diff --git a/multi/src/main/java/module-info.java b/multi/src/main/java/module-info.java
new file mode 100644
index 0000000000..b3fb0ea88f
--- /dev/null
+++ b/multi/src/main/java/module-info.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2002-2024 Manorrock.com. All Rights Reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/**
+ * This module delivers Piranha Multi.
+ *
+ * @author Manfred Riem (mriem@manorrock.com)
+ */
+module cloud.piranha.multi {
+
+ exports cloud.piranha.multi;
+ opens cloud.piranha.multi;
+ requires cloud.piranha.feature.exitonstop;
+ requires cloud.piranha.feature.http;
+ requires cloud.piranha.feature.https;
+ requires cloud.piranha.feature.logging;
+ requires cloud.piranha.feature.webapps;
+ requires java.logging;
+ requires java.naming;
+}
diff --git a/multi/src/main/zip/bin/run.sh b/multi/src/main/zip/bin/run.sh
new file mode 100755
index 0000000000..21a8bd38e1
--- /dev/null
+++ b/multi/src/main/zip/bin/run.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+set -m
+
+exec ./start.sh --run $*
diff --git a/multi/src/main/zip/bin/start.cmd b/multi/src/main/zip/bin/start.cmd
new file mode 100644
index 0000000000..be1ac74460
--- /dev/null
+++ b/multi/src/main/zip/bin/start.cmd
@@ -0,0 +1,2 @@
+cd ..
+java -jar lib\piranha-multi.jar %*
diff --git a/multi/src/main/zip/bin/start.sh b/multi/src/main/zip/bin/start.sh
new file mode 100755
index 0000000000..c421b207fe
--- /dev/null
+++ b/multi/src/main/zip/bin/start.sh
@@ -0,0 +1,48 @@
+#!/bin/bash
+set -m
+
+cd ..
+
+if [ -z ${JAVA_HOME} ]; then
+ echo Using default java
+ JAVA_BIN=java
+else
+ echo Using JAVA_HOME: ${JAVA_HOME}
+ JAVA_BIN=${JAVA_HOME}/bin/java
+fi
+
+if [[ "${PIRANHA_JPMS}" == "true" ]]; then
+ INIT_OPTIONS="--module-path lib -m cloud.piranha.multi"
+else
+ INIT_OPTIONS="-jar lib/piranha-multi.jar"
+fi
+
+
+if [[ "$*" == *"--suspend"* ]]; then
+ JAVA_ARGS="${JAVA_ARGS} -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:9009"
+fi
+
+#
+# Set the SSL debug mode which is useful for debugging SSL connections.
+#
+# SSL_DEBUG=-Djavax.net.debug=ssl
+
+CMD="${JAVA_BIN} ${JAVA_ARGS} ${SSL_DEBUG} \
+ -Djava.util.logging.config.file=etc/logging.properties \
+ ${INIT_OPTIONS} $*"
+
+echo Starting Piranha using command: ${CMD}
+
+touch tmp/piranha.pid
+
+if [[ "$*" == *"--verbose"* ]]; then
+ ${CMD}
+else
+ if [[ "$*" == *"--run"* ]]; then
+ echo $$ > tmp/piranha.pid
+ ${CMD}
+ else
+ ${CMD} &
+ echo $! > tmp/piranha.pid
+ fi
+fi
diff --git a/multi/src/main/zip/bin/stop.cmd b/multi/src/main/zip/bin/stop.cmd
new file mode 100644
index 0000000000..0e08b27eea
--- /dev/null
+++ b/multi/src/main/zip/bin/stop.cmd
@@ -0,0 +1,2 @@
+cd ..
+move tmp\piranha.pid tmp\piranha.pid.old
diff --git a/multi/src/main/zip/bin/stop.sh b/multi/src/main/zip/bin/stop.sh
new file mode 100755
index 0000000000..84626361ab
--- /dev/null
+++ b/multi/src/main/zip/bin/stop.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+set -m
+
+cd ..
+mv tmp/piranha.pid tmp/piranha.pid.old
diff --git a/multi/src/main/zip/etc/keystore.jks b/multi/src/main/zip/etc/keystore.jks
new file mode 100644
index 0000000000..04b040728f
Binary files /dev/null and b/multi/src/main/zip/etc/keystore.jks differ
diff --git a/multi/src/main/zip/etc/logging.properties b/multi/src/main/zip/etc/logging.properties
new file mode 100644
index 0000000000..3477f14e6a
--- /dev/null
+++ b/multi/src/main/zip/etc/logging.properties
@@ -0,0 +1,73 @@
+#
+# Copyright (c) 2002-2024 Manorrock.com. All Rights Reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# 3. Neither the name of the copyright holder nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+
+#
+# Logging handlers, by default the ConsoleHandler and the FileHandler are
+# enabled. If you want to add other handlers, see the java.util.logging
+# documentation for more information.
+#
+handlers = java.util.logging.ConsoleHandler java.util.logging.FileHandler
+
+#
+# Global log level
+#
+.level = INFO
+
+#
+# The configuration for the ConsoleHandler
+#
+# 1. The log level is set to INFO
+# 2. The formatter used is the SimpleFormatter
+#
+# IMPORTANT NOTE
+#
+# A Handler (in this case the ConsoleHandler) will only log up up to the level
+# specified for its Handler. So be default only INFO messages and higher will
+# show up in the log. E.g if you want FINEST messages to show for a particular
+# Logger you will have to set the level of the Handler to FINEST as well.
+#
+#
+java.util.logging.ConsoleHandler.level = INFO
+java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
+
+#
+# The configuration for the FileHandler
+#
+# 1. The log level is set to FINEST
+# 2. The formatter used is the SimpleFormatter
+# 3. The size of each log file is 100 MB.
+# 4. The number of log files to keep is 10
+# 5. The handler is set to append to an existing log file.
+# 6. The filename pattern is tmp/piranha-%g.log
+
+java.util.logging.FileHandler.level = FINEST
+java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
+java.util.logging.FileHandler.limit = 10485760
+java.util.logging.FileHandler.count = 10
+java.util.logging.FileHandler.append = true
+java.util.logging.FileHandler.pattern = tmp/piranha-%g.log
diff --git a/multi/src/main/zip/etc/truststore.jks b/multi/src/main/zip/etc/truststore.jks
new file mode 100644
index 0000000000..04b040728f
Binary files /dev/null and b/multi/src/main/zip/etc/truststore.jks differ
diff --git a/multi/src/main/zip/tmp/README b/multi/src/main/zip/tmp/README
new file mode 100644
index 0000000000..c383d2ee99
--- /dev/null
+++ b/multi/src/main/zip/tmp/README
@@ -0,0 +1,7 @@
+
+ README
+ ------
+
+ This directory is used for temporary files by Piranha. If the server is NOT
+ running you can safely delete the contents of the directory, but do NOT delete
+ the directory itself.
diff --git a/multi/src/main/zip/webapps/README b/multi/src/main/zip/webapps/README
new file mode 100644
index 0000000000..cb1111eb3c
--- /dev/null
+++ b/multi/src/main/zip/webapps/README
@@ -0,0 +1,7 @@
+
+ README
+ ------
+
+ This directory is used for your web applications. If a file ends with the .war
+ extension, or it is a sub directory it will be considered a web application,
+ anything else will be ignored.
diff --git a/multi/src/test/java/cloud/piranha/multi/MultiPiranhaBuilderTest.java b/multi/src/test/java/cloud/piranha/multi/MultiPiranhaBuilderTest.java
new file mode 100644
index 0000000000..29272b13cd
--- /dev/null
+++ b/multi/src/test/java/cloud/piranha/multi/MultiPiranhaBuilderTest.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) 2002-2024 Manorrock.com. All Rights Reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+package cloud.piranha.multi;
+
+import java.net.ConnectException;
+import java.net.Socket;
+import javax.net.SocketFactory;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.fail;
+import org.junit.jupiter.api.Test;
+
+/**
+ * The JUnit tests for the MultiPiranhaBuilder class.
+ *
+ * @author Manfred Riem (mriem@manorrock.com)
+ */
+class MultiPiranhaBuilderTest {
+
+ /**
+ * Test httpPort method.
+ *
+ * @throws Exception when a serious error occurs.
+ */
+ @Test
+ void testHttpPort() throws Exception {
+ MultiPiranha piranha = new MultiPiranhaBuilder()
+ .httpPort(Integer.parseInt(System.getProperty("httpPort")))
+ .build();
+ piranha.start();
+ Thread.sleep(5000);
+ try ( Socket socket = new Socket("localhost", Integer.parseInt(System.getProperty("httpPort")))) {
+ assertNotNull(socket.getOutputStream());
+ }
+ piranha.stop();
+ Thread.sleep(5000);
+ }
+
+ /**
+ * Test httpsPort method.
+ *
+ * @throws Exception when a serious error occurs.
+ */
+ @Test
+ void testHttpPort2() throws Exception {
+ MultiPiranha piranha = new MultiPiranhaBuilder()
+ .httpPort(-1)
+ .httpsPort(Integer.parseInt(System.getProperty("httpsPort2")))
+ .build();
+ piranha.start();
+ Thread.sleep(5000);
+ try ( Socket socket = new Socket("localhost",
+ Integer.parseInt(System.getProperty("httpPort2")))) {
+ fail();
+ } catch (ConnectException e) {
+ }
+ piranha.stop();
+ Thread.sleep(5000);
+ }
+
+ /**
+ * Test httpsPort method.
+ *
+ * @throws Exception when a serious error occurs.
+ */
+ @Test
+ void testHttpsPort2() throws Exception {
+ MultiPiranha piranha = new MultiPiranhaBuilder()
+ .httpsKeystoreFile("src/main/zip/etc/keystore.jks")
+ .httpsKeystorePassword("password")
+ .httpPort(8228)
+ .httpsPort(8338)
+ .build();
+ piranha.start();
+ Thread.sleep(5000);
+ SocketFactory factory = SSLSocketFactory.getDefault();
+ try ( SSLSocket socket = (SSLSocket) factory.createSocket("localhost", 8338)) {
+ assertNotNull(socket.getOutputStream());
+ assertNotNull(socket.getSSLParameters());
+ assertEquals("TLSv1.3", socket.getSSLParameters().getProtocols()[0]);
+ }
+ piranha.stop();
+ Thread.sleep(5000);
+ }
+
+ /**
+ * Test defaultExtensionClass method.
+ *
+ * @throws Exception when a serious error occurs.
+ */
+ @Test
+ void testDefaultExtensionClass() throws Exception {
+ MultiPiranha piranha = new MultiPiranhaBuilder()
+ .httpPort(8080)
+ .verbose(true)
+ .build();
+ piranha.start();
+ Thread.sleep(5000);
+ try ( Socket socket = new Socket("localhost", 8080)) {
+ assertNotNull(socket.getOutputStream());
+ } catch (ConnectException e) {
+ }
+ piranha.stop();
+ Thread.sleep(5000);
+ }
+}
diff --git a/multi/src/test/java/cloud/piranha/multi/MultiPiranhaIT.java b/multi/src/test/java/cloud/piranha/multi/MultiPiranhaIT.java
new file mode 100644
index 0000000000..46129bc68b
--- /dev/null
+++ b/multi/src/test/java/cloud/piranha/multi/MultiPiranhaIT.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (c) 2002-2024 Manorrock.com. All Rights Reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+package cloud.piranha.multi;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import org.junit.jupiter.api.Test;
+
+/**
+ * The JUnit integration tests for ServerPiranha class.
+ *
+ * @author Manfred Riem (mriem@manorrock.com)
+ */
+class MultiPiranhaIT {
+
+ /**
+ * Extract the zip input stream.
+ *
+ * @param zipInput the zip input stream.
+ * @param filePath the file path.
+ * @throws IOException when an I/O error occurs.
+ */
+ private void extractZipInputStream(ZipInputStream zipInput, String filePath) throws IOException {
+ try (BufferedOutputStream bufferOutput = new BufferedOutputStream(new FileOutputStream(filePath))) {
+ byte[] bytesIn = new byte[8192];
+ int read;
+ while ((read = zipInput.read(bytesIn)) != -1) {
+ bufferOutput.write(bytesIn, 0, read);
+ }
+ }
+ }
+
+ /**
+ * Extract the Server zip file.
+ */
+ private void extractServer(File zipFile) {
+ try (ZipInputStream zipInput = new ZipInputStream(new FileInputStream(zipFile))) {
+ ZipEntry entry = zipInput.getNextEntry();
+ while (entry != null) {
+ String filePath = "target" + File.separatorChar + entry.getName();
+ if (!entry.isDirectory()) {
+ File file = new File(filePath);
+ if (!file.getParentFile().exists()) {
+ file.getParentFile().mkdirs();
+ }
+ extractZipInputStream(zipInput, filePath);
+ }
+ zipInput.closeEntry();
+ entry = zipInput.getNextEntry();
+ }
+ } catch (IOException ioe) {
+ }
+ }
+
+ /**
+ * Test run method.
+ *
+ * @throws Exception when a serious error occurs.
+ */
+ @Test
+ void testRun() throws Exception {
+ extractServer(new File("target/piranha-multi.zip"));
+ ProcessBuilder builder = new ProcessBuilder();
+ Process process;
+
+ if (System.getProperty("os.name").toLowerCase().contains("windows")) {
+ process = builder.
+ directory(new File("target/piranha/bin")).
+ command("cmd", "/c", "start.cmd").
+ start();
+ } else {
+ process = builder.
+ directory(new File("target/piranha/bin")).
+ command("sh", "./start.sh").
+ start();
+ }
+ process.waitFor(5, TimeUnit.SECONDS);
+ Thread.sleep(5000);
+
+ File pidFile = new File("target/piranha/tmp/piranha.pid");
+ assertTrue(pidFile.exists());
+
+ if (System.getProperty("os.name").toLowerCase().contains("windows")) {
+ process = builder.
+ directory(new File("target/piranha/bin")).
+ command("cmd", "/c", "stop.cmd").
+ start();
+ } else {
+ process = builder.
+ directory(new File("target/piranha/bin")).
+ command("sh", "./stop.sh").
+ start();
+ }
+ process.waitFor(5, TimeUnit.SECONDS);
+ Thread.sleep(5000);
+
+ assertFalse(pidFile.exists());
+ }
+}
diff --git a/multi/tmp/piranha.stopped b/multi/tmp/piranha.stopped
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/pom.xml b/pom.xml
index 8fe7d9baab..097437d09a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -52,6 +52,7 @@
featurehttpmaven
+ multimicroresourcesingle