com.okta.sdk.framework.ApiClient.java Source code

Java tutorial

Introduction

Here is the source code for com.okta.sdk.framework.ApiClient.java

Source

/*!
 * Copyright (c) 2015-2017, Okta, Inc. and/or its affiliates. All rights reserved.
 * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
 *
 * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *
 * See the License for the specific language governing permissions and limitations under the License.
 */

package com.okta.sdk.framework;

import com.fasterxml.jackson.core.type.TypeReference;
import com.okta.sdk.exceptions.ApiException;
import com.okta.sdk.exceptions.RateLimitExceededException;
import com.okta.sdk.exceptions.SdkException;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.conn.routing.HttpRoutePlanner;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.StandardHttpRequestRetryHandler;
import org.apache.http.impl.conn.DefaultProxyRoutePlanner;
import org.apache.http.impl.conn.DefaultRoutePlanner;
import org.apache.http.message.BasicHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

public abstract class ApiClient {

    /**
     * Number of allowed attempts.
     */
    public static final int RETRY_COUNT = 3;

    /**
     * Public static values for the ApiClient.
     */
    public static final String AFTER_CURSOR = "after";
    public static final String LIMIT = "limit";
    public static final String FILTER = "filter";
    public static final String SEARCH = "search";
    public static final String SEARCH_QUERY = "q";

    protected HttpClient httpClient;
    protected String baseUrl;
    protected int apiVersion;
    protected String token;
    protected ApiClientConfiguration configuration;
    private final Logger LOGGER = LoggerFactory.getLogger(ApiClient.class);
    private AtomicReference<HttpResponse> lastHttpResponseRef = new AtomicReference<HttpResponse>();

    /**
     * Constructor for the ApiClient.
     *
     * Bootstraps an HTTPClient to make various requests to the Okta API.
     *
     * @param config {@link ApiClientConfiguration}
     */
    public ApiClient(ApiClientConfiguration config) {
        Proxy proxy = ProxySelector.getDefault().select(URI.create(config.getBaseUrl())).iterator().next();
        HttpRoutePlanner routePlanner;
        if (Proxy.Type.HTTP.equals(proxy.type())) {
            URI proxyUri = URI.create(proxy.type() + "://" + proxy.address());
            HttpHost proxyHost = new HttpHost(proxyUri.getHost(), proxyUri.getPort(), proxyUri.getScheme());
            routePlanner = new DefaultProxyRoutePlanner(proxyHost);
        } else {
            routePlanner = new DefaultRoutePlanner(null);
        }
        StandardHttpRequestRetryHandler requestRetryHandler = new StandardHttpRequestRetryHandler(RETRY_COUNT,
                true);
        HttpClient client = HttpClientBuilder.create().setRetryHandler(requestRetryHandler)
                .setUserAgent("OktaSDKJava_v" + Utils.getSdkVersion()).disableCookieManagement()
                .setRoutePlanner(routePlanner).build();

        this.httpClient = client;
        this.baseUrl = config.getBaseUrl();
        this.apiVersion = config.getApiVersion();
        this.configuration = config;
        this.token = config.getApiToken();

        initMarshaller();
    }

    /**
     * Abstract methods.
     */
    protected abstract void initMarshaller();

    protected abstract <T> T unmarshall(HttpResponse response, TypeReference<T> clazz) throws IOException;

    protected abstract HttpEntity buildRequestEntity(Object body) throws IOException;

    protected abstract String getFullPath(String relativePath);

    protected abstract void setAcceptHeader(HttpUriRequest httpUriRequest) throws IOException;

    protected abstract void setContentTypeHeader(HttpUriRequest httpUriRequest) throws IOException;

    ////////////////////////////////////////////
    // PUBLIC METHODS
    ////////////////////////////////////////////

    /**
     * Returns the API token from the ApiClient.
     * @return {@link String}
     */
    public String getToken() {
        return this.token;
    }

    /**
     * Sets the API token.
     * @param token {@link String}
     */
    public void setToken(String token) {
        this.token = token;
    }

    /**
     * Returns the rate limit context for managing requests with specified limits.
     * Note: There's a limit on the number of api requests in a time frame.
     *
     * @return A way to view the rate limit information
     */
    public RateLimitContext getRateLimitContext() {
        return new RateLimitContext(lastHttpResponseRef.get());
    }

    ////////////////////////////////////////////
    // HTTP METHODS
    ////////////////////////////////////////////

    /**
     * POST HTTP methods for communicating with the API.
     *
     * @param  uri {@link String}                    URI to make request to.
     * @param  httpEntity {@link Object}             HTTP entity object request.
     * @return {@link Map}                           API Response object.
     * @throws IOException                           If an input or output exception occurred.
     */
    protected Map post(String uri, Object httpEntity) throws IOException {
        return post(uri, httpEntity, new TypeReference<Map>() {
        });
    }

    /**
     * Generic HTTP GET method.
     *
     * @param  uri {@link String}                    URI to make request to.
     * @param  httpEntity {@link Object}             HTTP entity object request.
     * @param  clazz {@link TypeReference}           Generic type class reference.
     * @param  <T> {@link T}                         Generic type param.
     * @return {@link T}                             Returns a generic type object.
     * @throws IOException                           If an input or output exception occurred.
     */
    protected <T> T post(String uri, Object httpEntity, TypeReference<T> clazz) throws IOException {
        return post(uri, buildRequestEntity(httpEntity), clazz);
    }

    /**
     * Generic HTTP POST method.
     *
     * @param  uri {@link String}                    URI to make request to.
     * @param  httpEntity {@link Object}             HTTP entity object request.
     * @param  clazz {@link TypeReference}           Generic type class reference.
     * @param  <T> {@link T}                         Generic type param.
     * @return {@link T}                             Returns a generic type object.
     * @throws IOException                           If an input or output exception occurred.
     */
    protected <T> T post(String uri, HttpEntity httpEntity, TypeReference<T> clazz) throws IOException {
        HttpPost httpPost = new HttpPost();
        httpPost.setURI(getAbsoluteURI(uri));
        httpPost.setEntity(httpEntity);
        HttpResponse response = executeRequest(httpPost);
        return unmarshallResponse(clazz, response);
    }

    /**
     * GET HTTP methods for communicating with the API.
     *
     * @param  uri {@link String}                    URI to make request to.
     * @return {@link Map}                           API Response object.
     * @throws IOException                           If an input or output exception occurred.
     */
    protected Map get(String uri) throws IOException {
        return get(uri, new TypeReference<Map>() {
        });
    }

    /**
     * HTTP GET method.
     *
     * @param  uri {@link String}                    URI to make request to.
     * @return {@link HttpResponse}                  API HTTP response.
     * @throws IOException                           If an input or output exception occurred.
     */
    protected HttpResponse getHttpResponse(String uri) throws IOException {
        HttpGet httpGet = new HttpGet();
        httpGet.setURI(getAbsoluteURI(uri));
        return executeRequest(httpGet);
    }

    /**
     * Generic HTTP GET method.
     *
     * @param  uri {@link String}                    URI to make request to.
     * @param  clazz {@link TypeReference}           Generic type class reference.
     * @param  <T> {@link T}                         Generic type param.
     * @return {@link T}                             Returns a generic type object.
     * @throws IOException                           If an input or output exception occurred.
     */
    protected <T> T get(String uri, TypeReference<T> clazz) throws IOException {
        HttpGet httpGet = new HttpGet();
        httpGet.setURI(getAbsoluteURI(uri));
        HttpResponse response = executeRequest(httpGet);
        return unmarshallResponse(clazz, response);
    }

    /**
     * PUT HTTP methods for communicating with the API.
     *
     * @param  uri {@link String}                    URI to make request to.
     * @throws IOException                           If an input or output exception occurred.
        
     */
    protected void put(String uri) throws IOException {
        put(uri, "");
    }

    /**
     * Generic HTTP PUT method.
     *
     * @param  uri {@link String}                    URI to make request to.
     * @param  httpEntity {@link Object}             HTTP entity object request.
     * @return {@link Map}                           API Response object.
     * @throws IOException                           If an input or output exception occurred.
     */
    protected Map put(String uri, Object httpEntity) throws IOException {
        return put(uri, httpEntity, new TypeReference<Map>() {
        });
    }

    /**
     * Generic HTTP PUT method.
     *
     * @param  uri {@link String}                    URI to make request to.
     * @param  clazz {@link TypeReference}           Generic type class reference.
     * @param  <T> {@link T}                         Generic type param.
     * @return {@link T}                             Returns a generic type object.
     * @throws IOException                           If an input or output exception occurred.
     */
    protected <T> T put(String uri, TypeReference<T> clazz) throws IOException {
        return put(uri, "", clazz);
    }

    /**
     * Generic HTTP PUT method.
     *
     * @param  uri {@link String}                    URI to make request to.
     * @param  clazz {@link TypeReference}           Generic type class reference.
     * @param  <T> {@link T}                         Generic type param.
     * @param  httpEntity {@link Object}             HTTP entity object request.
     * @return {@link T}                             Returns a generic type object.
     * @throws IOException                           If an input or output exception occurred.
     */
    protected <T> T put(String uri, Object httpEntity, TypeReference<T> clazz) throws IOException {
        return put(uri, buildRequestEntity(httpEntity), clazz);
    }

    /**
     * Generic HTTP PUT method.
     *
     * @param  uri {@link String}                    URI to make request to.
     * @param  clazz {@link TypeReference}           Generic type class reference.
     * @param  <T> {@link T}                         Generic type param.
     * @param  httpEntity {@link Object}             HTTP entity object request.
     * @return {@link T}                             Returns a generic type object.
     * @throws IOException                           If an input or output exception occurred.
     */
    protected <T> T put(String uri, HttpEntity httpEntity, TypeReference<T> clazz) throws IOException {
        HttpPut httpPut = new HttpPut();
        httpPut.setURI(getAbsoluteURI(uri));
        httpPut.setEntity(httpEntity);
        HttpResponse response = executeRequest(httpPut);
        return unmarshallResponse(clazz, response);
    }

    /**
     * DELETE HTTP methods for communicating with the API.
     *
     * @param  uri {@link String}                    URI to make request to.
     * @throws IOException                           If an input or output exception occurred.
     */
    protected void delete(String uri) throws IOException {
        HttpDelete httpDelete = new HttpDelete();
        httpDelete.setURI(getAbsoluteURI(uri));
        HttpResponse response = executeRequest(httpDelete);
        unmarshallResponse(new TypeReference<Void>() {
        }, response);
    }

    /**
     * Generic HTTP DELETE method.
     *
     * @param  uri {@link String}                    URI to make request to.
     * @param  clazz {@link TypeReference}           Generic type class reference.
     * @param  <T> {@link T}                         Generic type param.
     * @return {@link T}                             Returns a generic type object.
     * @throws IOException                           If an input or output exception occurred.
     */
    protected <T> T delete(String uri, TypeReference<T> clazz) throws IOException {
        HttpDelete httpDelete = new HttpDelete();
        httpDelete.setURI(getAbsoluteURI(uri));
        HttpResponse response = executeRequest(httpDelete);
        return unmarshallResponse(clazz, response);
    }

    /**
     * Executes a base HTTP request.
     *
     * @param  httpUriRequest {@link HttpUriRequest} Current HTTP URI request object.
     * @return {@link HttpResponse}                  API Response object.
     * @throws IOException                           If an input or output exception occurred.
     */
    protected HttpResponse executeRequest(HttpUriRequest httpUriRequest) throws IOException {
        logRequest(httpUriRequest);
        setHeaders(httpUriRequest);
        return doExecute(httpUriRequest);
    }

    /**
     * Logs the request method and URI to the console.
     * @param  httpUriRequest {@link HttpUriRequest} Current HTTP URI request object.
     */
    protected void logRequest(HttpUriRequest httpUriRequest) {
        LOGGER.info(String.format("%s %s", httpUriRequest.getMethod(), httpUriRequest.getURI()));
    }

    /**
     * Sets the headers in the HTTP request.
     *
     * @param  httpUriRequest {@link HttpUriRequest} Current HTTP URI request object.
     * @throws IOException                           If an input or output exception occurred.
     */
    protected void setHeaders(HttpUriRequest httpUriRequest) throws IOException {
        setTokenHeader(httpUriRequest);

        // Content type and accept header are determined by the child class
        setContentTypeHeader(httpUriRequest);
        setAcceptHeader(httpUriRequest);

        // Walk through custom headers and set
        Map<String, String> headers = this.configuration.getHeaders();
        if (headers != null) {
            // Update headers
            for (Map.Entry<String, String> header : headers.entrySet()) {
                Header extraHeader = new BasicHeader(header.getKey(), header.getValue());
                httpUriRequest.setHeader(extraHeader);
            }
        }

    }

    /**
     * Sets the authorization header to Okta's custom SWSS + API_TOKEN format.
     *
     * @param  httpUriRequest {@link HttpUriRequest} Current HTTP URI request object.
     * @throws IOException                           If an input or output exception occurred.
     */
    protected void setTokenHeader(HttpUriRequest httpUriRequest) throws IOException {
        Header authHeader = new BasicHeader("Authorization", String.format("SSWS %s", this.token));
        httpUriRequest.setHeader(authHeader);
    }

    /**
     * Executes/fires the request.
     *
     * @param  httpUriRequest {@link HttpUriRequest} Current HTTP URI request object.
     * @return {@link HttpResponse}                  API response object.
     * @throws IOException                           If an input or output exception occurred.
     */
    protected HttpResponse doExecute(HttpUriRequest httpUriRequest) throws IOException {
        HttpResponse response = httpClient.execute(httpUriRequest);
        lastHttpResponseRef.set(response);
        return response;
    }

    /**
     * Utility method to encode given String.
     *
     * @param  str {@link String}                    String to be encoded.
     * @return {@link String}                        Encoded string.
     * @throws UnsupportedEncodingException          If character encoding is not supported.
     */
    protected static String encode(String str) throws UnsupportedEncodingException {
        return URLEncoder.encode(str, "UTF-8");
    }

    /**
     * Utility method to return an encoded path.
     *
     * @param  formatStr {@link String}              String to be formatted with args.
     * @param  args {@link String}                   Arguments to be encoded into body.
     * @return {@link String}                        Encoded path.
     * @throws IOException                           If an input or output exception occurred.
     */
    protected String getEncodedPath(String formatStr, String... args) throws IOException {
        return getFullPath(getEncodedString(formatStr, args));
    }

    /**
     * Utility method to return encoded String.
     *
     * @param  formatStr {@link String}              String to be formatted with args.
     * @param  args {@link String}                   Arguments to be encoded into body.
     * @return {@link String}                        Encoded string.
     * @throws UnsupportedEncodingException          If character encoding is not supported.
     */
    private String getEncodedString(String formatStr, String[] args) throws UnsupportedEncodingException {
        final int ARGS_LENGTH = args.length;
        String[] encodedArgs = new String[ARGS_LENGTH];
        for (int i = 0; i < ARGS_LENGTH; i++) {
            encodedArgs[i] = encode(args[i]);
        }
        return String.format(formatStr, encodedArgs);
    }

    /**
     * Utility method to encode path with query values.
     *
     * @param  formatStr {@link String}              String to be formatted with args.
     * @param  params {@link Map}                    Body params to be encoded.
     * @param  args {@link String}                   Arguments to be encoded into body.
     * @return {@link String}                        Encoded string.
     * @throws UnsupportedEncodingException          If character encoding is not supported.
     */
    protected String getEncodedPathWithQueryParams(String formatStr, Map<String, String> params, String... args)
            throws UnsupportedEncodingException {
        String uri = getEncodedString(formatStr, args);
        for (String key : params.keySet()) {
            uri = addParameterToUri(uri, key, params.get(key));
        }
        return getFullPath(uri);
    }

    /**
     * Utility method to add parameter to URI.
     *
     * @param  uri {@link String}                    URI to make request to.
     * @param  name {@link String}                   Name of parameter to add.
     * @param  value {@link String}                  Value of parameter to add.
     * @return {@link String}                        Encoded string.
     * @throws UnsupportedEncodingException          If character encoding is not supported.
     */
    private String addParameterToUri(String uri, String name, String value) throws UnsupportedEncodingException {
        int queryStringStart = uri.indexOf('?');
        if (queryStringStart == -1) {
            return uri + "?" + encode(name) + "=" + encode(value);
        } else if (queryStringStart == uri.length() - 1) {
            return uri + encode(name) + "=" + encode(value);
        } else {
            return uri + "&" + encode(name) + "=" + encode(value);
        }
    }

    /**
     * Utility method to return absolute URI from relative URI.
     *
     * @param  relativeURI {@link String}            Current long URI.
     * @return {@link URI}                           Absolute URI.
     * @throws IOException                           If an input or output exception occurred.
     */
    protected URI getAbsoluteURI(String relativeURI) throws IOException {
        try {
            if (relativeURI.startsWith("http:") || relativeURI.startsWith("https:")) {
                return new URI(relativeURI);
            } else {
                return new URL(String.format("%s%s", this.baseUrl, relativeURI)).toURI();
            }
        } catch (URISyntaxException e) {
            throw new IOException(e);
        }
    }

    /**
     * Utility method to unmarshall HTTP response if there is content.
     * Returns "null" if API response body was empty.
     *
     * @param  <T> {@link T}                         Generic type param.
     * @param  clazz {@link TypeReference}           Generic type class reference.
     * @param  httpResponse {@link HttpResponse}     Response object.
     * @return {@link T}                             Returns a generic type object.
     * @throws IOException                           If an input or output exception occurred.
     */
    protected <T> T unmarshallResponse(TypeReference<T> clazz, HttpResponse httpResponse) throws IOException {
        boolean contentReturned = checkResponse(httpResponse);
        if (contentReturned) {
            return unmarshall(httpResponse, clazz);
        } else {
            return null;
        }
    }

    /**
     * Utility method to check the formatting of the HttpResponse received.
     *
     * @param  response {@link HttpResponse}         Response object.
     * @return {@link Boolean}                       If request was successful.
     * @throws IOException                           If an input or output exception occurred.
     */
    protected boolean checkResponse(HttpResponse response) throws IOException {
        if (response == null) {
            throw new SdkException("A response wasn't received");
        }

        StatusLine statusLine = response.getStatusLine();
        if (statusLine == null) {
            throw new SdkException("A statusLine wasn't present in the response");
        }

        int statusCode = statusLine.getStatusCode();

        if ((statusCode == 200 || statusCode == 201) && response.getEntity() != null) {
            // success; content returned
            return true;
        } else if (statusCode == 204 && response.getEntity() == null) {
            // success; no content returned
            return false;
        } else {
            extractError(response);
            LOGGER.info("We found an api exception and didn't throw an error");
            return false; // will never hit this line
        }
    }

    /**
     * Utility method to extract the error from the HTTP response.
     *
     * @param  response {@link HttpResponse}         Response object.
     * @throws IOException                           If an input or output exception occurred.
     */
    protected void extractError(HttpResponse response) throws IOException {
        StatusLine statusLine = response.getStatusLine();
        int statusCode = statusLine.getStatusCode();
        ErrorResponse errors = unmarshall(response, new TypeReference<ErrorResponse>() {
        });
        if (statusCode == 429) {
            throw new RateLimitExceededException(errors);
        } else {
            throw new ApiException(statusCode, errors);
        }
    }
}