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 8bca0b2834c..eab86b3d140 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 @@ -175,6 +175,7 @@ public String[] getValidValues() { private static final String DEFAULT_MIN_SESSIONS = null; private static final String DEFAULT_MAX_SESSIONS = null; private static final String DEFAULT_NUM_CHANNELS = null; + static final String DEFAULT_ENDPOINT = null; private static final String DEFAULT_CHANNEL_PROVIDER = null; private static final String DEFAULT_DATABASE_ROLE = null; private static final String DEFAULT_USER_AGENT = null; @@ -234,6 +235,8 @@ public String[] getValidValues() { public static final String MAX_SESSIONS_PROPERTY_NAME = "maxSessions"; /** Name of the 'numChannels' connection property. */ public static final String NUM_CHANNELS_PROPERTY_NAME = "numChannels"; + /** Name of the 'endpoint' connection property. */ + public static final String ENDPOINT_PROPERTY_NAME = "endpoint"; /** Name of the 'channelProvider' connection property. */ public static final String CHANNEL_PROVIDER_PROPERTY_NAME = "channelProvider"; @@ -332,6 +335,12 @@ private static String generateGuardedConnectionPropertyError( ConnectionProperty.createStringProperty( NUM_CHANNELS_PROPERTY_NAME, "The number of gRPC channels to use to communicate with Cloud Spanner. The default is 4."), + ConnectionProperty.createStringProperty( + ENDPOINT_PROPERTY_NAME, + "The endpoint that the JDBC driver should connect to. " + + "The default is the default Spanner production endpoint when autoConfigEmulator=false, " + + "and the default Spanner emulator endpoint (localhost:9010) when autoConfigEmulator=true. " + + "This property takes precedence over any host name at the start of the connection URL."), ConnectionProperty.createStringProperty( CHANNEL_PROVIDER_PROPERTY_NAME, "The name of the channel provider class. The name must reference an implementation of ExternalChannelProvider. If this property is not set, the connection will use the default grpc channel provider."), @@ -738,7 +747,9 @@ private ConnectionOptions(Builder builder) { this.autoConfigEmulator = parseAutoConfigEmulator(this.uri); this.dialect = parseDialect(this.uri); this.usePlainText = this.autoConfigEmulator || parseUsePlainText(this.uri); - this.host = determineHost(matcher, autoConfigEmulator, usePlainText, System.getenv()); + this.host = + determineHost( + matcher, parseEndpoint(this.uri), autoConfigEmulator, usePlainText, System.getenv()); this.rpcPriority = parseRPCPriority(this.uri); this.delayTransactionStartUntilFirstWrite = parseDelayTransactionStartUntilFirstWrite(this.uri); this.trackSessionLeaks = parseTrackSessionLeaks(this.uri); @@ -829,10 +840,12 @@ private ConnectionOptions(Builder builder) { @VisibleForTesting static String determineHost( Matcher matcher, + String endpoint, boolean autoConfigEmulator, boolean usePlainText, Map environment) { - if (matcher.group(Builder.HOST_GROUP) == null) { + String host; + if (Objects.equals(endpoint, DEFAULT_ENDPOINT) && matcher.group(Builder.HOST_GROUP) == null) { if (autoConfigEmulator) { if (Strings.isNullOrEmpty(environment.get(SPANNER_EMULATOR_HOST_ENV_VAR))) { return DEFAULT_EMULATOR_HOST; @@ -842,13 +855,18 @@ static String determineHost( } else { return DEFAULT_HOST; } + } else if (!Objects.equals(endpoint, DEFAULT_ENDPOINT)) { + // Add '//' at the start of the endpoint to conform to the standard URL specification. + host = "//" + endpoint; } else { - if (usePlainText) { - return PLAIN_TEXT_PROTOCOL + matcher.group(Builder.HOST_GROUP); - } else { - return HOST_PROTOCOL + matcher.group(Builder.HOST_GROUP); - } + // 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 (usePlainText) { + return PLAIN_TEXT_PROTOCOL + host; + } + return HOST_PROTOCOL + host; } private static Integer parseIntegerProperty(String propertyName, String value) { @@ -1013,6 +1031,11 @@ static String parseNumChannels(String uri) { return value != null ? value : DEFAULT_NUM_CHANNELS; } + private static String parseEndpoint(String uri) { + String value = parseUriProperty(uri, ENDPOINT_PROPERTY_NAME); + return value != null ? value : DEFAULT_ENDPOINT; + } + @VisibleForTesting static String parseChannelProvider(String uri) { String value = parseUriProperty(uri, CHANNEL_PROVIDER_PROPERTY_NAME); 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 bdca80a214d..16ecc87bd6e 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 @@ -17,6 +17,7 @@ package com.google.cloud.spanner.connection; 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; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; @@ -172,6 +173,7 @@ public void testDetermineHost() { DEFAULT_HOST, determineHost( matcherWithoutHost, + DEFAULT_ENDPOINT, /* autoConfigEmulator= */ false, /* usePlainText= */ false, ImmutableMap.of())); @@ -179,6 +181,7 @@ public void testDetermineHost() { DEFAULT_HOST, determineHost( matcherWithoutHost, + DEFAULT_ENDPOINT, /* autoConfigEmulator= */ false, /* usePlainText= */ false, ImmutableMap.of("FOO", "bar"))); @@ -186,6 +189,7 @@ public void testDetermineHost() { "http://localhost:9010", determineHost( matcherWithoutHost, + DEFAULT_ENDPOINT, /* autoConfigEmulator= */ true, /* usePlainText= */ false, ImmutableMap.of())); @@ -193,6 +197,7 @@ public void testDetermineHost() { "http://localhost:9011", determineHost( matcherWithoutHost, + DEFAULT_ENDPOINT, /* autoConfigEmulator= */ true, /* usePlainText= */ false, ImmutableMap.of("SPANNER_EMULATOR_HOST", "localhost:9011"))); @@ -200,6 +205,7 @@ public void testDetermineHost() { "http://localhost:9010", determineHost( matcherWithoutHost, + DEFAULT_ENDPOINT, /* autoConfigEmulator= */ true, /* usePlainText= */ true, ImmutableMap.of())); @@ -207,6 +213,7 @@ public void testDetermineHost() { "http://localhost:9011", determineHost( matcherWithoutHost, + DEFAULT_ENDPOINT, /* autoConfigEmulator= */ true, /* usePlainText= */ true, ImmutableMap.of("SPANNER_EMULATOR_HOST", "localhost:9011"))); @@ -216,6 +223,7 @@ public void testDetermineHost() { "https://custom.host.domain:1234", determineHost( matcherWithHost, + DEFAULT_ENDPOINT, /* autoConfigEmulator= */ false, /* usePlainText= */ false, ImmutableMap.of())); @@ -223,6 +231,7 @@ public void testDetermineHost() { "http://custom.host.domain:1234", determineHost( matcherWithHost, + DEFAULT_ENDPOINT, /* autoConfigEmulator= */ false, /* usePlainText= */ true, ImmutableMap.of())); @@ -230,6 +239,7 @@ public void testDetermineHost() { "http://custom.host.domain:1234", determineHost( matcherWithHost, + DEFAULT_ENDPOINT, /* autoConfigEmulator= */ false, /* usePlainText= */ true, ImmutableMap.of())); @@ -237,6 +247,7 @@ public void testDetermineHost() { "https://custom.host.domain:1234", determineHost( matcherWithHost, + DEFAULT_ENDPOINT, /* autoConfigEmulator= */ true, /* usePlainText= */ false, ImmutableMap.of())); @@ -244,6 +255,7 @@ public void testDetermineHost() { "http://custom.host.domain:1234", determineHost( matcherWithHost, + DEFAULT_ENDPOINT, /* autoConfigEmulator= */ false, /* usePlainText= */ true, ImmutableMap.of("SPANNER_EMULATOR_HOST", "localhost:9011"))); @@ -251,9 +263,40 @@ public void testDetermineHost() { "https://custom.host.domain:1234", determineHost( matcherWithHost, + DEFAULT_ENDPOINT, /* autoConfigEmulator= */ true, /* usePlainText= */ false, ImmutableMap.of("SPANNER_EMULATOR_HOST", "localhost:9011"))); + + // The 'endpoint' connection URL property can also be used to connect to the emulator. + // Using this property is sometimes easier than adding the URL to the host part of the + // connection string, for example because it can be added to the Properties object that + // is used by JDBC. + assertEquals( + "http://localhost:9010", + determineHost( + matcherWithoutHost, + "localhost:9010", + /* autoConfigEmulator= */ false, + /* usePlainText= */ true, + ImmutableMap.of())); + // A value for the 'endpoint' connection property overrides any value in the host group. + assertEquals( + "https://my.endpoint:1234", + determineHost( + matcherWithHost, + "my.endpoint:1234", + /* autoConfigEmulator= */ false, + /* usePlainText= */ false, + ImmutableMap.of("SPANNER_EMULATOR_HOST", "localhost:9011"))); + assertEquals( + "http://my.endpoint.local:1234", + determineHost( + matcherWithHost, + "my.endpoint.local:1234", + /* autoConfigEmulator= */ false, + /* usePlainText= */ true, + ImmutableMap.of())); } @Test @@ -291,6 +334,20 @@ public void testBuildWithAutoConfigEmulatorAndHost() { assertTrue(options.isUsePlainText()); } + @Test + public void testBuildWithAutoConfigEmulatorAndEndpoint() { + ConnectionOptions.Builder builder = ConnectionOptions.newBuilder(); + builder.setUri( + "cloudspanner:/projects/test-project-123/instances/test-instance-123/databases/test-database-123?autoConfigEmulator=true;endpoint=central-emulator.local:8080"); + ConnectionOptions options = builder.build(); + assertEquals("http://central-emulator.local:8080", options.getHost()); + assertEquals("test-project-123", options.getProjectId()); + assertEquals("test-instance-123", options.getInstanceId()); + assertEquals("test-database-123", options.getDatabaseName()); + assertEquals(NoCredentials.getInstance(), options.getCredentials()); + assertTrue(options.isUsePlainText()); + } + @Test public void testBuildWithDefaultProjectPlaceholder() { ConnectionOptions.Builder builder = ConnectionOptions.newBuilder();