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

edu.byu.hbll.box.client.HttpBoxClient Maven / Gradle / Ivy

The newest version!
package edu.byu.hbll.box.client;

import com.fasterxml.jackson.databind.JsonNode;
import edu.byu.hbll.json.ObjectMapperFactory;
import edu.byu.hbll.json.UncheckedObjectMapper;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.time.Instant;
import java.util.Base64;
import lombok.Builder;
import lombok.Getter;
import lombok.NonNull;

/**
 * A client for communicating with Box through it's web api.
 *
 * @author Charles Draper
 */
public class HttpBoxClient extends AbstractHttpBoxClient {

  private static final UncheckedObjectMapper mapper = ObjectMapperFactory.newUnchecked();
  private static final int LOWEST_BAD_STATUS_CODE = 400;

  /** The base URI of the upstream Box source. Should include path to the desired source. */
  @NonNull private final URI uri;

  /** The underlying http client to use. */
  @Getter private final HttpClient client;

  /** Username for basic authentication. */
  @Getter private final String username;

  /** Password for basic authentication. */
  @Getter private final String password;

  /** Access token URI for OAuth2. */
  @Getter private final URI accessTokenUri;

  /** Client ID for OAuth2. */
  @Getter private final String clientId;

  /** Client secret for OAuth2. */
  @Getter private final String clientSecret;

  private final HttpClient oauth2Client =
      HttpClient.newBuilder().followRedirects(HttpClient.Redirect.ALWAYS).build();
  private String accessToken;
  private Instant accessTokenExpiration = Instant.MIN;

  @Builder(toBuilder = true)
  private HttpBoxClient(
      URI uri,
      HttpClient client,
      String username,
      String password,
      URI accessTokenUri,
      String clientId,
      String clientSecret) {
    super(uri);
    this.uri = uri;
    this.client =
        client != null
            ? client
            : HttpClient.newBuilder().followRedirects(HttpClient.Redirect.ALWAYS).build();
    this.username = username;
    this.password = password;
    this.accessTokenUri = accessTokenUri;
    this.clientId = clientId;
    this.clientSecret = clientSecret;
  }

  /**
   * Creates a new {@link HttpBoxClient} with the given base uri. A new {@link HttpClient} is used
   * with default settings plus it always follows redirects.
   *
   * @param uri the base uri of the box source (eg, http://localhost:8080/app/box)
   */
  public HttpBoxClient(URI uri) {
    this(uri, HttpClient.newBuilder().followRedirects(HttpClient.Redirect.ALWAYS).build());
  }

  /**
   * Creates a new {@link HttpBoxClient} with the given base uri and client.
   *
   * @param uri the base uri of the box source (eg, http://localhost:8080/app/box)
   * @param client http client to use
   */
  public HttpBoxClient(URI uri, HttpClient client) {
    this(uri, client, null, null, null, null, null);
  }

  @Override
  protected InputStream send(URI uri) {
    HttpResponse response;

    try {
      response =
          client.send(
              addAuthHeaders(HttpRequest.newBuilder(uri)).build(), BodyHandlers.ofInputStream());
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    } catch (IOException e) {
      throw new UncheckedIOException(e);
    }

    if (response.statusCode() >= LOWEST_BAD_STATUS_CODE) {
      throw new UncheckedIOException(
          new IOException(
              "Error retrieving documents at " + uri + ": HTTP " + response.statusCode()));
    }

    InputStream input = response.body();
    return input;
  }

  /**
   * Adds authentication headers to the request for either basic authentication or oauth2.
   *
   * @param builder the http request builder to add the headers to
   * @return the same http request builder
   */
  private HttpRequest.Builder addAuthHeaders(HttpRequest.Builder builder) {

    // if basic auth
    if (username != null) {
      builder.header(
          "Authorization",
          "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes()));
    } else if (clientId != null) {
      // if oauth2
      synchronized (this) {
        // if token expired
        if (Instant.now().isAfter(accessTokenExpiration)) {
          // get new token
          String tokenRequest =
              "client_id="
                  + clientId
                  + "&client_secret="
                  + clientSecret
                  + "&grant_type=client_credentials";

          HttpRequest oauth2Request =
              HttpRequest.newBuilder()
                  .header("Content-Type", "application/x-www-form-urlencoded")
                  .uri(accessTokenUri)
                  .POST(HttpRequest.BodyPublishers.ofString(tokenRequest))
                  .build();

          Instant requestTimestamp = Instant.now();
          HttpResponse oauth2Response;

          try {
            oauth2Response = oauth2Client.send(oauth2Request, HttpResponse.BodyHandlers.ofString());
          } catch (IOException e) {
            throw new UncheckedIOException(e);
          } catch (InterruptedException e) {
            throw new RuntimeException("Interruption occurred waiting for access token response");
          }

          JsonNode token = mapper.readTree(oauth2Response.body());

          accessToken = token.path("access_token").asText();
          accessTokenExpiration =
              requestTimestamp.plusSeconds(token.path("expires_in").asLong()).minusSeconds(1);
        }
      }

      builder.header("Authorization", "Bearer " + accessToken);
    }

    return builder;
  }

  /** Builder for {@link HttpBoxClient}. */
  public static class HttpBoxClientBuilder {

    /**
     * Sets the base URI of the upstream Box source. Should include path to the desired source.
     *
     * @param uri the base URI of the upstream Box source
     * @return this
     */
    public HttpBoxClientBuilder uri(URI uri) {
      this.uri = uri;
      return this;
    }

    /**
     * Sets the base URI of the upstream Box source. Should include path to the desired source.
     *
     * @param uri the base URI of the upstream Box source
     * @return this
     */
    public HttpBoxClientBuilder uri(String uri) {
      return uri(URI.create(uri));
    }

    /**
     * Sets the access token URI for OAuth2.
     *
     * @param accessTokenUri access token URI for OAuth2
     * @return this
     */
    public HttpBoxClientBuilder accessTokenUri(URI accessTokenUri) {
      this.accessTokenUri = accessTokenUri;
      return this;
    }

    /**
     * Sets the access token URI for OAuth2.
     *
     * @param accessTokenUri access token URI for OAuth2
     * @return this
     */
    public HttpBoxClientBuilder accessTokenUri(String accessTokenUri) {
      if (accessTokenUri == null) {
        return accessTokenUri((URI) null);
      } else {
        return accessTokenUri(URI.create(accessTokenUri));
      }
    }

    /**
     * Initialize to use basic authentication.
     *
     * @param username the username
     * @param password the password
     * @return this
     */
    public HttpBoxClientBuilder basicAuth(String username, String password) {
      username(username);
      password(password);
      return this;
    }

    /**
     * Initialize to use OAuth2.
     *
     * @param accessTokenUri the access token URI
     * @param clientId the client ID
     * @param clientSecret the client secret
     * @return this
     */
    public HttpBoxClientBuilder oauth2(
        String accessTokenUri, String clientId, String clientSecret) {
      accessTokenUri(accessTokenUri);
      clientId(clientId);
      clientSecret(clientSecret);
      return this;
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy