diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index 1458b89d2f7..99655d4d9ee 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -32,6 +32,7 @@ body:
- K3S
- K6
- Kafka
+ - Liberty
- LocalStack
- MariaDB
- Milvus
diff --git a/.github/ISSUE_TEMPLATE/enhancement.yaml b/.github/ISSUE_TEMPLATE/enhancement.yaml
index 2baf2be99eb..8d95de837b7 100644
--- a/.github/ISSUE_TEMPLATE/enhancement.yaml
+++ b/.github/ISSUE_TEMPLATE/enhancement.yaml
@@ -32,6 +32,7 @@ body:
- K3S
- K6
- Kafka
+ - Liberty
- LocalStack
- MariaDB
- Milvus
diff --git a/.github/ISSUE_TEMPLATE/feature.yaml b/.github/ISSUE_TEMPLATE/feature.yaml
index fb5013a41d3..b1d918fa1f9 100644
--- a/.github/ISSUE_TEMPLATE/feature.yaml
+++ b/.github/ISSUE_TEMPLATE/feature.yaml
@@ -32,6 +32,7 @@ body:
- K3S
- K6
- Kafka
+ - Liberty
- LocalStack
- MariaDB
- Milvus
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 0bb82dce9f4..a963fd94f21 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -34,6 +34,12 @@ updates:
# Explicit entry for each module
- package-ecosystem: "gradle"
+ directory: "/modules/application-server-commons"
+ schedule:
+ interval: "monthly"
+ open-pull-requests-limit: 10
+ - package-ecosystem: "gradle"
+ directory: "/modules/azure"
directory: "/modules/activemq"
schedule:
interval: "monthly"
@@ -161,6 +167,11 @@ updates:
schedule:
interval: "weekly"
open-pull-requests-limit: 10
+ - package-ecosystem: "gradle"
+ directory: "/modules/liberty"
+ schedule:
+ interval: "monthly"
+ open-pull-requests-limit: 10
- package-ecosystem: "gradle"
directory: "/modules/localstack"
schedule:
diff --git a/docs/modules/liberty.md b/docs/modules/liberty.md
new file mode 100644
index 00000000000..a25b54726f8
--- /dev/null
+++ b/docs/modules/liberty.md
@@ -0,0 +1,56 @@
+# Liberty Containers
+
+Testcontainers can be used to automatically instantiate and manage [Open Liberty](https://openliberty.io/) and [WebSphere Liberty](https://www.ibm.com/products/websphere-liberty/) containers.
+More precisely Testcontainers uses the official Docker images for [Open Liberty](https://hub.docker.com/_/open-liberty) or [WebSphere Liberty](https://hub.docker.com/_/websphere-liberty)
+
+## Benefits
+
+* Easier integration testing for application developers.
+* Easier functional testing for platform development.
+
+## Example
+
+Create a `LibertyContainer` to use it in your tests:
+
+[Creating a LibertyContainer](../../modules/liberty/src/test/java/org/testcontainers/liberty/LibertyContainerTest.java) inside_block:constructorWithVersion
+
+
+Now you can perform integration testing, in this example we are using [RestAssured](https://rest-assured.io/) to query a RESTful web service running in Liberty.
+
+
+[RESTful Test](../../modules/liberty/src/test/java/org/testcontainers/liberty/LibertyContainerTest.java) inside_block:testRestEndpoint
+
+
+## Multi-container usage
+
+If your Liberty server needs to connect to a data provider, message provider,
+or other service that can also be run as a container you can connect them using a network:
+
+* Run your other container on the same network as Liberty container, e.g.:
+
+[Network](../../modules/liberty/src/test/java/org/testcontainers/liberty/LibertyContainerTest.java) inside_block:constructorMockDatabase
+
+* Use network aliases and unmapped ports to configure an environment variable that can be access from your Application server.
+
+[Configure Liberty](../../modules/liberty/src/test/java/org/testcontainers/liberty/LibertyContainerTest.java) inside_block:configureLiberty
+
+
+You will need to explicitly create a network and set it on the Liberty container as well as on your other containers that Liberty communicates with.
+
+## Adding this module to your project dependencies
+
+Add the following dependency to your `pom.xml`/`build.gradle` file:
+
+=== "Gradle"
+```groovy
+testImplementation "org.testcontainers:liberty:{{latest_version}}"
+```
+=== "Maven"
+```xml
+
+org.testcontainers
+liberty
+{{latest_version}}
+test
+
+```
diff --git a/modules/application-server-commons/build.gradle b/modules/application-server-commons/build.gradle
new file mode 100644
index 00000000000..58404e95279
--- /dev/null
+++ b/modules/application-server-commons/build.gradle
@@ -0,0 +1,9 @@
+description = "Testcontainers :: Application Server Common"
+
+dependencies {
+ api project(':testcontainers')
+
+ compileOnly 'org.jetbrains:annotations:24.0.1'
+ implementation 'org.jboss.shrinkwrap:shrinkwrap-impl-base:1.2.6'
+ testImplementation 'org.assertj:assertj-core:3.24.2'
+}
diff --git a/modules/application-server-commons/src/main/java/org/testcontainers/applicationserver/ApplicationServerContainer.java b/modules/application-server-commons/src/main/java/org/testcontainers/applicationserver/ApplicationServerContainer.java
new file mode 100644
index 00000000000..0190a4beb94
--- /dev/null
+++ b/modules/application-server-commons/src/main/java/org/testcontainers/applicationserver/ApplicationServerContainer.java
@@ -0,0 +1,371 @@
+package org.testcontainers.applicationserver;
+
+import lombok.NonNull;
+import org.apache.commons.lang3.SystemUtils;
+import org.jboss.shrinkwrap.api.Archive;
+import org.jboss.shrinkwrap.api.exporter.ZipExporter;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.Base58;
+import org.testcontainers.utility.DockerImageName;
+import org.testcontainers.utility.MountableFile;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Future;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Represents a JavaEE, JakartaEE, or Microprofile application platform running inside a Docker container
+ */
+public abstract class ApplicationServerContainer extends GenericContainer {
+
+ // List of archive(s) to install
+ List archives = new ArrayList<>();
+
+ // Where to save runtime archives
+ private static final String TESTCONTAINERS_TMP_DIR_PREFIX = ".testcontainers-archive-";
+
+ private static final String OS_MAC_TMP_DIR = "/tmp";
+
+ private static Path tempDirectory;
+
+ // How to query an application
+ private Integer httpPort;
+
+ private String appContextRoot;
+
+ // How to query application for readiness
+ private Integer readinessPort;
+
+ private String readinessPath;
+
+ // How long to wait for readiness
+ @NonNull
+ private Duration httpWaitTimeout = Duration.ofSeconds(60);
+
+ // Expected path for Microprofile platforms to query for readiness
+ static final String MP_HEALTH_READINESS_PATH = "/health/ready";
+
+ //Constructors
+
+ public ApplicationServerContainer(@NonNull final Future image) {
+ super(image);
+ }
+
+ public ApplicationServerContainer(final DockerImageName dockerImageName) {
+ super(dockerImageName);
+ }
+
+ //Overrides
+
+ @Override
+ protected void configure() {
+ super.configure();
+
+ // Setup default wait strategy
+ waitingFor(
+ Wait
+ .forHttp(readinessPath != null ? readinessPath : appContextRoot)
+ .forPort(readinessPort != null ? readinessPort : httpPort)
+ .withStartupTimeout(httpWaitTimeout)
+ );
+
+ // Copy applications
+ for (MountableFile archive : archives) {
+ withCopyFileToContainer(archive, getApplicationInstallDirectory() + extractApplicationName(archive));
+ }
+ }
+
+ @Override
+ protected void containerIsCreated(String containerId) {
+ if (Objects.isNull(tempDirectory)) {
+ return;
+ }
+
+ try {
+ //Delete files in temp directory
+ for (String file : tempDirectory.toFile().list()) {
+ Files.deleteIfExists(Paths.get(tempDirectory.toString(), file));
+ }
+ //Delete temp directory
+ Files.deleteIfExists(tempDirectory);
+ } catch (IOException e) {
+ logger().info("Unable to delete temporary directory " + tempDirectory.toString(), e);
+ }
+ }
+
+ @Override
+ public void setExposedPorts(List exposedPorts) {
+ if (Objects.isNull(this.httpPort)) {
+ super.setExposedPorts(exposedPorts);
+ return;
+ }
+
+ super.setExposedPorts(appendPort(exposedPorts, this.httpPort));
+ }
+
+ //Configuration
+
+ /**
+ * One or more archives to be deployed to the application platform.
+ *
+ * Calling this method more than once will append new archives to a list to be deployed.
+ *
+ * @param archives - An archive created using shrinkwrap and test runtime
+ * @return self
+ */
+ public ApplicationServerContainer withArchives(@NonNull Archive>... archives) {
+ Stream
+ .of(archives)
+ .forEach(archive -> {
+ String name = archive.getName();
+ Path target = Paths.get(createTempDirectory().toString(), name);
+ archive.as(ZipExporter.class).exportTo(target.toFile(), true);
+ this.archives.add(MountableFile.forHostPath(target));
+ });
+
+ return this;
+ }
+
+ /**
+ * One or more archives to be deployed to the application platform.
+ *
+ * Calling this method more than once will append new archives to a list to be deployed.
+ *
+ * @param archives - A MountableFile which represents an archive that was created prior to test runtime.
+ * @return self
+ */
+ public ApplicationServerContainer withArchives(@NonNull MountableFile... archives) {
+ this.archives.addAll(Arrays.asList(archives));
+ return this;
+ }
+
+ /**
+ * This will set the port used to construct the base application URL, as well as
+ * the port used to determine container readiness in the case of Microprofile platforms.
+ *
+ * Calling this method more than once will replace the current httpPort if it was already set.
+ *
+ * @param httpPort - The HTTP port used by the Application platform
+ * @return self
+ */
+ public ApplicationServerContainer withHttpPort(int httpPort) {
+ if (Objects.nonNull(this.httpPort) && this.httpPort == this.readinessPort) {
+ int oldPort = this.httpPort;
+ this.readinessPort = this.httpPort = httpPort;
+ super.setExposedPorts(replacePort(getExposedPorts(), oldPort, this.httpPort));
+ } else {
+ this.httpPort = httpPort;
+ super.setExposedPorts(appendPort(getExposedPorts(), this.httpPort));
+ }
+
+ return this;
+ }
+
+ /**
+ * This will set the path used to construct the base application URL.
+ *
+ * Calling this method more than once will replace the current appContextRoot if it was already set.
+ *
+ * @param appContextRoot - The application path
+ * @return self
+ */
+ public ApplicationServerContainer withAppContextRoot(@NonNull String appContextRoot) {
+ if (Objects.nonNull(this.appContextRoot) && this.appContextRoot == this.readinessPath) {
+ this.readinessPath = this.appContextRoot = normalizePath(appContextRoot);
+ } else {
+ this.appContextRoot = normalizePath(appContextRoot);
+ }
+
+ return this;
+ }
+
+ /**
+ * By default, the ApplicationServerContainer will be configured with a HttpWaitStrategy and wait
+ * for a successful response from the {@link ApplicationServerContainer#getApplicationURL()}.
+ *
+ * This method will overwrite the path used by the HttpWaitStrategy but will keep the httpPort.
+ *
+ * Calling this method more than once will replace the current readinessPath if it was already set.
+ *
+ * @param readinessPath - The path to be polled for readiness.
+ * @return self
+ */
+ public ApplicationServerContainer withReadiness(String readinessPath) {
+ return withReadiness(readinessPath, this.httpPort);
+ }
+
+ /**
+ * By default, the ApplicationServerContainer will be configured with a HttpWaitStrategy and wait
+ * for a successful response from the {@link ApplicationServerContainer#getApplicationURL()}.
+ *
+ * This method will overwrite the path used by the HttpWaitStrategy.
+ * This method will overwrite the port used by the HttpWaitStrategy.
+ *
+ * Calling this method more than once will replace the current readinessPath and readinessPort if any were already set.
+ *
+ * @param readinessPath - The path to be polled for readiness.
+ * @param readinessPort - The port that should be used for the readiness check.
+ * @return self
+ */
+ public ApplicationServerContainer withReadiness(@NonNull String readinessPath, @NonNull Integer readinessPort) {
+ this.readinessPath = normalizePath(readinessPath);
+ this.readinessPort = readinessPort;
+ return this;
+ }
+
+ /**
+ * Set the timeout configured on the HttpWaitStrategy.
+ *
+ * Calling this method more than once will replace the current httpWaitTimeout if it was already set.
+ * Default value is 60 seconds
+ *
+ * @param httpWaitTimeout - The duration of time to wait for a successful http response
+ * @return self
+ */
+ public ApplicationServerContainer withHttpWaitTimeout(Duration httpWaitTimeout) {
+ this.httpWaitTimeout = httpWaitTimeout;
+ return this;
+ }
+
+ //Getters
+
+ /**
+ * The URL used to determine container readiness.
+ * If a readiness port and path were configured, those will be used to construct this URL.
+ * Otherwise, the readiness path will default to the httpPort and application context root.
+ *
+ * @return - The readiness URL
+ */
+ public String getReadinessURL() {
+ if (Objects.isNull(this.readinessPath) || Objects.isNull(this.readinessPort)) {
+ return getApplicationURL();
+ }
+
+ return "http://" + getHost() + ':' + getMappedPort(this.readinessPort) + this.readinessPath;
+ }
+
+ /**
+ * The URL where the application is running.
+ * The application URL is a concatenation of the baseURL {@link ApplicationServerContainer#getBaseURL()} with the appContextRoot.
+ * This is the URL that the test client should use to connect to an application running on the application platform.
+ *
+ * @return - The application URL
+ */
+ public String getApplicationURL() {
+ Objects.requireNonNull(this.appContextRoot);
+ return getBaseURL() + appContextRoot;
+ }
+
+ /**
+ * The base URL for the application platform.
+ * This is the URL that the test client should use to connect to the application platform.
+ *
+ * @return - The base URL
+ */
+ public String getBaseURL() {
+ Objects.requireNonNull(this.httpPort);
+ return "http://" + getHost() + ':' + getMappedPort(this.httpPort);
+ }
+
+ // Abstract
+
+ /**
+ * Each implementation will need to provide an install directory
+ * where their platform expects applications to be copied into.
+ *
+ * @return - the application install directory
+ */
+ protected abstract String getApplicationInstallDirectory();
+
+ // Helpers
+
+ /**
+ * Create a temporary directory where runtime archives can be exported and copied to the application container.
+ *
+ * @return - The temporary directory path
+ */
+ protected static Path createTempDirectory() {
+ if (Objects.nonNull(tempDirectory)) {
+ return tempDirectory;
+ }
+
+ try {
+ if (SystemUtils.IS_OS_MAC) {
+ tempDirectory = Files.createTempDirectory(Paths.get(OS_MAC_TMP_DIR), TESTCONTAINERS_TMP_DIR_PREFIX);
+ } else {
+ tempDirectory = Files.createTempDirectory(TESTCONTAINERS_TMP_DIR_PREFIX);
+ }
+ } catch (IOException e) {
+ tempDirectory = new File(TESTCONTAINERS_TMP_DIR_PREFIX + Base58.randomString(5)).toPath();
+ }
+
+ return tempDirectory;
+ }
+
+ private static String extractApplicationName(MountableFile file) throws IllegalArgumentException {
+ String path = file.getFilesystemPath();
+ //TODO would any application servers support .zip?
+ if (path.matches(".*\\.jar|.*\\.war|.*\\.ear|.*\\.rar")) {
+ return path.substring(path.lastIndexOf("/"));
+ }
+ throw new IllegalArgumentException("File did not contain an application archive");
+ }
+
+ /**
+ * Appends a port to a list of ports at position 0 and ensures port list does
+ * not contain duplicates to ensure this order is maintained.
+ *
+ * @param portList - List of existing ports
+ * @param appendingPort - The port to append
+ * @return - resulting list of ports
+ */
+ protected static List appendPort(List portList, int appendingPort) {
+ portList = new ArrayList<>(portList); //Ensure not immutable
+ portList.add(0, appendingPort);
+ return portList.stream().distinct().collect(Collectors.toList());
+ }
+
+ /**
+ * Replaces a port in a list of ports putting the new port at position 0 and ensures port
+ * list does not contain any duplicates values to ensure this order is maintained.
+ *
+ * @param portList - List of existing ports
+ * @param oldPort - The port to remove
+ * @param newPort - The port to add
+ * @return - resulting list of ports
+ */
+ protected static List replacePort(List portList, Integer oldPort, Integer newPort) {
+ portList = new ArrayList<>(portList); //Ensure not immutable
+ portList.remove(oldPort);
+ portList.add(0, newPort);
+ return portList.stream().distinct().collect(Collectors.toList());
+ }
+
+ /**
+ * Normalizes the path provided by developer input.
+ * - Ensures paths start with '/'
+ * - Ensures paths are separated with exactly one '/'
+ * - Ensures paths do not end with a '/'
+ *
+ * @param paths the list of paths to be normalized
+ * @return The normalized path
+ */
+ protected static String normalizePath(String... paths) {
+ return Stream
+ .of(paths)
+ .flatMap(path -> Stream.of(path.split("/")))
+ .filter(part -> !part.isEmpty())
+ .collect(Collectors.joining("/", "/", ""));
+ }
+}
diff --git a/modules/application-server-commons/src/test/java/org/testcontainers/applicationserver/ApplicationServerContainerTest.java b/modules/application-server-commons/src/test/java/org/testcontainers/applicationserver/ApplicationServerContainerTest.java
new file mode 100644
index 00000000000..ec3894a244b
--- /dev/null
+++ b/modules/application-server-commons/src/test/java/org/testcontainers/applicationserver/ApplicationServerContainerTest.java
@@ -0,0 +1,87 @@
+package org.testcontainers.applicationserver;
+
+import lombok.NonNull;
+import org.junit.Test;
+import org.testcontainers.utility.DockerImageName;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Future;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ApplicationServerContainerTest {
+
+ private static ApplicationServerContainer testContainer;
+
+ @Test
+ public void testNormalizePath() {
+ String expected, actual;
+
+ expected = "/path/to/application";
+ actual = ApplicationServerContainer.normalizePath("path", "to", "application");
+ assertThat(actual).isEqualTo(expected);
+
+ actual = ApplicationServerContainer.normalizePath("path/to", "application");
+ assertThat(actual).isEqualTo(expected);
+
+ actual = ApplicationServerContainer.normalizePath("path/to/application");
+ assertThat(actual).isEqualTo(expected);
+
+ actual = ApplicationServerContainer.normalizePath("path/", "to/", "application/");
+ assertThat(actual).isEqualTo(expected);
+
+ actual = ApplicationServerContainer.normalizePath("path/", "/to/", "/application/");
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @Test
+ public void httpPortMapping() {
+ List expected, actual;
+
+ expected = Arrays.asList(8080, 9080, 9443);
+
+ // Test expose ports, then add httpPort
+ testContainer =
+ new ApplicationServerContainerStub(DockerImageName.parse("open-liberty:kernel-slim-java11-openj9"));
+ testContainer.withExposedPorts(9080, 9443);
+ testContainer.withHttpPort(8080);
+
+ actual = testContainer.getExposedPorts();
+ assertThat(actual).containsExactlyElementsOf(expected);
+
+ // Test httpPort then expose ports
+ testContainer =
+ new ApplicationServerContainerStub(DockerImageName.parse("open-liberty:kernel-slim-java11-openj9"));
+ testContainer.withHttpPort(8080);
+ testContainer.withExposedPorts(9080, 9443);
+
+ actual = testContainer.getExposedPorts();
+ assertThat(actual).containsExactlyElementsOf(expected);
+
+ //Test httpPort then set exposed ports
+ testContainer =
+ new ApplicationServerContainerStub(DockerImageName.parse("open-liberty:kernel-slim-java11-openj9"));
+ testContainer.withHttpPort(8080);
+ testContainer.setExposedPorts(Arrays.asList(9080, 9443));
+
+ actual = testContainer.getExposedPorts();
+ assertThat(actual).containsExactlyElementsOf(expected);
+ }
+
+ static class ApplicationServerContainerStub extends ApplicationServerContainer {
+
+ public ApplicationServerContainerStub(@NonNull Future image) {
+ super(image);
+ }
+
+ public ApplicationServerContainerStub(DockerImageName dockerImageName) {
+ super(dockerImageName);
+ }
+
+ @Override
+ protected String getApplicationInstallDirectory() {
+ return "null";
+ }
+ }
+}
diff --git a/modules/application-server-commons/src/test/resources/logback-test.xml b/modules/application-server-commons/src/test/resources/logback-test.xml
new file mode 100644
index 00000000000..83ef7a1a3ef
--- /dev/null
+++ b/modules/application-server-commons/src/test/resources/logback-test.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n
+
+
+
+
+
+
+
+
+
diff --git a/modules/liberty/build.gradle b/modules/liberty/build.gradle
new file mode 100644
index 00000000000..a797a1af727
--- /dev/null
+++ b/modules/liberty/build.gradle
@@ -0,0 +1,14 @@
+description = "Testcontainers :: Application Server :: Liberty"
+
+dependencies {
+ api project(':application-server-commons')
+
+ compileOnly 'org.jetbrains:annotations:24.0.1'
+
+ implementation 'org.jboss.shrinkwrap:shrinkwrap-impl-base:1.2.6'
+
+ testImplementation project(':mockserver')
+ testImplementation 'io.rest-assured:rest-assured:5.3.0'
+ testImplementation 'jakarta.ws.rs:jakarta.ws.rs-api:3.1.0'
+ testImplementation 'org.assertj:assertj-core:3.24.2'
+}
diff --git a/modules/liberty/src/main/java/org/testcontainers/liberty/LibertyServerContainer.java b/modules/liberty/src/main/java/org/testcontainers/liberty/LibertyServerContainer.java
new file mode 100644
index 00000000000..6d84b0a230b
--- /dev/null
+++ b/modules/liberty/src/main/java/org/testcontainers/liberty/LibertyServerContainer.java
@@ -0,0 +1,128 @@
+package org.testcontainers.liberty;
+
+import lombok.NonNull;
+import org.testcontainers.applicationserver.ApplicationServerContainer;
+import org.testcontainers.images.builder.Transferable;
+import org.testcontainers.utility.DockerImageName;
+import org.testcontainers.utility.MountableFile;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents an Open Liberty or WebSphere Liberty container
+ */
+public class LibertyServerContainer extends ApplicationServerContainer {
+
+ // About the image
+ static final String IMAGE = "open-liberty";
+
+ static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse(IMAGE);
+
+ // Container defaults
+ static final int DEFAULT_HTTP_PORT = 9080;
+
+ static final int DEFAULT_HTTPS_PORT = 9443;
+
+ static final Duration DEFAULT_WAIT_TIMEOUT = Duration.ofSeconds(30);
+
+ private static final String SERVER_CONFIG_DIR = "/config/";
+
+ private static final String APPLICATION_DROPIN_DIR = "/config/dropins/";
+
+ private static final List DEFAULT_FEATURES = Arrays.asList("webProfile-10.0");
+
+ // Container fields
+ private Transferable serverConfiguration;
+
+ private List features = new ArrayList<>();
+
+ // Constructors
+ public LibertyServerContainer(String imageName) {
+ this(DockerImageName.parse(imageName));
+ }
+
+ public LibertyServerContainer(DockerImageName dockerImageName) {
+ super(dockerImageName);
+ dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
+ preconfigure();
+ }
+
+ /**
+ * Configure defaults that can be overridden by developer prior to start()
+ */
+ private void preconfigure() {
+ withHttpPort(DEFAULT_HTTP_PORT);
+ withHttpWaitTimeout(DEFAULT_WAIT_TIMEOUT);
+ }
+
+ // Overrides
+
+ @Override
+ public void configure() {
+ super.configure();
+
+ // Copy server configuration
+ if (Objects.nonNull(serverConfiguration)) {
+ withCopyToContainer(serverConfiguration, SERVER_CONFIG_DIR + "server.xml");
+ return;
+ }
+
+ if (!features.isEmpty()) {
+ withCopyToContainer(generateServerConfiguration(features), SERVER_CONFIG_DIR + "server.xml");
+ return;
+ }
+
+ withCopyToContainer(generateServerConfiguration(DEFAULT_FEATURES), SERVER_CONFIG_DIR + "server.xml");
+ }
+
+ @Override
+ protected String getApplicationInstallDirectory() {
+ return APPLICATION_DROPIN_DIR;
+ }
+
+ // Configuration
+
+ /**
+ * The server configuration file that will be copied to the Liberty container.
+ *
+ * Calling this method more than once will replace the existing serverConfig if set.
+ *
+ * @param serverConfig - server.xml
+ * @return self
+ */
+ public LibertyServerContainer withServerConfiguration(@NonNull MountableFile serverConfig) {
+ this.serverConfiguration = serverConfig;
+ return this;
+ }
+
+ /**
+ * A list of Liberty features to configure on the Liberty container.
+ *
+ * These features will be ignored if a serverConfig file is set.
+ *
+ * @param features - The list of features
+ * @return self
+ */
+ public LibertyServerContainer withFeatures(String... features) {
+ this.features.addAll(Arrays.asList(features));
+ return this;
+ }
+
+ // Helpers
+
+ private static final Transferable generateServerConfiguration(List features) {
+ String configContents = "";
+ configContents += "";
+ for (String feature : features) {
+ configContents += "" + feature + "";
+ }
+ configContents += "";
+ configContents += System.lineSeparator();
+
+ return Transferable.of(configContents);
+ }
+}
diff --git a/modules/liberty/src/test/java/org/testcontainers/liberty/LibertyContainerTest.java b/modules/liberty/src/test/java/org/testcontainers/liberty/LibertyContainerTest.java
new file mode 100644
index 00000000000..c475a5635e1
--- /dev/null
+++ b/modules/liberty/src/test/java/org/testcontainers/liberty/LibertyContainerTest.java
@@ -0,0 +1,132 @@
+package org.testcontainers.liberty;
+
+import io.restassured.builder.RequestSpecBuilder;
+import io.restassured.specification.RequestSpecification;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.testcontainers.applicationserver.ApplicationServerContainer;
+import org.testcontainers.containers.MockServerContainer;
+import org.testcontainers.containers.Network;
+import org.testcontainers.containers.output.OutputFrame;
+import org.testcontainers.utility.DockerImageName;
+import org.testcontainers.utility.MountableFile;
+
+import java.util.function.Consumer;
+
+import static io.restassured.RestAssured.given;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class LibertyContainerTest {
+
+ // constructorWithVersion {
+ static Network network = Network.newNetwork();
+
+ private static DockerImageName libertyImage = DockerImageName.parse("open-liberty:full-java17-openj9");
+
+ private static ApplicationServerContainer liberty = new LibertyServerContainer(libertyImage)
+ .withArchives(ShrinkWrap.create(WebArchive.class, "test.war").addPackage("org.testcontainers.liberty.app"))
+ .withAppContextRoot("test/app/service/")
+ .withNetwork(network);;
+ // }
+
+ // constructorMockDatabase {
+ private static DockerImageName mockDatabaseImage = DockerImageName.parse("mockserver/mockserver:mockserver-5.15.0");
+
+ private static MockServerContainer mockDatabase = new MockServerContainer(mockDatabaseImage)
+ .withNetwork(network)
+ .withNetworkAliases("mockDatabase");
+
+ // }
+
+ @BeforeClass
+ public static void setup() {
+ mockDatabase.withCopyFileToContainer(MountableFile.forClasspathResource("expectation.json"), "/expectation.json");
+ mockDatabase.withEnv("MOCKSERVER_WATCH_INITIALIZATION_JSON", "true");
+ mockDatabase.withEnv("MOCKSERVER_INITIALIZATION_JSON_PATH", "/expectation.json");
+ mockDatabase.start();
+
+ //Note cannot use mockDatabase.getEndpoint() since it will return http://localhost:56254 when instead we need http://mockDatabase:1080
+ //This is unintuitive and should have a better solution.
+
+ // configureLiberty {
+ liberty.withEnv("DB_URL", mockDatabase.getNetworkAliases().get(0) + ":1080");
+ // }
+
+ liberty.start();
+ }
+
+ @AfterClass
+ public static void teardown() {
+ liberty.stop();
+ mockDatabase.stop();
+ }
+
+ @Test
+ public void testURLs() {
+ String expectedURL, actualURL;
+
+ expectedURL =
+ "http://" + liberty.getHost() + ":" + liberty.getMappedPort(LibertyServerContainer.DEFAULT_HTTP_PORT);
+ actualURL = liberty.getBaseURL();
+ assertThat(actualURL).isEqualTo(expectedURL);
+
+ expectedURL += "/test/app/service";
+ actualURL = liberty.getApplicationURL();
+ assertThat(actualURL).isEqualTo(expectedURL);
+
+ actualURL = liberty.getReadinessURL();
+ assertThat(actualURL).isEqualTo(expectedURL);
+ }
+
+ // testRestEndpoint {
+ @Test
+ public void testRestEndpoint() {
+ RequestSpecification request = new RequestSpecBuilder().setBaseUri(liberty.getApplicationURL()).build();
+
+ String expected, actual;
+
+ //Add value to cache
+ given(request)
+ .header("Content-Type", "text/plain")
+ .queryParam("value", "post-it")
+ .when()
+ .post()
+ .then()
+ .statusCode(200);
+
+ //Verify value in cache
+ expected = "[post-it]";
+ actual = given(request).accept("text/plain").when().get("/").then().statusCode(200).extract().body().asString();
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ // }
+
+ @Test
+ public void testResourceConnection() {
+ RequestSpecification mockDbRequest = new RequestSpecBuilder().setBaseUri(mockDatabase.getEndpoint()).build();
+ RequestSpecification libertyRequest = new RequestSpecBuilder().setBaseUri(liberty.getBaseURL()).build();
+
+ String expected, actual;
+
+ expected = "Hello World!";
+
+ //Verify liberty could connect to database
+ actual =
+ given(libertyRequest)
+ .accept("text/plain")
+ .when()
+ .get("/test/app/resource")
+ .then()
+ .statusCode(200)
+ .extract()
+ .body()
+ .asString();
+
+ assertThat(actual).isEqualTo(expected);
+ }
+}
diff --git a/modules/liberty/src/test/java/org/testcontainers/liberty/app/TestApp.java b/modules/liberty/src/test/java/org/testcontainers/liberty/app/TestApp.java
new file mode 100644
index 00000000000..87e62f8d948
--- /dev/null
+++ b/modules/liberty/src/test/java/org/testcontainers/liberty/app/TestApp.java
@@ -0,0 +1,7 @@
+package org.testcontainers.liberty.app;
+
+import jakarta.ws.rs.ApplicationPath;
+import jakarta.ws.rs.core.Application;
+
+@ApplicationPath("/app")
+public class TestApp extends Application {}
diff --git a/modules/liberty/src/test/java/org/testcontainers/liberty/app/TestResource.java b/modules/liberty/src/test/java/org/testcontainers/liberty/app/TestResource.java
new file mode 100644
index 00000000000..a301fc7a1f2
--- /dev/null
+++ b/modules/liberty/src/test/java/org/testcontainers/liberty/app/TestResource.java
@@ -0,0 +1,37 @@
+package org.testcontainers.liberty.app;
+
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+@Path("/resource")
+@Produces(MediaType.TEXT_PLAIN)
+@Consumes(MediaType.TEXT_PLAIN)
+public class TestResource {
+
+ private static final String dbURL = System.getenv("DB_URL");
+
+ @GET
+ public String getConnection() {
+ try {
+ URL url = new URL(dbURL + "/hello");
+ System.out.println("KJA1017 url is: " + url.toString());
+ HttpURLConnection con = (HttpURLConnection) url.openConnection();
+ try (BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()))) {
+ String response = in.readLine();
+ System.out.println("KJA1017 response: " + response);
+ return response;
+ }
+ } catch (Exception e) {
+ System.out.println("KJA1017 error: " + e.toString());
+ }
+ return "FAILURE";
+ }
+}
diff --git a/modules/liberty/src/test/java/org/testcontainers/liberty/app/TestService.java b/modules/liberty/src/test/java/org/testcontainers/liberty/app/TestService.java
new file mode 100644
index 00000000000..e0b6e852311
--- /dev/null
+++ b/modules/liberty/src/test/java/org/testcontainers/liberty/app/TestService.java
@@ -0,0 +1,58 @@
+package org.testcontainers.liberty.app;
+
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.core.MediaType;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Path("/service")
+@Produces(MediaType.TEXT_PLAIN)
+@Consumes(MediaType.TEXT_PLAIN)
+public class TestService {
+
+ private static final List cache = new ArrayList<>();
+
+ @GET
+ public String getAll() {
+ System.out.println("Calling getAll");
+ return cache.toString();
+ }
+
+ @GET
+ @Path("/{value}")
+ public boolean isCached(@PathParam("value") String value) {
+ System.out.println("Calling isCached with value " + value);
+ return cache.contains(value);
+ }
+
+ @POST
+ public String cacheIt(@QueryParam("value") String value) {
+ System.out.println("Calling cacheIt with value " + value);
+ cache.add(value);
+ return value;
+ }
+
+ @POST
+ @Path("/{value}")
+ public boolean updateIt(@PathParam("value") String oldValue, @QueryParam("value") String newValue) {
+ System.out.println("Calling updateIt with oldValue " + oldValue + " and new value " + newValue);
+ boolean result = cache.remove(oldValue);
+ cache.add(newValue);
+ return result;
+ }
+
+ @DELETE
+ @Path("/{value}")
+ public boolean removeIt(@PathParam("value") String value) {
+ System.out.println("Calling removeIt with value " + value);
+ return cache.remove(value);
+ }
+}
diff --git a/modules/liberty/src/test/resources/expectation.json b/modules/liberty/src/test/resources/expectation.json
new file mode 100644
index 00000000000..aced7a9f6c4
--- /dev/null
+++ b/modules/liberty/src/test/resources/expectation.json
@@ -0,0 +1,10 @@
+[
+ {
+ "httpRequest": {
+ "path": "/hello"
+ },
+ "httpResponse": {
+ "body": "Hello World!"
+ }
+ }
+]
diff --git a/modules/liberty/src/test/resources/logback-test.xml b/modules/liberty/src/test/resources/logback-test.xml
new file mode 100644
index 00000000000..83ef7a1a3ef
--- /dev/null
+++ b/modules/liberty/src/test/resources/logback-test.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n
+
+
+
+
+
+
+
+
+