diff --git a/fuse-products/src/main/java/software/tnb/product/ProductFactory.java b/fuse-products/src/main/java/software/tnb/product/ProductFactory.java
index 533d48e16..72f724195 100644
--- a/fuse-products/src/main/java/software/tnb/product/ProductFactory.java
+++ b/fuse-products/src/main/java/software/tnb/product/ProductFactory.java
@@ -2,6 +2,8 @@
import software.tnb.common.config.OpenshiftConfiguration;
import software.tnb.common.config.TestConfiguration;
+import software.tnb.product.csb.TomcatCamelSpringBoot;
+import software.tnb.product.csb.configuration.SpringBootConfiguration;
import java.util.Optional;
import java.util.ServiceLoader;
@@ -28,6 +30,7 @@ public static Product create() {
public static
P create(Class
clazz) {
final Optional product = StreamSupport.stream(ServiceLoader.load(Product.class).spliterator(), false)
.filter(p -> p.getClass().getSimpleName().toLowerCase().contains(TestConfiguration.product().getValue()))
+ .filter(p -> p instanceof TomcatCamelSpringBoot == SpringBootConfiguration.isTomcat())
.filter(p -> p instanceof OpenshiftProduct == OpenshiftConfiguration.isOpenshift())
.findFirst();
if (product.isEmpty()) {
diff --git a/fuse-products/src/main/java/software/tnb/product/configuration/ProductConfiguration.java b/fuse-products/src/main/java/software/tnb/product/configuration/ProductConfiguration.java
index 1adc770e4..dc2d6a730 100644
--- a/fuse-products/src/main/java/software/tnb/product/configuration/ProductConfiguration.java
+++ b/fuse-products/src/main/java/software/tnb/product/configuration/ProductConfiguration.java
@@ -4,12 +4,13 @@
import software.tnb.common.config.TestConfiguration;
import software.tnb.common.product.ProductType;
import software.tnb.product.cq.configuration.QuarkusConfiguration;
+import software.tnb.product.csb.configuration.SpringBootConfiguration;
public enum ProductConfiguration {
ALL,
QUARKUS, QUARKUS_JVM, QUARKUS_JVM_LOCAL, QUARKUS_JVM_OPENSHIFT, QUARKUS_NATIVE, QUARKUS_NATIVE_LOCAL, QUARKUS_NATIVE_OPENSHIFT,
CAMEL_K,
- SPRINGBOOT, SPRINGBOOT_JVM_LOCAL, SPRINGBOOT_JVM_OPENSHIFT;
+ SPRINGBOOT, SPRINGBOOT_JVM_LOCAL, SPRINGBOOT_JVM_OPENSHIFT, SPRINGBOOT_TOMCAT;
public boolean isCurrentEnv() {
return switch (this) {
@@ -33,6 +34,8 @@ public boolean isCurrentEnv() {
&& !OpenshiftConfiguration.isOpenshift();
case SPRINGBOOT_JVM_OPENSHIFT -> TestConfiguration.product() == ProductType.CAMEL_SPRINGBOOT
&& OpenshiftConfiguration.isOpenshift();
+ case SPRINGBOOT_TOMCAT -> TestConfiguration.product() == ProductType.CAMEL_SPRINGBOOT
+ && SpringBootConfiguration.isTomcat();
default -> true;
};
}
diff --git a/fuse-products/src/main/java/software/tnb/product/csb/TomcatCamelSpringBoot.java b/fuse-products/src/main/java/software/tnb/product/csb/TomcatCamelSpringBoot.java
new file mode 100644
index 000000000..d906463bc
--- /dev/null
+++ b/fuse-products/src/main/java/software/tnb/product/csb/TomcatCamelSpringBoot.java
@@ -0,0 +1,31 @@
+package software.tnb.product.csb;
+
+import software.tnb.product.LocalProduct;
+import software.tnb.product.Product;
+import software.tnb.product.application.App;
+import software.tnb.product.csb.application.TomcatSpringBootApp;
+import software.tnb.product.integration.builder.AbstractIntegrationBuilder;
+
+import com.google.auto.service.AutoService;
+
+@AutoService(Product.class)
+public class TomcatCamelSpringBoot extends LocalProduct {
+
+ private TomcatSpringBootApp app;
+
+ @Override
+ public App createIntegrationApp(AbstractIntegrationBuilder> integrationBuilder) {
+ app = new TomcatSpringBootApp(integrationBuilder);
+ return app;
+ }
+
+ @Override
+ public void setupProduct() {
+ super.setupProduct();
+ }
+
+ @Override
+ public void teardownProduct() {
+ app.stop();
+ }
+}
diff --git a/fuse-products/src/main/java/software/tnb/product/csb/application/TomcatSpringBootApp.java b/fuse-products/src/main/java/software/tnb/product/csb/application/TomcatSpringBootApp.java
new file mode 100644
index 000000000..5a666c0b7
--- /dev/null
+++ b/fuse-products/src/main/java/software/tnb/product/csb/application/TomcatSpringBootApp.java
@@ -0,0 +1,233 @@
+package software.tnb.product.csb.application;
+
+import software.tnb.common.config.TestConfiguration;
+import software.tnb.product.application.App;
+import software.tnb.product.application.Phase;
+import software.tnb.product.csb.configuration.SpringBootConfiguration;
+import software.tnb.product.customizer.Customizer;
+import software.tnb.product.customizer.component.rest.RestCustomizer;
+import software.tnb.product.integration.builder.AbstractIntegrationBuilder;
+import software.tnb.product.log.FileLog;
+import software.tnb.product.log.Log;
+import software.tnb.product.log.stream.LogStream;
+import software.tnb.product.util.ZipUtils;
+import software.tnb.product.util.maven.BuildRequest;
+import software.tnb.product.util.maven.Maven;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.maven.model.Dependency;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class TomcatSpringBootApp extends App {
+ private static final Logger LOG = LoggerFactory.getLogger(TomcatSpringBootApp.class);
+
+ private AbstractIntegrationBuilder> integrationBuilder;
+ private Path tomcatTmpDirectory;
+ private Path tomcatHome = null;
+ private Process tomcatProcess = null;
+ private static final String TOMCAT_PARENT_DIRECTORY = "tomcat";
+ private static final String TOMCAT_ARCHIVE_NAME = "tomcat-archive.zip";
+
+ @Override
+ public Log getLog() {
+ return new FileLog(getLogPath());
+ }
+
+ public TomcatSpringBootApp(AbstractIntegrationBuilder> integrationBuilder) {
+ super(integrationBuilder.getIntegrationName());
+
+ downloadTomcat();
+
+ this.integrationBuilder = integrationBuilder;
+ }
+
+ private void startTomcat() {
+ File[] file = tomcatTmpDirectory.resolve(TOMCAT_PARENT_DIRECTORY).toFile().listFiles();
+ for (File f : file) {
+ if (f.isDirectory()) {
+ if (f.getName().contains("apache-tomcat")) {
+ tomcatHome = f.toPath();
+ } else if (f.getName().contains("jws")) {
+ tomcatHome = f.toPath().resolve("tomcat"); // Case JWS
+ }
+ }
+ }
+
+ if (tomcatHome == null) {
+ throw new RuntimeException("Could not find Tomcat home in " + tomcatTmpDirectory.resolve(TOMCAT_PARENT_DIRECTORY));
+ }
+
+ Path logFile = getLogPath();
+ // startup.sh starts on another process, let's use catalina run so that we can control the lifecyle
+ ProcessBuilder processBuilder = new ProcessBuilder(tomcatHome + File.separator + "bin" + File.separator + "catalina.sh", "run")
+ .redirectError(logFile.toFile())
+ .redirectOutput(logFile.toFile());
+
+ try {
+ tomcatProcess = processBuilder.start();
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+ if (tomcatProcess != null) {
+ tomcatProcess.destroyForcibly();
+ }
+ }));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ LOG.info("Starting tomcat in {}", tomcatHome);
+ }
+
+ private void downloadTomcat() {
+ try {
+ tomcatTmpDirectory = Files.createTempDirectory("tnb-tomcat");
+ LOG.info("Downloading tomcat in {}", tomcatTmpDirectory);
+ FileUtils.copyURLToFile(new URL(SpringBootConfiguration.tomcatZipUrl()), tomcatTmpDirectory.resolve(TOMCAT_ARCHIVE_NAME).toFile());
+
+ ZipUtils.unzip(tomcatTmpDirectory.resolve(TOMCAT_ARCHIVE_NAME), tomcatTmpDirectory.resolve(TOMCAT_PARENT_DIRECTORY));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void start() {
+ startTomcat();
+
+ // Let's remove restcustomizer, since it exclude tomcat
+ List customizers = integrationBuilder.getCustomizers()
+ .stream().filter(customizer -> customizer instanceof RestCustomizer)
+ .collect(Collectors.toList());
+ integrationBuilder.getCustomizers().remove(customizers);
+
+ // spring web is needed and tomcat is provided
+ Dependency providedTomcatDependency = new Dependency();
+ providedTomcatDependency.setArtifactId("spring-boot-starter-tomcat");
+ providedTomcatDependency.setGroupId("org.springframework.boot");
+ providedTomcatDependency.setScope("provided");
+
+ integrationBuilder.dependencies(
+ Maven.createDependency("org.springframework.boot:spring-boot-starter-web"),
+ providedTomcatDependency);
+
+ // Create the integration
+ new SpringBootApp(integrationBuilder) {
+ @Override
+ public void start() {
+ // We just need the application to be generated, not run
+ }
+
+ @Override
+ public void stop() {
+ // do nothing
+ }
+
+ @Override
+ public boolean isReady() {
+ return true;
+ }
+
+ @Override
+ public boolean isFailed() {
+ return false;
+ }
+ }.start();
+
+ // replace packaging and extend SpringBootServletInitializer
+ try {
+ Path pomPath = TestConfiguration.appLocation().resolve(name).resolve("pom.xml");
+ String pom = Files.readString(pomPath);
+ pom = pom.replace("" + name + "",
+ "" + name + "war");
+ Files.write(pomPath, pom.getBytes(StandardCharsets.UTF_8));
+
+ Path mainPath = TestConfiguration.appLocation().resolve(name)
+ .resolve("src")
+ .resolve("main")
+ .resolve("java")
+ .resolve("com")
+ .resolve("test")
+ .resolve("MySpringBootApplication.java");
+ String main = Files.readString(mainPath);
+
+ main = main.replace("class MySpringBootApplication {",
+ """
+ class MySpringBootApplication extends org.springframework.boot.web.servlet.support.SpringBootServletInitializer {
+
+ @Override
+ protected org.springframework.boot.builder.SpringApplicationBuilder
+ configure(org.springframework.boot.builder.SpringApplicationBuilder application) {
+ return application.sources(MySpringBootApplication.class);
+ }
+ """);
+ Files.write(mainPath, main.getBytes(StandardCharsets.UTF_8));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ // package application
+ BuildRequest.Builder requestBuilder = new BuildRequest.Builder()
+ .withBaseDirectory(TestConfiguration.appLocation().resolve(name))
+ .withGoals("clean", "package")
+ .withProperties(Map.of(
+ "skipTests", "true"
+ ))
+ .withLogFile(getLogPath(Phase.BUILD))
+ .withLogMarker(LogStream.marker(name, Phase.BUILD));
+
+ LOG.info("Building {} application project for tomcat", name);
+ Maven.invoke(requestBuilder.build());
+
+ // copy generated WAR in tomcat
+ String warName = name + "-1.0.0-SNAPSHOT.war";
+ try {
+ Files.copy(TestConfiguration.appLocation().resolve(name).resolve("target").resolve(warName),
+ tomcatHome.resolve("webapps").resolve(warName));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void stop() {
+ if (logStream != null) {
+ logStream.stop();
+ }
+
+ if (log != null) {
+ log.save();
+ }
+
+ if (tomcatProcess != null) {
+ try {
+ Process shutdown = new ProcessBuilder(tomcatHome + File.separator + "bin" + File.separator + "shutdown.sh")
+ .start();
+ shutdown.waitFor();
+
+ tomcatProcess.destroyForcibly();
+ } catch (IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ @Override
+ public boolean isReady() {
+ return tomcatProcess != null && tomcatProcess.isAlive();
+ }
+
+ @Override
+ public boolean isFailed() {
+ return tomcatProcess != null && !tomcatProcess.isAlive();
+ }
+}
diff --git a/fuse-products/src/main/java/software/tnb/product/csb/configuration/SpringBootConfiguration.java b/fuse-products/src/main/java/software/tnb/product/csb/configuration/SpringBootConfiguration.java
index 00f5b533a..621be0c6e 100644
--- a/fuse-products/src/main/java/software/tnb/product/csb/configuration/SpringBootConfiguration.java
+++ b/fuse-products/src/main/java/software/tnb/product/csb/configuration/SpringBootConfiguration.java
@@ -31,6 +31,9 @@ public class SpringBootConfiguration extends CamelConfiguration {
public static final String OPENSHIFT_SB_RESULT_IMAGE_REPOSITORY = "openshift-sb.result-image.repo";
+ public static final String TOMCAT_ZIP_DOWNLOAD_URL = "tomcat.zip.download.url";
+ public static final String USE_TOMCAT = "test.use.tomcat";
+
public static String springBootVersion() {
return getProperty(SPRINGBOOT_VERSION, "2.6.1");
}
@@ -104,4 +107,12 @@ public static String openshiftBaseImage() {
public static String openshiftResultImageRepository() {
return getProperty(OPENSHIFT_SB_RESULT_IMAGE_REPOSITORY);
}
+
+ public static String tomcatZipUrl() {
+ return getProperty(TOMCAT_ZIP_DOWNLOAD_URL, "https://dlcdn.apache.org/tomcat/tomcat-10/v10.1.23/bin/apache-tomcat-10.1.23.zip");
+ }
+
+ public static boolean isTomcat() {
+ return getBoolean(USE_TOMCAT, false);
+ }
}
diff --git a/fuse-products/src/main/java/software/tnb/product/util/ZipUtils.java b/fuse-products/src/main/java/software/tnb/product/util/ZipUtils.java
new file mode 100644
index 000000000..ebb57339f
--- /dev/null
+++ b/fuse-products/src/main/java/software/tnb/product/util/ZipUtils.java
@@ -0,0 +1,77 @@
+package software.tnb.product.util;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+public class ZipUtils {
+
+ private ZipUtils() {
+ }
+
+ public static void unzip(Path source, Path target) throws IOException {
+ Set executePermissions = new HashSet<>();
+ executePermissions.add(PosixFilePermission.OTHERS_EXECUTE);
+ executePermissions.add(PosixFilePermission.OTHERS_WRITE);
+ executePermissions.add(PosixFilePermission.OTHERS_READ);
+ executePermissions.add(PosixFilePermission.GROUP_EXECUTE);
+ executePermissions.add(PosixFilePermission.GROUP_WRITE);
+ executePermissions.add(PosixFilePermission.GROUP_READ);
+ executePermissions.add(PosixFilePermission.OWNER_EXECUTE);
+ executePermissions.add(PosixFilePermission.OWNER_WRITE);
+ executePermissions.add(PosixFilePermission.OWNER_READ);
+
+ try (ZipInputStream zis = new ZipInputStream(new FileInputStream(source.toFile()))) {
+
+ // list files in zip
+ ZipEntry zipEntry = zis.getNextEntry();
+
+ while (zipEntry != null) {
+
+ boolean isDirectory = zipEntry.getName().endsWith(File.separator);
+
+ Path newPath = zipSlipProtect(zipEntry, target);
+
+ if (isDirectory) {
+ Files.createDirectories(newPath);
+ } else {
+ if (newPath.getParent() != null) {
+ if (Files.notExists(newPath.getParent())) {
+ Files.createDirectories(newPath.getParent());
+ }
+ }
+
+ Files.copy(zis, newPath, StandardCopyOption.REPLACE_EXISTING);
+ if (newPath.getFileName().toString().endsWith(".sh")) {
+ Files.setPosixFilePermissions(newPath, executePermissions);
+ }
+ }
+
+ zipEntry = zis.getNextEntry();
+ }
+ zis.closeEntry();
+
+ }
+
+ }
+
+ private static Path zipSlipProtect(ZipEntry zipEntry, Path targetDir)
+ throws IOException {
+ Path targetDirResolved = targetDir.resolve(zipEntry.getName());
+
+ Path normalizePath = targetDirResolved.normalize();
+ if (!normalizePath.startsWith(targetDir)) {
+ throw new IOException("Bad zip entry: " + zipEntry.getName());
+ }
+
+ return normalizePath;
+ }
+}
diff --git a/fuse-products/src/test/java/software/tnb/product/TomcatSpringBootTest.java b/fuse-products/src/test/java/software/tnb/product/TomcatSpringBootTest.java
new file mode 100644
index 000000000..ccc23fb92
--- /dev/null
+++ b/fuse-products/src/test/java/software/tnb/product/TomcatSpringBootTest.java
@@ -0,0 +1,53 @@
+package software.tnb.product;
+
+import software.tnb.common.utils.WaitUtils;
+import software.tnb.product.application.App;
+import software.tnb.product.csb.integration.builder.SpringBootIntegrationBuilder;
+import software.tnb.product.parent.TestParent;
+import software.tnb.util.maven.TestMaven;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Optional;
+
+@Tag("integration")
+public class TomcatSpringBootTest extends TestParent {
+
+ @Test
+ public void test() throws Exception {
+ String productName = "camelspringboot";
+
+ System.setProperty("test.product", productName);
+ System.setProperty("test.use.tomcat", "true");
+ TestMaven.setupDefaultMaven();
+ Product product = ProductFactory.create();
+
+ String appName = "xml-timer-app-" + productName;
+
+ App application = product.createIntegration(new SpringBootIntegrationBuilder(appName)
+ .fromSpringBootXmlCamelContext(
+ SpringBootXmlGeneratorTest.class.getPackageName().replace(".", File.separator) + File.separator
+ + "camel-context.xml")
+ .dependencies("cron", "log")
+ );
+
+ Path app = Paths.get("target", appName);
+
+ Optional routeBuilderFile = Files.walk(app).filter(file -> "camel-context.xml".equals(file.getFileName().toString()))
+ .findAny();
+
+ Assertions.assertTrue(routeBuilderFile.isPresent(), "camel context xml not present");
+
+ WaitUtils.sleep(3000L);
+
+ Assertions.assertTrue(application.getLog().contains("The message contains I was fired at"));
+
+ application.stop();
+ }
+}