Skip to content

Commit 801346a

Browse files
authored
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
1 parent a3d74b0 commit 801346a

File tree

2 files changed

+71
-6
lines changed

2 files changed

+71
-6
lines changed

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java

+34-6
Original file line numberDiff line numberDiff line change
@@ -628,11 +628,16 @@ private Builder() {}
628628
public static final String SPANNER_URI_FORMAT =
629629
"(?: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])+))?)?(?:[?|;].*)?";
630630

631+
public static final String EXTERNAL_HOST_FORMAT =
632+
"(?:cloudspanner:)(?<HOSTGROUP>//[\\w.-]+(?::\\d+)?)(/instances/(?<INSTANCEGROUP>[a-z0-9-]+))?(/databases/(?<DATABASEGROUP>[a-z0-9_-]+))(?:[?;].*)?";
631633
private static final String SPANNER_URI_REGEX = "(?is)^" + SPANNER_URI_FORMAT + "$";
632634

633635
@VisibleForTesting
634636
static final Pattern SPANNER_URI_PATTERN = Pattern.compile(SPANNER_URI_REGEX);
635637

638+
@VisibleForTesting
639+
static final Pattern EXTERNAL_HOST_PATTERN = Pattern.compile(EXTERNAL_HOST_FORMAT);
640+
636641
private static final String HOST_GROUP = "HOSTGROUP";
637642
private static final String PROJECT_GROUP = "PROJECTGROUP";
638643
private static final String INSTANCE_GROUP = "INSTANCEGROUP";
@@ -643,6 +648,10 @@ private boolean isValidUri(String uri) {
643648
return SPANNER_URI_PATTERN.matcher(uri).matches();
644649
}
645650

651+
private boolean isValidExternalHostUri(String uri) {
652+
return EXTERNAL_HOST_PATTERN.matcher(uri).matches();
653+
}
654+
646655
/**
647656
* Sets the URI of the Cloud Spanner database to connect to. A connection URI must be specified
648657
* in this format:
@@ -700,9 +709,11 @@ private boolean isValidUri(String uri) {
700709
* @return this builder
701710
*/
702711
public Builder setUri(String uri) {
703-
Preconditions.checkArgument(
704-
isValidUri(uri),
705-
"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]*]?\"");
712+
if (!isValidExternalHostUri(uri)) {
713+
Preconditions.checkArgument(
714+
isValidUri(uri),
715+
"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]*]?\"");
716+
}
706717
ConnectionPropertyValue<Boolean> value =
707718
cast(ConnectionProperties.parseValues(uri).get(LENIENT.getKey()));
708719
checkValidProperties(value != null && value.getValue(), uri);
@@ -829,7 +840,14 @@ public static Builder newBuilder() {
829840
private final SpannerOptionsConfigurator configurator;
830841

831842
private ConnectionOptions(Builder builder) {
832-
Matcher matcher = Builder.SPANNER_URI_PATTERN.matcher(builder.uri);
843+
Matcher matcher;
844+
boolean isExternalHost = false;
845+
if (builder.isValidExternalHostUri(builder.uri)) {
846+
matcher = Builder.EXTERNAL_HOST_PATTERN.matcher(builder.uri);
847+
isExternalHost = true;
848+
} else {
849+
matcher = Builder.SPANNER_URI_PATTERN.matcher(builder.uri);
850+
}
833851
Preconditions.checkArgument(
834852
matcher.find(), String.format("Invalid connection URI specified: %s", builder.uri));
835853

@@ -947,12 +965,18 @@ && getInitialConnectionPropertyValue(OAUTH_TOKEN) == null
947965
this.sessionPoolOptions = SessionPoolOptions.newBuilder().setAutoDetectDialect(true).build();
948966
}
949967

950-
String projectId = matcher.group(Builder.PROJECT_GROUP);
968+
String projectId = "default";
969+
String instanceId = matcher.group(Builder.INSTANCE_GROUP);
970+
if (!isExternalHost) {
971+
projectId = matcher.group(Builder.PROJECT_GROUP);
972+
} else if (instanceId == null) {
973+
instanceId = "default";
974+
}
951975
if (Builder.DEFAULT_PROJECT_ID_PLACEHOLDER.equalsIgnoreCase(projectId)) {
952976
projectId = getDefaultProjectId(this.credentials);
953977
}
954978
this.projectId = projectId;
955-
this.instanceId = matcher.group(Builder.INSTANCE_GROUP);
979+
this.instanceId = instanceId;
956980
this.databaseName = matcher.group(Builder.DATABASE_GROUP);
957981
}
958982

@@ -981,6 +1005,10 @@ static String determineHost(
9811005
// The leading '//' is already included in the regex for the connection URL, so we don't need
9821006
// to add the leading '//' to the host name here.
9831007
host = matcher.group(Builder.HOST_GROUP);
1008+
if (Builder.EXTERNAL_HOST_FORMAT.equals(matcher.pattern().pattern())
1009+
&& !host.matches(".*:\\d+$")) {
1010+
host = String.format("%s:15000", host);
1011+
}
9841012
}
9851013
if (usePlainText) {
9861014
return PLAIN_TEXT_PROTOCOL + host;

google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionOptionsTest.java

+37
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.google.cloud.spanner.connection;
1818

19+
import static com.google.cloud.spanner.connection.ConnectionOptions.Builder.EXTERNAL_HOST_PATTERN;
1920
import static com.google.cloud.spanner.connection.ConnectionOptions.Builder.SPANNER_URI_PATTERN;
2021
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_ENDPOINT;
2122
import static com.google.cloud.spanner.connection.ConnectionOptions.determineHost;
@@ -1211,4 +1212,40 @@ public void testEnableApiTracing() {
12111212
.build()
12121213
.isEnableApiTracing());
12131214
}
1215+
1216+
@Test
1217+
public void testExternalHostPatterns() {
1218+
Matcher matcherWithoutInstance =
1219+
EXTERNAL_HOST_PATTERN.matcher("cloudspanner://localhost:15000/databases/test-db");
1220+
assertTrue(matcherWithoutInstance.matches());
1221+
assertNull(matcherWithoutInstance.group("INSTANCEGROUP"));
1222+
assertEquals("test-db", matcherWithoutInstance.group("DATABASEGROUP"));
1223+
Matcher matcherWithProperty =
1224+
EXTERNAL_HOST_PATTERN.matcher(
1225+
"cloudspanner://localhost:15000/instances/default/databases/singers-db?usePlainText=true");
1226+
assertTrue(matcherWithProperty.matches());
1227+
assertEquals("default", matcherWithProperty.group("INSTANCEGROUP"));
1228+
assertEquals("singers-db", matcherWithProperty.group("DATABASEGROUP"));
1229+
Matcher matcherWithoutPort =
1230+
EXTERNAL_HOST_PATTERN.matcher(
1231+
"cloudspanner://localhost/instances/default/databases/test-db");
1232+
assertTrue(matcherWithoutPort.matches());
1233+
assertEquals("default", matcherWithoutPort.group("INSTANCEGROUP"));
1234+
assertEquals("test-db", matcherWithoutPort.group("DATABASEGROUP"));
1235+
assertEquals(
1236+
"http://localhost:15000",
1237+
determineHost(
1238+
matcherWithoutPort,
1239+
DEFAULT_ENDPOINT,
1240+
/* autoConfigEmulator= */ true,
1241+
/* usePlainText= */ true,
1242+
ImmutableMap.of()));
1243+
Matcher matcherWithProject =
1244+
EXTERNAL_HOST_PATTERN.matcher(
1245+
"cloudspanner://localhost:15000/projects/default/instances/default/databases/singers-db");
1246+
assertFalse(matcherWithProject.matches());
1247+
Matcher matcherWithoutHost =
1248+
EXTERNAL_HOST_PATTERN.matcher("cloudspanner:/instances/default/databases/singers-db");
1249+
assertFalse(matcherWithoutHost.matches());
1250+
}
12141251
}

0 commit comments

Comments
 (0)