2020import com .google .api .client .util .ObjectParser ;
2121import com .google .api .client .util .Preconditions ;
2222import com .google .api .client .util .Strings ;
23+ import com .google .auth .Credentials ;
24+ import com .google .auth .http .HttpCredentialsAdapter ;
25+ import com .google .common .annotations .VisibleForTesting ;
2326import java .io .IOException ;
2427import java .util .logging .Logger ;
28+ import java .util .regex .Matcher ;
29+ import java .util .regex .Pattern ;
2530
2631/**
2732 * Abstract thread-safe Google client.
@@ -33,6 +38,8 @@ public abstract class AbstractGoogleClient {
3338
3439 private static final Logger logger = Logger .getLogger (AbstractGoogleClient .class .getName ());
3540
41+ private static final String GOOGLE_CLOUD_UNIVERSE_DOMAIN = "GOOGLE_CLOUD_UNIVERSE_DOMAIN" ;
42+
3643 /** The request factory for connections to the server. */
3744 private final HttpRequestFactory requestFactory ;
3845
@@ -68,13 +75,18 @@ public abstract class AbstractGoogleClient {
6875 /** Whether discovery required parameter checks should be suppressed. */
6976 private final boolean suppressRequiredParameterChecks ;
7077
78+ private final String universeDomain ;
79+
80+ private final HttpRequestInitializer httpRequestInitializer ;
81+
7182 /**
7283 * @param builder builder
7384 * @since 1.14
7485 */
7586 protected AbstractGoogleClient (Builder builder ) {
7687 googleClientRequestInitializer = builder .googleClientRequestInitializer ;
77- rootUrl = normalizeRootUrl (builder .rootUrl );
88+ universeDomain = determineUniverseDomain (builder );
89+ rootUrl = normalizeRootUrl (determineEndpoint (builder ));
7890 servicePath = normalizeServicePath (builder .servicePath );
7991 batchPath = builder .batchPath ;
8092 if (Strings .isNullOrEmpty (builder .applicationName )) {
@@ -88,6 +100,75 @@ protected AbstractGoogleClient(Builder builder) {
88100 objectParser = builder .objectParser ;
89101 suppressPatternChecks = builder .suppressPatternChecks ;
90102 suppressRequiredParameterChecks = builder .suppressRequiredParameterChecks ;
103+ httpRequestInitializer = builder .httpRequestInitializer ;
104+ }
105+
106+ /**
107+ * Resolve the Universe Domain to be used when resolving the endpoint. The logic for resolving the
108+ * universe domain is the following order: 1. Use the user configured value is set, 2. Use the
109+ * Universe Domain Env Var if set, 3. Default to the Google Default Universe
110+ */
111+ private String determineUniverseDomain (Builder builder ) {
112+ String resolvedUniverseDomain = builder .universeDomain ;
113+ if (resolvedUniverseDomain == null ) {
114+ resolvedUniverseDomain = System .getenv (GOOGLE_CLOUD_UNIVERSE_DOMAIN );
115+ }
116+ return resolvedUniverseDomain == null
117+ ? Credentials .GOOGLE_DEFAULT_UNIVERSE
118+ : resolvedUniverseDomain ;
119+ }
120+
121+ /**
122+ * Resolve the endpoint based on user configurations. If the user has configured a custom rootUrl,
123+ * use that value. Otherwise, construct the endpoint based on the serviceName and the
124+ * universeDomain.
125+ */
126+ private String determineEndpoint (Builder builder ) {
127+ boolean mtlsEnabled = builder .rootUrl .contains (".mtls." );
128+ if (mtlsEnabled && !universeDomain .equals (Credentials .GOOGLE_DEFAULT_UNIVERSE )) {
129+ throw new IllegalStateException (
130+ "mTLS is not supported in any universe other than googleapis.com" );
131+ }
132+ // If the serviceName is null, we cannot construct a valid resolved endpoint. Simply return
133+ // the rootUrl as this was custom rootUrl passed in.
134+ if (builder .isUserConfiguredEndpoint || builder .serviceName == null ) {
135+ return builder .rootUrl ;
136+ }
137+ if (mtlsEnabled ) {
138+ return "https://" + builder .serviceName + ".mtls." + universeDomain + "/" ;
139+ }
140+ return "https://" + builder .serviceName + "." + universeDomain + "/" ;
141+ }
142+
143+ /**
144+ * Check that the User configured universe domain matches the Credentials' universe domain. This
145+ * uses the HttpRequestInitializer to get the Credentials and is enforced that the
146+ * HttpRequestInitializer is of the {@see <a
147+ * href="https://github.com/googleapis/google-auth-library-java/blob/main/oauth2_http/java/com/google/auth/http/HttpCredentialsAdapter.java">HttpCredentialsAdapter</a>}
148+ * from the google-auth-library.
149+ *
150+ * <p>To use a non-GDU Credentials, you must use the HttpCredentialsAdapter class.
151+ *
152+ * @throws IOException if there is an error reading the Universe Domain from the credentials
153+ * @throws IllegalStateException if the configured Universe Domain does not match the Universe
154+ * Domain in the Credentials
155+ */
156+ public void validateUniverseDomain () throws IOException {
157+ if (!(httpRequestInitializer instanceof HttpCredentialsAdapter )) {
158+ return ;
159+ }
160+ Credentials credentials = ((HttpCredentialsAdapter ) httpRequestInitializer ).getCredentials ();
161+ // No need for a null check as HttpCredentialsAdapter cannot be initialized with null
162+ // Credentials
163+ String expectedUniverseDomain = credentials .getUniverseDomain ();
164+ if (!expectedUniverseDomain .equals (getUniverseDomain ())) {
165+ throw new IllegalStateException (
166+ String .format (
167+ "The configured universe domain (%s) does not match the universe domain found"
168+ + " in the credentials (%s). If you haven't configured the universe domain"
169+ + " explicitly, `googleapis.com` is the default." ,
170+ getUniverseDomain (), expectedUniverseDomain ));
171+ }
91172 }
92173
93174 /**
@@ -139,6 +220,18 @@ public final GoogleClientRequestInitializer getGoogleClientRequestInitializer()
139220 return googleClientRequestInitializer ;
140221 }
141222
223+ /**
224+ * Universe Domain is the domain for Google Cloud Services. It follows the format of
225+ * `{ServiceName}.{UniverseDomain}`. For example, speech.googleapis.com would have a Universe
226+ * Domain value of `googleapis.com` and cloudasset.test.com would have a Universe Domain of
227+ * `test.com`. If this value is not set, this will default to `googleapis.com`.
228+ *
229+ * @return The configured Universe Domain or the Google Default Universe (googleapis.com)
230+ */
231+ public final String getUniverseDomain () {
232+ return universeDomain ;
233+ }
234+
142235 /**
143236 * Returns the object parser or {@code null} for none.
144237 *
@@ -173,6 +266,7 @@ public ObjectParser getObjectParser() {
173266 * @param httpClientRequest Google client request type
174267 */
175268 protected void initialize (AbstractGoogleClientRequest <?> httpClientRequest ) throws IOException {
269+ validateUniverseDomain ();
176270 if (getGoogleClientRequestInitializer () != null ) {
177271 getGoogleClientRequestInitializer ().initialize (httpClientRequest );
178272 }
@@ -311,6 +405,33 @@ public abstract static class Builder {
311405 /** Whether discovery required parameter checks should be suppressed. */
312406 boolean suppressRequiredParameterChecks ;
313407
408+ /** User configured Universe Domain. Defaults to `googleapis.com`. */
409+ String universeDomain ;
410+
411+ /**
412+ * Regex pattern to check if the URL passed in matches the default endpoint configured from a
413+ * discovery doc. Follows the format of `https://{serviceName}(.mtls).googleapis.com/`
414+ */
415+ Pattern defaultEndpointRegex =
416+ Pattern .compile ("https://([a-zA-Z]*)(\\ .mtls)?\\ .googleapis.com/?" );
417+
418+ /**
419+ * Whether the user has configured an endpoint via {@link #setRootUrl(String)}. This is added in
420+ * because the rootUrl is set in the Builder's constructor. ,
421+ *
422+ * <p>Apiary clients don't allow user configurations to this Builder's constructor, so this
423+ * would be set to false by default for Apiary libraries. User configuration to the rootUrl is
424+ * done via {@link #setRootUrl(String)}.
425+ *
426+ * <p>For other uses cases that touch this Builder's constructor directly, check if the rootUrl
427+ * passed matches the default endpoint regex. If it doesn't match, it is a user configured
428+ * endpoint.
429+ */
430+ boolean isUserConfiguredEndpoint ;
431+
432+ /** The parsed serviceName value from the rootUrl from the Discovery Doc. */
433+ String serviceName ;
434+
314435 /**
315436 * Returns an instance of a new builder.
316437 *
@@ -328,9 +449,15 @@ protected Builder(
328449 HttpRequestInitializer httpRequestInitializer ) {
329450 this .transport = Preconditions .checkNotNull (transport );
330451 this .objectParser = objectParser ;
331- setRootUrl (rootUrl );
332- setServicePath (servicePath );
452+ this . rootUrl = normalizeRootUrl (rootUrl );
453+ this . servicePath = normalizeServicePath (servicePath );
333454 this .httpRequestInitializer = httpRequestInitializer ;
455+ Matcher matcher = defaultEndpointRegex .matcher (rootUrl );
456+ boolean matches = matcher .matches ();
457+ // Checked here for the use case where users extend this class and may pass in
458+ // a custom endpoint
459+ this .isUserConfiguredEndpoint = !matches ;
460+ this .serviceName = matches ? matcher .group (1 ) : null ;
334461 }
335462
336463 /** Builds a new instance of {@link AbstractGoogleClient}. */
@@ -371,6 +498,7 @@ public final String getRootUrl() {
371498 * changing the return type, but nothing else.
372499 */
373500 public Builder setRootUrl (String rootUrl ) {
501+ this .isUserConfiguredEndpoint = true ;
374502 this .rootUrl = normalizeRootUrl (rootUrl );
375503 return this ;
376504 }
@@ -515,5 +643,24 @@ public Builder setSuppressRequiredParameterChecks(boolean suppressRequiredParame
515643 public Builder setSuppressAllChecks (boolean suppressAllChecks ) {
516644 return setSuppressPatternChecks (true ).setSuppressRequiredParameterChecks (true );
517645 }
646+
647+ /**
648+ * Sets the user configured Universe Domain value. This value will be used to try and construct
649+ * the endpoint to connect to GCP services.
650+ *
651+ * @throws IllegalArgumentException if universeDomain is passed in with an empty string ("")
652+ */
653+ public Builder setUniverseDomain (String universeDomain ) {
654+ if (universeDomain != null && universeDomain .isEmpty ()) {
655+ throw new IllegalArgumentException ("The universe domain value cannot be empty." );
656+ }
657+ this .universeDomain = universeDomain ;
658+ return this ;
659+ }
660+
661+ @ VisibleForTesting
662+ String getServiceName () {
663+ return serviceName ;
664+ }
518665 }
519666}
0 commit comments