package org.testcontainers.containers;
import org.jetbrains.annotations.NotNull;
import org.rnorth.ducttape.ratelimits.RateLimiter;
import org.rnorth.ducttape.ratelimits.RateLimiterBuilder;
import org.rnorth.ducttape.unreliables.Unreliables;
import org.testcontainers.containers.traits.LinkableContainer;
import org.testcontainers.utility.MountableFile;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
/**
* Base class for containers that expose a JDBC connection
*
* @author richardnorth
*/
public abstract class JdbcDatabaseContainer<SELF extends JdbcDatabaseContainer<SELF>> extends GenericContainer<SELF> implements LinkableContainer {
private static final Object DRIVER_LOAD_MUTEX = new Object();
private Driver driver;
protected Map<String, String> parameters = new HashMap<>();
private static final RateLimiter DB_CONNECT_RATE_LIMIT = RateLimiterBuilder.newBuilder()
.withRate(10, TimeUnit.SECONDS)
.withConstantThroughput()
.build();
public JdbcDatabaseContainer(String dockerImageName) {
super(dockerImageName);
}
/**
* @return the name of the actual JDBC driver to use
*/
protected abstract String getDriverClassName();
/**
* @return a JDBC URL that may be used to connect to the dockerized DB
*/
public abstract String getJdbcUrl();
/**
* @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();
@Override
protected void waitUntilContainerStarted() {
// Repeatedly try and open a connection to the DB and execute a test query
logger().info("Waiting for database connection to become available at {} using query '{}'", getJdbcUrl(), getTestQueryString());
Unreliables.retryUntilSuccess(120, TimeUnit.SECONDS, () -> {
if (!isRunning()) {
throw new ContainerLaunchException("Container failed to start");
}
Connection connection = DB_CONNECT_RATE_LIMIT.getWhenReady(() -> createConnection(""));
boolean success = connection.createStatement().execute(JdbcDatabaseContainer.this.getTestQueryString());
if (success) {
logger().info("Obtained a connection to container ({})", JdbcDatabaseContainer.this.getJdbcUrl());
return connection;
} else {
throw new SQLException("Failed to execute test query");
}
});
}
/**
* Obtain an instance of the correct JDBC driver for this particular database container type
* @return a JDBC Driver
*/
public Driver getJdbcDriverInstance() {
synchronized (DRIVER_LOAD_MUTEX) {
if (driver == null) {
try {
driver = (Driver) Class.forName(this.getDriverClassName()).newInstance();
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
throw new RuntimeException("Could not get Driver", e);
}
}
}
return driver;
}
/**
* Creates a connection to the underlying containerized database instance.
*
* @param queryString any special 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 {
final Properties info = new Properties();
info.put("user", this.getUsername());
info.put("password", this.getPassword());
final String url = this.getJdbcUrl() + queryString;
final Driver jdbcDriverInstance = getJdbcDriverInstance();
try {
return Unreliables.retryUntilSuccess(120, TimeUnit.SECONDS, () -> jdbcDriverInstance.connect(url, info));
} catch (Exception e) {
throw new SQLException("Could not create new connection", e);
}
}
protected void optionallyMapResourceParameterAsVolume(@NotNull String paramName, @NotNull String pathNameInContainer, @NotNull String defaultResource) {
String resourceName = parameters.getOrDefault(paramName, defaultResource);
if (resourceName != null) {
final MountableFile mountableFile = MountableFile.forClasspathResource(resourceName);
addFileSystemBind(mountableFile.getResolvedPath(), pathNameInContainer, BindMode.READ_ONLY);
}
}
@Override
protected abstract Integer getLivenessCheckPort();
public void setParameters(Map<String, String> parameters) {
this.parameters = parameters;
}
@SuppressWarnings("unused")
public void addParameter(String paramName, String value) {
this.parameters.put(paramName, value);
}
}