Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(spanner): add jdbc support for external hosts #3536

Merged
merged 5 commits into from
Dec 16, 2024
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
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) {
sagnghos marked this conversation as resolved.
Show resolved Hide resolved
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());
}
}
Loading