Skip to content

Commit d682475

Browse files
committed
Change mssql url parsing to honor passing parameters (#5728)
1 parent df5e1ee commit d682475

File tree

5 files changed

+237
-18
lines changed

5 files changed

+237
-18
lines changed

docs/modules/databases/mssqlserver.md

+6
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ Add the following dependency to your `pom.xml`/`build.gradle` file:
4747
!!! hint
4848
Adding this Testcontainers library JAR will not automatically add a database driver JAR to your project. You should ensure that your project also has a suitable database driver as a dependency.
4949

50+
## Testcontainer related options
51+
52+
The Testcontainer allows the developer to customize its behavior by passing specific [parameters](./jdbc.md) through the connection URL.
53+
54+
It is worth noting, however, that the connection URL on MS SQL Server does not follow the [RFC-3986](https://www.rfc-editor.org/rfc/rfc3986) convention. In its case, each parameter is separated by a semi-colon, and we must slightly change the examples when using MS SQL Server. For instance, what would be `jdbc:tc:sqlserver://localhost:1433?TC_TMPFS=/testtmpfs:rw` should become `jdbc:tc:sqlserver://localhost:1433;TC_TMPFS=/testtmpfs:rw` instead.
55+
5056
## License
5157

5258
See [LICENSE](https://raw.githubusercontent.com/testcontainers/testcontainers-java/master/modules/mssqlserver/LICENSE).

modules/jdbc/src/main/java/org/testcontainers/jdbc/ConnectionUrl.java

+50-17
Original file line numberDiff line numberDiff line change
@@ -82,22 +82,30 @@ public static boolean accepts(final String url) {
8282
* To avoid mutation after class is instantiated, this method should not be publicly accessible.
8383
*/
8484
private void parseUrl() {
85+
boolean parsingMSSQLUrl = true;
8586
/*
8687
Extract from the JDBC connection URL:
8788
* The database type (e.g. mysql, postgresql, ...)
8889
* The docker tag, if provided.
8990
* The URL query string, if provided
9091
*/
91-
Matcher urlMatcher = Patterns.URL_MATCHING_PATTERN.matcher(this.getUrl());
92+
Matcher urlMatcher = Patterns.MSSQL_URL_MATCHING_PATTERN.matcher(this.getUrl());
93+
9294
if (!urlMatcher.matches()) {
93-
//Try for Oracle pattern
94-
urlMatcher = Patterns.ORACLE_URL_MATCHING_PATTERN.matcher(this.getUrl());
95+
parsingMSSQLUrl = false;
96+
//Try for regular pattern
97+
urlMatcher = Patterns.URL_MATCHING_PATTERN.matcher(this.getUrl());
9598
if (!urlMatcher.matches()) {
96-
throw new IllegalArgumentException(
97-
"JDBC URL matches jdbc:tc: prefix but the database or tag name could not be identified"
98-
);
99+
//Try for Oracle pattern
100+
urlMatcher = Patterns.ORACLE_URL_MATCHING_PATTERN.matcher(this.getUrl());
101+
if (!urlMatcher.matches()) {
102+
throw new IllegalArgumentException(
103+
"JDBC URL matches jdbc:tc: prefix but the database or tag name could not be identified"
104+
);
105+
}
99106
}
100107
}
108+
101109
databaseType = urlMatcher.group("databaseType");
102110

103111
imageTag = Optional.ofNullable(urlMatcher.group("imageTag"));
@@ -106,29 +114,41 @@ private void parseUrl() {
106114
//Clients can further parse it as needed.
107115
dbHostString = urlMatcher.group("dbHostString");
108116

109-
//In case it matches to the default pattern
110-
Matcher dbInstanceMatcher = Patterns.DB_INSTANCE_MATCHING_PATTERN.matcher(dbHostString);
111-
if (dbInstanceMatcher.matches()) {
112-
databaseHost = Optional.of(dbInstanceMatcher.group("databaseHost"));
113-
databasePort = Optional.ofNullable(dbInstanceMatcher.group("databasePort")).map(Integer::valueOf);
114-
databaseName = Optional.of(dbInstanceMatcher.group("databaseName"));
115-
}
116-
117117
queryParameters =
118118
Collections.unmodifiableMap(
119119
parseQueryParameters(Optional.ofNullable(urlMatcher.group("queryParameters")).orElse(""))
120120
);
121121

122+
String delimiter = (parsingMSSQLUrl) ? ";" : "&";
123+
122124
String query = queryParameters
123125
.entrySet()
124126
.stream()
125127
.map(e -> e.getKey() + "=" + e.getValue())
126-
.collect(Collectors.joining("&"));
128+
.collect(Collectors.joining(delimiter));
127129

128130
if (query.trim().length() == 0) {
129131
queryString = Optional.empty();
130132
} else {
131-
queryString = Optional.of("?" + query);
133+
String startingOfQUeryParameters = ((parsingMSSQLUrl) ? ";" : "?");
134+
queryString = Optional.of(startingOfQUeryParameters + query);
135+
}
136+
137+
if (parsingMSSQLUrl) {
138+
Matcher dbInstanceMatcher = Patterns.MSSQL_DB_INSTANCE_MATCHING_PATTERN.matcher(dbHostString);
139+
if (dbInstanceMatcher.matches()) {
140+
databaseHost = Optional.of(dbInstanceMatcher.group("databaseHost"));
141+
databasePort = Optional.ofNullable(dbInstanceMatcher.group("databasePort")).map(Integer::valueOf);
142+
databaseName = Optional.ofNullable(queryParameters.get("databaseName"));
143+
}
144+
} else {
145+
//In case it matches to the default pattern
146+
Matcher dbInstanceMatcher = Patterns.DB_INSTANCE_MATCHING_PATTERN.matcher(dbHostString);
147+
if (dbInstanceMatcher.matches()) {
148+
databaseHost = Optional.of(dbInstanceMatcher.group("databaseHost"));
149+
databasePort = Optional.ofNullable(dbInstanceMatcher.group("databasePort")).map(Integer::valueOf);
150+
databaseName = Optional.of(dbInstanceMatcher.group("databaseName"));
151+
}
132152
}
133153

134154
containerParameters = Collections.unmodifiableMap(parseContainerParameters());
@@ -216,6 +236,15 @@ public interface Patterns {
216236
"(?<queryParameters>\\?.*)?"
217237
);
218238

239+
Pattern MSSQL_URL_MATCHING_PATTERN = Pattern.compile(
240+
"jdbc:tc:" +
241+
"(?<databaseType>[sqlserver]+)" +
242+
"(:(?<imageTag>[^:]+))?" +
243+
"://" +
244+
"(?<dbHostString>[^;]+)" +
245+
"(?<queryParameters>;?.*)?"
246+
);
247+
219248
Pattern ORACLE_URL_MATCHING_PATTERN = Pattern.compile(
220249
"jdbc:tc:" +
221250
"(?<databaseType>[a-z]+)" +
@@ -229,6 +258,10 @@ public interface Patterns {
229258
"(?<queryParameters>\\?.*)?"
230259
);
231260

261+
Pattern MSSQL_DB_INSTANCE_MATCHING_PATTERN = Pattern.compile(
262+
"(?<databaseHost>[^:|\\/^]+)" + "(\\/(?<databaseInstance>[^:]+))?" + "(:(?<databasePort>[0-9]+))?"
263+
);
264+
232265
//Matches to part of string - hostname:port/databasename
233266
Pattern DB_INSTANCE_MATCHING_PATTERN = Pattern.compile(
234267
"(?<databaseHost>[^:]+)" +
@@ -261,7 +294,7 @@ public interface Patterns {
261294

262295
Pattern TC_PARAM_MATCHING_PATTERN = Pattern.compile(TC_PARAM_NAME_PATTERN + "=([^?&]+)");
263296

264-
Pattern QUERY_PARAM_MATCHING_PATTERN = Pattern.compile("([^?&=]+)=([^?&]*)");
297+
Pattern QUERY_PARAM_MATCHING_PATTERN = Pattern.compile("([^?&;=]+)=([^?&;]*)");
265298
}
266299

267300
@Getter

modules/jdbc/src/test/java/org/testcontainers/jdbc/ConnectionUrlTest.java

+143
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,149 @@ public class ConnectionUrlTest {
1111
@Rule
1212
public ExpectedException thrown = ExpectedException.none();
1313

14+
@Test
15+
public void testMssqlConnectionUrlWithoutPort() {
16+
String urlString = "jdbc:tc:sqlserver:5.7.34://somehostname";
17+
ConnectionUrl url = ConnectionUrl.newInstance(urlString);
18+
19+
assertThat(url.getDatabaseType()).as("Database Type value is as expected").isEqualTo("sqlserver");
20+
assertThat(url.getImageTag()).as("Database Image tag value is as expected").contains("5.7.34");
21+
assertThat(url.getDbHostString()).as("Database Host String is as expected").isEqualTo("somehostname");
22+
assertThat(url.getQueryString()).as("Query String value is as expected").isNotPresent();
23+
assertThat(url.getDatabaseHost()).as("Database Host value is as expected").contains("somehostname");
24+
assertThat(url.getDatabasePort()).as("Database Port value is as expected").isNotPresent();
25+
assertThat(url.getDatabaseName()).as("Database Name value is as expected").isEmpty();
26+
27+
assertThat(url.getQueryParameters()).as("Parameters are empty").isEmpty();
28+
}
29+
30+
@Test
31+
public void testMssqlConnectionUrlWithInstanceName() {
32+
String urlString = "jdbc:tc:sqlserver:5.7.34://somehostname/someinstance:12345";
33+
ConnectionUrl url = ConnectionUrl.newInstance(urlString);
34+
35+
assertThat(url.getDatabaseType()).as("Database Type value is as expected").isEqualTo("sqlserver");
36+
assertThat(url.getImageTag()).as("Database Image tag value is as expected").contains("5.7.34");
37+
assertThat(url.getDbHostString())
38+
.as("Database Host String is as expected")
39+
.isEqualTo("somehostname/someinstance:12345");
40+
assertThat(url.getQueryString()).as("Query String value is as expected").isNotPresent();
41+
assertThat(url.getDatabaseHost()).as("Database Host value is as expected").contains("somehostname");
42+
assertThat(url.getDatabasePort()).as("Database Port value is as expected").contains(12345);
43+
assertThat(url.getDatabaseName()).as("Database Name value is as expected").isEmpty();
44+
45+
assertThat(url.getQueryParameters()).as("Parameters are empty").isEmpty();
46+
}
47+
48+
@Test
49+
public void testPlainMssqlConnectionUrl() {
50+
String urlString = "jdbc:tc:sqlserver:5.7.34://somehostname:3306";
51+
ConnectionUrl url = ConnectionUrl.newInstance(urlString);
52+
53+
assertThat(url.getDatabaseType()).as("Database Type value is as expected").isEqualTo("sqlserver");
54+
assertThat(url.getImageTag()).as("Database Image tag value is as expected").contains("5.7.34");
55+
assertThat(url.getDbHostString()).as("Database Host String is as expected").isEqualTo("somehostname:3306");
56+
assertThat(url.getQueryString()).as("Query String value is as expected").isNotPresent();
57+
assertThat(url.getDatabaseHost()).as("Database Host value is as expected").contains("somehostname");
58+
assertThat(url.getDatabasePort()).as("Database Port value is as expected").contains(3306);
59+
assertThat(url.getDatabaseName()).as("Database Name value is as expected").isEmpty();
60+
61+
assertThat(url.getQueryParameters()).as("Parameters are empty").isEmpty();
62+
}
63+
64+
@Test
65+
public void testMssqlInitScriptPathCapture() {
66+
String urlString = "jdbc:tc:sqlserver:5.7.34://somehostname:3306;a=b;c=d;TC_INITSCRIPT=somepath/init_mssql.sql";
67+
ConnectionUrl url = ConnectionUrl.newInstance(urlString);
68+
69+
assertThat(url.getInitScriptPath())
70+
.as("Database Type value is as expected")
71+
.contains("somepath/init_mssql.sql");
72+
assertThat(url.getQueryString()).as("Query String value is as expected").contains(";a=b;c=d");
73+
assertThat(url.getContainerParameters())
74+
.as("INIT SCRIPT Path exists in Container Parameters")
75+
.containsEntry("TC_INITSCRIPT", "somepath/init_mssql.sql");
76+
77+
//Parameter sets are unmodifiable
78+
thrown.expect(UnsupportedOperationException.class);
79+
url.getContainerParameters().remove("TC_INITSCRIPT");
80+
url.getQueryParameters().remove("a");
81+
}
82+
83+
@Test
84+
public void testMssqlInitFunctionCapture() {
85+
String urlString =
86+
"jdbc:tc:sqlserver:5.7.34://somehostname;a=b;c=d;TC_INITFUNCTION=org.testcontainers.jdbc.JDBCDriverTest::sampleInitFunction";
87+
ConnectionUrl url = ConnectionUrl.newInstance(urlString);
88+
89+
assertThat(url.getInitFunction()).as("Init Function parameter exists").isPresent();
90+
91+
assertThat(url.getInitFunction().get().getClassName())
92+
.as("Init function class is as expected")
93+
.isEqualTo("org.testcontainers.jdbc.JDBCDriverTest");
94+
assertThat(url.getInitFunction().get().getMethodName())
95+
.as("Init function class is as expected")
96+
.isEqualTo("sampleInitFunction");
97+
}
98+
99+
@Test
100+
public void testMssqlTmpfsOption() {
101+
String urlString = "jdbc:tc:sqlserver://somehostname;TC_TMPFS=key:value,key1:value1";
102+
ConnectionUrl url = ConnectionUrl.newInstance(urlString);
103+
104+
assertThat(url.getQueryParameters()).as("Connection Parameters set is empty").isEmpty();
105+
assertThat(url.getContainerParameters()).as("Container Parameters set is not empty").isNotEmpty();
106+
assertThat(url.getContainerParameters())
107+
.as("Container Parameter TC_TMPFS is true")
108+
.containsEntry("TC_TMPFS", "key:value,key1:value1");
109+
assertThat(url.getTmpfsOptions()).as("tmpfs option key has correct value").containsEntry("key", "value");
110+
assertThat(url.getTmpfsOptions()).as("tmpfs option key1 has correct value").containsEntry("key1", "value1");
111+
}
112+
113+
@Test
114+
public void testMssqlConnectionUrlWithoutDatabaseName() {
115+
String urlString = "jdbc:tc:sqlserver:5.7.34://somehostname:3306;a=b;c=d;";
116+
ConnectionUrl url = ConnectionUrl.newInstance(urlString);
117+
118+
assertThat(url.getDatabaseType()).as("Database Type value is as expected").isEqualTo("sqlserver");
119+
assertThat(url.getImageTag()).as("Database Image tag value is as expected").contains("5.7.34");
120+
assertThat(url.getDbHostString()).as("Database Host String is as expected").isEqualTo("somehostname:3306");
121+
assertThat(url.getQueryString()).as("Query String value is as expected").contains(";a=b;c=d");
122+
assertThat(url.getDatabaseHost()).as("Database Host value is as expected").contains("somehostname");
123+
assertThat(url.getDatabasePort()).as("Database Port value is as expected").contains(3306);
124+
assertThat(url.getDatabaseName()).as("Database Name value is as expected").isNotPresent();
125+
126+
assertThat(url.getQueryParameters()).as("Parameter a is captured").containsEntry("a", "b");
127+
assertThat(url.getQueryParameters()).as("Parameter c is captured").containsEntry("c", "d");
128+
}
129+
130+
@Test
131+
public void testMssqlConnection() {
132+
String urlString = "jdbc:tc:sqlserver:5.7.34://somehostname:3306;a=b;c=d;databaseName=database";
133+
ConnectionUrl url = ConnectionUrl.newInstance(urlString);
134+
135+
assertThat(url.getDatabaseType()).as("Database Type value is as expected").isEqualTo("sqlserver");
136+
assertThat(url.getImageTag()).as("Database Image tag value is as expected").contains("5.7.34");
137+
assertThat(url.getDbHostString()).as("Database Host String is as expected").isEqualTo("somehostname:3306");
138+
assertThat(url.getQueryString())
139+
.as("Query String value is as expected")
140+
.contains(";a=b;c=d;databaseName=database");
141+
assertThat(url.getDatabaseHost()).as("Database Host value is as expected").contains("somehostname");
142+
assertThat(url.getDatabasePort()).as("Database Port value is as expected").contains(3306);
143+
assertThat(url.getDatabaseName()).as("Database Name value is as expected").contains("database");
144+
145+
assertThat(url.getQueryParameters()).as("Parameter a is captured").containsEntry("a", "b");
146+
assertThat(url.getQueryParameters()).as("Parameter c is captured").containsEntry("c", "d");
147+
}
148+
149+
@Test
150+
public void testMssqlDaemonCapture() {
151+
String urlString = "jdbc:tc:sqlserver:5.7.34://somehostname/instance:3306;a=b;c=d;TC_DAEMON=true";
152+
ConnectionUrl url = ConnectionUrl.newInstance(urlString);
153+
154+
assertThat(url.isInDaemonMode()).as("Daemon flag is set to true.").isTrue();
155+
}
156+
14157
@Test
15158
public void testConnectionUrl1() {
16159
String urlString = "jdbc:tc:mysql:5.7.34://somehostname:3306/databasename?a=b&c=d";

modules/mssqlserver/src/main/java/org/testcontainers/containers/MSSQLServerContainer.java

+29-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ public class MSSQLServerContainer<SELF extends MSSQLServerContainer<SELF>> exten
3434

3535
private static final int DEFAULT_CONNECT_TIMEOUT_SECONDS = 240;
3636

37+
private static final Pattern[] PARAMETERS_SANITIZATION_PATTERNS = new Pattern[] {
38+
Pattern.compile("(;databaseName=([^;]*)?)"),
39+
};
40+
3741
private static final Pattern[] PASSWORD_CATEGORY_VALIDATION_PATTERNS = new Pattern[] {
3842
Pattern.compile("[A-Z]+"),
3943
Pattern.compile("[a-z]+"),
@@ -100,7 +104,19 @@ protected String constructUrlForConnection(String queryString) {
100104
if (urlParameters.keySet().stream().map(String::toLowerCase).noneMatch("encrypt"::equals)) {
101105
urlParameters.put("encrypt", "false");
102106
}
103-
return super.constructUrlForConnection(queryString);
107+
String sanitizeQueryString = sanitize(queryString);
108+
109+
String baseUrl = getJdbcUrl();
110+
111+
if ("".equals(sanitizeQueryString)) {
112+
return baseUrl;
113+
}
114+
115+
if (!sanitizeQueryString.startsWith(";")) {
116+
throw new IllegalArgumentException("The ';' character must be included");
117+
}
118+
119+
return baseUrl.contains(";") ? baseUrl + ';' + sanitizeQueryString.substring(1) : baseUrl + sanitizeQueryString;
104120
}
105121

106122
@Override
@@ -109,6 +125,18 @@ public String getJdbcUrl() {
109125
return "jdbc:sqlserver://" + getHost() + ":" + getMappedPort(MS_SQL_SERVER_PORT) + additionalUrlParams;
110126
}
111127

128+
private String sanitize(String queryString) {
129+
// We cannot forward some parameters to the database.
130+
// One of those is the 'databaseName' parameter. The container starts uses the master database
131+
// by default.
132+
133+
String sanitizeQueryString = queryString;
134+
for (Pattern p : PARAMETERS_SANITIZATION_PATTERNS) {
135+
sanitizeQueryString = p.matcher(sanitizeQueryString).replaceAll("");
136+
}
137+
return sanitizeQueryString;
138+
}
139+
112140
@Override
113141
public String getUsername() {
114142
return DEFAULT_USER;

modules/mssqlserver/src/test/java/org/testcontainers/jdbc/mssqlserver/MSSQLServerJDBCDriverTest.java

+9
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,19 @@ public class MSSQLServerJDBCDriverTest extends AbstractJDBCDriverTest {
1414
public static Iterable<Object[]> data() {
1515
return Arrays.asList(
1616
new Object[][] {
17+
{ "jdbc:tc:sqlserver:2017-CU12://hostname:hostport", EnumSet.noneOf(Options.class) },
1718
{
1819
"jdbc:tc:sqlserver:2017-CU12://hostname:hostport;databaseName=databasename",
1920
EnumSet.noneOf(Options.class),
2021
},
22+
{
23+
"jdbc:tc:sqlserver:2017-CU12://hostname:hostport;sendStringParametersAsUnicode=false",
24+
EnumSet.noneOf(Options.class),
25+
},
26+
{
27+
"jdbc:tc:sqlserver:2017-CU12://hostname:hostport;databaseName=databasename;sendStringParametersAsUnicode=false",
28+
EnumSet.noneOf(Options.class),
29+
},
2130
}
2231
);
2332
}

0 commit comments

Comments
 (0)