Skip to content

Commit c4a5a34

Browse files
somayajsnicoll
authored andcommitted
Fix detection logic for embedded databases
Closes gh-23721
1 parent 9c672de commit c4a5a34

File tree

5 files changed

+188
-17
lines changed

5 files changed

+188
-17
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ public String determineUsername() {
325325
if (StringUtils.hasText(this.username)) {
326326
return this.username;
327327
}
328-
if (EmbeddedDatabaseConnection.isEmbedded(determineDriverClassName())) {
328+
if (EmbeddedDatabaseConnection.isEmbedded(determineDriverClassName(), determineUrl())) {
329329
return "sa";
330330
}
331331
return null;
@@ -353,7 +353,7 @@ public String determinePassword() {
353353
if (StringUtils.hasText(this.password)) {
354354
return this.password;
355355
}
356-
if (EmbeddedDatabaseConnection.isEmbedded(determineDriverClassName())) {
356+
if (EmbeddedDatabaseConnection.isEmbedded(determineDriverClassName(), determineUrl())) {
357357
return "";
358358
}
359359
return null;

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourcePropertiesTests.java

+36
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,24 @@ void determineUsername() throws Exception {
9898
assertThat(properties.determineUsername()).isEqualTo("sa");
9999
}
100100

101+
@Test
102+
void determineUsernameWhenEmpty() throws Exception {
103+
DataSourceProperties properties = new DataSourceProperties();
104+
properties.setUsername("");
105+
properties.afterPropertiesSet();
106+
assertThat(properties.getUsername());
107+
assertThat(properties.determineUsername()).isEqualTo("sa");
108+
}
109+
110+
@Test
111+
void determineUsernameWhenNull() throws Exception {
112+
DataSourceProperties properties = new DataSourceProperties();
113+
properties.setUsername(null);
114+
properties.afterPropertiesSet();
115+
assertThat(properties.getUsername());
116+
assertThat(properties.determineUsername()).isEqualTo("sa");
117+
}
118+
101119
@Test
102120
void determineUsernameWithExplicitConfig() throws Exception {
103121
DataSourceProperties properties = new DataSourceProperties();
@@ -107,6 +125,15 @@ void determineUsernameWithExplicitConfig() throws Exception {
107125
assertThat(properties.determineUsername()).isEqualTo("foo");
108126
}
109127

128+
@Test
129+
void determineUsernameWithNonEmbeddedUrl() throws Exception {
130+
DataSourceProperties properties = new DataSourceProperties();
131+
properties.setUrl("jdbc:h2:~/test");
132+
properties.afterPropertiesSet();
133+
assertThat(properties.getPassword()).isNull();
134+
assertThat(properties.determineUsername()).isNull();
135+
}
136+
110137
@Test
111138
void determinePassword() throws Exception {
112139
DataSourceProperties properties = new DataSourceProperties();
@@ -124,6 +151,15 @@ void determinePasswordWithExplicitConfig() throws Exception {
124151
assertThat(properties.determinePassword()).isEqualTo("bar");
125152
}
126153

154+
@Test
155+
void determinePasswordWithNonEmbeddedUrl() throws Exception {
156+
DataSourceProperties properties = new DataSourceProperties();
157+
properties.setUrl("jdbc:h2:~/test");
158+
properties.afterPropertiesSet();
159+
assertThat(properties.getPassword()).isNull();
160+
assertThat(properties.determinePassword()).isNull();
161+
}
162+
127163
@Test
128164
void determineCredentialsForSchemaScripts() {
129165
DataSourceProperties properties = new DataSourceProperties();

spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto.adoc

+2-2
Original file line numberDiff line numberDiff line change
@@ -1927,8 +1927,8 @@ This is controlled through two external properties:
19271927
You can set `spring.jpa.hibernate.ddl-auto` explicitly and the standard Hibernate property values are `none`, `validate`, `update`, `create`, and `create-drop`.
19281928
Spring Boot chooses a default value for you based on whether it thinks your database is embedded.
19291929
It defaults to `create-drop` if no schema manager has been detected or `none` in all other cases.
1930-
An embedded database is detected by looking at the `Connection` type.
1931-
`hsqldb`, `h2`, and `derby` are embedded, and others are not.
1930+
An embedded database is detected by looking at the `Connection` type and JDBC url.
1931+
`hsqldb`, `h2`, and `derby` are candidates, and others are not.
19321932
Be careful when switching from in-memory to a '`real`' database that you do not make assumptions about the existence of the tables and data in the new platform.
19331933
You either have to set `ddl-auto` explicitly or use one of the other mechanisms to initialize the database.
19341934

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/EmbeddedDatabaseConnection.java

+53-13
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@
1717
package org.springframework.boot.jdbc;
1818

1919
import java.sql.Connection;
20+
import java.sql.DatabaseMetaData;
2021
import java.sql.SQLException;
2122
import java.util.Locale;
23+
import java.util.function.Predicate;
24+
import java.util.stream.Stream;
2225

2326
import javax.sql.DataSource;
2427

@@ -43,24 +46,25 @@ public enum EmbeddedDatabaseConnection {
4346
/**
4447
* No Connection.
4548
*/
46-
NONE(null, null, null),
49+
NONE(null, null, null, (url) -> false),
4750

4851
/**
4952
* H2 Database Connection.
5053
*/
5154
H2(EmbeddedDatabaseType.H2, DatabaseDriver.H2.getDriverClassName(),
52-
"jdbc:h2:mem:%s;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"),
55+
"jdbc:h2:mem:%s;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE", (url) -> url.contains(":h2:mem")),
5356

5457
/**
5558
* Derby Database Connection.
5659
*/
57-
DERBY(EmbeddedDatabaseType.DERBY, DatabaseDriver.DERBY.getDriverClassName(), "jdbc:derby:memory:%s;create=true"),
60+
DERBY(EmbeddedDatabaseType.DERBY, DatabaseDriver.DERBY.getDriverClassName(), "jdbc:derby:memory:%s;create=true",
61+
(url) -> true),
5862

5963
/**
6064
* HSQL Database Connection.
6165
*/
6266
HSQL(EmbeddedDatabaseType.HSQL, DatabaseDriver.HSQLDB.getDriverClassName(), "org.hsqldb.jdbcDriver",
63-
"jdbc:hsqldb:mem:%s");
67+
"jdbc:hsqldb:mem:%s", (url) -> url.contains(":hsqldb:mem:"));
6468

6569
private final EmbeddedDatabaseType type;
6670

@@ -70,15 +74,20 @@ public enum EmbeddedDatabaseConnection {
7074

7175
private final String url;
7276

73-
EmbeddedDatabaseConnection(EmbeddedDatabaseType type, String driverClass, String url) {
74-
this(type, driverClass, null, url);
77+
private final Predicate<String> embeddedUrl;
78+
79+
EmbeddedDatabaseConnection(EmbeddedDatabaseType type, String driverClass, String url,
80+
Predicate<String> embeddedUrl) {
81+
this(type, driverClass, null, url, embeddedUrl);
7582
}
7683

77-
EmbeddedDatabaseConnection(EmbeddedDatabaseType type, String driverClass, String fallbackDriverClass, String url) {
84+
EmbeddedDatabaseConnection(EmbeddedDatabaseType type, String driverClass, String fallbackDriverClass, String url,
85+
Predicate<String> embeddedUrl) {
7886
this.type = type;
7987
this.driverClass = driverClass;
8088
this.alternativeDriverClass = fallbackDriverClass;
8189
this.url = url;
90+
this.embeddedUrl = embeddedUrl;
8291
}
8392

8493
/**
@@ -107,19 +116,48 @@ public String getUrl(String databaseName) {
107116
return (this.url != null) ? String.format(this.url, databaseName) : null;
108117
}
109118

119+
boolean isEmbeddedUrl(String url) {
120+
return this.embeddedUrl.test(url);
121+
}
122+
123+
boolean isDriverCompatible(String driverClass) {
124+
return (driverClass != null
125+
&& (driverClass.equals(this.driverClass) || driverClass.equals(this.alternativeDriverClass)));
126+
}
127+
110128
/**
111129
* Convenience method to determine if a given driver class name represents an embedded
112130
* database type.
113131
* @param driverClass the driver class
114132
* @return true if the driver class is one of the embedded types
133+
* @deprecated since 2.3.5 in favor of {@link #isEmbedded(String, String)}
115134
*/
135+
@Deprecated
116136
public static boolean isEmbedded(String driverClass) {
117-
return driverClass != null
118-
&& (matches(HSQL, driverClass) || matches(H2, driverClass) || matches(DERBY, driverClass));
137+
return isEmbedded(driverClass, null);
138+
}
139+
140+
/**
141+
* Convenience method to determine if a given driver class name and url represent an
142+
* embedded database type.
143+
* @param driverClass the driver class
144+
* @param url the jdbc url (can be {@code null)}
145+
* @return true if the driver class and url refer to an embedded database
146+
*/
147+
public static boolean isEmbedded(String driverClass, String url) {
148+
if (driverClass == null) {
149+
return false;
150+
}
151+
EmbeddedDatabaseConnection connection = getEmbeddedDatabaseConnection(driverClass);
152+
if (connection == NONE) {
153+
return false;
154+
}
155+
return (url == null || connection.isEmbeddedUrl(url));
119156
}
120157

121-
private static boolean matches(EmbeddedDatabaseConnection candidate, String driverClass) {
122-
return driverClass.equals(candidate.driverClass) || driverClass.equals(candidate.alternativeDriverClass);
158+
private static EmbeddedDatabaseConnection getEmbeddedDatabaseConnection(String driverClass) {
159+
return Stream.of(H2, HSQL, DERBY).filter((connection) -> connection.isDriverCompatible(driverClass)).findFirst()
160+
.orElse(NONE);
123161
}
124162

125163
/**
@@ -160,15 +198,17 @@ private static class IsEmbedded implements ConnectionCallback<Boolean> {
160198

161199
@Override
162200
public Boolean doInConnection(Connection connection) throws SQLException, DataAccessException {
163-
String productName = connection.getMetaData().getDatabaseProductName();
201+
DatabaseMetaData metaData = connection.getMetaData();
202+
String productName = metaData.getDatabaseProductName();
164203
if (productName == null) {
165204
return false;
166205
}
167206
productName = productName.toUpperCase(Locale.ENGLISH);
168207
EmbeddedDatabaseConnection[] candidates = EmbeddedDatabaseConnection.values();
169208
for (EmbeddedDatabaseConnection candidate : candidates) {
170209
if (candidate != NONE && productName.contains(candidate.name())) {
171-
return true;
210+
String url = metaData.getURL();
211+
return (url == null || candidate.isEmbeddedUrl(url));
172212
}
173213
}
174214
return false;

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jdbc/EmbeddedDatabaseConnectionTests.java

+95
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,24 @@
1616

1717
package org.springframework.boot.jdbc;
1818

19+
import java.sql.Connection;
20+
import java.sql.DatabaseMetaData;
21+
import java.sql.SQLException;
22+
23+
import javax.sql.DataSource;
24+
1925
import org.junit.jupiter.api.Test;
26+
import org.junit.jupiter.params.ParameterizedTest;
27+
import org.junit.jupiter.params.provider.MethodSource;
28+
29+
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
30+
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
31+
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
2032

2133
import static org.assertj.core.api.Assertions.assertThat;
2234
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
35+
import static org.mockito.BDDMockito.given;
36+
import static org.mockito.Mockito.mock;
2337

2438
/**
2539
* Tests for {@link EmbeddedDatabaseConnection}.
@@ -57,4 +71,85 @@ void getUrlWithEmptyDatabaseName() {
5771
.withMessageContaining("DatabaseName must not be empty");
5872
}
5973

74+
@ParameterizedTest(name = "{0} - {1}")
75+
@MethodSource("embeddedDriverAndUrlParameters")
76+
void isEmbeddedWithDriverAndUrl(String driverClassName, String url, boolean embedded) {
77+
assertThat(EmbeddedDatabaseConnection.isEmbedded(driverClassName, url)).isEqualTo(embedded);
78+
}
79+
80+
static Object[] embeddedDriverAndUrlParameters() {
81+
return new Object[] {
82+
new Object[] { EmbeddedDatabaseConnection.H2.getDriverClassName(), "jdbc:h2:~/test", false },
83+
new Object[] { EmbeddedDatabaseConnection.H2.getDriverClassName(), "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1",
84+
true },
85+
new Object[] { EmbeddedDatabaseConnection.H2.getDriverClassName(), null, true },
86+
new Object[] { EmbeddedDatabaseConnection.HSQL.getDriverClassName(), "jdbc:hsqldb:hsql://localhost",
87+
false },
88+
new Object[] { EmbeddedDatabaseConnection.HSQL.getDriverClassName(), "jdbc:hsqldb:mem:test", true },
89+
new Object[] { EmbeddedDatabaseConnection.HSQL.getDriverClassName(), null, true },
90+
new Object[] { EmbeddedDatabaseConnection.DERBY.getDriverClassName(), "jdbc:derby:memory:test", true },
91+
new Object[] { EmbeddedDatabaseConnection.DERBY.getDriverClassName(), null, true },
92+
new Object[] { "com.mysql.cj.jdbc.Driver", "jdbc:mysql:mem:test", false },
93+
new Object[] { "com.mysql.cj.jdbc.Driver", null, false },
94+
new Object[] { null, "jdbc:none:mem:test", false }, new Object[] { null, null, false } };
95+
}
96+
97+
@Test
98+
void isEmbeddedWithH2DataSource() {
99+
testEmbeddedDatabase(new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2).build());
100+
}
101+
102+
@Test
103+
void isEmbeddedWithHsqlDataSource() {
104+
testEmbeddedDatabase(new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.HSQL).build());
105+
}
106+
107+
@Test
108+
void isEmbeddedWithDerbyDataSource() {
109+
testEmbeddedDatabase(new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.DERBY).build());
110+
}
111+
112+
void testEmbeddedDatabase(EmbeddedDatabase database) {
113+
try {
114+
assertThat(EmbeddedDatabaseConnection.isEmbedded(database)).isTrue();
115+
}
116+
finally {
117+
database.shutdown();
118+
}
119+
}
120+
121+
@Test
122+
void isEmbeddedWithUnknownDataSource() throws SQLException {
123+
assertThat(EmbeddedDatabaseConnection.isEmbedded(mockDataSource("unknown-db", null))).isFalse();
124+
}
125+
126+
@Test
127+
void isEmbeddedWithH2File() throws SQLException {
128+
assertThat(EmbeddedDatabaseConnection
129+
.isEmbedded(mockDataSource(EmbeddedDatabaseConnection.H2.getDriverClassName(), "jdbc:h2:~/test")))
130+
.isFalse();
131+
}
132+
133+
@Test
134+
void isEmbeddedWithMissingDriverClassMetadata() throws SQLException {
135+
assertThat(EmbeddedDatabaseConnection.isEmbedded(mockDataSource(null, "jdbc:h2:meme:test"))).isFalse();
136+
}
137+
138+
@Test
139+
void isEmbeddedWithMissingUrlMetadata() throws SQLException {
140+
assertThat(EmbeddedDatabaseConnection
141+
.isEmbedded(mockDataSource(EmbeddedDatabaseConnection.H2.getDriverClassName(), null))).isTrue();
142+
}
143+
144+
DataSource mockDataSource(String productName, String connectionUrl) throws SQLException {
145+
DatabaseMetaData metaData = mock(DatabaseMetaData.class);
146+
given(metaData.getDatabaseProductName()).willReturn(productName);
147+
given(metaData.getURL()).willReturn(connectionUrl);
148+
Connection connection = mock(Connection.class);
149+
given(connection.getMetaData()).willReturn(metaData);
150+
DataSource dataSource = mock(DataSource.class);
151+
given(dataSource.getConnection()).willReturn(connection);
152+
return dataSource;
153+
}
154+
60155
}

0 commit comments

Comments
 (0)