org.testcontainers.containers.JdbcDatabaseContainer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jdbc Show documentation
Show all versions of jdbc Show documentation
Isolated container management for Java code testing
The newest version!
package org.testcontainers.containers;
import com.github.dockerjava.api.command.InspectContainerResponse;
import lombok.NonNull;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.testcontainers.containers.traits.LinkableContainer;
import org.testcontainers.delegate.DatabaseDelegate;
import org.testcontainers.ext.ScriptUtils;
import org.testcontainers.jdbc.JdbcDatabaseDelegate;
import org.testcontainers.utility.DockerImageName;
import org.testcontainers.utility.MountableFile;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* Base class for containers that expose a JDBC connection
*/
public abstract class JdbcDatabaseContainer>
extends GenericContainer
implements LinkableContainer {
private static final Object DRIVER_LOAD_MUTEX = new Object();
private Driver driver;
private List initScriptPaths = new ArrayList<>();
protected Map parameters = new HashMap<>();
protected Map urlParameters = new HashMap<>();
private int startupTimeoutSeconds = 120;
private int connectTimeoutSeconds = 120;
private static final String QUERY_PARAM_SEPARATOR = "&";
/**
* @deprecated use {@link #JdbcDatabaseContainer(DockerImageName)} instead
*/
public JdbcDatabaseContainer(@NonNull final String dockerImageName) {
this(DockerImageName.parse(dockerImageName));
}
public JdbcDatabaseContainer(@NonNull final Future image) {
super(image);
}
public JdbcDatabaseContainer(final DockerImageName dockerImageName) {
super(dockerImageName);
}
/**
* @return the name of the actual JDBC driver to use
*/
public abstract String getDriverClassName();
/**
* @return a JDBC URL that may be used to connect to the dockerized DB
*/
public abstract String getJdbcUrl();
/**
* @return the database name
*/
public String getDatabaseName() {
throw new UnsupportedOperationException();
}
/**
* @return the standard database username that should be used for connections
*/
public abstract String getUsername();
/**
* @return the standard password that should be used for connections
*/
public abstract String getPassword();
/**
* @return a test query string suitable for testing that this particular database type is alive
*/
protected abstract String getTestQueryString();
public SELF withUsername(String username) {
throw new UnsupportedOperationException();
}
public SELF withPassword(String password) {
throw new UnsupportedOperationException();
}
public SELF withDatabaseName(String dbName) {
throw new UnsupportedOperationException();
}
public SELF withUrlParam(String paramName, String paramValue) {
urlParameters.put(paramName, paramValue);
return self();
}
/**
* Set startup time to allow, including image pull time, in seconds.
*
* @param startupTimeoutSeconds startup time to allow, including image pull time, in seconds
* @return self
*/
public SELF withStartupTimeoutSeconds(int startupTimeoutSeconds) {
this.startupTimeoutSeconds = startupTimeoutSeconds;
return self();
}
/**
* Set time to allow for the database to start and establish an initial connection, in seconds.
*
* @param connectTimeoutSeconds time to allow for the database to start and establish an initial connection in seconds
* @return self
*/
public SELF withConnectTimeoutSeconds(int connectTimeoutSeconds) {
this.connectTimeoutSeconds = connectTimeoutSeconds;
return self();
}
/**
* Sets a script for initialization.
*
* @param initScriptPath path to the script file
* @return self
*/
public SELF withInitScript(String initScriptPath) {
this.initScriptPaths = new ArrayList<>();
this.initScriptPaths.add(initScriptPath);
return self();
}
/**
* Sets an ordered array of scripts for initialization.
*
* @param initScriptPaths paths to the script files
* @return self
*/
public SELF withInitScripts(String... initScriptPaths) {
return withInitScripts(Arrays.asList(initScriptPaths));
}
/**
* Sets an ordered collection of scripts for initialization.
*
* @param initScriptPaths paths to the script files
* @return self
*/
public SELF withInitScripts(Iterable initScriptPaths) {
this.initScriptPaths = new ArrayList<>();
initScriptPaths.forEach(this.initScriptPaths::add);
return self();
}
@SneakyThrows(InterruptedException.class)
@Override
protected void waitUntilContainerStarted() {
logger()
.info(
"Waiting for database connection to become available at {} using query '{}'",
getJdbcUrl(),
getTestQueryString()
);
// Repeatedly try and open a connection to the DB and execute a test query
long start = System.nanoTime();
Exception lastConnectionException = null;
while ((System.nanoTime() - start) < TimeUnit.SECONDS.toNanos(startupTimeoutSeconds)) {
if (!isRunning()) {
Thread.sleep(100L);
} else {
try (Connection connection = createConnection(""); Statement statement = connection.createStatement()) {
boolean testQuerySucceeded = statement.execute(this.getTestQueryString());
if (testQuerySucceeded) {
return;
}
} catch (NoDriverFoundException e) {
// we explicitly want this exception to fail fast without retries
throw e;
} catch (Exception e) {
lastConnectionException = e;
// ignore so that we can try again
logger().debug("Failure when trying test query", e);
Thread.sleep(100L);
}
}
}
throw new IllegalStateException(
String.format(
"Container is started, but cannot be accessed by (JDBC URL: %s), please check container logs",
this.getJdbcUrl()
),
lastConnectionException
);
}
@Override
protected void containerIsStarted(InspectContainerResponse containerInfo) {
logger().info("Container is started (JDBC URL: {})", this.getJdbcUrl());
runInitScriptIfRequired();
}
/**
* Obtain an instance of the correct JDBC driver for this particular database container type
*
* @return a JDBC Driver
*/
public Driver getJdbcDriverInstance() throws NoDriverFoundException {
synchronized (DRIVER_LOAD_MUTEX) {
if (driver == null) {
try {
driver = (Driver) Class.forName(this.getDriverClassName()).newInstance();
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
throw new NoDriverFoundException("Could not get Driver", e);
}
}
}
return driver;
}
/**
* Creates a connection to the underlying containerized database instance.
*
* @param queryString query string parameters that should be appended to the JDBC connection URL.
* The '?' character must be included
* @return a Connection
* @throws SQLException if there is a repeated failure to create the connection
*/
public Connection createConnection(String queryString) throws SQLException, NoDriverFoundException {
return createConnection(queryString, new Properties());
}
/**
* Creates a connection to the underlying containerized database instance.
*
* @param queryString query string parameters that should be appended to the JDBC connection URL.
* The '?' character must be included
* @param info additional properties to be passed to the JDBC driver
* @return a Connection
* @throws SQLException if there is a repeated failure to create the connection
*/
public Connection createConnection(String queryString, Properties info)
throws SQLException, NoDriverFoundException {
Properties properties = new Properties(info);
properties.put("user", this.getUsername());
properties.put("password", this.getPassword());
final String url = constructUrlForConnection(queryString);
final Driver jdbcDriverInstance = getJdbcDriverInstance();
SQLException lastException = null;
try {
long start = System.nanoTime();
// give up if we hit the time limit or the container stops running for some reason
while ((System.nanoTime() - start < TimeUnit.SECONDS.toNanos(connectTimeoutSeconds)) && isRunning()) {
try {
logger()
.debug(
"Trying to create JDBC connection using {} to {} with properties: {}",
jdbcDriverInstance.getClass().getName(),
url,
properties
);
return jdbcDriverInstance.connect(url, properties);
} catch (SQLException e) {
lastException = e;
Thread.sleep(100L);
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
throw new SQLException("Could not create new connection", lastException);
}
/**
* Template method for constructing the JDBC URL to be used for creating {@link Connection}s.
* This should be overridden if the JDBC URL and query string concatenation or URL string
* construction needs to be different to normal.
*
* @param queryString query string parameters that should be appended to the JDBC connection URL.
* The '?' character must be included
* @return a full JDBC URL including queryString
*/
protected String constructUrlForConnection(String queryString) {
String baseUrl = getJdbcUrl();
if ("".equals(queryString)) {
return baseUrl;
}
if (!queryString.startsWith("?")) {
throw new IllegalArgumentException("The '?' character must be included");
}
return baseUrl.contains("?")
? baseUrl + QUERY_PARAM_SEPARATOR + queryString.substring(1)
: baseUrl + queryString;
}
protected String constructUrlParameters(String startCharacter, String delimiter) {
return constructUrlParameters(startCharacter, delimiter, StringUtils.EMPTY);
}
protected String constructUrlParameters(String startCharacter, String delimiter, String endCharacter) {
String urlParameters = "";
if (!this.urlParameters.isEmpty()) {
String additionalParameters =
this.urlParameters.entrySet().stream().map(Object::toString).collect(Collectors.joining(delimiter));
urlParameters = startCharacter + additionalParameters + endCharacter;
}
return urlParameters;
}
@Deprecated
protected void optionallyMapResourceParameterAsVolume(
@NotNull String paramName,
@NotNull String pathNameInContainer,
@NotNull String defaultResource
) {
optionallyMapResourceParameterAsVolume(paramName, pathNameInContainer, defaultResource, null);
}
protected void optionallyMapResourceParameterAsVolume(
@NotNull String paramName,
@NotNull String pathNameInContainer,
@NotNull String defaultResource,
@Nullable Integer fileMode
) {
String resourceName = parameters.getOrDefault(paramName, defaultResource);
if (resourceName != null) {
final MountableFile mountableFile = MountableFile.forClasspathResource(resourceName, fileMode);
withCopyFileToContainer(mountableFile, pathNameInContainer);
}
}
/**
* Load init script content and apply it to the database if initScriptPath is set
*/
protected void runInitScriptIfRequired() {
initScriptPaths
.stream()
.filter(Objects::nonNull)
.forEach(path -> ScriptUtils.runInitScript(getDatabaseDelegate(), path));
}
public void setParameters(Map parameters) {
this.parameters = parameters;
}
@SuppressWarnings("unused")
public void addParameter(String paramName, String value) {
this.parameters.put(paramName, value);
}
/**
* @return startup time to allow, including image pull time, in seconds
* @deprecated should not be overridden anymore, use {@link #withStartupTimeoutSeconds(int)} in constructor instead
*/
@Deprecated
protected int getStartupTimeoutSeconds() {
return startupTimeoutSeconds;
}
/**
* @return time to allow for the database to start and establish an initial connection, in seconds
* @deprecated should not be overridden anymore, use {@link #withConnectTimeoutSeconds(int)} in constructor instead
*/
@Deprecated
protected int getConnectTimeoutSeconds() {
return connectTimeoutSeconds;
}
protected DatabaseDelegate getDatabaseDelegate() {
return new JdbcDatabaseDelegate(this, "");
}
public static class NoDriverFoundException extends RuntimeException {
public NoDriverFoundException(String message, Throwable e) {
super(message, e);
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy