1717package com .google .cloud .spanner .connection ;
1818
1919import com .google .api .core .InternalApi ;
20+ import com .google .api .gax .core .CredentialsProvider ;
2021import com .google .api .gax .rpc .TransportChannelProvider ;
2122import com .google .auth .Credentials ;
2223import com .google .auth .oauth2 .AccessToken ;
3637import com .google .common .base .Preconditions ;
3738import com .google .common .collect .Sets ;
3839import com .google .spanner .v1 .ExecuteSqlRequest .QueryOptions ;
40+ import java .io .IOException ;
41+ import java .lang .reflect .Constructor ;
42+ import java .lang .reflect .InvocationTargetException ;
3943import java .net .URL ;
4044import java .util .ArrayList ;
4145import java .util .Arrays ;
4246import java .util .Collections ;
4347import java .util .HashSet ;
4448import java .util .List ;
49+ import java .util .Objects ;
4550import java .util .Set ;
4651import java .util .regex .Matcher ;
4752import java .util .regex .Pattern ;
53+ import java .util .stream .Stream ;
4854import javax .annotation .Nullable ;
4955
5056/**
@@ -182,6 +188,8 @@ public String[] getValidValues() {
182188 public static final String CREDENTIALS_PROPERTY_NAME = "credentials" ;
183189 /** Name of the 'encodedCredentials' connection property. */
184190 public static final String ENCODED_CREDENTIALS_PROPERTY_NAME = "encodedCredentials" ;
191+ /** Name of the 'credentialsProvider' connection property. */
192+ public static final String CREDENTIALS_PROVIDER_PROPERTY_NAME = "credentialsProvider" ;
185193 /**
186194 * OAuth token to use for authentication. Cannot be used in combination with a credentials file.
187195 */
@@ -231,6 +239,9 @@ public String[] getValidValues() {
231239 ConnectionProperty .createStringProperty (
232240 ENCODED_CREDENTIALS_PROPERTY_NAME ,
233241 "Base64-encoded credentials to use for this connection. If neither this property or a credentials location are set, the connection will use the default Google Cloud credentials for the runtime environment." ),
242+ ConnectionProperty .createStringProperty (
243+ CREDENTIALS_PROVIDER_PROPERTY_NAME ,
244+ "The class name of the com.google.api.gax.core.CredentialsProvider implementation that should be used to obtain credentials for connections." ),
234245 ConnectionProperty .createStringProperty (
235246 OAUTH_TOKEN_PROPERTY_NAME ,
236247 "A valid pre-existing OAuth token to use for authentication for this connection. Setting this property will take precedence over any value set for a credentials file." ),
@@ -386,6 +397,12 @@ private boolean isValidUri(String uri) {
386397 * <li>encodedCredentials (String): A Base64 encoded string containing the Google credentials
387398 * to use. You should only set either this property or the `credentials` (file location)
388399 * property, but not both at the same time.
400+ * <li>credentialsProvider (String): Class name of the {@link
401+ * com.google.api.gax.core.CredentialsProvider} that should be used to get credentials for
402+ * a connection that is created by this {@link ConnectionOptions}. The credentials will be
403+ * retrieved from the {@link com.google.api.gax.core.CredentialsProvider} when a new
404+ * connection is created. A connection will use the credentials that were obtained at
405+ * creation during its lifetime.
389406 * <li>autocommit (boolean): Sets the initial autocommit mode for the connection. Default is
390407 * true.
391408 * <li>readonly (boolean): Sets the initial readonly mode for the connection. Default is
@@ -501,6 +518,7 @@ public static Builder newBuilder() {
501518 private final String warnings ;
502519 private final String credentialsUrl ;
503520 private final String encodedCredentials ;
521+ private final CredentialsProvider credentialsProvider ;
504522 private final String oauthToken ;
505523 private final Credentials fixedCredentials ;
506524
@@ -537,22 +555,22 @@ private ConnectionOptions(Builder builder) {
537555 this .credentialsUrl =
538556 builder .credentialsUrl != null ? builder .credentialsUrl : parseCredentials (builder .uri );
539557 this .encodedCredentials = parseEncodedCredentials (builder .uri );
540- // Check that not both a credentials location and encoded credentials have been specified in the
541- // connection URI.
542- Preconditions .checkArgument (
543- this .credentialsUrl == null || this .encodedCredentials == null ,
544- "Cannot specify both a credentials URL and encoded credentials. Only set one of the properties." );
545-
558+ this .credentialsProvider = parseCredentialsProvider (builder .uri );
546559 this .oauthToken =
547560 builder .oauthToken != null ? builder .oauthToken : parseOAuthToken (builder .uri );
548- this . fixedCredentials = builder . credentials ;
549- // Check that not both credentials and an OAuth token have been specified .
561+ // Check that at most one of credentials location, encoded credentials, credentials provider and
562+ // OUAuth token has been specified in the connection URI .
550563 Preconditions .checkArgument (
551- (builder .credentials == null
552- && this .credentialsUrl == null
553- && this .encodedCredentials == null )
554- || this .oauthToken == null ,
555- "Cannot specify both credentials and an OAuth token." );
564+ Stream .of (
565+ this .credentialsUrl ,
566+ this .encodedCredentials ,
567+ this .credentialsProvider ,
568+ this .oauthToken )
569+ .filter (Objects ::nonNull )
570+ .count ()
571+ <= 1 ,
572+ "Specify only one of credentialsUrl, encodedCredentials, credentialsProvider and OAuth token" );
573+ this .fixedCredentials = builder .credentials ;
556574
557575 this .userAgent = parseUserAgent (this .uri );
558576 QueryOptions .Builder queryOptionsBuilder = QueryOptions .newBuilder ();
@@ -570,14 +588,24 @@ private ConnectionOptions(Builder builder) {
570588 // Using credentials on a plain text connection is not allowed, so if the user has not specified
571589 // any credentials and is using a plain text connection, we should not try to get the
572590 // credentials from the environment, but default to NoCredentials.
573- if (builder . credentials == null
591+ if (this . fixedCredentials == null
574592 && this .credentialsUrl == null
575593 && this .encodedCredentials == null
594+ && this .credentialsProvider == null
576595 && this .oauthToken == null
577596 && this .usePlainText ) {
578597 this .credentials = NoCredentials .getInstance ();
579598 } else if (this .oauthToken != null ) {
580599 this .credentials = new GoogleCredentials (new AccessToken (oauthToken , null ));
600+ } else if (this .credentialsProvider != null ) {
601+ try {
602+ this .credentials = this .credentialsProvider .getCredentials ();
603+ } catch (IOException exception ) {
604+ throw SpannerExceptionFactory .newSpannerException (
605+ ErrorCode .INVALID_ARGUMENT ,
606+ "Failed to get credentials from CredentialsProvider: " + exception .getMessage (),
607+ exception );
608+ }
581609 } else if (this .fixedCredentials != null ) {
582610 this .credentials = fixedCredentials ;
583611 } else if (this .encodedCredentials != null ) {
@@ -691,18 +719,49 @@ static boolean parseRetryAbortsInternally(String uri) {
691719 }
692720
693721 @ VisibleForTesting
694- static String parseCredentials (String uri ) {
722+ static @ Nullable String parseCredentials (String uri ) {
695723 String value = parseUriProperty (uri , CREDENTIALS_PROPERTY_NAME );
696724 return value != null ? value : DEFAULT_CREDENTIALS ;
697725 }
698726
699727 @ VisibleForTesting
700- static String parseEncodedCredentials (String uri ) {
728+ static @ Nullable String parseEncodedCredentials (String uri ) {
701729 return parseUriProperty (uri , ENCODED_CREDENTIALS_PROPERTY_NAME );
702730 }
703731
704732 @ VisibleForTesting
705- static String parseOAuthToken (String uri ) {
733+ static @ Nullable CredentialsProvider parseCredentialsProvider (String uri ) {
734+ String name = parseUriProperty (uri , CREDENTIALS_PROVIDER_PROPERTY_NAME );
735+ if (name != null ) {
736+ try {
737+ Class <? extends CredentialsProvider > clazz =
738+ (Class <? extends CredentialsProvider >) Class .forName (name );
739+ Constructor <? extends CredentialsProvider > constructor = clazz .getDeclaredConstructor ();
740+ return constructor .newInstance ();
741+ } catch (ClassNotFoundException classNotFoundException ) {
742+ throw SpannerExceptionFactory .newSpannerException (
743+ ErrorCode .INVALID_ARGUMENT ,
744+ "Unknown or invalid CredentialsProvider class name: " + name ,
745+ classNotFoundException );
746+ } catch (NoSuchMethodException noSuchMethodException ) {
747+ throw SpannerExceptionFactory .newSpannerException (
748+ ErrorCode .INVALID_ARGUMENT ,
749+ "Credentials provider " + name + " does not have a public no-arg constructor." ,
750+ noSuchMethodException );
751+ } catch (InvocationTargetException
752+ | InstantiationException
753+ | IllegalAccessException exception ) {
754+ throw SpannerExceptionFactory .newSpannerException (
755+ ErrorCode .INVALID_ARGUMENT ,
756+ "Failed to create an instance of " + name + ": " + exception .getMessage (),
757+ exception );
758+ }
759+ }
760+ return null ;
761+ }
762+
763+ @ VisibleForTesting
764+ static @ Nullable String parseOAuthToken (String uri ) {
706765 String value = parseUriProperty (uri , OAUTH_TOKEN_PROPERTY_NAME );
707766 return value != null ? value : DEFAULT_OAUTH_TOKEN ;
708767 }
@@ -849,6 +908,10 @@ Credentials getFixedCredentials() {
849908 return this .fixedCredentials ;
850909 }
851910
911+ CredentialsProvider getCredentialsProvider () {
912+ return this .credentialsProvider ;
913+ }
914+
852915 /** The {@link SessionPoolOptions} of this {@link ConnectionOptions}. */
853916 public SessionPoolOptions getSessionPoolOptions () {
854917 return sessionPoolOptions ;
0 commit comments