Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 186 additions & 5 deletions src/java/src/main/java/org/openapitools/config/DataSourceConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashSet;
import java.util.Set;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
Expand All @@ -20,6 +25,7 @@
*/
@Configuration
@Conditional(OnDatabaseUrlCondition.class)
@SuppressWarnings("PMD.GodClass")
public class DataSourceConfig {

@Value("${SPRING_DATASOURCE_URL:}")
Expand All @@ -40,6 +46,12 @@ public class DataSourceConfig {
@Value("${spring.datasource.driver-class-name:org.postgresql.Driver}")
private String driverClassName;

@Value("${K_SERVICE:}")
private String cloudRunService;

@Value("${K_REVISION:}")
private String cloudRunRevision;

/**
* Creates a HikariConfig bean configured from application.properties.
*
Expand All @@ -49,12 +61,14 @@ public class DataSourceConfig {
@ConfigurationProperties(prefix = "spring.datasource.hikari")
public HikariConfig hikariConfig() {
HikariConfig config = new HikariConfig();
String jdbcUrl = resolveJdbcUrl();

// Set core JDBC properties
config.setJdbcUrl(resolveJdbcUrl());
config.setJdbcUrl(jdbcUrl);
config.setUsername(username);
config.setPassword(password);
config.setDriverClassName(driverClassName);
configureCloudSqlProperties(config, jdbcUrl);

return config;
}
Expand All @@ -72,19 +86,186 @@ private String resolveJdbcUrl() {
}

private String normalizeDatabaseUrl(String url) {
if (url.startsWith("postgresql://")) {
return "jdbc:" + url;
if (url.startsWith("jdbc:postgresql://")) {
return url;
}
if (url.startsWith("postgres://")) {
return "jdbc:postgresql://" + url.substring("postgres://".length());
if (url.startsWith("postgresql://") || url.startsWith("postgres://")) {
return toJdbcPostgresUrl(url);
}

return url;
}

private String toJdbcPostgresUrl(String url) {
String normalized =
url.startsWith("postgres://")
? "postgresql://" + url.substring("postgres://".length())
: url;
URI uri = URI.create(normalized);
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The URI.create() method on line 104 can throw IllegalArgumentException if the URL string violates RFC 2396. There is no error handling for malformed URLs, which could cause the application to fail during startup if an invalid DATABASE_URL is provided. Consider wrapping this in a try-catch block and either providing a clear error message or falling back to a safe default behavior.

Suggested change
URI uri = URI.create(normalized);
URI uri;
try {
uri = URI.create(normalized);
} catch (IllegalArgumentException ex) {
throw new IllegalArgumentException("Invalid PostgreSQL database URL: " + url, ex);
}

Copilot uses AI. Check for mistakes.

String host = isNotBlank(uri.getHost()) ? uri.getHost() : "localhost";

StringBuilder jdbcUrl = new StringBuilder("jdbc:postgresql://");
jdbcUrl.append(host);

if (uri.getPort() != -1) {
jdbcUrl.append(':').append(uri.getPort());
}

if (isNotBlank(uri.getRawPath())) {
jdbcUrl.append(uri.getRawPath());
} else {
jdbcUrl.append('/');
}

String query =
appendCredentialsIfNeeded(uri.getRawQuery(), extractRawUserInfo(uri, normalized));
if (isNotBlank(query)) {
jdbcUrl.append('?').append(query);
}

return jdbcUrl.toString();
}

@SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"})
private String appendCredentialsIfNeeded(String rawQuery, String rawUserInfo) {
if (!isNotBlank(rawUserInfo)) {
return rawQuery;
}

Set<String> existingKeys = queryKeys(rawQuery);
String[] userInfoParts = rawUserInfo.split(":", 2);
String user = userInfoParts.length > 0 ? userInfoParts[0] : "";
String password = userInfoParts.length > 1 ? userInfoParts[1] : "";

StringBuilder query = new StringBuilder(rawQuery == null ? "" : rawQuery);
if (!existingKeys.contains("user") && isNotBlank(user)) {
appendQueryParam(query, "user", user);
}
if (!existingKeys.contains("password") && isNotBlank(password)) {
appendQueryParam(query, "password", password);
Comment on lines +137 to +146
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Credentials extracted from the URL are appended to the query string without proper URL encoding. The code uses getRawUserInfo() which returns URL-encoded values, but then splits and appends them directly. If the credentials contain special characters that need encoding (e.g., '@', '&', '=', '%', spaces), and they are not already properly encoded in the source URL, the resulting JDBC URL will be malformed. The safest approach is to: 1) URL-decode the extracted credentials using URLDecoder, 2) URL-encode them again before appending using URLEncoder.encode(value, StandardCharsets.UTF_8). This ensures correct handling regardless of whether the source URL was properly encoded.

Copilot uses AI. Check for mistakes.
}

return query.length() == 0 ? null : query.toString();
}

private String extractRawUserInfo(URI uri, String rawUrl) {
if (isNotBlank(uri.getRawUserInfo())) {
return uri.getRawUserInfo();
}

int schemeSeparator = rawUrl.indexOf("://");
int authorityStart = schemeSeparator >= 0 ? schemeSeparator + 3 : 0;
int pathStart = rawUrl.indexOf('/', authorityStart);
int userInfoEnd = rawUrl.indexOf('@', authorityStart);

if (userInfoEnd > authorityStart && (pathStart == -1 || userInfoEnd < pathStart)) {
return rawUrl.substring(authorityStart, userInfoEnd);
}

return null;
}

private Set<String> queryKeys(String rawQuery) {
Set<String> keys = new LinkedHashSet<>();
if (!isNotBlank(rawQuery)) {
return keys;
}

for (String part : rawQuery.split("&")) {
if (!isNotBlank(part)) {
continue;
}

int separator = part.indexOf('=');
String key = separator >= 0 ? part.substring(0, separator) : part;
if (isNotBlank(key)) {
keys.add(key);
}
}

return keys;
}

private void appendQueryParam(StringBuilder query, String key, String value) {
if (query.length() > 0) {
query.append('&');
}
query.append(key).append('=').append(value);
}

private boolean isNotBlank(String value) {
return value != null && !value.isBlank();
}

private void configureCloudSqlProperties(HikariConfig config, String jdbcUrl) {
String instanceUnixSocket = extractUnixSocketPath(jdbcUrl);
if (!isNotBlank(instanceUnixSocket)) {
return;
}

config.addDataSourceProperty("socketFactory", "com.google.cloud.sql.postgres.SocketFactory");
config.addDataSourceProperty("unixSocketPath", instanceUnixSocket);

if (isCloudRun()) {
config.addDataSourceProperty("cloudSqlRefreshStrategy", "lazy");
}
}

private String extractUnixSocketPath(String jdbcUrl) {
String rawQuery = extractRawQuery(jdbcUrl);
if (!isNotBlank(rawQuery)) {
return null;
}

String host = extractQueryParam(rawQuery, "host");
if (isNotBlank(host) && decodeQueryValue(host).startsWith("/cloudsql/")) {
return decodeQueryValue(host);
Comment on lines +222 to +223
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extractUnixSocketPath method decodes the host query parameter value twice on line 222 and line 223. The decodeQueryValue(host) is called twice for the same value - once in the condition check and once in the return statement. This is inefficient and could lead to double-decoding issues if the value contains URL-encoded characters. Consider decoding once and storing the result in a variable.

Suggested change
if (isNotBlank(host) && decodeQueryValue(host).startsWith("/cloudsql/")) {
return decodeQueryValue(host);
if (isNotBlank(host)) {
String decodedHost = decodeQueryValue(host);
if (decodedHost.startsWith("/cloudsql/")) {
return decodedHost;
}

Copilot uses AI. Check for mistakes.
}

String unixSocketPath = extractQueryParam(rawQuery, "unixSocketPath");
if (isNotBlank(unixSocketPath)) {
return decodeQueryValue(unixSocketPath);
}

return null;
}

private String extractRawQuery(String jdbcUrl) {
int queryStart = jdbcUrl.indexOf('?');
if (queryStart < 0 || queryStart + 1 >= jdbcUrl.length()) {
return null;
}
return jdbcUrl.substring(queryStart + 1);
}

private String extractQueryParam(String rawQuery, String key) {
for (String part : rawQuery.split("&")) {
if (!isNotBlank(part)) {
continue;
}

int separator = part.indexOf('=');
if (separator < 0) {
continue;
}

String partKey = part.substring(0, separator);
if (key.equals(partKey)) {
return part.substring(separator + 1);
}
}
return null;
}

private String decodeQueryValue(String value) {
return URLDecoder.decode(value, StandardCharsets.UTF_8);
}

private boolean isCloudRun() {
return isNotBlank(cloudRunService) || isNotBlank(cloudRunRevision);
}

/**
* Creates a HikariCP DataSource bean from the configured HikariConfig.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ void hikariConfig_ShouldUseSpringDatasourceUrlAsIs() throws Exception {
setField(config, "username", "user");
setField(config, "password", "pass");
setField(config, "driverClassName", "org.postgresql.Driver");
setField(config, "cloudRunService", "");
setField(config, "cloudRunRevision", "");

HikariConfig result = config.hikariConfig();

Expand All @@ -38,6 +40,8 @@ void hikariConfig_ShouldNormalizeDatabaseUrlPostgresqlScheme() throws Exception
setField(config, "username", "user");
setField(config, "password", "pass");
setField(config, "driverClassName", "org.postgresql.Driver");
setField(config, "cloudRunService", "");
setField(config, "cloudRunRevision", "");

HikariConfig result = config.hikariConfig();

Expand All @@ -54,12 +58,71 @@ void hikariConfig_ShouldNormalizeDatabaseUrlPostgresScheme() throws Exception {
setField(config, "username", "user");
setField(config, "password", "pass");
setField(config, "driverClassName", "org.postgresql.Driver");
setField(config, "cloudRunService", "");
setField(config, "cloudRunRevision", "");

HikariConfig result = config.hikariConfig();

assertThat(result.getJdbcUrl()).isEqualTo("jdbc:postgresql://localhost:5432/lamp");
}

@Test
void hikariConfig_ShouldNormalizeCloudSqlSocketStyleDatabaseUrl() throws Exception {
DataSourceConfig config = new DataSourceConfig();

setField(config, "springDatasourceUrl", "");
setField(
config,
"databaseUrl",
"postgresql://postgres:redacted-password@/lamp-control?"
+ "host=/cloudsql/project-id:region:instance-id&connect_timeout=5");
setField(config, "fallbackJdbcUrl", "jdbc:postgresql://localhost:5432/fallback");
setField(config, "username", "ignored");
setField(config, "password", "ignored");
setField(config, "driverClassName", "org.postgresql.Driver");
setField(config, "cloudRunService", "");
setField(config, "cloudRunRevision", "");

HikariConfig result = config.hikariConfig();

assertThat(result.getJdbcUrl())
.isEqualTo(
"jdbc:postgresql://localhost/lamp-control"
+ "?host=/cloudsql/project-id:region:instance-id"
+ "&connect_timeout=5"
+ "&user=postgres"
+ "&password=redacted-password");
assertThat(result.getDataSourceProperties())
.containsEntry("socketFactory", "com.google.cloud.sql.postgres.SocketFactory")
.containsEntry("unixSocketPath", "/cloudsql/project-id:region:instance-id")
.doesNotContainKey("cloudSqlRefreshStrategy");
}
Comment on lines +69 to +99
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test coverage is missing for edge cases such as URLs with special characters in credentials (e.g., passwords containing '@', '&', '=', '%'), malformed URLs, and URLs without credentials. These edge cases are important to test given the complex URL parsing and credential extraction logic. Consider adding test cases for: passwords with special characters that need URL encoding, URLs with only username (no password), and URLs with unusual but valid formats.

Copilot uses AI. Check for mistakes.

@Test
void hikariConfig_ShouldSetCloudSqlRefreshStrategyOnCloudRunInSocketMode() throws Exception {
DataSourceConfig config = new DataSourceConfig();

setField(config, "springDatasourceUrl", "");
setField(
config,
"databaseUrl",
"postgresql://postgres:redacted-password@/lamp-control?"
+ "host=/cloudsql/project-id:region:instance-id&connect_timeout=5");
setField(config, "fallbackJdbcUrl", "jdbc:postgresql://localhost:5432/fallback");
setField(config, "username", "ignored");
setField(config, "password", "ignored");
setField(config, "driverClassName", "org.postgresql.Driver");
setField(config, "cloudRunService", "lamp-control-service");
setField(config, "cloudRunRevision", "");
Comment on lines +115 to +116
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test coverage for Cloud Run detection only tests the K_SERVICE environment variable (line 115), but the isCloudRun() method also checks K_REVISION. Consider adding a test case where only K_REVISION is set to ensure both environment variables are properly handled.

Copilot uses AI. Check for mistakes.

HikariConfig result = config.hikariConfig();

assertThat(result.getDataSourceProperties())
.containsEntry("socketFactory", "com.google.cloud.sql.postgres.SocketFactory")
.containsEntry("unixSocketPath", "/cloudsql/project-id:region:instance-id")
.containsEntry("cloudSqlRefreshStrategy", "lazy");
}

@Test
void hikariConfig_ShouldKeepJdbcDatabaseUrlUnchanged() throws Exception {
DataSourceConfig config = new DataSourceConfig();
Expand All @@ -70,6 +133,8 @@ void hikariConfig_ShouldKeepJdbcDatabaseUrlUnchanged() throws Exception {
setField(config, "username", "user");
setField(config, "password", "pass");
setField(config, "driverClassName", "org.postgresql.Driver");
setField(config, "cloudRunService", "");
setField(config, "cloudRunRevision", "");

HikariConfig result = config.hikariConfig();

Expand All @@ -86,6 +151,8 @@ void hikariConfig_ShouldFallbackToSpringDatasourceProperty() throws Exception {
setField(config, "username", "user");
setField(config, "password", "pass");
setField(config, "driverClassName", "org.postgresql.Driver");
setField(config, "cloudRunService", "");
setField(config, "cloudRunRevision", "");

HikariConfig result = config.hikariConfig();

Expand Down