Java tutorial
/* * 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; } }