synapticloop.b2.request.BaseB2Request.java Source code

Java tutorial

Introduction

Here is the source code for synapticloop.b2.request.BaseB2Request.java

Source

package synapticloop.b2.request;

import static org.apache.http.entity.ContentType.*;

/*
 * Copyright (c) 2016 Synapticloop.
 *
 * All rights reserved.
 *
 * This code may contain contributions from other parties which, where
 * applicable, will be listed in the default build file for the project
 * ~and/or~ in a file named CONTRIBUTORS.txt in the root of the project.
 *
 * This source code and any derived binaries are covered by the terms and
 * conditions of the Licence agreement ("the Licence").  You may not use this
 * source code or any derived binaries except in compliance with the Licence.
 * A copy of the Licence is available in the file named LICENSE.txt shipped with
 * this source code or binaries.
 */

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.apache.http.HttpEntity;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpResponseException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import synapticloop.b2.exception.B2ApiException;
import synapticloop.b2.response.B2AuthorizeAccountResponse;

import static org.apache.http.entity.ContentType.APPLICATION_JSON;

public abstract class BaseB2Request {
    private static final Logger LOGGER = LoggerFactory.getLogger(BaseB2Request.class);

    protected static final String BASE_API_HOST = "https://api.backblazeb2.com";
    protected static final String BASE_API_VERSION = "/b2api/v1/";
    protected static final String BASE_API = BASE_API_HOST + BASE_API_VERSION;

    public static final String VALUE_APPLICATION_X_WWW_FORM_URLENCODED = "application/x-www-form-urlencoded";
    public static final String VALUE_UTF_8 = "UTF-8";

    private final Map<String, String> requestHeaders = new HashMap<>();

    /**
     * Query parameters for request URI
     */
    private final Map<String, String> requestParameters = new HashMap<>();

    /**
     * POST data key value pairs
     */
    private final Map<String, Object> requestBodyData = new HashMap<>();

    private final CloseableHttpClient client;

    private final String url;

    /**
     * Instantiate the base B2 with no authorization header, this is used as the
     * request that will authorize the account.
     *
     * @param client Shared HTTP client
     * @param url Fully qualified request URI
     */
    protected BaseB2Request(CloseableHttpClient client, final String url) {
        this.client = client;
        this.url = url;
    }

    /**
     * Instantiate the base B2 request which adds headers with the authorization
     * token.
     *
     * @param client Shared HTTP client
     * @param b2AuthorizeAccountResponse the authorize account response
     * @param url Fully qualified request URI
     */
    protected BaseB2Request(CloseableHttpClient client, B2AuthorizeAccountResponse b2AuthorizeAccountResponse,
            String url) {
        this(client, b2AuthorizeAccountResponse, url, Collections.<String, String>emptyMap());
    }

    /**
     * Instantiate the base B2 request which adds headers with the authorization
     * token.
     *
     * @param client Shared HTTP client
     * @param b2AuthorizeAccountResponse the authorize account response
     * @param url Fully qualified request URI
     * @param headers the map of the headers that are required
     */

    protected BaseB2Request(CloseableHttpClient client, B2AuthorizeAccountResponse b2AuthorizeAccountResponse,
            String url, Map<String, String> headers) {
        this(client, url);

        this.addHeader(HttpHeaders.CONTENT_TYPE, VALUE_APPLICATION_X_WWW_FORM_URLENCODED);
        this.addHeader(HttpHeaders.AUTHORIZATION, b2AuthorizeAccountResponse.getAuthorizationToken());
    }

    /**
     * Add header to request replacing previous if any
     *
     * @param key the key to add
     * @param value the value to add
     */
    protected void addHeader(String key, String value) {
        requestHeaders.put(key, value);
    }

    /**
     * Add query parameter to request replacing previous if any
     *
     * @param key the key to add
     * @param value the value to add
     */
    protected void addParameter(String key, String value) {
        requestParameters.put(key, value);
    }

    /**
     * Add property to JSON request body
     *
     * @param key the key to add
     * @param value the value to add
     */
    protected void addProperty(String key, Object value) {
        requestBodyData.put(key, value);
    }

    /**
     * Execute an HTTP HEAD request and return the response for further parsing
     *
     * @return the response object
     *
     * @throws B2ApiException if something went wrong with the call
     * @throws IOException if there was an error communicating with the API service
     */
    protected CloseableHttpResponse executeHead() throws B2ApiException, IOException {
        URI uri = this.buildUri();

        LOGGER.debug("HEAD request to URL '{}'", uri.toString());

        HttpHead httpHead = new HttpHead(uri);

        CloseableHttpResponse httpResponse = this.execute(httpHead);

        switch (httpResponse.getStatusLine().getStatusCode()) {
        case HttpStatus.SC_OK:
            return httpResponse;
        }

        final B2ApiException failure = new B2ApiException(EntityUtils.toString(httpResponse.getEntity()),
                new HttpResponseException(httpResponse.getStatusLine().getStatusCode(),
                        httpResponse.getStatusLine().getReasonPhrase()));
        if (httpResponse.containsHeader(HttpHeaders.RETRY_AFTER)) {
            throw failure
                    .withRetry(Integer.valueOf(httpResponse.getFirstHeader(HttpHeaders.RETRY_AFTER).getValue()));
        }
        throw failure;
    }

    /**
     * Execute a GET request, returning the data stream from the response.
     *
     * @return The response from the GET request
     *
     * @throws B2ApiException if there was an error with the request
     * @throws IOException if there was an error communicating with the API service
     */
    protected CloseableHttpResponse executeGet() throws B2ApiException, IOException {
        URI uri = this.buildUri();

        HttpGet httpGet = new HttpGet(uri);

        CloseableHttpResponse httpResponse = this.execute(httpGet);

        // you will either get an OK or a partial content
        switch (httpResponse.getStatusLine().getStatusCode()) {
        case HttpStatus.SC_OK:
        case HttpStatus.SC_PARTIAL_CONTENT:
            return httpResponse;
        }

        final B2ApiException failure = new B2ApiException(EntityUtils.toString(httpResponse.getEntity()),
                new HttpResponseException(httpResponse.getStatusLine().getStatusCode(),
                        httpResponse.getStatusLine().getReasonPhrase()));
        if (httpResponse.containsHeader(HttpHeaders.RETRY_AFTER)) {
            throw failure
                    .withRetry(Integer.valueOf(httpResponse.getFirstHeader(HttpHeaders.RETRY_AFTER).getValue()));
        }
        throw failure;
    }

    /**
     * Execute a POST request returning the response data as a String
     *
     * @return the response data as a string
     *
     * @throws B2ApiException if there was an error with the call, most notably
     *     a non OK status code (i.e. not 200)
     * @throws IOException if there was an error communicating with the API service
     */
    protected CloseableHttpResponse executePost() throws B2ApiException, IOException {
        URI uri = this.buildUri();

        String postData = convertPostData();
        HttpPost httpPost = new HttpPost(uri);

        httpPost.setEntity(new StringEntity(postData, APPLICATION_JSON));
        CloseableHttpResponse httpResponse = this.execute(httpPost);

        switch (httpResponse.getStatusLine().getStatusCode()) {
        case HttpStatus.SC_OK:
            return httpResponse;
        }

        final B2ApiException failure = new B2ApiException(EntityUtils.toString(httpResponse.getEntity()),
                new HttpResponseException(httpResponse.getStatusLine().getStatusCode(),
                        httpResponse.getStatusLine().getReasonPhrase()));
        if (httpResponse.containsHeader(HttpHeaders.RETRY_AFTER)) {
            throw failure
                    .withRetry(Integer.valueOf(httpResponse.getFirstHeader(HttpHeaders.RETRY_AFTER).getValue()));
        }
        throw failure;
    }

    /**
     * Execute a POST request with the contents of a file.
     *
     * @param entity Content to write
     *
     * @return the string representation of the response
     *
     * @throws B2ApiException if there was an error with the call, most notably
     *     a non OK status code (i.e. not 200)
     * @throws IOException if there was an error communicating with the API service
     */
    protected CloseableHttpResponse executePost(HttpEntity entity) throws B2ApiException, IOException {
        URI uri = this.buildUri();

        HttpPost httpPost = new HttpPost(uri);

        httpPost.setEntity(entity);
        CloseableHttpResponse httpResponse = this.execute(httpPost);

        switch (httpResponse.getStatusLine().getStatusCode()) {
        case HttpStatus.SC_OK:
            return httpResponse;
        }

        throw new B2ApiException(EntityUtils.toString(httpResponse.getEntity()), new HttpResponseException(
                httpResponse.getStatusLine().getStatusCode(), httpResponse.getStatusLine().getReasonPhrase()));
    }

    /**
     * Convert the stringData and integerData Maps to JSON format, to be included
     * in the POST body of the request.
     *
     * @return the JSON string of the data
     *
     * @throws IOException if there was an error converting the data.
     */
    protected String convertPostData() throws IOException {
        JSONObject jsonObject = new JSONObject();

        for (final String key : requestBodyData.keySet()) {
            try {
                LOGGER.debug("Setting key '{}' to value '{}'", key, obfuscateData(key, requestBodyData.get(key)));
                jsonObject.put(key, requestBodyData.get(key));
            } catch (JSONException ex) {
                throw new IOException(ex);
            }
        }
        return jsonObject.toString();
    }

    /**
     * Return the URI for this request, which adds any parameters found in the
     * 'parameters' data structure
     *
     * @return The URI for this request, with properly encoded parameters
     *
     * @throws IOException If there was an error building the URI
     */
    protected URI buildUri() throws IOException {
        try {
            URIBuilder uriBuilder = new URIBuilder(url);

            for (final String key : requestParameters.keySet()) {
                uriBuilder.addParameter(key, requestParameters.get(key));
            }

            return uriBuilder.build();
        } catch (URISyntaxException e) {
            throw new IOException(e);
        }
    }

    /**
     * Set the headers safely, go through the headers Map and add them to the http
     * request with properly encode values.  If they already exist on the http
     * request, it will be ignored.
     *
     * To override what headers are set, this should be done in the constructor
     * of the base request object.
     *
     * @param request The HTTP request to set the headers on
     *
     * @throws B2ApiException if there was an error setting the headers
     */
    protected void setHeaders(HttpUriRequest request) throws B2ApiException {
        for (String headerKey : requestHeaders.keySet()) {
            if (!request.containsHeader(headerKey)) {
                final String headerValue = requestHeaders.get(headerKey);
                LOGGER.trace("Setting header '" + headerKey + "' to '" + headerValue + "'.");
                request.setHeader(headerKey, headerValue);
            } else {
                LOGGER.warn("Ignore duplicate header " + headerKey);
            }
        }
    }

    protected CloseableHttpResponse execute(final HttpUriRequest request) throws IOException, B2ApiException {
        this.setHeaders(request);
        LOGGER.debug("{} request to URL '{}'", request.getMethod(), request.getURI());
        final CloseableHttpResponse httpResponse = client.execute(request);
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Received status code of: {}, for {} request to url '{}'",
                    httpResponse.getStatusLine().getStatusCode(), request.getMethod(), request.getURI());
        }
        return httpResponse;
    }

    /**
     * Obfuscate the data by removing the accountId and replacing it with the
     * string "[redacted]"
     *
     * @param data the data to obfuscate
     *
     * @return the obfuscated data
     */
    private Object obfuscateData(String key, Object data) {
        if (LOGGER.isDebugEnabled()) {
            if ("accountId".equals(key)) {
                return ("[redacted]");
            }
        }
        return (data);
    }
}