package com.belladati.sdk.impl;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.security.GeneralSecurityException;
import java.util.List;
import javax.net.ssl.SSLContext;
import oauth.signpost.OAuth;
import oauth.signpost.OAuthConsumer;
import oauth.signpost.exception.OAuthException;
import oauth.signpost.http.HttpParameters;
import com.belladati.httpclientandroidlib.HttpEntity;
import com.belladati.httpclientandroidlib.NameValuePair;
import com.belladati.httpclientandroidlib.client.config.RequestConfig;
import com.belladati.httpclientandroidlib.client.entity.UrlEncodedFormEntity;
import com.belladati.httpclientandroidlib.client.methods.CloseableHttpResponse;
import com.belladati.httpclientandroidlib.client.methods.HttpGet;
import com.belladati.httpclientandroidlib.client.methods.HttpPost;
import com.belladati.httpclientandroidlib.client.methods.HttpRequestBase;
import com.belladati.httpclientandroidlib.conn.ssl.SSLContexts;
import com.belladati.httpclientandroidlib.conn.ssl.TrustSelfSignedStrategy;
import com.belladati.httpclientandroidlib.entity.StringEntity;
import com.belladati.httpclientandroidlib.impl.client.CloseableHttpClient;
import com.belladati.httpclientandroidlib.impl.client.cache.CacheConfig;
import com.belladati.httpclientandroidlib.impl.client.cache.CachingHttpClientBuilder;
import com.belladati.httpclientandroidlib.impl.conn.PoolingHttpClientConnectionManager;
import com.belladati.sdk.exception.BellaDatiRuntimeException;
import com.belladati.sdk.exception.ConnectionException;
import com.belladati.sdk.exception.InternalConfigurationException;
import com.belladati.sdk.exception.auth.AuthorizationException;
import com.belladati.sdk.exception.auth.AuthorizationException.Reason;
import com.belladati.sdk.exception.auth.InvalidTimestampException;
import com.belladati.sdk.exception.server.InternalErrorException;
import com.belladati.sdk.exception.server.InvalidJsonException;
import com.belladati.sdk.exception.server.NotFoundException;
import com.belladati.sdk.exception.server.UnexpectedResponseException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
class BellaDatiClient implements Serializable {
/** The serialVersionUID */
private static final long serialVersionUID = 9138881190417975299L;
private final String baseUrl;
private final boolean trustSelfSigned;
private final transient CloseableHttpClient client;
BellaDatiClient(String baseUrl, boolean trustSelfSigned) {
this.baseUrl = baseUrl.endsWith("/") ? baseUrl : (baseUrl + "/");
this.trustSelfSigned = trustSelfSigned;
this.client = buildClient(trustSelfSigned);
}
/**
* Builds the HTTP client to connect to the server.
*
* @param trustSelfSigned <tt>true</tt> if the client should accept
* self-signed certificates
* @return a new client instance
*/
private CloseableHttpClient buildClient(boolean trustSelfSigned) {
try {
// if required, define custom SSL context allowing self-signed certs
SSLContext sslContext = !trustSelfSigned ? SSLContexts.createSystemDefault() : SSLContexts.custom()
.loadTrustMaterial(null, new TrustSelfSignedStrategy()).build();
// set timeouts for the HTTP client
int globalTimeout = readFromProperty("bdTimeout", 10000);
int connectTimeout = readFromProperty("bdConnectTimeout", globalTimeout);
int connectionRequestTimeout = readFromProperty("bdConnectionRequestTimeout", globalTimeout);
int socketTimeout = readFromProperty("bdSocketTimeout", globalTimeout);
RequestConfig requestConfig = RequestConfig.copy(RequestConfig.DEFAULT).setConnectTimeout(connectTimeout)
.setSocketTimeout(socketTimeout).setConnectionRequestTimeout(connectionRequestTimeout).build();
// configure caching
CacheConfig cacheConfig = CacheConfig.copy(CacheConfig.DEFAULT).setSharedCache(false).setMaxCacheEntries(1000)
.setMaxObjectSize(2 * 1024 * 1024).build();
// configure connection pooling
PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager();
int connectionLimit = readFromProperty("bdMaxConnections", 40);
// there's only one server to connect to, so max per route matters
connManager.setMaxTotal(connectionLimit);
connManager.setDefaultMaxPerRoute(connectionLimit);
// create the HTTP client
return CachingHttpClientBuilder.create().setCacheConfig(cacheConfig).setSslcontext(sslContext)
.setDefaultRequestConfig(requestConfig).setConnectionManager(connManager).build();
} catch (GeneralSecurityException e) {
throw new InternalConfigurationException("Failed to set up SSL context", e);
}
}
private int readFromProperty(String property, int defaultValue) {
try {
return Integer.parseInt(System.getProperty(property));
} catch (NumberFormatException e) {
return defaultValue;
}
}
public byte[] post(String relativeUrl, TokenHolder tokenHolder) {
return post(relativeUrl, tokenHolder, null, null);
}
public byte[] post(String relativeUrl, TokenHolder tokenHolder, HttpParameters oauthParams) {
return post(relativeUrl, tokenHolder, oauthParams, null);
}
public byte[] post(String relativeUrl, TokenHolder tokenHolder, List<? extends NameValuePair> parameters) {
return post(relativeUrl, tokenHolder, null, parameters);
}
public byte[] post(String relativeUrl, TokenHolder tokenHolder, HttpParameters oauthParams,
List<? extends NameValuePair> parameters) {
HttpPost post = new HttpPost(baseUrl + relativeUrl);
if (parameters != null) {
try {
post.setEntity(new UrlEncodedFormEntity(parameters, "UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException("Invalid URL encoding", e);
}
}
return doRequest(post, tokenHolder, oauthParams);
}
public byte[] postUpload(String relativeUrl, TokenHolder tokenHolder, String content) {
HttpPost post = new HttpPost(baseUrl + relativeUrl);
try {
StringEntity entity = new StringEntity(content, "UTF-8");
entity.setContentType("application/octet-stream");
post.setEntity(entity);
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException("Invalid URL encoding", e);
}
return doRequest(post, tokenHolder);
}
public TokenHolder postToken(String relativeUrl, TokenHolder tokenHolder) {
return postToken(relativeUrl, tokenHolder, null, null);
}
public TokenHolder postToken(String relativeUrl, TokenHolder tokenHolder, HttpParameters oauthParams) {
return postToken(relativeUrl, tokenHolder, oauthParams, null);
}
public TokenHolder postToken(String relativeUrl, TokenHolder tokenHolder, List<? extends NameValuePair> parameters) {
return postToken(relativeUrl, tokenHolder, null, parameters);
}
public TokenHolder postToken(String relativeUrl, TokenHolder tokenHolder, HttpParameters oauthParams,
List<? extends NameValuePair> parameters) {
byte[] response = post(relativeUrl, tokenHolder, oauthParams, parameters);
try {
HttpParameters responseParams = OAuth.decodeForm(new ByteArrayInputStream(response));
String token = responseParams.getFirst(OAuth.OAUTH_TOKEN);
String tokenSecret = responseParams.getFirst(OAuth.OAUTH_TOKEN_SECRET);
tokenHolder.setToken(token, tokenSecret);
return tokenHolder;
} catch (IOException e) {
throw new IllegalArgumentException("Failed to load OAuth token from response", e);
}
}
public byte[] get(String relativeUrl, TokenHolder tokenHolder) {
return doRequest(new HttpGet(baseUrl + relativeUrl), tokenHolder);
}
public JsonNode getJson(String relativeUrl, TokenHolder tokenHolder) {
byte[] response = get(relativeUrl, tokenHolder);
try {
return new ObjectMapper().readTree(response);
} catch (IOException e) {
throw new InvalidJsonException("Could not parse JSON response, was " + new String(response), e);
}
}
public String getBaseUrl() {
return baseUrl;
}
private byte[] doRequest(HttpRequestBase request, TokenHolder tokenHolder) {
return doRequest(request, tokenHolder, null);
}
private byte[] doRequest(HttpRequestBase request, TokenHolder tokenHolder, HttpParameters oauthParams) {
CloseableHttpResponse response = null;
try {
OAuthConsumer consumer = tokenHolder.createConsumer();
consumer.setAdditionalParameters(oauthParams);
consumer.sign(request);
response = client.execute(request);
int statusCode = response.getStatusLine().getStatusCode();
HttpEntity entity = response.getEntity();
byte[] content = entity != null ? readBytes(entity.getContent()) : new byte[0];
switch (statusCode) {
case 200:
case 204:
// all is well, return
return content;
// there was some sort of error - throw the right exception
case 400:
case 401:
case 403:
throw buildException(statusCode, content, tokenHolder.hasToken());
case 404:
throw new NotFoundException(request.getRequestLine().getUri());
case 500:
throw new InternalErrorException();
default:
throw new UnexpectedResponseException(statusCode, new String(content));
}
} catch (OAuthException e) {
throw new InternalConfigurationException("Failed to create OAuth signature", e);
} catch (IOException e) {
throw new ConnectionException("Failed to connect to BellaDati", e);
} finally {
try {
if (response != null) {
response.close();
}
} catch (IOException e) {
throw new ConnectionException("Failed to connect to BellaDati", e);
}
request.releaseConnection();
}
}
/**
* Builds an exception based on the given content, assuming that it has been
* returned as an error from the server.
*
* @param code response code returned by the server
* @param content content returned by the server
* @param hasToken <tt>true</tt> if the request was made using a request or
* access token
* @return an exception to throw for the given content
*/
private BellaDatiRuntimeException buildException(int code, byte[] content, boolean hasToken) {
try {
HttpParameters oauthParams = OAuth.decodeForm(new ByteArrayInputStream(content));
if (oauthParams.containsKey("oauth_problem")) {
String problem = oauthParams.getFirst("oauth_problem");
if ("missing_consumer".equals(problem) || "invalid_consumer".equals(problem)) {
return new AuthorizationException(Reason.CONSUMER_KEY_UNKNOWN);
} else if ("invalid_signature".equals(problem) || "signature_invalid".equals(problem)) {
return new AuthorizationException(hasToken ? Reason.TOKEN_INVALID : Reason.CONSUMER_SECRET_INVALID);
} else if ("domain_expired".equals(problem)) {
return new AuthorizationException(Reason.DOMAIN_EXPIRED);
} else if ("missing_token".equals(problem) || "invalid_token".equals(problem)) {
return new AuthorizationException(Reason.TOKEN_INVALID);
} else if ("unauthorized_token".equals(problem)) {
return new AuthorizationException(Reason.TOKEN_UNAUTHORIZED);
} else if ("token_expired".equals(problem)) {
return new AuthorizationException(Reason.TOKEN_EXPIRED);
} else if ("x_auth_disabled".equals(problem)) {
return new AuthorizationException(Reason.X_AUTH_DISABLED);
} else if ("piccolo_not_enabled".equals(problem)) {
return new AuthorizationException(Reason.BD_MOBILE_DISABLED);
} else if ("missing_username".equals(problem) || "missing_password".equals(problem)
|| "invalid_credentials".equals(problem) || "permission_denied".equals(problem)) {
return new AuthorizationException(Reason.USER_CREDENTIALS_INVALID);
} else if ("account_locked".equals(problem) || "user_not_active".equals(problem)) {
return new AuthorizationException(Reason.USER_ACCOUNT_LOCKED);
} else if ("domain_restricted".equals(problem)) {
return new AuthorizationException(Reason.USER_DOMAIN_MISMATCH);
} else if ("timestamp_refused".equals(problem)) {
String acceptable = oauthParams.getFirst("oauth_acceptable_timestamps");
if (acceptable != null && acceptable.contains("-")) {
return new InvalidTimestampException(Long.parseLong(acceptable.split("-")[0]), Long.parseLong(acceptable
.split("-")[1]));
}
}
return new AuthorizationException(Reason.OTHER, problem);
}
return new UnexpectedResponseException(code, new String(content));
} catch (IOException e) {
throw new UnexpectedResponseException(code, new String(content), e);
}
}
private static byte[] readBytes(InputStream in) throws IOException {
int len;
byte[] buffer = new byte[128];
ByteArrayOutputStream buf = new ByteArrayOutputStream(8192);
while ((len = in.read(buffer, 0, buffer.length)) != -1) {
buf.write(buffer, 0, len);
}
buf.flush();
return buf.toByteArray();
}
/** Deserialization. Sets up an HTTP client instance. */
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
try {
Field client = getClass().getDeclaredField("client");
client.setAccessible(true);
client.set(this, buildClient(trustSelfSigned));
} catch (NoSuchFieldException e) {
throw new InternalConfigurationException("Failed to set client fields", e);
} catch (IllegalAccessException e) {
throw new InternalConfigurationException("Failed to set client fields", e);
} catch (SecurityException e) {
throw new InternalConfigurationException("Failed to set client fields", e);
} catch (IllegalArgumentException e) {
throw new InternalConfigurationException("Failed to set client fields", e);
}
}
}