Skip to content

Commit

Permalink
feat(spanner): add jdbc support for external hosts (googleapis#3536)
Browse files Browse the repository at this point in the history
* feat(spanner): add jdbc support for external hosts

* feat(spanner): added default port value and unit tests

* feat(spanner): fixed redundant class name typo
  • Loading branch information
sagnghos authored Dec 16, 2024
1 parent a3d74b0 commit 801346a
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -628,11 +628,16 @@ private Builder() {}
public static final String SPANNER_URI_FORMAT =
"(?:cloudspanner:)(?<HOSTGROUP>//[\\w.-]+(?:\\.[\\w\\.-]+)*[\\w\\-\\._~:/?#\\[\\]@!\\$&'\\(\\)\\*\\+,;=.]+)?/projects/(?<PROJECTGROUP>(([a-z]|[-.:]|[0-9])+|(DEFAULT_PROJECT_ID)))(/instances/(?<INSTANCEGROUP>([a-z]|[-]|[0-9])+)(/databases/(?<DATABASEGROUP>([a-z]|[-]|[_]|[0-9])+))?)?(?:[?|;].*)?";

public static final String EXTERNAL_HOST_FORMAT =
"(?:cloudspanner:)(?<HOSTGROUP>//[\\w.-]+(?::\\d+)?)(/instances/(?<INSTANCEGROUP>[a-z0-9-]+))?(/databases/(?<DATABASEGROUP>[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";
Expand All @@ -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:
Expand Down Expand Up @@ -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<Boolean> value =
cast(ConnectionProperties.parseValues(uri).get(LENIENT.getKey()));
checkValidProperties(value != null && value.getValue(), uri);
Expand Down Expand Up @@ -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));

Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}
}

0 comments on commit 801346a

Please sign in to comment.