org.testcontainers.jdbc.ConnectionUrl Maven / Gradle / Ivy
Show all versions of jdbc Show documentation
package org.testcontainers.jdbc;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.testcontainers.UnstableAPI;
import static java.util.stream.Collectors.toMap;
/**
* This is an Immutable class holding JDBC Connection Url and its parsed components, used by {@link ContainerDatabaseDriver}.
*
* {@link ConnectionUrl#parseUrl()} method must be called after instantiating this class.
*
* @author manikmagar
*/
@EqualsAndHashCode(of = "url")
@Getter
public class ConnectionUrl {
private String url;
private String databaseType;
private Optional imageTag;
/**
* This is a part of the connection string that may specify host:port/databasename.
* It may vary for different clients and so clients can parse it as needed.
*/
private String dbHostString;
private boolean inDaemonMode = false;
private Optional databaseHost = Optional.empty();
private Optional databasePort = Optional.empty();
private Optional databaseName = Optional.empty();
private Optional initScriptPath = Optional.empty();
@UnstableAPI
private boolean reusable = false;
private Optional initFunction = Optional.empty();
private Optional queryString;
private Map containerParameters;
private Map queryParameters;
private Map tmpfsOptions = new HashMap<>();
public static ConnectionUrl newInstance(final String url) {
ConnectionUrl connectionUrl = new ConnectionUrl(url);
connectionUrl.parseUrl();
return connectionUrl;
}
private ConnectionUrl(final String url) {
this.url = Objects.requireNonNull(url, "Connection URL cannot be null");
}
public static boolean accepts(final String url) {
return url.startsWith("jdbc:tc:");
}
/**
* This method applies various REGEX Patterns to parse the URL associated with this instance.
* This is called from a @{@link ConnectionUrl#newInstance(String)} static factory method to create immutable instance of
* {@link ConnectionUrl}.
* To avoid mutation after class is instantiated, this method should not be publicly accessible.
*/
private void parseUrl() {
/*
Extract from the JDBC connection URL:
* The database type (e.g. mysql, postgresql, ...)
* The docker tag, if provided.
* The URL query string, if provided
*/
Matcher urlMatcher = Patterns.URL_MATCHING_PATTERN.matcher(this.getUrl());
if (!urlMatcher.matches()) {
//Try for Oracle pattern
urlMatcher = Patterns.ORACLE_URL_MATCHING_PATTERN.matcher(this.getUrl());
if (!urlMatcher.matches()) {
throw new IllegalArgumentException("JDBC URL matches jdbc:tc: prefix but the database or tag name could not be identified");
}
}
databaseType = urlMatcher.group(1);
imageTag = Optional.ofNullable(urlMatcher.group(3));
//String like hostname:port/database name, which may vary based on target database.
//Clients can further parse it as needed.
dbHostString = urlMatcher.group(4);
//In case it matches to the default pattern
Matcher dbInstanceMatcher = Patterns.DB_INSTANCE_MATCHING_PATTERN.matcher(dbHostString);
if (dbInstanceMatcher.matches()) {
databaseHost = Optional.of(dbInstanceMatcher.group(1));
databasePort = Optional.ofNullable(dbInstanceMatcher.group(3)).map(value -> Integer.valueOf(value));
databaseName = Optional.of(dbInstanceMatcher.group(4));
}
queryParameters = Collections.unmodifiableMap(
parseQueryParameters(
Optional.ofNullable(urlMatcher.group(5)).orElse("")));
String query = queryParameters
.entrySet()
.stream()
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&"));
if (query == null || query.trim().length() == 0) {
queryString = Optional.empty();
} else {
queryString = Optional.of("?" + query);
}
containerParameters = Collections.unmodifiableMap(parseContainerParameters());
tmpfsOptions = parseTmpfsOptions(containerParameters);
initScriptPath = Optional.ofNullable(containerParameters.get("TC_INITSCRIPT"));
reusable = Boolean.parseBoolean(containerParameters.get("TC_REUSABLE"));
Matcher funcMatcher = Patterns.INITFUNCTION_MATCHING_PATTERN.matcher(this.getUrl());
if (funcMatcher.matches()) {
initFunction = Optional.of(new InitFunctionDef(funcMatcher.group(2), funcMatcher.group(4)));
}
Matcher daemonMatcher = Patterns.DAEMON_MATCHING_PATTERN.matcher(this.getUrl());
inDaemonMode = daemonMatcher.matches() && Boolean.parseBoolean(daemonMatcher.group(2));
}
private Map parseTmpfsOptions(Map containerParameters) {
if(!containerParameters.containsKey("TC_TMPFS")){
return Collections.emptyMap();
}
String tmpfsOptions = containerParameters.get("TC_TMPFS");
return Stream.of(tmpfsOptions.split(","))
.collect(toMap(
string -> string.split(":")[0],
string -> string.split(":")[1]));
}
/**
* Get the TestContainers Parameters such as Init Function, Init Script path etc.
*
* @return {@link Map}
*/
private Map parseContainerParameters() {
Map results = new HashMap<>();
Matcher matcher = Patterns.TC_PARAM_MATCHING_PATTERN.matcher(this.getUrl());
while (matcher.find()) {
String key = matcher.group(1);
String value = matcher.group(2);
results.put(key, value);
}
return results;
}
/**
* Get all Query parameters specified in the Connection URL after ?. This DOES NOT include TestContainers (TC_*) parameters.
*
* @return {@link Map}
*/
private Map parseQueryParameters(final String queryString) {
Map results = new HashMap<>();
Matcher matcher = Patterns.QUERY_PARAM_MATCHING_PATTERN.matcher(queryString);
while (matcher.find()) {
String key = matcher.group(1);
String value = matcher.group(2);
if (!key.matches(Patterns.TC_PARAM_NAME_PATTERN))
results.put(key, value);
}
return results;
}
public Map getTmpfsOptions() {
return Collections.unmodifiableMap(tmpfsOptions);
}
/**
* This interface defines the Regex Patterns used by {@link ConnectionUrl}.
*
* @author manikmagar
*/
public interface Patterns {
Pattern URL_MATCHING_PATTERN = Pattern.compile("jdbc:tc:([a-z0-9]+)(:([^:]+))?://([^\\?]+)(\\?.*)?");
Pattern ORACLE_URL_MATCHING_PATTERN = Pattern.compile("jdbc:tc:([a-z]+)(:([^(thin:)]+))?:thin:@([^\\?]+)(\\?.*)?");
//Matches to part of string - hostname:port/databasename
Pattern DB_INSTANCE_MATCHING_PATTERN = Pattern.compile("([^:]+)(:([0-9]+))?/([^\\\\?]+)");
Pattern DAEMON_MATCHING_PATTERN = Pattern.compile(".*([\\?&]?)TC_DAEMON=([^\\?&]+).*");
Pattern INITSCRIPT_MATCHING_PATTERN = Pattern.compile(".*([\\?&]?)TC_INITSCRIPT=([^\\?&]+).*");
Pattern INITFUNCTION_MATCHING_PATTERN = Pattern.compile(".*([\\?&]?)TC_INITFUNCTION=" +
"((\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*\\.)*\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)" +
"::" +
"(\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)" +
".*");
String TC_PARAM_NAME_PATTERN = "(TC_[A-Z_]+)";
Pattern TC_PARAM_MATCHING_PATTERN = Pattern.compile(TC_PARAM_NAME_PATTERN + "=([^\\?&]+)");
Pattern QUERY_PARAM_MATCHING_PATTERN = Pattern.compile("([^\\?&=]+)=([^\\?&]*)");
}
@Getter
@AllArgsConstructor
public class InitFunctionDef {
private String className;
private String methodName;
}
}