From b5178afa215f6b36d05f4fee5aff78225aaefe2e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 25 Apr 2023 10:03:53 +0100 Subject: [PATCH] Add Docker Compose support for MS SQL Server using JDBC Closes gh-35146 --- .../spring-boot-docker-compose/build.gradle | 3 +- .../connection/jdbc/JdbcUrlBuilder.java | 24 ++++- ...DockerComposeConnectionDetailsFactory.java | 91 +++++++++++++++++++ .../main/resources/META-INF/spring.factories | 1 + .../connection/jdbc/JdbcUrlBuilderTests.java | 15 +-- ...nectionDetailsFactoryIntegrationTests.java | 61 +++++++++++++ .../asciidoc/features/docker-compose.adoc | 4 +- 7 files changed, 186 insertions(+), 13 deletions(-) create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/sqlserver/MsSqlServerJdbcDockerComposeConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/sqlserver/MsSqlServerJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java diff --git a/spring-boot-project/spring-boot-docker-compose/build.gradle b/spring-boot-project/spring-boot-docker-compose/build.gradle index 7acd2c094ae..4fd5afde009 100644 --- a/spring-boot-project/spring-boot-docker-compose/build.gradle +++ b/spring-boot-project/spring-boot-docker-compose/build.gradle @@ -20,7 +20,6 @@ dependencies { optional("org.mongodb:mongodb-driver-core") optional("org.springframework.data:spring-data-r2dbc") - testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) testImplementation(project(":spring-boot-project:spring-boot-test")) testImplementation("org.springframework:spring-core-test") @@ -29,4 +28,6 @@ dependencies { testImplementation("org.mockito:mockito-core") testImplementation("ch.qos.logback:logback-classic") testImplementation("org.junit.jupiter:junit-jupiter") + + testRuntimeOnly("com.microsoft.sqlserver:mssql-jdbc") } diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/jdbc/JdbcUrlBuilder.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/jdbc/JdbcUrlBuilder.java index 3103dee935a..144686bd321 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/jdbc/JdbcUrlBuilder.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/jdbc/JdbcUrlBuilder.java @@ -47,6 +47,15 @@ public class JdbcUrlBuilder { this.containerPort = containerPort; } + /** + * Build a JDBC URL for the given {@link RunningService}. + * @param service the running service + * @return a new JDBC URL + */ + public String build(RunningService service) { + return build(service, null); + } + /** * Build a JDBC URL for the given {@link RunningService} and database. * @param service the running service @@ -54,11 +63,20 @@ public class JdbcUrlBuilder { * @return a new JDBC URL */ public String build(RunningService service, String database) { + return urlFor(service, database); + } + + private String urlFor(RunningService service, String database) { Assert.notNull(service, "Service must not be null"); - Assert.notNull(database, "Database must not be null"); String parameters = getParameters(service); - return "jdbc:%s://%s:%d/%s%s".formatted(this.driverProtocol, service.host(), - service.ports().get(this.containerPort), database, parameters); + StringBuilder url = new StringBuilder("jdbc:%s://%s:%d".formatted(this.driverProtocol, service.host(), + service.ports().get(this.containerPort))); + if (StringUtils.hasLength(database)) { + url.append("/"); + url.append(database); + } + url.append(parameters); + return url.toString(); } private String getParameters(RunningService service) { diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/sqlserver/MsSqlServerJdbcDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/sqlserver/MsSqlServerJdbcDockerComposeConnectionDetailsFactory.java new file mode 100644 index 00000000000..ebd204bf9c8 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/sqlserver/MsSqlServerJdbcDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,91 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.sqlserver; + +import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource; +import org.springframework.boot.docker.compose.service.connection.jdbc.JdbcUrlBuilder; + +/** + * {@link DockerComposeConnectionDetailsFactory} to create {@link JdbcConnectionDetails} + * for a {@code mssql/server} service. + * + * @author Andy Wilkinson + */ +class MsSqlServerJdbcDockerComposeConnectionDetailsFactory + extends DockerComposeConnectionDetailsFactory { + + protected MsSqlServerJdbcDockerComposeConnectionDetailsFactory() { + super("mssql/server"); + } + + @Override + protected JdbcConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { + return new MsSqlJdbcDockerComposeConnectionDetails(source.getRunningService()); + } + + /** + * {@link JdbcConnectionDetails} backed by a {@code mssql/server} + * {@link RunningService}. + */ + static class MsSqlJdbcDockerComposeConnectionDetails extends DockerComposeConnectionDetails + implements JdbcConnectionDetails { + + private static final JdbcUrlBuilder jdbcUrlBuilder = new JdbcUrlBuilder("sqlserver", 1433); + + private final MsSqlServerEnvironment environment; + + private final String jdbcUrl; + + MsSqlJdbcDockerComposeConnectionDetails(RunningService service) { + super(service); + this.environment = new MsSqlServerEnvironment(service.env()); + this.jdbcUrl = disableEncryptionIfNecessary(jdbcUrlBuilder.build(service, "")); + } + + private String disableEncryptionIfNecessary(String jdbcUrl) { + if (jdbcUrl.contains(";encrypt=false;")) { + return jdbcUrl; + } + StringBuilder jdbcUrlBuilder = new StringBuilder(jdbcUrl); + if (!jdbcUrl.endsWith(";")) { + jdbcUrlBuilder.append(";"); + } + jdbcUrlBuilder.append("encrypt=false;"); + return jdbcUrlBuilder.toString(); + } + + @Override + public String getUsername() { + return this.environment.getUsername(); + } + + @Override + public String getPassword() { + return this.environment.getPassword(); + } + + @Override + public String getJdbcUrl() { + return this.jdbcUrl; + } + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories index d3b47a99a75..6b7f5142e2b 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories @@ -16,5 +16,6 @@ org.springframework.boot.docker.compose.service.connection.postgres.PostgresJdbc org.springframework.boot.docker.compose.service.connection.postgres.PostgresR2dbcDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.rabbit.RabbitDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.redis.RedisDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.docker.compose.service.connection.sqlserver.MsSqlServerJdbcDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.sqlserver.MsSqlServerR2dbcDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.zipkin.ZipkinDockerComposeConnectionDetailsFactory diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/jdbc/JdbcUrlBuilderTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/jdbc/JdbcUrlBuilderTests.java index 060bdc90db7..6a3e15d8527 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/jdbc/JdbcUrlBuilderTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/jdbc/JdbcUrlBuilderTests.java @@ -47,7 +47,14 @@ class JdbcUrlBuilderTests { } @Test - void buildBuildsUrl() { + void buildBuildsUrlForService() { + RunningService service = mockService(456); + String url = this.builder.build(service); + assertThat(url).isEqualTo("jdbc:mydb://myhost:456"); + } + + @Test + void buildBuildsUrlForServiceAndDatabase() { RunningService service = mockService(456); String url = this.builder.build(service, "mydb"); assertThat(url).isEqualTo("jdbc:mydb://myhost:456/mydb"); @@ -66,12 +73,6 @@ class JdbcUrlBuilderTests { .withMessage("Service must not be null"); } - @Test - void buildWhenDatabaseIsNullThrowsException() { - assertThatIllegalArgumentException().isThrownBy(() -> this.builder.build(mockService(456), null)) - .withMessage("Database must not be null"); - } - private RunningService mockService(int mappedPort) { return mockService(mappedPort, Collections.emptyMap()); } diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/sqlserver/MsSqlServerJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/sqlserver/MsSqlServerJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 00000000000..a9d6d0e101e --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/sqlserver/MsSqlServerJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.sqlserver; + +import java.sql.Driver; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; +import org.springframework.boot.jdbc.DatabaseDriver; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.SimpleDriverDataSource; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link MsSqlServerJdbcDockerComposeConnectionDetailsFactory} + * + * @author Andy Wilkinson + */ +class MsSqlServerJdbcDockerComposeConnectionDetailsFactoryIntegrationTests + extends AbstractDockerComposeIntegrationTests { + + MsSqlServerJdbcDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("mssqlserver-compose.yaml"); + } + + @Test + @SuppressWarnings("unchecked") + void runCreatesConnectionDetailsThatCanBeUsedToAccessDatabase() throws ClassNotFoundException, LinkageError { + JdbcConnectionDetails connectionDetails = run(JdbcConnectionDetails.class); + assertThat(connectionDetails.getUsername()).isEqualTo("SA"); + assertThat(connectionDetails.getPassword()).isEqualTo("verYs3cret"); + assertThat(connectionDetails.getJdbcUrl()).startsWith("jdbc:sqlserver://"); + SimpleDriverDataSource dataSource = new SimpleDriverDataSource(); + dataSource.setUrl(connectionDetails.getJdbcUrl()); + dataSource.setUsername(connectionDetails.getUsername()); + dataSource.setPassword(connectionDetails.getPassword()); + dataSource.setDriverClass((Class) ClassUtils.forName(connectionDetails.getDriverClassName(), + getClass().getClassLoader())); + JdbcTemplate template = new JdbcTemplate(dataSource); + assertThat(template.queryForObject(DatabaseDriver.SQLSERVER.getValidationQuery(), Integer.class)).isEqualTo(1); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc index 314f152b2ee..625c335dc9a 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc @@ -44,13 +44,13 @@ The following service connections are currently supported: | Containers named "elasticsearch" | `JdbcConnectionDetails` -| Containers named "mariadb", "mysql" or "postgres" +| Containers named "mariadb", "mssql/server", "mysql", or "postgres" | `MongoConnectionDetails` | Containers named "mongo" | `R2dbcConnectionDetails` -| Containers named "mariadb", "mssql/server", "mysql" or "postgres" +| Containers named "mariadb", "mssql/server", "mysql", or "postgres" | `RabbitConnectionDetails` | Containers named "rabbitmq"