/*
 * Copyright 2015, Google Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *    * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *    * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *
 *    * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.google.auth.oauth2;

import static com.google.common.base.MoreObjects.firstNonNull;

import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpHeaders;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpResponseException;
import com.google.api.client.http.HttpStatusCodes;
import com.google.api.client.json.JsonObjectParser;
import com.google.api.client.util.GenericData;
import com.google.auth.Credentials;
import com.google.auth.Retryable;
import com.google.auth.ServiceAccountSigner;
import com.google.auth.http.HttpTransportFactory;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects.ToStringHelper;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * OAuth2 credentials representing the built-in service account for a Google Compute Engine VM.
 *
 * <p>Fetches access tokens from the Google Compute Engine metadata server.
 *
 * <p>These credentials use the IAM API to sign data. See {@link #sign(byte[])} for more details.
 */
public class ComputeEngineCredentials extends GoogleCredentials
    implements ServiceAccountSigner, IdTokenProvider {

  // Decrease timing margins on GCE.
  // This is needed because GCE VMs maintain their own OAuth cache that expires T-4 mins, attempting
  // to refresh a token before then, will yield the same stale token. To enable pre-emptive
  // refreshes, the margins must be shortened. This shouldn't cause problems since the clock skew
  // on the VM and metadata proxy should be non-existent.
  static final Duration COMPUTE_EXPIRATION_MARGIN = Duration.ofMinutes(3);
  static final Duration COMPUTE_REFRESH_MARGIN = Duration.ofMinutes(3).plusSeconds(45);

  private static final Logger LOGGER = Logger.getLogger(ComputeEngineCredentials.class.getName());

  static final String DEFAULT_METADATA_SERVER_URL = "http://metadata.google.internal";

  static final String SIGN_BLOB_URL_FORMAT =
      "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:signBlob";

  // Note: the explicit `timeout` and `tries` below is a workaround. The underlying
  // issue is that resolving an unknown host on some networks will take
  // 20-30 seconds; making this timeout short fixes the issue, but
  // could lead to false negatives in the event that we are on GCE, but
  // the metadata resolution was particularly slow. The latter case is
  // "unlikely" since the expected 4-nines time is about 0.5 seconds.
  // This allows us to limit the total ping maximum timeout to 1.5 seconds
  // for developer desktop scenarios.
  static final int MAX_COMPUTE_PING_TRIES = 3;
  static final int COMPUTE_PING_CONNECTION_TIMEOUT_MS = 500;

  private static final String METADATA_FLAVOR = "Metadata-Flavor";
  private static final String GOOGLE = "Google";
  private static final String WINDOWS = "windows";
  private static final String LINUX = "linux";

  private static final String PARSE_ERROR_PREFIX = "Error parsing token refresh response. ";
  private static final String PARSE_ERROR_ACCOUNT = "Error parsing service account response. ";
  private static final long serialVersionUID = -4113476462526554235L;

  private final String transportFactoryClassName;

  private final Collection<String> scopes;

  private transient HttpTransportFactory transportFactory;
  private transient String serviceAccountEmail;

  private String universeDomainFromMetadata = null;

  /**
   * An internal constructor
   *
   * @param builder A builder for {@link ComputeEngineCredentials} See {@link
   *     ComputeEngineCredentials.Builder}
   */
  private ComputeEngineCredentials(ComputeEngineCredentials.Builder builder) {
    super(builder);

    this.transportFactory =
        firstNonNull(
            builder.getHttpTransportFactory(),
            getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY));
    this.transportFactoryClassName = this.transportFactory.getClass().getName();
    // Use defaultScopes only when scopes don't exist.
    Collection<String> scopesToUse = builder.scopes;
    if (scopesToUse == null || scopesToUse.isEmpty()) {
      scopesToUse = builder.getDefaultScopes();
    }
    if (scopesToUse == null) {
      this.scopes = ImmutableSet.<String>of();
    } else {
      List<String> scopeList = new ArrayList<String>(scopesToUse);
      scopeList.removeAll(Arrays.asList("", null));
      this.scopes = ImmutableSet.<String>copyOf(scopeList);
    }
  }

  /** Clones the compute engine account with the specified scopes. */
  @Override
  public GoogleCredentials createScoped(Collection<String> newScopes) {
    ComputeEngineCredentials.Builder builder =
        this.toBuilder().setHttpTransportFactory(transportFactory).setScopes(newScopes);
    return new ComputeEngineCredentials(builder);
  }

  /** Clones the compute engine account with the specified scopes and default scopes. */
  @Override
  public GoogleCredentials createScoped(
      Collection<String> newScopes, Collection<String> newDefaultScopes) {
    ComputeEngineCredentials.Builder builder =
        ComputeEngineCredentials.newBuilder()
            .setHttpTransportFactory(transportFactory)
            .setScopes(newScopes)
            .setDefaultScopes(newDefaultScopes);
    return new ComputeEngineCredentials(builder);
  }

  /**
   * Create a new ComputeEngineCredentials instance with default behavior.
   *
   * @return new ComputeEngineCredentials
   */
  public static ComputeEngineCredentials create() {
    return new ComputeEngineCredentials(ComputeEngineCredentials.newBuilder());
  }

  public final Collection<String> getScopes() {
    return scopes;
  }

  /**
   * If scopes is specified, add "?scopes=comma-separated-list-of-scopes" to the token url.
   *
   * @return token url with the given scopes
   */
  String createTokenUrlWithScopes() {
    GenericUrl tokenUrl = new GenericUrl(getTokenServerEncodedUrl());
    if (!scopes.isEmpty()) {
      tokenUrl.set("scopes", Joiner.on(',').join(scopes));
    }
    return tokenUrl.toString();
  }

  /**
   * Gets the universe domain from the GCE metadata server.
   *
   * <p>Returns an explicit universe domain if it was provided during credential initialization.
   *
   * <p>Returns the {@link Credentials#GOOGLE_DEFAULT_UNIVERSE} if universe domain endpoint is not
   * found (404) or returns an empty string.
   *
   * <p>Otherwise, returns universe domain from GCE metadata service.
   *
   * <p>Any above value is cached for the credential lifetime.
   *
   * @throws IOException if a call to GCE metadata service was unsuccessful. Check if exception
   *     implements the {@link Retryable} and {@code isRetryable()} will return true if the
   *     operation may be retried.
   * @return string representing a universe domain in the format some-domain.xyz
   */
  @Override
  public String getUniverseDomain() throws IOException {
    if (isExplicitUniverseDomain()) {
      return super.getUniverseDomain();
    }

    synchronized (this) {
      if (this.universeDomainFromMetadata != null) {
        return this.universeDomainFromMetadata;
      }
    }

    String universeDomainFromMetadata = getUniverseDomainFromMetadata();
    synchronized (this) {
      this.universeDomainFromMetadata = universeDomainFromMetadata;
    }
    return universeDomainFromMetadata;
  }

  private String getUniverseDomainFromMetadata() throws IOException {
    HttpResponse response = getMetadataResponse(getUniverseDomainUrl());
    int statusCode = response.getStatusCode();
    if (statusCode == HttpStatusCodes.STATUS_CODE_NOT_FOUND) {
      return Credentials.GOOGLE_DEFAULT_UNIVERSE;
    }
    if (statusCode != HttpStatusCodes.STATUS_CODE_OK) {
      IOException cause =
          new IOException(
              String.format(
                  "Unexpected Error code %s trying to get universe domain"
                      + " from Compute Engine metadata for the default service account: %s",
                  statusCode, response.parseAsString()));
      throw new GoogleAuthException(true, cause);
    }
    String responseString = response.parseAsString();

    /* Earlier versions of MDS that supports universe_domain return empty string instead of GDU. */
    if (responseString.isEmpty()) {
      return Credentials.GOOGLE_DEFAULT_UNIVERSE;
    }
    return responseString;
  }

  /** Refresh the access token by getting it from the GCE metadata server */
  @Override
  public AccessToken refreshAccessToken() throws IOException {
    HttpResponse response = getMetadataResponse(createTokenUrlWithScopes());
    int statusCode = response.getStatusCode();
    if (statusCode == HttpStatusCodes.STATUS_CODE_NOT_FOUND) {
      throw new IOException(
          String.format(
              "Error code %s trying to get security access token from"
                  + " Compute Engine metadata for the default service account. This may be because"
                  + " the virtual machine instance does not have permission scopes specified."
                  + " It is possible to skip checking for Compute Engine metadata by specifying the environment "
                  + " variable "
                  + DefaultCredentialsProvider.NO_GCE_CHECK_ENV_VAR
                  + "=true.",
              statusCode));
    }
    if (statusCode != HttpStatusCodes.STATUS_CODE_OK) {
      throw new IOException(
          String.format(
              "Unexpected Error code %s trying to get security access"
                  + " token from Compute Engine metadata for the default service account: %s",
              statusCode, response.parseAsString()));
    }
    InputStream content = response.getContent();
    if (content == null) {
      // Throw explicitly here on empty content to avoid NullPointerException from parseAs call.
      // Mock transports will have success code with empty content by default.
      throw new IOException("Empty content from metadata token server request.");
    }
    GenericData responseData = response.parseAs(GenericData.class);
    String accessToken =
        OAuth2Utils.validateString(responseData, "access_token", PARSE_ERROR_PREFIX);
    int expiresInSeconds =
        OAuth2Utils.validateInt32(responseData, "expires_in", PARSE_ERROR_PREFIX);
    long expiresAtMilliseconds = clock.currentTimeMillis() + expiresInSeconds * 1000;
    return new AccessToken(accessToken, new Date(expiresAtMilliseconds));
  }

  /**
   * Returns a Google ID Token from the metadata server on ComputeEngine
   *
   * @param targetAudience the aud: field the IdToken should include
   * @param options list of Credential specific options for the token. For example, an IDToken for a
   *     ComputeEngineCredential could have the full formatted claims returned if
   *     IdTokenProvider.Option.FORMAT_FULL) is provided as a list option. Valid option values are:
   *     <br>
   *     IdTokenProvider.Option.FORMAT_FULL<br>
   *     IdTokenProvider.Option.LICENSES_TRUE<br>
   *     If no options are set, the defaults are "&amp;format=standard&amp;licenses=false"
   * @throws IOException if the attempt to get an IdToken failed
   * @return IdToken object which includes the raw id_token, JsonWebSignature
   */
  @Override
  public IdToken idTokenWithAudience(String targetAudience, List<IdTokenProvider.Option> options)
      throws IOException {
    GenericUrl documentUrl = new GenericUrl(getIdentityDocumentUrl());
    if (options != null) {
      if (options.contains(IdTokenProvider.Option.FORMAT_FULL)) {
        documentUrl.set("format", "full");
      }
      if (options.contains(IdTokenProvider.Option.LICENSES_TRUE)) {
        // license will only get returned if format is also full
        documentUrl.set("format", "full");
        documentUrl.set("license", "TRUE");
      }
    }
    documentUrl.set("audience", targetAudience);
    HttpResponse response = getMetadataResponse(documentUrl.toString());
    InputStream content = response.getContent();
    if (content == null) {
      throw new IOException("Empty content from metadata token server request.");
    }
    String rawToken = response.parseAsString();
    return IdToken.create(rawToken);
  }

  private HttpResponse getMetadataResponse(String url) throws IOException {
    GenericUrl genericUrl = new GenericUrl(url);
    HttpRequest request =
        transportFactory.create().createRequestFactory().buildGetRequest(genericUrl);
    JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY);
    request.setParser(parser);
    request.getHeaders().set(METADATA_FLAVOR, GOOGLE);
    request.setThrowExceptionOnExecuteError(false);
    HttpResponse response;
    try {
      response = request.execute();
    } catch (UnknownHostException exception) {
      throw new IOException(
          "ComputeEngineCredentials cannot find the metadata server. This is"
              + " likely because code is not running on Google Compute Engine.",
          exception);
    }

    if (response.getStatusCode() == 503) {
      throw GoogleAuthException.createWithTokenEndpointResponseException(
          new HttpResponseException(response));
    }

    return response;
  }

  /**
   * Implements an algorithm to detect whether the code is running on Google Compute Environment
   * (GCE) or equivalent runtime. <a href="https://google.aip.dev/auth/4115">See AIP-4115 for more
   * details</a> The algorithm consists of active and passive checks: <br>
   * <b>Active:</b> to check that GCE Metadata service is present by sending a http request to send
   * a request to {@code ComputeEngineCredentials.DEFAULT_METADATA_SERVER_URL}
   *
   * <p><b>Passive:</b> to check if SMBIOS variable is present and contains expected value. This
   * step is platform specific:
   *
   * <p><b>For Linux:</b> check if the file "/sys/class/dmi/id/product_name" exists and contains a
   * line that starts with Google.
   *
   * <p><b>For Windows:</b> to be implemented
   *
   * <p><b>Other platforms:</b> not supported
   *
   * <p>This algorithm can be disabled with environment variable {@code
   * DefaultCredentialsProvider.NO_GCE_CHECK_ENV_VAR} set to {@code true}. In this case, the
   * algorithm will always return {@code false} Returns {@code true} if currently running on Google
   * Compute Environment (GCE) or equivalent runtime. Returns {@code false} if detection fails,
   * platform is not supported or if detection disabled using the environment variable.
   */
  static synchronized boolean isOnGce(
      HttpTransportFactory transportFactory, DefaultCredentialsProvider provider) {
    // If the environment has requested that we do no GCE checks, return immediately.
    if (Boolean.parseBoolean(provider.getEnv(DefaultCredentialsProvider.NO_GCE_CHECK_ENV_VAR))) {
      return false;
    }

    boolean result = pingComputeEngineMetadata(transportFactory, provider);

    if (!result) {
      result = checkStaticGceDetection(provider);
    }

    if (!result) {
      LOGGER.log(Level.FINE, "Failed to detect whether running on Google Compute Engine.");
    }

    return result;
  }

  @VisibleForTesting
  static boolean checkProductNameOnLinux(BufferedReader reader) throws IOException {
    String name = reader.readLine().trim();
    return name.startsWith(GOOGLE);
  }

  @VisibleForTesting
  static boolean checkStaticGceDetection(DefaultCredentialsProvider provider) {
    String osName = provider.getOsName();
    try {
      if (osName.startsWith(LINUX)) {
        // Checks GCE residency on Linux platform.
        File linuxFile = new File("/sys/class/dmi/id/product_name");
        return checkProductNameOnLinux(
            new BufferedReader(new InputStreamReader(provider.readStream(linuxFile))));
      } else if (osName.startsWith(WINDOWS)) {
        // Checks GCE residency on Windows platform.
        // TODO: implement registry check via FFI
        return false;
      }
    } catch (IOException e) {
      LOGGER.log(Level.FINE, "Encountered an unexpected exception when checking SMBIOS value", e);
      return false;
    }
    // Platforms other than Linux and Windows are not supported.
    return false;
  }

  private static boolean pingComputeEngineMetadata(
      HttpTransportFactory transportFactory, DefaultCredentialsProvider provider) {
    GenericUrl tokenUrl = new GenericUrl(getMetadataServerUrl(provider));
    for (int i = 1; i <= MAX_COMPUTE_PING_TRIES; ++i) {
      try {
        HttpRequest request =
            transportFactory.create().createRequestFactory().buildGetRequest(tokenUrl);
        request.setConnectTimeout(COMPUTE_PING_CONNECTION_TIMEOUT_MS);
        request.getHeaders().set(METADATA_FLAVOR, GOOGLE);

        HttpResponse response = request.execute();
        try {
          // Internet providers can return a generic response to all requests, so it is necessary
          // to check that metadata header is present also.
          HttpHeaders headers = response.getHeaders();
          return OAuth2Utils.headersContainValue(headers, METADATA_FLAVOR, GOOGLE);
        } finally {
          response.disconnect();
        }
      } catch (SocketTimeoutException expected) {
        // Ignore logging timeouts which is the expected failure mode in non GCE environments.
      } catch (IOException e) {
        LOGGER.log(
            Level.FINE,
            "Encountered an unexpected exception when checking"
                + " if running on Google Compute Engine using Metadata Service ping.",
            e);
      }
    }
    return false;
  }

  public static String getMetadataServerUrl(DefaultCredentialsProvider provider) {
    String metadataServerAddress =
        provider.getEnv(DefaultCredentialsProvider.GCE_METADATA_HOST_ENV_VAR);
    if (metadataServerAddress != null) {
      return "http://" + metadataServerAddress;
    }
    return DEFAULT_METADATA_SERVER_URL;
  }

  public static String getMetadataServerUrl() {
    return getMetadataServerUrl(DefaultCredentialsProvider.DEFAULT);
  }

  public static String getTokenServerEncodedUrl(DefaultCredentialsProvider provider) {
    return getMetadataServerUrl(provider)
        + "/computeMetadata/v1/instance/service-accounts/default/token";
  }

  public static String getTokenServerEncodedUrl() {
    return getTokenServerEncodedUrl(DefaultCredentialsProvider.DEFAULT);
  }

  public static String getUniverseDomainUrl() {
    return getMetadataServerUrl(DefaultCredentialsProvider.DEFAULT)
        + "/computeMetadata/v1/universe/universe_domain";
  }

  public static String getServiceAccountsUrl() {
    return getMetadataServerUrl(DefaultCredentialsProvider.DEFAULT)
        + "/computeMetadata/v1/instance/service-accounts/?recursive=true";
  }

  public static String getIdentityDocumentUrl() {
    return getMetadataServerUrl(DefaultCredentialsProvider.DEFAULT)
        + "/computeMetadata/v1/instance/service-accounts/default/identity";
  }

  @Override
  public int hashCode() {
    return Objects.hash(transportFactoryClassName);
  }

  @Override
  protected ToStringHelper toStringHelper() {
    synchronized (this) {
      return super.toStringHelper()
          .add("transportFactoryClassName", transportFactoryClassName)
          .add("scopes", scopes)
          .add("universeDomainFromMetadata", universeDomainFromMetadata);
    }
  }

  @Override
  public boolean equals(Object obj) {
    if (!(obj instanceof ComputeEngineCredentials)) {
      return false;
    }
    if (!super.equals(obj)) {
      return false;
    }
    ComputeEngineCredentials other = (ComputeEngineCredentials) obj;
    return Objects.equals(this.transportFactoryClassName, other.transportFactoryClassName)
        && Objects.equals(this.scopes, other.scopes)
        && Objects.equals(this.universeDomainFromMetadata, other.universeDomainFromMetadata);
  }

  private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
    input.defaultReadObject();
    transportFactory = newInstance(transportFactoryClassName);
  }

  @Override
  public Builder toBuilder() {
    return new Builder(this);
  }

  public static Builder newBuilder() {
    return new Builder();
  }

  /**
   * Returns the email address associated with the GCE default service account.
   *
   * @throws RuntimeException if the default service account cannot be read
   */
  @Override
  // todo(#314) getAccount should not throw a RuntimeException
  public String getAccount() {
    if (serviceAccountEmail == null) {
      try {
        serviceAccountEmail = getDefaultServiceAccount();
      } catch (IOException ex) {
        throw new RuntimeException("Failed to get service account", ex);
      }
    }
    return serviceAccountEmail;
  }

  /**
   * Signs the provided bytes using the private key associated with the service account.
   *
   * <p>The Compute Engine's project must enable the Identity and Access Management (IAM) API and
   * the instance's service account must have the iam.serviceAccounts.signBlob permission.
   *
   * @param toSign bytes to sign
   * @return signed bytes
   * @throws SigningException if the attempt to sign the provided bytes failed
   * @see <a
   *     href="https://cloud.google.com/iam/credentials/reference/rest/v1/projects.serviceAccounts/signBlob">Blob
   *     Signing</a>
   */
  @Override
  public byte[] sign(byte[] toSign) {
    try {
      String account = getAccount();
      return IamUtils.sign(
          account, this, transportFactory.create(), toSign, Collections.<String, Object>emptyMap());
    } catch (SigningException ex) {
      throw ex;
    } catch (RuntimeException ex) {
      throw new SigningException("Signing failed", ex);
    }
  }

  private String getDefaultServiceAccount() throws IOException {
    HttpResponse response = getMetadataResponse(getServiceAccountsUrl());
    int statusCode = response.getStatusCode();
    if (statusCode == HttpStatusCodes.STATUS_CODE_NOT_FOUND) {
      throw new IOException(
          String.format(
              "Error code %s trying to get service accounts from"
                  + " Compute Engine metadata. This may be because the virtual machine instance"
                  + " does not have permission scopes specified.",
              statusCode));
    }
    if (statusCode != HttpStatusCodes.STATUS_CODE_OK) {
      throw new IOException(
          String.format(
              "Unexpected Error code %s trying to get service accounts"
                  + " from Compute Engine metadata: %s",
              statusCode, response.parseAsString()));
    }
    InputStream content = response.getContent();
    if (content == null) {
      // Throw explicitly here on empty content to avoid NullPointerException from parseAs call.
      // Mock transports will have success code with empty content by default.
      throw new IOException("Empty content from metadata token server request.");
    }
    GenericData responseData = response.parseAs(GenericData.class);
    Map<String, Object> defaultAccount =
        OAuth2Utils.validateMap(responseData, "default", PARSE_ERROR_ACCOUNT);
    return OAuth2Utils.validateString(defaultAccount, "email", PARSE_ERROR_ACCOUNT);
  }

  public static class Builder extends GoogleCredentials.Builder {
    private HttpTransportFactory transportFactory;
    private Collection<String> scopes;
    private Collection<String> defaultScopes;

    protected Builder() {
      setRefreshMargin(COMPUTE_REFRESH_MARGIN);
      setExpirationMargin(COMPUTE_EXPIRATION_MARGIN);
    }

    protected Builder(ComputeEngineCredentials credentials) {
      super(credentials);
      this.transportFactory = credentials.transportFactory;
      this.scopes = credentials.scopes;
    }

    @CanIgnoreReturnValue
    public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) {
      this.transportFactory = transportFactory;
      return this;
    }

    @CanIgnoreReturnValue
    public Builder setScopes(Collection<String> scopes) {
      this.scopes = scopes;
      return this;
    }

    @CanIgnoreReturnValue
    public Builder setDefaultScopes(Collection<String> defaultScopes) {
      this.defaultScopes = defaultScopes;
      return this;
    }

    @CanIgnoreReturnValue
    public Builder setUniverseDomain(String universeDomain) {
      this.universeDomain = universeDomain;
      return this;
    }

    @CanIgnoreReturnValue
    public Builder setQuotaProjectId(String quotaProjectId) {
      super.quotaProjectId = quotaProjectId;
      return this;
    }

    public HttpTransportFactory getHttpTransportFactory() {
      return transportFactory;
    }

    public Collection<String> getScopes() {
      return scopes;
    }

    public Collection<String> getDefaultScopes() {
      return defaultScopes;
    }

    @Override
    public ComputeEngineCredentials build() {
      return new ComputeEngineCredentials(this);
    }
  }
}
