org.thevortex.lighting.jinks.client.WinkClient.java Source code

Java tutorial

Introduction

Here is the source code for org.thevortex.lighting.jinks.client.WinkClient.java

Source

/*
 * Copyright (c) 2014 by the author(s).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with 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 org.thevortex.lighting.jinks.client;

import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.NoHttpResponseException;
import org.apache.http.StatusLine;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.*;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The "raw" client for Wink<sup>tm</sup> API. This class only handles the communications with the cloud.
 * <p>
 * Authentication (retrieval of a session token) and re-authentication are handled automatically as the need arises,
 * but can be invoked directly if need be.
 * </p>
 *
 * @author E. A. Graham Jr.
 */
public class WinkClient {
    private final String appName;

    protected String sessionToken;
    protected String renewToken;
    protected HttpHost winkHost = new HttpHost("api.wink.com", -1, "https");
    /**
     * If we can't connect in 10 seconds, give it up.
     */
    protected int connectionTimeout = 10;
    /**
     * If we can't get a socket in 30 seconds, give it up.
     */
    protected int socketTimeout = 30;

    public static final String OAUTH = "/oauth2/token";

    /**
     * Custom Jackson processing for translating to objects.
     */
    public static final ObjectMapper MAPPER = new ObjectMapper()
            // skip unknown stuff
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
            // skip nulls on serialization
            .setSerializationInclusion(JsonInclude.Include.NON_NULL)
            // swap underscores for camelCase
            .setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES)
            // use custom date processing
            .registerModule(DateSerializationHandler.DATE_MODULE);

    /**
     * Initial/simplified serialization/deserialization where it needs less whacking on. Used primarily to translate
     * between strings/bytes and JSON objects.
     */
    protected static final ObjectMapper payloadMapper = new ObjectMapper()
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
            .setSerializationInclusion(JsonInclude.Include.NON_NULL);

    public static final int AUTH_RETRY = 2;

    protected final Logger logger = LoggerFactory.getLogger(WinkClient.class);

    /**
     * The client.
     */
    private CloseableHttpClient httpClient;

    /**
     * Builds a basic auth
     */
    protected final Supplier<ObjectNode> basicAuthSupplier;

    public WinkClient(String clientId, String clientSecret, String userName, String userPassword) {
        this(clientId, clientSecret, userName, userPassword, "jinks");
    }

    public WinkClient(String clientId, String clientSecret, String userName, String userPassword, String appName) {
        basicAuthSupplier = () -> {
            ObjectNode authRequest = payloadMapper.createObjectNode();
            authRequest.put("client_id", clientId);
            authRequest.put("client_secret", clientSecret);
            authRequest.put("username", userName);
            authRequest.put("password", userPassword);
            authRequest.put("grant_type", "password");
            return authRequest;
        };
        this.appName = appName;
    }

    /**
     * Over-rides the default (standard) API with a different one.
     *
     * @param host the other host
     */
    public void setHost(String host) {
        winkHost = new HttpHost(host);
    }

    /**
     * Only takes effect if no request has been made. Default 10.
     *
     * @param connectionTimeout in seconds
     */
    public void setConnectionTimeout(int connectionTimeout) {
        this.connectionTimeout = connectionTimeout;
    }

    /**
     * Only takes effect if no request has been made. Default 30.
     *
     * @param socketTimeout in seconds
     */
    public void setSocketTimeout(int socketTimeout) {
        this.socketTimeout = socketTimeout;
    }

    public void close() {
        try {
            getClient().close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public JsonNode doGet(String getUri) throws IOException {
        return execute(new HttpGet(getUri), null);
    }

    public JsonNode doPut(String putUri, String payload) throws IOException {
        return execute(new HttpPut(putUri), payload);
    }

    public JsonNode doPost(String postUri, String payload) throws IOException {
        return execute(new HttpPost(postUri), payload);
    }

    public JsonNode doDelete(String deleteUri) throws IOException {
        return execute(new HttpDelete(deleteUri), null);
    }

    /**
     * Execute the message, retrying on token renewals.
     *
     * @param message the message
     * @return it's response
     * @throws IOException well...
     */
    protected synchronized JsonNode execute(HttpRequestBase message, String payload) throws IOException {
        // have we authenticated?
        if (sessionToken == null) {
            doAuthenticate();
        }

        logger.debug("Requesting {}", message.getURI());
        if (payload != null) {
            logger.debug("With payload: {}", payload);
            ((HttpEntityEnclosingRequestBase) message).setEntity(new StringEntity(payload));
        }

        message.setHeader("Authorization", "Bearer " + sessionToken);
        message.setHeader("Content-Type", "application/json");
        message.setHeader("User-Agent", appName);

        for (int i = 0; i < AUTH_RETRY; i++) {
            logger.debug("Request {}", (i + 1));
            try (CloseableHttpResponse response = getClient().execute(winkHost, message)) {
                StatusLine status = response.getStatusLine();

                // because we could get error payloads, extract the response if there is any to be had
                HttpEntity entity = response.getEntity();
                String responsePayload = entity == null ? null : EntityUtils.toString(entity);
                int statusCode = status.getStatusCode();
                logger.debug("Received: code = {}\n{}", status, responsePayload);

                switch (statusCode) {
                case 200: // OK
                case 201: // created
                case 202: // accepted (processing not complete)
                {
                    // if no response payload, return none
                    if (entity == null) {
                        logger.debug("No response payload for code {}", statusCode);
                        return null;
                    }
                    return payloadMapper.readValue(responsePayload, ObjectNode.class);
                }
                case 204: // no content
                    return null;
                case 401:
                    break;
                default:
                    logger.error("Response payload on error {}:\n{}", statusCode, responsePayload);
                    handleErrorCodes(status);
                }
            }
            // interesting new wrinkle...
            catch (NoHttpResponseException e) {
                logger.warn("Stale connection - {}: retrying", e.getLocalizedMessage());
                i--;
            }

            // attempt to renew the token
            doRenewAuth();
        }
        throw new AuthenticationException("Unauthorized");
    }

    /**
     * Get the session token.
     */
    public void doAuthenticate() throws IOException {
        handleAuthRequest(basicAuthSupplier);
    }

    /**
     * Renew the token
     *
     * @throws IOException if the request fails
     */
    public void doRenewAuth() throws IOException {
        logger.warn("Renewing authentication token");
        handleAuthRequest(() -> {
            ObjectNode authRequest = basicAuthSupplier.get();
            authRequest.put("refresh_token", renewToken);
            authRequest.put("grant_type", "refresh_token");
            return authRequest;
        });
    }

    /**
     * Runs an authentication request.
     *
     * @param authRequest the request
     * @throws IOException dang
     */
    protected void handleAuthRequest(Supplier<ObjectNode> authRequest) throws IOException {
        String requestBody = payloadMapper.writeValueAsString(authRequest.get());

        // just pop off the POST request, nothing fancy
        HttpPost post = new HttpPost(OAUTH);
        post.setEntity(new StringEntity(requestBody));
        post.setHeader("Content-Type", "application/json");
        post.setHeader("User-Agent", appName);
        logger.info("Starting authentication...");
        try (CloseableHttpResponse response = getClient().execute(winkHost, post)) {
            StatusLine status = response.getStatusLine();
            int statusCode = status.getStatusCode();
            switch (statusCode) {
            case 200:
            case 201: {
                String body = EntityUtils.toString(response.getEntity());
                ObjectNode auth = payloadMapper.readValue(body, ObjectNode.class);
                sessionToken = auth.path("data").get("access_token").asText();
                logger.debug("SessionToken: {}", sessionToken);
                renewToken = auth.path("data").get("refresh_token").asText();
                logger.debug("RenewToken: {}", renewToken);
                logger.info("Authenticated!");
                return;
            }
            case 401:
                throw new AuthenticationException("Unauthorized");
            case 403:
                throw new AuthenticationException("Forbidden");
            default:
                handleErrorCodes(status);
            }
        }
    }

    protected void handleErrorCodes(StatusLine statusLine) throws IOException {
        throw new IOException(
                String.format("HTTP Error (%d): %s", statusLine.getStatusCode(), statusLine.getReasonPhrase()));
    }

    /**
     * Basic setup for a client builder, including the connection manager.
     *
     * @return the builder
     */
    public synchronized CloseableHttpClient getClient() {
        if (httpClient == null) {
            PoolingHttpClientConnectionManager mgr = new PoolingHttpClientConnectionManager(1, TimeUnit.MINUTES);
            mgr.setDefaultMaxPerRoute(5);
            mgr.setMaxTotal(15);

            HttpClientBuilder httpClientBuilder = HttpClientBuilder.create().setConnectionManager(mgr);
            RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(connectionTimeout * 1000)
                    .setSocketTimeout(socketTimeout * 1000).build();
            httpClient = httpClientBuilder.setDefaultRequestConfig(requestConfig).build();
        }
        return httpClient;
    }
}