diff --git a/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDriver.java b/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDriver.java index 11199c372..68b00cb6c 100644 --- a/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDriver.java +++ b/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDriver.java @@ -17,6 +17,7 @@ package com.google.cloud.bigquery.jdbc; import com.google.cloud.bigquery.exception.BigQueryJdbcException; +import com.google.cloud.bigquery.exception.BigQueryJdbcRuntimeException; import io.grpc.LoadBalancerRegistry; import io.grpc.internal.PickFirstLoadBalancerProvider; import java.io.IOException; @@ -123,6 +124,11 @@ public Connection connect(String url, Properties info) throws SQLException { // strip 'jdbc:' from the URL, add any extra properties String connectionUri = BigQueryJdbcUrlUtility.appendPropertiesToURL(url.substring(5), this.toString(), info); + try { + BigQueryJdbcUrlUtility.parseUrl(connectionUri); + } catch (BigQueryJdbcRuntimeException e) { + throw new BigQueryJdbcException(e.getMessage(), e); + } // LogLevel String logLevelStr = diff --git a/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java b/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java index 50d0d33bb..ab506b59f 100644 --- a/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java +++ b/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java @@ -19,10 +19,16 @@ import com.google.api.client.util.escape.CharEscapers; import com.google.cloud.bigquery.BigQueryOptions; import com.google.cloud.bigquery.exception.BigQueryJdbcRuntimeException; +import com.google.common.collect.ImmutableList; +import com.google.common.net.UrlEscapers; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -39,6 +45,14 @@ */ final class BigQueryJdbcUrlUtility { + private static final Map> PARSE_CACHE = + Collections.synchronizedMap( + new LinkedHashMap>(50, 0.75f, true) { + protected boolean removeEldestEntry(Map.Entry> eldest) { + return size() > 50; // bound cache size + } + }); + // TODO: Add all Connection options static final String ALLOW_LARGE_RESULTS_PROPERTY_NAME = "AllowLargeResults"; static final String LARGE_RESULTS_TABLE_PROPERTY_NAME = "LargeResultTable"; @@ -122,6 +136,10 @@ final class BigQueryJdbcUrlUtility { static final String BYOID_SUBJECT_TOKEN_TYPE_PROPERTY_NAME = "BYOID_SubjectTokenType"; static final String BYOID_TOKEN_URI_PROPERTY_NAME = "BYOID_TokenUri"; static final String PARTNER_TOKEN_PROPERTY_NAME = "PartnerToken"; + private static final Pattern PARTNER_TOKEN_PATTERN = + Pattern.compile( + "(?:^|(?<=;))" + PARTNER_TOKEN_PROPERTY_NAME + "=\\s*((?:\\([^)]*\\)|[^;])*?)(?=(?:;|$))", + Pattern.CASE_INSENSITIVE); static final String METADATA_FETCH_THREAD_COUNT_PROPERTY_NAME = "MetaDataFetchThreadCount"; static final int DEFAULT_METADATA_FETCH_THREAD_COUNT_VALUE = 32; static final String RETRY_TIMEOUT_IN_SECS_PROPERTY_NAME = "Timeout"; @@ -591,6 +609,37 @@ final class BigQueryJdbcUrlUtility { + " header.") .build()))); + private static final List NETWORK_PROPERTIES = + ImmutableList.of( + PARTNER_TOKEN_PROPERTY_NAME, + ENDPOINT_OVERRIDES_PROPERTY_NAME, + PRIVATE_SERVICE_CONNECT_PROPERTY_NAME); + + private static final Map PROPERTY_NAME_MAP; + + static { + Map map = new HashMap<>(); + for (BigQueryConnectionProperty p : VALID_PROPERTIES) { + map.put(p.getName().toUpperCase(), p.getName()); + } + for (BigQueryConnectionProperty p : AUTH_PROPERTIES) { + map.put(p.getName().toUpperCase(), p.getName()); + } + for (BigQueryConnectionProperty p : PROXY_PROPERTIES) { + map.put(p.getName().toUpperCase(), p.getName()); + } + for (String p : OVERRIDE_PROPERTIES) { + map.put(p.toUpperCase(), p); + } + for (String p : BYOID_PROPERTIES) { + map.put(p.toUpperCase(), p); + } + for (String p : NETWORK_PROPERTIES) { + map.put(p.toUpperCase(), p); + } + PROPERTY_NAME_MAP = Collections.unmodifiableMap(map); + } + private BigQueryJdbcUrlUtility() {} /** @@ -601,12 +650,72 @@ private BigQueryJdbcUrlUtility() {} * @return The String value of the property, or the default value if the property is not found. */ static String parseUriProperty(String uri, String property) { - Pattern pattern = Pattern.compile(String.format("(?is)(?:;|\\?)%s=(.*?)(?:;|$)", property)); - Matcher matcher = pattern.matcher(uri); - if (matcher.find() && matcher.groupCount() == 1) { - return CharEscapers.decodeUriPath(matcher.group(1)); + Map map = parseUrl(uri); + if (PROPERTY_NAME_MAP.containsKey(property.toUpperCase())) { + return map.get(PROPERTY_NAME_MAP.get(property.toUpperCase())); } - return null; + return map.get(property); + } + + /** + * Parses the URL into a map of key-value pairs, validating that all keys are known properties. + * + * @param url The URL to parse. + * @return A map of property names to values. + * @throws BigQueryJdbcRuntimeException if an unknown property is found or the URL is malformed. + */ + static Map parseUrl(String url) { + if (url == null) { + return Collections.emptyMap(); + } + return PARSE_CACHE.computeIfAbsent(url, BigQueryJdbcUrlUtility::parseUrlInternal); + } + + private static Map parseUrlInternal(String url) { + Map map = new HashMap<>(); + if (url == null) { + return map; + } + + String[] urlParts = url.split(";", 2); + if (urlParts.length < 2) { + return map; + } + + String urlToParse = urlParts[1]; + + // Parse PartnerToken separately as it contains ';' + Matcher matcher = PARTNER_TOKEN_PATTERN.matcher(urlToParse); + if (matcher.find()) { + String rawToken = matcher.group(1).trim(); + String token = + (rawToken.startsWith("(") && rawToken.endsWith(")")) + ? rawToken.substring(1, rawToken.length() - 1).trim() + : rawToken; + + if (token.toUpperCase().startsWith("GPN:")) { + map.put(PARTNER_TOKEN_PROPERTY_NAME, " (" + token + ")"); + } + urlToParse = matcher.replaceFirst(""); + } + + String[] parts = urlToParse.split(";"); + for (String part : parts) { + if (part.trim().isEmpty()) { + continue; + } + String[] kv = part.split("=", 2); + String key = kv[0].trim().toUpperCase(); + if (kv.length != 2 || !PROPERTY_NAME_MAP.containsKey(key)) { + String ref = (kv.length == 2) ? key : part; + String safeRef = ref.length() > 32 ? ref.substring(0, 32) + "..." : ref; + throw new BigQueryJdbcRuntimeException( + String.format("Wrong value or unknown setting: %s", safeRef)); + } + + map.put(PROPERTY_NAME_MAP.get(key), CharEscapers.decodeUriPath(kv[1])); + } + return Collections.unmodifiableMap(map); } /** @@ -622,7 +731,11 @@ static String appendPropertiesToURL(String url, String callerClassName, Properti for (Entry entry : properties.entrySet()) { if (entry.getValue() != null && !"".equals(entry.getValue())) { LOG.finest("Appending %s with value %s to URL", entry.getKey(), entry.getValue()); - urlBuilder.append(";").append(entry.getKey()).append("=").append(entry.getValue()); + String encodedValue = + UrlEscapers.urlFormParameterEscaper() + .escape((String) entry.getValue()) + .replace("+", "%20"); + urlBuilder.append(";").append(entry.getKey()).append("=").append(encodedValue); } } return urlBuilder.toString(); @@ -697,22 +810,17 @@ public static String parsePartnerTokenProperty(String url, String callerClassNam LOG.finest("++enter++\t" + callerClassName); // This property is expected to be set by partners only. For more details on exact format // supported, refer b/396086960 - String regex = - PARTNER_TOKEN_PROPERTY_NAME + "=\\s*\\(\\s*(GPN:[^;]*?)\\s*(?:;\\s*([^)]*?))?\\s*\\)"; - Pattern pattern = Pattern.compile(regex); - Matcher matcher = pattern.matcher(url); - + Matcher matcher = PARTNER_TOKEN_PATTERN.matcher(url); if (matcher.find()) { - String gpnPart = matcher.group(1); - String environmentPart = matcher.group(2); - StringBuilder partnerToken = new StringBuilder(" ("); - partnerToken.append(gpnPart); - if (environmentPart != null && !environmentPart.trim().isEmpty()) { - partnerToken.append("; "); - partnerToken.append(environmentPart); + String rawToken = matcher.group(1).trim(); + String token = + (rawToken.startsWith("(") && rawToken.endsWith(")")) + ? rawToken.substring(1, rawToken.length() - 1).trim() + : rawToken; + + if (token.toUpperCase().startsWith("GPN:")) { + return " (" + token + ")"; } - partnerToken.append(")"); - return partnerToken.toString(); } return null; } @@ -793,7 +901,12 @@ static Map parseOverrideProperties(String url, String callerClas Matcher matcher = pattern.matcher(url); String overridePropertiesString; if (matcher.find() && matcher.groupCount() >= 1) { - overridePropertiesString = matcher.group(2); + try { + overridePropertiesString = + URLDecoder.decode(matcher.group(2), StandardCharsets.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + throw new BigQueryJdbcRuntimeException(e); + } } else { return overrideProps; } diff --git a/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/DataSource.java b/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/DataSource.java index b1501890b..0050976f2 100644 --- a/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/DataSource.java +++ b/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/DataSource.java @@ -78,6 +78,17 @@ public class DataSource implements javax.sql.DataSource { private Integer metadataFetchThreadCount; private String sslTrustStorePath; private String sslTrustStorePassword; + private Map labels; + private String requestReason; + private Integer timeout; + private Integer jobTimeout; + private Integer retryInitialDelay; + private Integer retryMaxDelay; + private Integer httpConnectTimeout; + private Integer httpReadTimeout; + private Long maximumBytesBilled; + private Integer swaActivationRowCount; + private Integer swaAppendRowCount; // Make sure the JDBC driver class is loaded. static { @@ -145,7 +156,7 @@ private Properties createProperties() { } if (this.queryProperties != null) { connectionProperties.setProperty( - BigQueryJdbcUrlUtility.QUERY_PROPERTIES_NAME, this.queryProperties.toString()); + BigQueryJdbcUrlUtility.QUERY_PROPERTIES_NAME, serializeMap(this.queryProperties)); } if (this.enableSession != null) { connectionProperties.setProperty( @@ -283,9 +294,73 @@ private Properties createProperties() { BigQueryJdbcUrlUtility.SSL_TRUST_STORE_PWD_PROPERTY_NAME, String.valueOf(this.sslTrustStorePassword)); } + if (this.labels != null) { + connectionProperties.setProperty( + BigQueryJdbcUrlUtility.LABELS_PROPERTY_NAME, serializeMap(this.labels)); + } + if (this.requestReason != null) { + connectionProperties.setProperty( + BigQueryJdbcUrlUtility.REQUEST_REASON_PROPERTY_NAME, this.requestReason); + } + if (this.timeout != null) { + connectionProperties.setProperty( + BigQueryJdbcUrlUtility.RETRY_TIMEOUT_IN_SECS_PROPERTY_NAME, String.valueOf(this.timeout)); + } + if (this.jobTimeout != null) { + connectionProperties.setProperty( + BigQueryJdbcUrlUtility.JOB_TIMEOUT_PROPERTY_NAME, String.valueOf(this.jobTimeout)); + } + if (this.retryInitialDelay != null) { + connectionProperties.setProperty( + BigQueryJdbcUrlUtility.RETRY_INITIAL_DELAY_PROPERTY_NAME, + String.valueOf(this.retryInitialDelay)); + } + if (this.retryMaxDelay != null) { + connectionProperties.setProperty( + BigQueryJdbcUrlUtility.RETRY_MAX_DELAY_PROPERTY_NAME, String.valueOf(this.retryMaxDelay)); + } + if (this.httpConnectTimeout != null) { + connectionProperties.setProperty( + BigQueryJdbcUrlUtility.HTTP_CONNECT_TIMEOUT_PROPERTY_NAME, + String.valueOf(this.httpConnectTimeout)); + } + if (this.httpReadTimeout != null) { + connectionProperties.setProperty( + BigQueryJdbcUrlUtility.HTTP_READ_TIMEOUT_PROPERTY_NAME, + String.valueOf(this.httpReadTimeout)); + } + if (this.maximumBytesBilled != null) { + connectionProperties.setProperty( + BigQueryJdbcUrlUtility.MAX_BYTES_BILLED_PROPERTY_NAME, + String.valueOf(this.maximumBytesBilled)); + } + if (this.swaActivationRowCount != null) { + connectionProperties.setProperty( + BigQueryJdbcUrlUtility.SWA_ACTIVATION_ROW_COUNT_PROPERTY_NAME, + String.valueOf(this.swaActivationRowCount)); + } + if (this.swaAppendRowCount != null) { + connectionProperties.setProperty( + BigQueryJdbcUrlUtility.SWA_APPEND_ROW_COUNT_PROPERTY_NAME, + String.valueOf(this.swaAppendRowCount)); + } return connectionProperties; } + private String serializeMap(Map map) { + if (map == null || map.isEmpty()) { + return ""; + } + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : map.entrySet()) { + if (sb.length() > 0) { + sb.append(","); + } + sb.append(entry.getKey()).append("=").append(entry.getValue()); + } + return sb.toString(); + } + @Override public Connection getConnection(String username, String password) throws SQLException { LOG.warning( @@ -631,6 +706,94 @@ public void setSSLTrustStorePassword(String sslTrustStorePassword) { this.sslTrustStorePassword = sslTrustStorePassword; } + public Map getLabels() { + return labels; + } + + public void setLabels(Map labels) { + this.labels = labels; + } + + public String getRequestReason() { + return requestReason; + } + + public void setRequestReason(String requestReason) { + this.requestReason = requestReason; + } + + public Integer getTimeout() { + return timeout; + } + + public void setTimeout(Integer timeout) { + this.timeout = timeout; + } + + public Integer getJobTimeout() { + return jobTimeout; + } + + public void setJobTimeout(Integer jobTimeout) { + this.jobTimeout = jobTimeout; + } + + public Integer getRetryInitialDelay() { + return retryInitialDelay; + } + + public void setRetryInitialDelay(Integer retryInitialDelay) { + this.retryInitialDelay = retryInitialDelay; + } + + public Integer getRetryMaxDelay() { + return retryMaxDelay; + } + + public void setRetryMaxDelay(Integer retryMaxDelay) { + this.retryMaxDelay = retryMaxDelay; + } + + public Integer getHttpConnectTimeout() { + return httpConnectTimeout; + } + + public void setHttpConnectTimeout(Integer httpConnectTimeout) { + this.httpConnectTimeout = httpConnectTimeout; + } + + public Integer getHttpReadTimeout() { + return httpReadTimeout; + } + + public void setHttpReadTimeout(Integer httpReadTimeout) { + this.httpReadTimeout = httpReadTimeout; + } + + public Long getMaximumBytesBilled() { + return maximumBytesBilled; + } + + public void setMaximumBytesBilled(Long maximumBytesBilled) { + this.maximumBytesBilled = maximumBytesBilled; + } + + public Integer getSwaActivationRowCount() { + return swaActivationRowCount; + } + + public void setSwaActivationRowCount(Integer swaActivationRowCount) { + this.swaActivationRowCount = swaActivationRowCount; + } + + public Integer getSwaAppendRowCount() { + return swaAppendRowCount; + } + + public void setSwaAppendRowCount(Integer swaAppendRowCount) { + this.swaAppendRowCount = swaAppendRowCount; + } + @Override public PrintWriter getLogWriter() { return null; diff --git a/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryDriverTest.java b/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryDriverTest.java index 6a33c6144..2d7664f5a 100644 --- a/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryDriverTest.java +++ b/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryDriverTest.java @@ -16,7 +16,10 @@ package com.google.cloud.bigquery.jdbc; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import com.google.cloud.bigquery.exception.BigQueryJdbcException; +import com.google.cloud.bigquery.exception.BigQueryJdbcRuntimeException; import java.sql.Connection; import java.sql.DriverPropertyInfo; import java.sql.SQLException; @@ -93,4 +96,17 @@ public void testGetParentLoggerReturnsLogger() { public void testJDBCCompliantReturnsFalse() { assertThat(bigQueryDriver.jdbcCompliant()).isFalse(); } + + @Test + public void testConnectWithInvalidUrlChainsException() { + try { + bigQueryDriver.connect( + "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;InvalidProperty=Value", + new Properties()); + fail("Expected SQLException"); + } catch (SQLException e) { + assertThat((Throwable) e).isInstanceOf(BigQueryJdbcException.class); + assertThat(e.getCause()).isInstanceOf(BigQueryJdbcRuntimeException.class); + } + } } diff --git a/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcBaseTest.java b/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcBaseTest.java index 616f3bab9..1ee627b8a 100644 --- a/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcBaseTest.java +++ b/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcBaseTest.java @@ -51,7 +51,7 @@ protected static URIBuilder getBaseUri(int authType) { } protected static URIBuilder getBaseUri(int authType, String projectId) { - return getBaseUri(authType).append("PROJECT_ID", projectId); + return getBaseUri(authType).append("ProjectId", projectId); } protected static URIBuilder getUriOAuthServiceAccount() { diff --git a/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java b/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java index b16d1a398..830e51533 100644 --- a/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java +++ b/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java @@ -47,6 +47,30 @@ public void testParsePropertyWithNoDefault() { assertThat(result).isNull(); } + @Test + public void testParseUrlWithUnknownProperty_throwsException() { + String url = + "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" + + "ProjectId=MyBigQueryProject;" + + "UnknownProperty=SomeValue"; + + assertThrows( + BigQueryJdbcRuntimeException.class, + () -> BigQueryJdbcUrlUtility.parseUriProperty(url, "ProjectId")); + } + + @Test + public void testParseUrlWithTypo_throwsException() { + String url = + "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" + + "ProjectId=MyBigQueryProject;" + + "ProjeectId=TypoValue"; + + assertThrows( + BigQueryJdbcRuntimeException.class, + () -> BigQueryJdbcUrlUtility.parseUriProperty(url, "ProjectId")); + } + @Test public void testParsePropertyWithDefault() { String url = @@ -106,21 +130,36 @@ public void testConnectionPropertiesFromURI() { @Test public void testConnectionPropertiesFromURIMultiline() { String connection_uri = - "bigquery://https://www.googleapis.com/bigquery/v2:443;Multiline=value1\nvalue2\n;"; + "bigquery://https://www.googleapis.com/bigquery/v2:443;OAuthPvtKey=value1\nvalue2\n;"; - assertThat(BigQueryJdbcUrlUtility.parseUriProperty(connection_uri, "Multiline")) + assertThat(BigQueryJdbcUrlUtility.parseUriProperty(connection_uri, "OAuthPvtKey")) .isEqualTo("value1\nvalue2\n"); } @Test public void testConnectionPropertiesFromURIMultilineNoSemicolon() { String connection_uri = - "bigquery://https://www.googleapis.com/bigquery/v2:443;Multiline=value1\nvalue2"; + "bigquery://https://www.googleapis.com/bigquery/v2:443;OAuthPvtKey=value1\nvalue2"; - assertThat(BigQueryJdbcUrlUtility.parseUriProperty(connection_uri, "Multiline")) + assertThat(BigQueryJdbcUrlUtility.parseUriProperty(connection_uri, "OAuthPvtKey")) .isEqualTo("value1\nvalue2"); } + @Test + public void testParseUrl_longUnknownProperty_sanitized() { + String longKey = String.join("", Collections.nCopies(50, "a")); + String url = "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" + longKey + "=value"; + + BigQueryJdbcRuntimeException e = + assertThrows( + BigQueryJdbcRuntimeException.class, () -> BigQueryJdbcUrlUtility.parseUrl(url)); + + assertThat(e.getMessage()).contains("Wrong value or unknown setting: "); + assertThat(e.getMessage()).contains("..."); + assertThat(e.getMessage()).doesNotContain(longKey); + assertThat(e.getMessage().length()).isLessThan(100); + } + @Test public void testOverridePropertiesFromURICompatibility() { String connection_uri = @@ -436,7 +475,7 @@ public void testParsePartnerTokenProperty() { url = "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" + "PartnerToken= ( GPN: partner_name ; test_env ) ;"; - expected = " (GPN: partner_name; test_env)"; + expected = " (GPN: partner_name ; test_env)"; result = BigQueryJdbcUrlUtility.parsePartnerTokenProperty(url, "testParsePartnerTokenProperty"); assertThat(result).isEqualTo(expected); } @@ -641,19 +680,22 @@ public void testParseListenerPoolSizeInvalidLong() { public void testParseStringListProperty_NullOrEmpty() { String url = "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" - + "OAuthType=2;ProjectId=MyBigQueryProject;SomeProp="; + + "OAuthType=2;ProjectId=MyBigQueryProject;ServiceAccountImpersonationScopes="; List result = BigQueryJdbcUrlUtility.parseStringListProperty(url, "NonExistentProp", "TestClass"); assertEquals(Collections.emptyList(), result); - result = BigQueryJdbcUrlUtility.parseStringListProperty(url, "SomeProp", "TestClass"); + result = + BigQueryJdbcUrlUtility.parseStringListProperty( + url, "ServiceAccountImpersonationScopes", "TestClass"); assertEquals(Collections.emptyList(), result); String urlWithEmptyList = "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" - + "OAuthType=2;ProjectId=MyBigQueryProject;ListProp=,,"; + + "OAuthType=2;ProjectId=MyBigQueryProject;ServiceAccountImpersonationScopes=,,"; result = - BigQueryJdbcUrlUtility.parseStringListProperty(urlWithEmptyList, "ListProp", "TestClass"); + BigQueryJdbcUrlUtility.parseStringListProperty( + urlWithEmptyList, "ServiceAccountImpersonationScopes", "TestClass"); assertEquals(Collections.emptyList(), result); } @@ -661,9 +703,10 @@ public void testParseStringListProperty_NullOrEmpty() { public void testParseStringListProperty_SingleValue() { String url = "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" - + "OAuthType=2;ProjectId=MyBigQueryProject;ListProp=project1"; + + "OAuthType=2;ProjectId=MyBigQueryProject;ServiceAccountImpersonationScopes=project1"; List result = - BigQueryJdbcUrlUtility.parseStringListProperty(url, "ListProp", "TestClass"); + BigQueryJdbcUrlUtility.parseStringListProperty( + url, "ServiceAccountImpersonationScopes", "TestClass"); assertEquals(Collections.singletonList("project1"), result); } @@ -671,9 +714,10 @@ public void testParseStringListProperty_SingleValue() { public void testParseStringListProperty_MultipleValues() { String url = "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" - + "OAuthType=2;ProjectId=MyBigQueryProject;ListProp=project1,project2,project3"; + + "OAuthType=2;ProjectId=MyBigQueryProject;ServiceAccountImpersonationScopes=project1,project2,project3"; List result = - BigQueryJdbcUrlUtility.parseStringListProperty(url, "ListProp", "TestClass"); + BigQueryJdbcUrlUtility.parseStringListProperty( + url, "ServiceAccountImpersonationScopes", "TestClass"); assertEquals(Arrays.asList("project1", "project2", "project3"), result); } @@ -681,10 +725,11 @@ public void testParseStringListProperty_MultipleValues() { public void testParseIntProperty_ValidInteger() { String url = "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" - + "OAuthType=2;ProjectId=MyBigQueryProject;SomeIntProp=123"; + + "OAuthType=2;ProjectId=MyBigQueryProject;HttpConnectTimeout=123"; Integer defaultValue = 0; Integer result = - BigQueryJdbcUrlUtility.parseIntProperty(url, "SomeIntProp", defaultValue, "TestClass"); + BigQueryJdbcUrlUtility.parseIntProperty( + url, "HttpConnectTimeout", defaultValue, "TestClass"); assertEquals(Integer.valueOf(123), result); } @@ -692,10 +737,11 @@ public void testParseIntProperty_ValidInteger() { public void testParseIntProperty_PropertyNotPresent() { String url = "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" - + "OAuthType=2;ProjectId=MyBigQueryProject;SomeIntProp=123"; + + "OAuthType=2;ProjectId=MyBigQueryProject;"; Integer defaultValue = 42; Integer result = - BigQueryJdbcUrlUtility.parseIntProperty(url, "MissingIntProp", defaultValue, "TestClass"); + BigQueryJdbcUrlUtility.parseIntProperty( + url, "HttpConnectTimeout", defaultValue, "TestClass"); assertEquals(defaultValue, result); } @@ -703,26 +749,26 @@ public void testParseIntProperty_PropertyNotPresent() { public void testParseIntProperty_InvalidIntegerValue() { String url = "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" - + "OAuthType=2;ProjectId=MyBigQueryProject;InvalidIntProp=abc"; + + "OAuthType=2;ProjectId=MyBigQueryProject;HttpConnectTimeout=abc"; Integer defaultValue = 77; assertThrows( IllegalArgumentException.class, () -> BigQueryJdbcUrlUtility.parseIntProperty( - url, "InvalidIntProp", defaultValue, "TestClass")); + url, "HttpConnectTimeout", defaultValue, "TestClass")); } @Test public void testParseIntProperty_EmptyStringValue() { String url = "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" - + "OAuthType=2;ProjectId=MyBigQueryProject;EmptyIntProp="; + + "OAuthType=2;ProjectId=MyBigQueryProject;HttpConnectTimeout="; Integer defaultValue = 88; assertThrows( IllegalArgumentException.class, () -> BigQueryJdbcUrlUtility.parseIntProperty( - url, "EmptyIntProp", defaultValue, "TestClass")); + url, "HttpConnectTimeout", defaultValue, "TestClass")); } @Test @@ -860,4 +906,38 @@ public void testParseRequestReason() { "testParseRequestReason"); assertEquals("testingRequestReason", requestReason); } + + @Test + public void testAppendPropertiesToURL_propertyWithSemicolon_isEscaped() throws Exception { + String url = "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;"; + Properties properties = new Properties(); + String complexValue = "value;ExtraProperty=injection"; + properties.setProperty("ProjectId", complexValue); + + String updatedUrl = BigQueryJdbcUrlUtility.appendPropertiesToURL(url, null, properties); + + Map parsedProperties = BigQueryJdbcUrlUtility.parseUrl(updatedUrl); + + assertThat(parsedProperties.get("ProjectId")).isEqualTo(complexValue); + assertFalse(parsedProperties.containsKey("ExtraProperty")); + } + + @Test + public void testEndpointOverrides_viaProperties() throws Exception { + String url = "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443"; + Properties props = new Properties(); + + String overrides = "OAUTH2=http://localhost:1234,BIGQUERY=http://localhost:9090"; + props.setProperty("EndpointOverrides", overrides); + + String updatedUrl = BigQueryJdbcUrlUtility.appendPropertiesToURL(url, null, props); + + Map parsedOverrides = + BigQueryJdbcUrlUtility.parseOverrideProperties(updatedUrl, null); + + assertThat(parsedOverrides).containsKey("OAUTH2"); + assertThat(parsedOverrides.get("OAUTH2")).isEqualTo("http://localhost:1234"); + assertThat(parsedOverrides).containsKey("BIGQUERY"); + assertThat(parsedOverrides.get("BIGQUERY")).isEqualTo("http://localhost:9090"); + } }