diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java index 0a19bed7a2c8..bdb45536023f 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java @@ -22,6 +22,7 @@ import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.net.UrlEscapers; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -711,8 +712,10 @@ private static Map parseUrlInternal(String url) { } String[] urlParts = url.split(";", 2); + parseAuthority(urlParts[0], map); + if (urlParts.length < 2) { - return map; + return Collections.unmodifiableMap(map); } String urlToParse = urlParts[1]; @@ -754,7 +757,13 @@ private static Map parseUrlInternal(String url) { } String propertyName = PROPERTY_NAME_MAP.get(key); String value = CharEscapers.decodeUriPath(kv[1].replace("+", "%2B")); - map.put(propertyName, value); + if (propertyName.equals(ENDPOINT_OVERRIDES_PROPERTY_NAME) + && map.containsKey(ENDPOINT_OVERRIDES_PROPERTY_NAME)) { + String existing = map.get(ENDPOINT_OVERRIDES_PROPERTY_NAME); + map.put(propertyName, mergeEndpointOverrides(existing, value)); + } else { + map.put(propertyName, value); + } } return Collections.unmodifiableMap(map); } @@ -866,4 +875,81 @@ static Map parsePropertiesMapFromValue( } return propertiesMap; } + + private static void parseAuthority(String urlPart, Map map) { + String authority = urlPart.trim(); + if (authority.startsWith("jdbc:")) { + authority = authority.substring(5); + } + if (authority.startsWith("bigquery://")) { + authority = authority.substring(11); + } else if (authority.startsWith("bigquery:")) { + authority = authority.substring(9); + } + authority = authority.trim(); + if (authority.isEmpty()) { + return; + } + + if (authority.startsWith("http://") || authority.startsWith("https://")) { + map.put( + ENDPOINT_OVERRIDES_PROPERTY_NAME, + BIGQUERY_ENDPOINT_OVERRIDE_PROPERTY_NAME + "=" + authority); + return; + } + + if (authority.startsWith("[")) { + int closingBracketIndex = authority.indexOf(']'); + if (closingBracketIndex != -1) { + String host = authority.substring(0, closingBracketIndex + 1).trim(); + String rest = authority.substring(closingBracketIndex + 1).trim(); + map.put(PROXY_HOST_PROPERTY_NAME, host); + if (rest.startsWith(":")) { + String port = rest.substring(1).trim(); + if (!port.isEmpty()) { + map.put(PROXY_PORT_PROPERTY_NAME, port); + } + } + return; + } + } + + int colonIndex = authority.indexOf(':'); + if (colonIndex == -1) { + map.put(PROXY_HOST_PROPERTY_NAME, authority); + return; + } + + String host = authority.substring(0, colonIndex).trim(); + String port = authority.substring(colonIndex + 1).trim(); + if (!host.isEmpty()) { + map.put(PROXY_HOST_PROPERTY_NAME, host); + } + if (!port.isEmpty()) { + map.put(PROXY_PORT_PROPERTY_NAME, port); + } + } + + private static String mergeEndpointOverrides(String existing, String newValue) { + Map merged = new LinkedHashMap<>(); + parseOverridesIntoMap(existing, merged); + parseOverridesIntoMap(newValue, merged); + List parts = new ArrayList<>(); + for (Map.Entry entry : merged.entrySet()) { + parts.add(entry.getKey() + "=" + entry.getValue()); + } + return String.join(",", parts); + } + + private static void parseOverridesIntoMap(String overrides, Map targetMap) { + if (overrides == null || overrides.isEmpty()) { + return; + } + for (String part : overrides.split(",")) { + String[] kv = part.split("=", 2); + if (kv.length == 2) { + targetMap.put(kv[0].trim(), kv[1].trim()); + } + } + } } diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java index 3a09813a035e..acae230275c5 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtilityTest.java @@ -30,12 +30,12 @@ public class BigQueryJdbcUrlUtilityTest extends BigQueryJdbcLoggingBaseTest { + private static final String BASE_URL = + "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;"; + @Test public void testParsePropertyWithNoDefault() { - String url = - "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" - + "ProjectId=MyBigQueryProject;" - + "OAuthAccessToken=RedactedToken"; + String url = BASE_URL + "ProjectId=MyBigQueryProject;OAuthAccessToken=RedactedToken"; String result = BigQueryJdbcUrlUtility.parseUriProperty(url, "OAuthType"); assertThat(result).isNull(); @@ -43,10 +43,7 @@ public void testParsePropertyWithNoDefault() { @Test public void testParseUrlWithUnknownProperty_no_exception() { - String url = - "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" - + "ProjectId=MyBigQueryProject;" - + "UnknownProperty=SomeValue"; + String url = BASE_URL + "ProjectId=MyBigQueryProject;UnknownProperty=SomeValue"; BigQueryJdbcUrlUtility.parseUriProperty(url, "ProjectId"); assertThat(assertLogContains("Wrong value or unknown setting")).isTrue(); @@ -54,10 +51,7 @@ public void testParseUrlWithUnknownProperty_no_exception() { @Test public void testParseUrlWithTypo_no_exception() { - String url = - "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" - + "ProjectId=MyBigQueryProject;" - + "ProjeectId=TypoValue"; + String url = BASE_URL + "ProjectId=MyBigQueryProject;ProjeectId=TypoValue"; assertDoesNotThrow(() -> BigQueryJdbcUrlUtility.parseUriProperty(url, "ProjectId")); assertThat(assertLogContains("Wrong value or unknown setting")).isTrue(); @@ -65,10 +59,7 @@ public void testParseUrlWithTypo_no_exception() { @Test public void testParsePropertyWithDefault() { - String url = - "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" - + "ProjectId=MyBigQueryProject;" - + "OAuthAccessToken=RedactedToken"; + String url = BASE_URL + "ProjectId=MyBigQueryProject;OAuthAccessToken=RedactedToken"; String result = BigQueryJdbcUrlUtility.parseUriProperty(url, "OAuthType"); assertThat(result).isNull(); @@ -76,10 +67,7 @@ public void testParsePropertyWithDefault() { @Test public void testParsePropertyWithValue() { - String url = - "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" - + "ProjectId=MyBigQueryProject;" - + "OAuthAccessToken=RedactedToken"; + String url = BASE_URL + "ProjectId=MyBigQueryProject;OAuthAccessToken=RedactedToken"; String result = BigQueryJdbcUrlUtility.parseUriProperty(url, "ProjectId"); assertThat(result).isEqualTo("MyBigQueryProject"); @@ -87,10 +75,7 @@ public void testParsePropertyWithValue() { @Test public void testParsePropertyWithValueCaseInsensitive() { - String url = - "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" - + "PROJECTID=MyBigQueryProject;" - + "OAuthAccessToken=RedactedToken"; + String url = BASE_URL + "PROJECTID=MyBigQueryProject;OAuthAccessToken=RedactedToken"; String result = BigQueryJdbcUrlUtility.parseUriProperty(url, "ProjectId"); assertThat(result).isEqualTo("MyBigQueryProject"); @@ -98,10 +83,7 @@ public void testParsePropertyWithValueCaseInsensitive() { @Test public void testAppendPropertiesToURL() { - String url = - "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" - + "ProjectId=MyBigQueryProject;" - + "OAuthAccessToken=RedactedToken"; + String url = BASE_URL + "ProjectId=MyBigQueryProject;OAuthAccessToken=RedactedToken"; Properties properties = new Properties(); properties.setProperty("OAuthType", "3"); @@ -140,7 +122,7 @@ public void testConnectionPropertiesFromURIMultilineNoSemicolon() { @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"; + String url = BASE_URL + longKey + "=value"; assertDoesNotThrow(() -> BigQueryJdbcUrlUtility.parseUrl(url)); String message = capturedLogs.get(0).getMessage(); @@ -153,42 +135,34 @@ public void testParseUrl_longUnknownProperty_sanitized() { @Test public void testParsePartnerTokenProperty() { // Case with partner name and environment - String url = - "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" - + "PartnerToken=(GPN:partner_company; dev);ProjectId=MyBigQueryProject;"; + String url = BASE_URL + "PartnerToken=(GPN:partner_company; dev);ProjectId=MyBigQueryProject;"; String expected = " (GPN:partner_company; dev)"; String result = BigQueryJdbcUrlUtility.parseUriProperty(url, "PartnerToken"); assertThat(result).isEqualTo(expected); // Case with only partner name - url = - "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" - + "PartnerToken=(GPN:another_partner);ProjectId=MyBigQueryProject;"; + url = BASE_URL + "PartnerToken=(GPN:another_partner);ProjectId=MyBigQueryProject;"; expected = " (GPN:another_partner)"; result = BigQueryJdbcUrlUtility.parseUriProperty(url, "PartnerToken"); assertThat(result).isEqualTo(expected); // Case when PartnerToken property is not present - url = - "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" - + "ProjectId=MyBigQueryProject;"; + url = BASE_URL + "ProjectId=MyBigQueryProject;"; result = BigQueryJdbcUrlUtility.parseUriProperty(url, "PartnerToken"); assertNull(result); // Case when PartnerToken property is present but empty - url = "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;PartnerToken=();"; + url = BASE_URL + "PartnerToken=();"; result = BigQueryJdbcUrlUtility.parseUriProperty(url, "PartnerToken"); assertNull(result); // Case when PartnerToken property is present but without partner name - url = "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;PartnerToken=(env);"; + url = BASE_URL + "PartnerToken=(env);"; result = BigQueryJdbcUrlUtility.parseUriProperty(url, "PartnerToken"); assertNull(result); // Case with extra spaces around the values - url = - "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;" - + "PartnerToken= ( GPN: partner_name ; test_env ) ;"; + url = BASE_URL + "PartnerToken= ( GPN: partner_name ; test_env ) ;"; expected = " (GPN: partner_name ; test_env)"; result = BigQueryJdbcUrlUtility.parseUriProperty(url, "PartnerToken"); assertThat(result).isEqualTo(expected); @@ -196,7 +170,7 @@ public void testParsePartnerTokenProperty() { @Test public void testAppendPropertiesToURL_propertyWithSemicolon_isEscaped() throws Exception { - String url = "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;"; + String url = BASE_URL; Properties properties = new Properties(); String complexValue = "value;ExtraProperty=injection"; properties.setProperty("ProjectId", complexValue); @@ -260,4 +234,93 @@ public void testUnrecognizedConnectionProperties() { String url2 = "jdbc:bigquery://;MalformedProperty"; assertThrows(BigQueryJdbcRuntimeException.class, () -> DataSource.fromUrl(url2)); } + + @Test + public void testParseAuthorityEndpointOverride() { + String url = "jdbc:bigquery://https://custom-endpoint.com:443;ProjectId=MyProject"; + String endpointOverride = BigQueryJdbcUrlUtility.parseUriProperty(url, "EndpointOverrides"); + assertThat(endpointOverride).isEqualTo("BIGQUERY=https://custom-endpoint.com:443"); + } + + @Test + public void testParseAuthorityEndpointOverrideNoSemicolon() { + String url = "jdbc:bigquery://https://custom-endpoint.com:443"; + String endpointOverride = BigQueryJdbcUrlUtility.parseUriProperty(url, "EndpointOverrides"); + assertThat(endpointOverride).isEqualTo("BIGQUERY=https://custom-endpoint.com:443"); + } + + @Test + public void testParseAuthorityProxy() { + String url = "jdbc:bigquery://proxy.example.com:8080;ProjectId=MyProject"; + String proxyHost = BigQueryJdbcUrlUtility.parseUriProperty(url, "ProxyHost"); + String proxyPort = BigQueryJdbcUrlUtility.parseUriProperty(url, "ProxyPort"); + assertThat(proxyHost).isEqualTo("proxy.example.com"); + assertThat(proxyPort).isEqualTo("8080"); + } + + @Test + public void testParseAuthorityProxyNoPort() { + String url = "jdbc:bigquery://proxy.example.com;ProjectId=MyProject"; + String proxyHost = BigQueryJdbcUrlUtility.parseUriProperty(url, "ProxyHost"); + String proxyPort = BigQueryJdbcUrlUtility.parseUriProperty(url, "ProxyPort"); + assertThat(proxyHost).isEqualTo("proxy.example.com"); + assertThat(proxyPort).isNull(); + } + + @Test + public void testParseAuthorityAndMergeEndpointOverrides() { + String url = + BASE_URL + + "EndpointOverrides=READ_API=https://storage-endpoint.com:443;ProjectId=MyProject"; + String endpointOverride = BigQueryJdbcUrlUtility.parseUriProperty(url, "EndpointOverrides"); + assertThat(endpointOverride) + .isEqualTo( + "BIGQUERY=https://www.googleapis.com/bigquery/v2:443,READ_API=https://storage-endpoint.com:443"); + } + + @Test + public void testParseAuthorityAndOverwriteEndpointOverrides() { + String url = + BASE_URL + + "EndpointOverrides=BIGQUERY=https://another-endpoint.com:443,READ_API=https://storage-endpoint.com:443;ProjectId=MyProject"; + String endpointOverride = BigQueryJdbcUrlUtility.parseUriProperty(url, "EndpointOverrides"); + assertThat(endpointOverride) + .isEqualTo( + "BIGQUERY=https://another-endpoint.com:443,READ_API=https://storage-endpoint.com:443"); + } + + @Test + public void testDataSourceFromUrlAuthorityEndpoint() { + String url = BASE_URL + "ProjectId=MyProject"; + DataSource ds = DataSource.fromUrl(url); + assertThat(ds.getProjectId()).isEqualTo("MyProject"); + assertThat(ds.getOverrideProperties().get("BIGQUERY")) + .isEqualTo("https://www.googleapis.com/bigquery/v2:443"); + } + + @Test + public void testDataSourceFromUrlAuthorityProxy() { + String url = "jdbc:bigquery://proxy.example.com:8080;ProjectId=MyProject"; + DataSource ds = DataSource.fromUrl(url); + assertThat(ds.getProxyHost()).isEqualTo("proxy.example.com"); + assertThat(ds.getProxyPort()).isEqualTo("8080"); + } + + @Test + public void testParseAuthorityProxyIpv6() { + String url = "jdbc:bigquery://[::1]:8080;ProjectId=MyProject"; + String proxyHost = BigQueryJdbcUrlUtility.parseUriProperty(url, "ProxyHost"); + String proxyPort = BigQueryJdbcUrlUtility.parseUriProperty(url, "ProxyPort"); + assertThat(proxyHost).isEqualTo("[::1]"); + assertThat(proxyPort).isEqualTo("8080"); + } + + @Test + public void testParseAuthorityProxyIpv6NoPort() { + String url = "jdbc:bigquery://[::1];ProjectId=MyProject"; + String proxyHost = BigQueryJdbcUrlUtility.parseUriProperty(url, "ProxyHost"); + String proxyPort = BigQueryJdbcUrlUtility.parseUriProperty(url, "ProxyPort"); + assertThat(proxyHost).isEqualTo("[::1]"); + assertThat(proxyPort).isNull(); + } }