forked from testcontainers/testcontainers-java
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathJdbcDatabaseContainer.java
More file actions
409 lines (351 loc) · 14.1 KB
/
JdbcDatabaseContainer.java
File metadata and controls
409 lines (351 loc) · 14.1 KB
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
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
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<SELF extends JdbcDatabaseContainer<SELF>>
extends GenericContainer<SELF>
implements LinkableContainer {
private static final Object DRIVER_LOAD_MUTEX = new Object();
private Driver driver;
private List<String> initScriptPaths = new ArrayList<>();
protected Map<String, String> parameters = new HashMap<>();
protected Map<String, String> 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<String> 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<String> 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());
properties.putAll(info);
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<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);
}
}
}