From 801346a1b2efe7d0144f7442e1568eb5b02ddcbc Mon Sep 17 00:00:00 2001 From: Sagnik Ghosh Date: Mon, 16 Dec 2024 07:00:34 +0000 Subject: [PATCH] feat(spanner): add jdbc support for external hosts (#3536) * feat(spanner): add jdbc support for external hosts * feat(spanner): added default port value and unit tests * feat(spanner): fixed redundant class name typo --- .../spanner/connection/ConnectionOptions.java | 40 ++++++++++++++++--- .../connection/ConnectionOptionsTest.java | 37 +++++++++++++++++ 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java index 2be2d7980b2..6e991816ab6 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java @@ -628,11 +628,16 @@ private Builder() {} public static final String SPANNER_URI_FORMAT = "(?:cloudspanner:)(?//[\\w.-]+(?:\\.[\\w\\.-]+)*[\\w\\-\\._~:/?#\\[\\]@!\\$&'\\(\\)\\*\\+,;=.]+)?/projects/(?(([a-z]|[-.:]|[0-9])+|(DEFAULT_PROJECT_ID)))(/instances/(?([a-z]|[-]|[0-9])+)(/databases/(?([a-z]|[-]|[_]|[0-9])+))?)?(?:[?|;].*)?"; + public static final String EXTERNAL_HOST_FORMAT = + "(?:cloudspanner:)(?//[\\w.-]+(?::\\d+)?)(/instances/(?[a-z0-9-]+))?(/databases/(?[a-z0-9_-]+))(?:[?;].*)?"; private static final String SPANNER_URI_REGEX = "(?is)^" + SPANNER_URI_FORMAT + "$"; @VisibleForTesting static final Pattern SPANNER_URI_PATTERN = Pattern.compile(SPANNER_URI_REGEX); + @VisibleForTesting + static final Pattern EXTERNAL_HOST_PATTERN = Pattern.compile(EXTERNAL_HOST_FORMAT); + private static final String HOST_GROUP = "HOSTGROUP"; private static final String PROJECT_GROUP = "PROJECTGROUP"; private static final String INSTANCE_GROUP = "INSTANCEGROUP"; @@ -643,6 +648,10 @@ private boolean isValidUri(String uri) { return SPANNER_URI_PATTERN.matcher(uri).matches(); } + private boolean isValidExternalHostUri(String uri) { + return EXTERNAL_HOST_PATTERN.matcher(uri).matches(); + } + /** * Sets the URI of the Cloud Spanner database to connect to. A connection URI must be specified * in this format: @@ -700,9 +709,11 @@ private boolean isValidUri(String uri) { * @return this builder */ public Builder setUri(String uri) { - Preconditions.checkArgument( - isValidUri(uri), - "The specified URI is not a valid Cloud Spanner connection URI. Please specify a URI in the format \"cloudspanner:[//host[:port]]/projects/project-id[/instances/instance-id[/databases/database-name]][\\?property-name=property-value[;property-name=property-value]*]?\""); + if (!isValidExternalHostUri(uri)) { + Preconditions.checkArgument( + isValidUri(uri), + "The specified URI is not a valid Cloud Spanner connection URI. Please specify a URI in the format \"cloudspanner:[//host[:port]]/projects/project-id[/instances/instance-id[/databases/database-name]][\\?property-name=property-value[;property-name=property-value]*]?\""); + } ConnectionPropertyValue value = cast(ConnectionProperties.parseValues(uri).get(LENIENT.getKey())); checkValidProperties(value != null && value.getValue(), uri); @@ -829,7 +840,14 @@ public static Builder newBuilder() { private final SpannerOptionsConfigurator configurator; private ConnectionOptions(Builder builder) { - Matcher matcher = Builder.SPANNER_URI_PATTERN.matcher(builder.uri); + Matcher matcher; + boolean isExternalHost = false; + if (builder.isValidExternalHostUri(builder.uri)) { + matcher = Builder.EXTERNAL_HOST_PATTERN.matcher(builder.uri); + isExternalHost = true; + } else { + matcher = Builder.SPANNER_URI_PATTERN.matcher(builder.uri); + } Preconditions.checkArgument( matcher.find(), String.format("Invalid connection URI specified: %s", builder.uri)); @@ -947,12 +965,18 @@ && getInitialConnectionPropertyValue(OAUTH_TOKEN) == null this.sessionPoolOptions = SessionPoolOptions.newBuilder().setAutoDetectDialect(true).build(); } - String projectId = matcher.group(Builder.PROJECT_GROUP); + String projectId = "default"; + String instanceId = matcher.group(Builder.INSTANCE_GROUP); + if (!isExternalHost) { + projectId = matcher.group(Builder.PROJECT_GROUP); + } else if (instanceId == null) { + instanceId = "default"; + } if (Builder.DEFAULT_PROJECT_ID_PLACEHOLDER.equalsIgnoreCase(projectId)) { projectId = getDefaultProjectId(this.credentials); } this.projectId = projectId; - this.instanceId = matcher.group(Builder.INSTANCE_GROUP); + this.instanceId = instanceId; this.databaseName = matcher.group(Builder.DATABASE_GROUP); } @@ -981,6 +1005,10 @@ static String determineHost( // The leading '//' is already included in the regex for the connection URL, so we don't need // to add the leading '//' to the host name here. host = matcher.group(Builder.HOST_GROUP); + if (Builder.EXTERNAL_HOST_FORMAT.equals(matcher.pattern().pattern()) + && !host.matches(".*:\\d+$")) { + host = String.format("%s:15000", host); + } } if (usePlainText) { return PLAIN_TEXT_PROTOCOL + host; diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionOptionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionOptionsTest.java index f826ec08dfc..69c4a010327 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionOptionsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionOptionsTest.java @@ -16,6 +16,7 @@ package com.google.cloud.spanner.connection; +import static com.google.cloud.spanner.connection.ConnectionOptions.Builder.EXTERNAL_HOST_PATTERN; import static com.google.cloud.spanner.connection.ConnectionOptions.Builder.SPANNER_URI_PATTERN; import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_ENDPOINT; import static com.google.cloud.spanner.connection.ConnectionOptions.determineHost; @@ -1211,4 +1212,40 @@ public void testEnableApiTracing() { .build() .isEnableApiTracing()); } + + @Test + public void testExternalHostPatterns() { + Matcher matcherWithoutInstance = + EXTERNAL_HOST_PATTERN.matcher("cloudspanner://localhost:15000/databases/test-db"); + assertTrue(matcherWithoutInstance.matches()); + assertNull(matcherWithoutInstance.group("INSTANCEGROUP")); + assertEquals("test-db", matcherWithoutInstance.group("DATABASEGROUP")); + Matcher matcherWithProperty = + EXTERNAL_HOST_PATTERN.matcher( + "cloudspanner://localhost:15000/instances/default/databases/singers-db?usePlainText=true"); + assertTrue(matcherWithProperty.matches()); + assertEquals("default", matcherWithProperty.group("INSTANCEGROUP")); + assertEquals("singers-db", matcherWithProperty.group("DATABASEGROUP")); + Matcher matcherWithoutPort = + EXTERNAL_HOST_PATTERN.matcher( + "cloudspanner://localhost/instances/default/databases/test-db"); + assertTrue(matcherWithoutPort.matches()); + assertEquals("default", matcherWithoutPort.group("INSTANCEGROUP")); + assertEquals("test-db", matcherWithoutPort.group("DATABASEGROUP")); + assertEquals( + "http://localhost:15000", + determineHost( + matcherWithoutPort, + DEFAULT_ENDPOINT, + /* autoConfigEmulator= */ true, + /* usePlainText= */ true, + ImmutableMap.of())); + Matcher matcherWithProject = + EXTERNAL_HOST_PATTERN.matcher( + "cloudspanner://localhost:15000/projects/default/instances/default/databases/singers-db"); + assertFalse(matcherWithProject.matches()); + Matcher matcherWithoutHost = + EXTERNAL_HOST_PATTERN.matcher("cloudspanner:/instances/default/databases/singers-db"); + assertFalse(matcherWithoutHost.matches()); + } }