All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.plaid.client.PlaidClient Maven / Gradle / Ivy

There is a newer version: 27.0.0
Show newest version
package com.plaid.client;

import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.plaid.client.internal.Util;
import com.plaid.client.internal.gson.CredentialInjectingTypeAdapterFactory;
import com.plaid.client.internal.gson.ImmutableListTypeAdapterFactory;
import com.plaid.client.internal.gson.BaseOptionsSerializer;
import com.plaid.client.internal.gson.OptionalTypeAdapterFactory;
import com.plaid.client.internal.gson.RequiredFieldTypeAdapterFactory;
import com.plaid.client.request.TransactionsGetRequest;
import com.plaid.client.response.ErrorResponse;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.security.KeyStore;
import java.util.Arrays;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import okhttp3.ConnectionSpec;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.ResponseBody;
import okhttp3.TlsVersion;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Converter;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public final class PlaidClient {
  // a more restrictive connection spec based on the MODERN_TLS spec already present in OkHttp
  private static final ConnectionSpec CONNECTION_SPEC =
    new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
      .tlsVersions(TlsVersion.TLS_1_2)
      .build();
  private static String headerVersion;

  private final PlaidApiService plaidApiService;
  private final Retrofit retrofit;

  private PlaidClient(PlaidApiService plaidApiService, Retrofit retrofit) {
    this.plaidApiService = plaidApiService;
    this.retrofit = retrofit;
  }

  /**
   * Get the Retrofit {@link PlaidApiService} which backs this client.
   *
   * All Plaid API calls are called on this service object.
   *
   * @return the {@link PlaidApiService}
   */
  public PlaidApiService service() {
    return plaidApiService;
  }

  /**
   * A helper to assist with decoding unsuccessful responses.
   *
   * This is not done automatically, because an unsuccessful result may have many causes
   * such as network issues, intervening HTTP proxies, load balancers, partial responses, etc,
   * which means that a response can easily be incomplete, or not even the expected well-formed
   * JSON error.
   *
   * Therefore, even when using this helper, be prepared for it to throw an exception instead of
   * successfully decoding every error response!
   *
   * @param response the unsuccessful response object to deserialize.
   * @return the resulting {@link ErrorResponse}, assuming deserialization succeeded.
   * @throws RuntimeException if the response cannot be deserialized
   */
  public ErrorResponse parseError(Response response) {
    if (response.isSuccessful()) {
      throw new IllegalArgumentException("Response must be unsuccessful.");
    }

    Converter responseBodyObjectConverter =
      retrofit.responseBodyConverter(ErrorResponse.class, new Annotation[0]);

    try {
      return responseBodyObjectConverter.convert(response.errorBody());
    } catch (IOException ex) {
      throw new RuntimeException("Could not parse error response", ex);
    }
  }

  /**
   * Visible for testing.
   *
   * @return the underlying Retrofit client.
   */
  public Retrofit getRetrofit() {
    return retrofit;
  }

  /**
   * Start here! Creates a new {@link Builder} so you can make a {@link PlaidClient}.
   *
   * @return A brand new {@link Builder}
   */
  public static Builder newBuilder() {
    return new Builder();
  }

  public static class PlaidApiHeadersInterceptor implements Interceptor {
    @Override public okhttp3.Response intercept(Interceptor.Chain chain) throws IOException {
      Request originalRequest = chain.request();
      Request transformedRequest = originalRequest.newBuilder()
        .addHeader(PlaidHeaders.PLAID_API_VERSION_OVERRIDE_HEADER,
          PlaidHeaders.PLAID_API_VERSION)
        .addHeader(PlaidHeaders.PLAID_API_USER_AGENT_HEADER, getUserAgentHeader())
        .build();
      return chain.proceed(transformedRequest);
    }
  }

  private static String getUserAgentHeader() {
    return String.format("Plaid Java v%s", PlaidVersion.PLAID_VERSION);
  }

  public static class Builder {
    public static String DEFAULT_PRODUCTION_BASE_URL = "https://production.plaid.com";
    public static String DEFAULT_DEVELOPMENT_BASE_URL = "https://development.plaid.com";
    public static String DEFAULT_SANDBOX_BASE_URL = "https://sandbox.plaid.com";
    public static long DEFAULT_READ_TIMEOUT_SECONDS = 300;
    public static long DEFAULT_CONNECT_TIMEOUT_SECONDS = 5;

    private final OkHttpClient.Builder okHttpClientBuilder;
    private String baseUrl;
    private HttpLoggingInterceptor.Level httpLogLevel;
    private long readTimeoutSeconds;
    private long connectTimeoutSeconds;
    private String publicKey;
    private String clientId;
    private String secret;

    private Builder() {
      this.okHttpClientBuilder = new OkHttpClient.Builder();
      this.readTimeoutSeconds = DEFAULT_READ_TIMEOUT_SECONDS;
      this.connectTimeoutSeconds = DEFAULT_CONNECT_TIMEOUT_SECONDS;
    }

    /**
     * Validate builder parameters, create, and return a new {@link PlaidClient}.
     *
     * @return A brand new {@link PlaidClient}
     */
    public PlaidClient build() {
      if (baseUrl == null) {
        throw new IllegalArgumentException(
          "must set baseUrl. You probably want to call productionBaseUrl(), developmentBaseUrl(), or sandboxBaseUrl().");
      }

      if (publicKey == null && (clientId == null || secret == null)) {
        throw new IllegalArgumentException("must set a publicKey, clientIdAndSecret, or both!");
      }

      Retrofit retrofit = new Retrofit.Builder()
        .baseUrl(baseUrl)
        .validateEagerly(true)
        .addConverterFactory(GsonConverterFactory.create(buildGson()))
        .client(buildOkHttpClient())
        .build();

      return new PlaidClient(retrofit.create(PlaidApiService.class), retrofit);
    }

    private Gson buildGson() {
      return new GsonBuilder()
        .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
        .registerTypeAdapterFactory(
          new CredentialInjectingTypeAdapterFactory(publicKey, clientId, secret))
        .registerTypeAdapterFactory(new RequiredFieldTypeAdapterFactory())
        .registerTypeAdapterFactory(new OptionalTypeAdapterFactory())
        .registerTypeAdapterFactory(new ImmutableListTypeAdapterFactory())
        .registerTypeAdapter(TransactionsGetRequest.Options.class, new BaseOptionsSerializer())
        .create();
    }

    private OkHttpClient buildOkHttpClient() {
      okHttpClientBuilder
        .readTimeout(readTimeoutSeconds, TimeUnit.SECONDS)
        .connectTimeout(connectTimeoutSeconds, TimeUnit.SECONDS)
        .followSslRedirects(false)
        .addInterceptor(new PlaidApiHeadersInterceptor())
        .connectionSpecs(Collections.singletonList(CONNECTION_SPEC));

      if (httpLogLevel != null) {
        okHttpClientBuilder.addInterceptor(new HttpLoggingInterceptor().setLevel(httpLogLevel));
      }

      checkRuntimeSupportsTls12(okHttpClientBuilder);

      return okHttpClientBuilder.build();
    }

    /**
     * Plaid's API requires TLSv1.2, which is well-supported
     * on JDK 8, but spotty on JDK 7 and below.
     *
     * This attempts to detect whether TLSv1.2 is already enabled by default,
     * and if not, enable it, or failing that throw an error early.
     *
     * @param okHttpClientBuilder the OkHttpClient builder
     */
    private void checkRuntimeSupportsTls12(OkHttpClient.Builder okHttpClientBuilder) {
      SSLSocket testSslSocket = null;

      try {
        // create a temporary client and test socket to check for desired cipher and protocol support
        OkHttpClient testOkHttpClient = okHttpClientBuilder.build();
        testSslSocket = (SSLSocket) testOkHttpClient.sslSocketFactory().createSocket();

        // does the test socket work with our connection spec's cipher suite and tls version as-is?
        if (CONNECTION_SPEC.isCompatible(testSslSocket)) {
          return; // no further questions!
        }

        // perhaps TLSv1.2 is supported, just not enabled by default (some versions of Java 7)
        if (!Arrays.asList(testSslSocket.getSupportedProtocols())
          .contains(TlsVersion.TLS_1_2.javaName())) {
          throw new RuntimeException(
            "This JRE's SSL implementation does not support TLSv1.2. Bailing out.");
        }

        // supported but not enabled by default. In this case, we'll have our OkHTTP
        // client use an SSLSocketFactory which does enable it.

        // The following SSLSocketFactory setup code is from
        // OkHttpClient.Builder#sslSocketFactory()'s javadocs
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
          TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init((KeyStore) null);
        TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
        if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
          throw new IllegalStateException("Unexpected default trust managers:"
            + Arrays.toString(trustManagers));
        }
        X509TrustManager trustManager = (X509TrustManager) trustManagers[0];

        SSLContext sslContext = SSLContext.getInstance(TlsVersion.TLS_1_2.javaName());
        sslContext.init(null, new TrustManager[] {trustManager}, null);
        SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
        okHttpClientBuilder.sslSocketFactory(sslSocketFactory, trustManager);
      } catch (Exception ex) {
        throw new RuntimeException(ex);
      } finally {
        if (testSslSocket != null) {
          try {
            testSslSocket.close();
          } catch (IOException ex) {
            // oh well
          }
        }
      }
    }

    /**
     * Generally, you should not need this!
     * 

* Direct access to the underlying OkHTTP client builder for advanced settings * like SSL, proxy, logging, and timeout options if needed. * * @return the underlying builder. */ public OkHttpClient.Builder okHttpClientBuilder() { return okHttpClientBuilder; } /** * Convenience method to set the HTTP client's logging level. * * @param level Desired logging level. * @return this. To satisfy the builder pattern. */ public Builder logLevel(HttpLoggingInterceptor.Level level) { this.httpLogLevel = level; return this; } /** * Set a custom base API url, if {@link #productionBaseUrl()} or {@link #sandboxBaseUrl()} aren't sufficient. * * @param baseUrl The base URL you wish to use. * @return this. To satisfy the builder pattern. */ public Builder baseUrl(String baseUrl) { this.baseUrl = baseUrl; return this; } /** * Configure the client to use the default {@link #DEFAULT_PRODUCTION_BASE_URL production URL}. * * @return this. To satisfy the builder pattern. */ public Builder productionBaseUrl() { return baseUrl(DEFAULT_PRODUCTION_BASE_URL); } /** * Configure the client to use the default {@link #DEFAULT_SANDBOX_BASE_URL sandbox URL}. * * @return this. To satisfy the builder pattern. */ public Builder sandboxBaseUrl() { return baseUrl(DEFAULT_SANDBOX_BASE_URL); } /** * Configure the client to use the default {@link #DEFAULT_DEVELOPMENT_BASE_URL sandbox URL}. * * @return this. To satisfy the builder pattern. */ public Builder developmentBaseUrl() { return baseUrl(DEFAULT_DEVELOPMENT_BASE_URL); } /** * Set the public key credential. * * Only required if this client will be used to call API endpoints * that need a public key. * * Can be found in your Account Dashboard. See documentation for details. * * @param publicKey Your Plaid API Public key. * @return This {@link Builder} to satisfy the builder pattern. */ public Builder publicKey(String publicKey) { Util.notNull(publicKey, "publicKey"); this.publicKey = publicKey; return this; } /** * Set the client ID and secret credentials. * * Most API calls require these, you'll almost always want to set them. * * Can be found in your Account Dashboard. * * @param clientId Your Plaid API Client ID * @param secret Your Plaid API Secret * @return This {@link Builder} to satisfy the builder pattern. */ public Builder clientIdAndSecret(String clientId, String secret) { Util.notNull(clientId, "clientId"); Util.notNull(secret, "secret"); this.clientId = clientId; this.secret = secret; return this; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy