From 126e87e44dba044f339f48933adee3d5beb26f6d Mon Sep 17 00:00:00 2001
From: Andy Wilkinson <andy.wilkinson@broadcom.com>
Date: Wed, 19 Jun 2024 15:42:18 +0100
Subject: [PATCH] Fix appending of JDBC parameters to SQL Server JDBC URL

Fixes gh-41146
---
 .../connection/jdbc/JdbcUrlBuilder.java       | 23 +++++++++++++++----
 ...DockerComposeConnectionDetailsFactory.java | 19 ++++++++++++---
 .../connection/jdbc/JdbcUrlBuilderTests.java  | 16 ++++++++++++-
 ...nectionDetailsFactoryIntegrationTests.java | 16 ++++++++++++-
 ...qlserver-with-jdbc-parameters-compose.yaml | 11 +++++++++
 5 files changed, 75 insertions(+), 10 deletions(-)
 create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/sqlserver/mssqlserver-with-jdbc-parameters-compose.yaml

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 144686bd321e..da4815943c4a 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
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2023 the original author or authors.
+ * Copyright 2012-2024 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.
@@ -68,20 +68,33 @@ public String build(RunningService service, String database) {
 
 	private String urlFor(RunningService service, String database) {
 		Assert.notNull(service, "Service must not be null");
-		String parameters = getParameters(service);
 		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);
+		String parameters = getParameters(service);
+		if (StringUtils.hasLength(parameters)) {
+			appendParameters(url, parameters);
+		}
 		return url.toString();
 	}
 
+	/**
+	 * Appends to the given {@code url} the given {@code parameters}.
+	 * <p>
+	 * The default implementation appends a {@code ?} followed by the {@code parameters}.
+	 * @param url the url
+	 * @param parameters the parameters
+	 * @since 3.2.7
+	 */
+	protected void appendParameters(StringBuilder url, String parameters) {
+		url.append("?").append(parameters);
+	}
+
 	private String getParameters(RunningService service) {
-		String parameters = service.labels().get(PARAMETERS_LABEL);
-		return (StringUtils.hasLength(parameters)) ? "?" + parameters : "";
+		return service.labels().get(PARAMETERS_LABEL);
 	}
 
 }
diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/sqlserver/SqlServerJdbcDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/sqlserver/SqlServerJdbcDockerComposeConnectionDetailsFactory.java
index 90d68b909f28..c10f6a585734 100644
--- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/sqlserver/SqlServerJdbcDockerComposeConnectionDetailsFactory.java
+++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/sqlserver/SqlServerJdbcDockerComposeConnectionDetailsFactory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2023 the original author or authors.
+ * Copyright 2012-2024 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.
@@ -47,7 +47,7 @@ protected JdbcConnectionDetails getDockerComposeConnectionDetails(DockerComposeC
 	static class SqlServerJdbcDockerComposeConnectionDetails extends DockerComposeConnectionDetails
 			implements JdbcConnectionDetails {
 
-		private static final JdbcUrlBuilder jdbcUrlBuilder = new JdbcUrlBuilder("sqlserver", 1433);
+		private static final JdbcUrlBuilder jdbcUrlBuilder = new SqlServerJdbcUrlBuilder("sqlserver", 1433);
 
 		private final SqlServerEnvironment environment;
 
@@ -56,7 +56,7 @@ static class SqlServerJdbcDockerComposeConnectionDetails extends DockerComposeCo
 		SqlServerJdbcDockerComposeConnectionDetails(RunningService service) {
 			super(service);
 			this.environment = new SqlServerEnvironment(service.env());
-			this.jdbcUrl = disableEncryptionIfNecessary(jdbcUrlBuilder.build(service, ""));
+			this.jdbcUrl = disableEncryptionIfNecessary(jdbcUrlBuilder.build(service));
 		}
 
 		private String disableEncryptionIfNecessary(String jdbcUrl) {
@@ -86,6 +86,19 @@ public String getJdbcUrl() {
 			return this.jdbcUrl;
 		}
 
+		private static final class SqlServerJdbcUrlBuilder extends JdbcUrlBuilder {
+
+			private SqlServerJdbcUrlBuilder(String driverProtocol, int containerPort) {
+				super(driverProtocol, containerPort);
+			}
+
+			@Override
+			protected void appendParameters(StringBuilder url, String parameters) {
+				url.append(";").append(parameters);
+			}
+
+		}
+
 	}
 
 }
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 6a3e15d8527d..bb0589a16ae1 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
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2023 the original author or authors.
+ * Copyright 2012-2024 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.
@@ -67,6 +67,20 @@ void buildWhenHasParamsLabelBuildsUrl() {
 		assertThat(url).isEqualTo("jdbc:mydb://myhost:456/mydb?foo=bar");
 	}
 
+	@Test
+	void buildWithCustomAppendParametersWhenHasParamsLabelBuildsUrl() {
+		RunningService service = mockService(456, Map.of("org.springframework.boot.jdbc.parameters", "foo=bar"));
+		String url = new JdbcUrlBuilder("mydb", 1234) {
+
+			@Override
+			protected void appendParameters(StringBuilder url, String parameters) {
+				url.append(";").append(parameters);
+			}
+
+		}.build(service, "mydb");
+		assertThat(url).isEqualTo("jdbc:mydb://myhost:456/mydb;foo=bar");
+	}
+
 	@Test
 	void buildWhenServiceIsNullThrowsException() {
 		assertThatIllegalArgumentException().isThrownBy(() -> this.builder.build(null, "mydb"))
diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/sqlserver/SqlServerJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/sqlserver/SqlServerJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java
index 74b1ea45f4f0..f4983cf35018 100644
--- a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/sqlserver/SqlServerJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java
+++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/sqlserver/SqlServerJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java
@@ -40,13 +40,27 @@
 		disabledReason = "The SQL server image has no ARM support")
 class SqlServerJdbcDockerComposeConnectionDetailsFactoryIntegrationTests {
 
-	@SuppressWarnings("unchecked")
 	@DockerComposeTest(composeFile = "mssqlserver-compose.yaml", image = TestImage.SQL_SERVER)
 	void runCreatesConnectionDetailsThatCanBeUsedToAccessDatabase(JdbcConnectionDetails connectionDetails)
 			throws ClassNotFoundException, LinkageError {
 		assertThat(connectionDetails.getUsername()).isEqualTo("SA");
 		assertThat(connectionDetails.getPassword()).isEqualTo("verYs3cret");
 		assertThat(connectionDetails.getJdbcUrl()).startsWith("jdbc:sqlserver://");
+		checkDatabaseAccess(connectionDetails);
+	}
+
+	@DockerComposeTest(composeFile = "mssqlserver-with-jdbc-parameters-compose.yaml", image = TestImage.SQL_SERVER)
+	void runWithJdbcParametersCreatesConnectionDetailsThatCanBeUsedToAccessDatabase(
+			JdbcConnectionDetails connectionDetails) throws ClassNotFoundException {
+		assertThat(connectionDetails.getUsername()).isEqualTo("SA");
+		assertThat(connectionDetails.getPassword()).isEqualTo("verYs3cret");
+		assertThat(connectionDetails.getJdbcUrl()).startsWith("jdbc:sqlserver://")
+			.contains(";sendStringParametersAsUnicode=false;");
+		checkDatabaseAccess(connectionDetails);
+	}
+
+	@SuppressWarnings("unchecked")
+	private void checkDatabaseAccess(JdbcConnectionDetails connectionDetails) throws ClassNotFoundException {
 		SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
 		dataSource.setUrl(connectionDetails.getJdbcUrl());
 		dataSource.setUsername(connectionDetails.getUsername());
diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/sqlserver/mssqlserver-with-jdbc-parameters-compose.yaml b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/sqlserver/mssqlserver-with-jdbc-parameters-compose.yaml
new file mode 100644
index 000000000000..76dd4998aeea
--- /dev/null
+++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/sqlserver/mssqlserver-with-jdbc-parameters-compose.yaml
@@ -0,0 +1,11 @@
+services:
+  database:
+    image: '{imageName}'
+    ports:
+      - '1433'
+    environment:
+      - 'MSSQL_PID=express'
+      - 'MSSQL_SA_PASSWORD=verYs3cret'
+      - 'ACCEPT_EULA=yes'
+    labels:
+      org.springframework.boot.jdbc.parameters: sendStringParametersAsUnicode=false
\ No newline at end of file