-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
JdbcDatabaseContainer.java
303 lines (258 loc) · 10.9 KB
/
JdbcDatabaseContainer.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
package org.testcontainers.containers;
import com.github.dockerjava.api.command.InspectContainerResponse;
import lombok.NonNull;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
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.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.Future;
import static java.util.stream.Collectors.joining;
/**
* 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;
private String initScriptPath;
protected Map<String, String> parameters = new HashMap<>();
protected Map<String, String> urlParameters = new HashMap<>();
private int startupTimeoutSeconds = 120;
private int connectTimeoutSeconds = 120;
public JdbcDatabaseContainer(@NonNull final String dockerImageName) {
super(dockerImageName);
}
public JdbcDatabaseContainer(@NonNull final Future<String> image) {
super(image);
}
/**
* @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();
}
public SELF withInitScript(String initScriptPath) {
this.initScriptPath = initScriptPath;
return self();
}
@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.currentTimeMillis();
try {
while (System.currentTimeMillis() < start + (1000 * startupTimeoutSeconds)) {
try {
if (!isRunning()) {
Thread.sleep(100L);
continue; // Don't attempt to connect yet
}
try (Connection connection = createConnection("")) {
boolean testQuerySucceeded = connection.createStatement().execute(this.getTestQueryString());
if (testQuerySucceeded) {
break;
}
}
} catch (NoDriverFoundException e) {
// we explicitly want this exception to fail fast without retries
throw e;
} catch (Exception e) {
// ignore so that we can try again
logger().debug("Failure when trying test query", e);
Thread.sleep(100L);
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ContainerLaunchException("Container startup wait was interrupted", e);
}
logger().info("Container is started (JDBC URL: {})", JdbcDatabaseContainer.this.getJdbcUrl());
}
@Override
protected void containerIsStarted(InspectContainerResponse containerInfo) {
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 {
final Properties info = new Properties();
info.put("user", this.getUsername());
info.put("password", this.getPassword());
final String url = constructUrlForConnection(queryString);
final Driver jdbcDriverInstance = getJdbcDriverInstance();
SQLException lastException = null;
try {
long start = System.currentTimeMillis();
// give up if we hit the time limit or the container stops running for some reason
while (System.currentTimeMillis() < start + (1000 * connectTimeoutSeconds) && isRunning()) {
try {
logger().debug("Trying to create JDBC connection using {} to {} with properties: {}", driver.getClass().getName(), url, info);
return jdbcDriverInstance.connect(url, info);
} 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) {
return getJdbcUrl() + 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(joining(delimiter));
urlParameters = startCharacter + additionalParameters + endCharacter;
}
return urlParameters;
}
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);
withCopyFileToContainer(mountableFile, pathNameInContainer);
}
}
/**
* Load init script content and apply it to the database if initScriptPath is set
*/
protected void runInitScriptIfRequired() {
if (initScriptPath != null) {
ScriptUtils.runInitScript(getDatabaseDelegate(), initScriptPath);
}
}
public void setParameters(Map<String, String> 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);
}
}
}