/**
* Copyright (c) 2010-2016 by the respective copyright holders.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.openhab.binding.garadget.internal;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import org.apache.commons.httpclient.Credentials;
import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.URIException;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.methods.DeleteMethod;
import org.apache.commons.httpclient.methods.EntityEnclosingMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.PutMethod;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.apache.commons.httpclient.params.HttpMethodParams;
import org.apache.commons.io.IOUtils;
import org.codehaus.jackson.annotate.JsonProperty;
import org.codehaus.jackson.map.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This Class handles the connection via REST API.
*
* @author John Cocula
* @since 1.9.0
*/
public class Connection {
private final Logger logger = LoggerFactory.getLogger(Connection.class);
protected static final ObjectMapper JSON = new ObjectMapper();
private static final String HTTP_GET = "GET";
private static final String HTTP_PUT = "PUT";
private static final String HTTP_POST = "POST";
private static final String HTTP_DELETE = "DELETE";
private static final String APPLICATION_FORM_URLENCODED = "application/x-www-form-urlencoded";
private static final String APPLICATION_JSON = "application/json";
private static final String API_BASE_URL = "https://api.particle.io";
// POST
private static final String TOKEN_URL = API_BASE_URL + "/oauth/token";
// DELETE
private static final String ACCESS_TOKENS_URL = API_BASE_URL + "/v1/access_tokens/%1$s";
// GET
private static final String GET_DEVICES_URL = API_BASE_URL + "/v1/devices?access_token=%1$s";
// GET or POST
private static final String DEVICE_FUNC_URL = API_BASE_URL + "/v1/devices/%1$s/%2$s?format=raw&access_token=%3$s";
private String username;
private String password;
private int timeout;
public static class TokenResponse {
@JsonProperty("access_token")
String accessToken;
@JsonProperty("token_type")
String tokenType;
@JsonProperty("expires_in")
Integer expiresIn;
@JsonProperty("refresh_token")
String refreshToken;
}
private TokenResponse tokens;
public Connection(String username, String password, int timeout) {
this.username = username;
this.password = password;
this.timeout = timeout;
}
/**
* Use the configured <code>username</code> and <code>password</code> credentials to obtain access and refresh
* tokens for later use.
*/
public void login() {
String content = String.format("grant_type=password&username=%1$s&password=%2$s&expires_in=0",
urlEncode(username), urlEncode(password));
sendCommand(null, "createToken", "particle", "particle", content, new HttpResponseHandler() {
@Override
public void handleResponse(int statusCode, String responseBody) {
logger.trace("Processing login: statusCode={}, responseBody={}", statusCode, responseBody);
if (statusCode == HttpStatus.SC_OK) {
try {
tokens = JSON.readValue(responseBody, TokenResponse.class);
} catch (Exception e) {
logger.warn("Unable to parse token response.", e);
}
}
}
});
}
/**
* Return true if this Connection had previously successfully called <code>login()</code>.
*
* @return
* true if we had previously successfully logged in.
*/
public boolean isLoggedIn() {
return tokens != null;
}
/**
* Attempt to delete the access token on the server for this Connection, and forget the tokens locally.
*/
public void logout() {
sendCommand(null, "deleteToken", null, new HttpResponseHandler() {
@Override
public void handleResponse(int statusCode, String responseBody) {
logger.trace("Processing logout response: statusCode={}, responseBody={}", statusCode, responseBody);
tokens = null;
}
});
}
/**
* Return a Map of device IDs to device objects, or an empty Map if no devices could be retrieved.
*
* @return
* a Map of device IDs to device objects, or an empty Map if no devices could be retrieved.
*/
public Map<String, GaradgetDevice> getDevices() {
final Map<String, GaradgetDevice> devices = new HashMap<String, GaradgetDevice>();
sendCommand(null, "getDevices", null, new HttpResponseHandler() {
@Override
public void handleResponse(int statusCode, String responseBody) {
try {
GaradgetDevice[] deviceList = JSON.readValue(responseBody, GaradgetDevice[].class);
for (int i = 0; i < deviceList.length; i++) {
devices.put(deviceList[i].getId(), deviceList[i]);
}
} catch (Exception e) {
logger.warn("Unable to parse getDevices response.", e);
}
}
});
return devices;
}
/**
* Send a command to the Particle REST API (convenience function).
*
* @param device
* the device context, or <code>null</code> if not needed for this command.
* @param funcName
* the function name to call, or variable/field to retrieve if <code>command</code> is
* <code>null</code>.
* @param command
* the command to send to the API.
* @param proc
* a callback object that receives the status code and response body, or <code>null</code> if not
* needed.
*/
public void sendCommand(AbstractDevice device, String funcName, String command, HttpResponseHandler proc) {
sendCommand(device, funcName, username, password, command, proc);
}
/**
* Send a command to the Particle REST API (convenience function).
*
* @param device
* the device context, or <code>null</code> if not needed for this command.
* @param funcName
* the function name to call, or variable/field to retrieve if <code>command</code> is
* <code>null</code>.
* @param user
* the user name to use in Basic Authentication if the funcName would require Basic Authentication.
* @param pass
* the password to use in Basic Authentication if the funcName would require Basic Authentication.
* @param command
* the command to send to the API.
* @param proc
* a callback object that receives the status code and response body, or <code>null</code> if not
* needed.
*/
public void sendCommand(AbstractDevice device, String funcName, String user, String pass, String command,
HttpResponseHandler proc) {
String url = null;
String httpMethod = null;
String content = null;
String contentType = null;
Properties headers = new Properties();
logger.trace("sendCommand: funcName={}", funcName);
switch (funcName) {
case "createToken":
httpMethod = HTTP_POST;
url = TOKEN_URL;
content = command;
contentType = APPLICATION_FORM_URLENCODED;
break;
case "deleteToken":
httpMethod = HTTP_DELETE;
url = String.format(ACCESS_TOKENS_URL, tokens.accessToken);
break;
case "getDevices":
httpMethod = HTTP_GET;
url = String.format(GET_DEVICES_URL, tokens.accessToken);
break;
default:
url = String.format(DEVICE_FUNC_URL, device.getId(), funcName, tokens.accessToken);
if (command == null) {
// retrieve a variable
httpMethod = HTTP_GET;
} else {
// call a function
httpMethod = HTTP_POST;
content = command;
contentType = APPLICATION_JSON;
}
break;
}
HttpClient client = new HttpClient();
// Only perform basic authentication when we aren't using OAuth
if (!url.contains("access_token=")) {
Credentials credentials = new UsernamePasswordCredentials(user, pass);
client.getParams().setAuthenticationPreemptive(true);
client.getState().setCredentials(AuthScope.ANY, credentials);
}
HttpMethod method = createHttpMethod(httpMethod, url);
method.getParams().setSoTimeout(timeout);
method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler(3, false));
for (String httpHeaderKey : headers.stringPropertyNames()) {
method.addRequestHeader(new Header(httpHeaderKey, headers.getProperty(httpHeaderKey)));
logger.trace("Header key={}, value={}", httpHeaderKey, headers.getProperty(httpHeaderKey));
}
try {
// add content if a valid method is given ...
if (method instanceof EntityEnclosingMethod && content != null) {
EntityEnclosingMethod eeMethod = (EntityEnclosingMethod) method;
eeMethod.setRequestEntity(new StringRequestEntity(content, contentType, null));
logger.trace("content='{}', contentType='{}'", content, contentType);
}
if (logger.isDebugEnabled()) {
try {
logger.debug("About to execute '{}'", method.getURI());
} catch (URIException e) {
logger.debug(e.getMessage());
}
}
int statusCode = client.executeMethod(method);
if (statusCode >= HttpStatus.SC_BAD_REQUEST) {
logger.debug("Method failed: " + method.getStatusLine());
}
String responseBody = IOUtils.toString(method.getResponseBodyAsStream());
if (!responseBody.isEmpty()) {
logger.debug("Body of response: {}", responseBody);
}
if (proc != null) {
proc.handleResponse(statusCode, responseBody);
}
} catch (HttpException he) {
logger.warn("{}", he);
} catch (IOException ioe) {
logger.debug("{}", ioe);
} finally {
method.releaseConnection();
}
}
/**
* Factory method to create a {@link HttpMethod}-object according to the
* given String <code>httpMethod</code>
*
* @param httpMethodString
* the name of the {@link HttpMethod} to create
* @param url
* the URL to include in the returned Method
* @return
* an object of type {@link GetMethod}, {@link PutMethod},
* {@link PostMethod} or {@link DeleteMethod}
* @throws
* IllegalArgumentException if <code>httpMethod</code> is none of
* <code>GET</code>, <code>PUT</code>, <code>POST</POST> or <code>DELETE</code>
*/
private static HttpMethod createHttpMethod(String httpMethodString, String url) {
if (HTTP_GET.equals(httpMethodString)) {
return new GetMethod(url);
} else if (HTTP_PUT.equals(httpMethodString)) {
return new PutMethod(url);
} else if (HTTP_POST.equals(httpMethodString)) {
return new PostMethod(url);
} else if (HTTP_DELETE.equals(httpMethodString)) {
return new DeleteMethod(url);
} else {
throw new IllegalArgumentException("given httpMethod '" + httpMethodString + "' is unknown");
}
}
/**
* URL Encode a string using UTF-8 encoding
*
* @param str
* the string to encode
* @return
* the encoded string
*/
private String urlEncode(String str) {
try {
return URLEncoder.encode(str, "UTF-8");
} catch (UnsupportedEncodingException e) {
logger.warn("Could not encode string '{}'", str, e);
return str;
}
}
}