package com.datdo.mobilib.api;
import android.net.Uri;
import android.os.Handler;
import android.util.Log;
import com.datdo.mobilib.api.MblRequest.MblStatusCodeValidator;
import com.datdo.mobilib.cache.MblDatabaseCache;
import com.datdo.mobilib.util.MblUtils;
import junit.framework.Assert;
import org.apache.http.Header;
import org.apache.http.HeaderElement;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.ParseException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
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.params.ClientPNames;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.MultipartEntity;
import org.apache.http.entity.mime.content.InputStreamBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* <pre>
* Util class for communicating with server via HTTP/HTTPS
*
* Sample code:
* {@code
* MblApi.run(new MblRequest()
* .setMethod(MblApi.Method.GET)
* .setUrl("http://your.website.com/img/logo.png")
* .setParams(
* "user-name", username,
* "avatar", new File(uploadedFilePath)
* )
* .setHeaderParams("access-token", accessToken)
* .setVerifySSL(true)
* .setCacheDuration(1000l * 60l * 60l * 24l)
* .setCallback(new MblApi.MblApiCallback() {
*
* @Override
* public void onSuccess(int statusCode, byte[] data) {
* // ...
* };
*
* @Override
* public void onFailure(int error, String errorMessage) {
* // ...
* }
* }));
* }
* </pre>
*/
@SuppressWarnings("deprecation")
public class MblApi {
private static final String TAG = MblApi.class.getSimpleName();
private static final String UTF8 = "UTF-8";
private static final Charset CHARSET_UTF8 = Charset.forName("UTF-8");
/**
* <pre>
* Common callback for all methods.
* </pre>
*/
public static interface MblApiCallback {
/**
* <pre>
* Invoked on request success.
* </pre>
*/
public void onSuccess(MblResponse response);
/**
* <pre>
* Invoked on request failure.
* </pre>
*/
public abstract void onFailure(MblResponse response);
}
/**
* <pre>
* General method to run an arbitrary request.
* </pre>
*/
public static void run(final MblRequest request) {
if (request == null) {
throw new RuntimeException("request must not be NULL");
}
if (request.getUrl() == null || request.getMethod() == null) {
throw new RuntimeException("request.url and request.method must not be NULL");
}
final MblApiCallback callback;
if (request.getCallback() != null && request.getTimeout() > 0) {
final long requestedAt = System.currentTimeMillis();
final Runnable timeout = new Runnable() {
@Override
public void run() {
MblResponse response = new MblResponse();
response.setRequest(request);
response.setStatusCode(-1);
response.setStatusCodeReason("Time out");
request.getCallback().onFailure(response);
}
};
MblUtils.getMainThreadHandler().postDelayed(timeout, request.getTimeout());
callback = new MblApiCallback() {
boolean isExpired() {
return System.currentTimeMillis() - requestedAt > request.getTimeout();
}
@Override
public void onSuccess(MblResponse response) {
MblUtils.getMainThreadHandler().removeCallbacks(timeout);
if (isExpired()) {
return;
}
request.getCallback().onSuccess(response);
}
@Override
public void onFailure(MblResponse response) {
MblUtils.getMainThreadHandler().removeCallbacks(timeout);
if (isExpired()) {
return;
}
request.getCallback().onFailure(response);
}
};
} else {
callback = request.getCallback();
}
if (request.getMethod() == Method.GET) {
get( request.getUrl(),
request.getParams(),
request.getHeaderParams(),
request.getCacheDuration(),
!request.isVerifySSL(),
callback,
request.getCallbackHandler(),
request.getStatusCodeValidator(),
request.isRedirectEnabled(),
request);
} else {
sendRequestWithBody(
request.getMethod(),
request.getUrl(),
request.getParams(),
request.getHeaderParams(),
!request.isVerifySSL(),
callback,
request.getCallbackHandler(),
request.getStatusCodeValidator(),
request.getData(),
request.isRedirectEnabled(),
request);
}
}
@SuppressWarnings("unchecked")
private static void get(
final String url,
Map<String, ? extends Object> params,
final Map<String, String> headerParams,
final long cacheDuration,
final boolean isIgnoreSSLCertificate,
final MblApiCallback callback,
Handler callbackHandler,
final MblStatusCodeValidator statusCodeValidator,
final boolean redirectEnabled,
final MblRequest request) {
final boolean isCacheEnabled = cacheDuration > 0;
Map<String, ? extends Object> paramsNoEmptyVal = getParamsIgnoreEmptyValues(params);
final Handler fCallbackHandler;
if (callbackHandler != null) {
fCallbackHandler = callbackHandler;
} else {
fCallbackHandler = MblUtils.getMainThreadHandler();
}
if (!MblUtils.isEmpty(paramsNoEmptyVal)) {
for (String key : paramsNoEmptyVal.keySet()) {
Object val = paramsNoEmptyVal.get(key);
if (val instanceof String) {
continue;
}
if (val instanceof Long) {
continue;
}
if (val instanceof Integer) {
continue;
}
if (val instanceof Double) {
continue;
}
if (val instanceof Float) {
continue;
}
final String message = "params " + key + " must be String, Long, Integer, Double, Float, current value is " + val.getClass().getSimpleName();
Log.e(TAG, "GET '" + url + "': " + message);
if (callback != null) {
MblUtils.executeOnHandlerThread(fCallbackHandler, new Runnable() {
@Override
public void run() {
callback.onFailure(new MblResponse()
.setRequest(request)
.setStatusCode(-1)
.setStatusCodeReason(message));
}
});
}
return;
}
}
final String fullUrl = generateGetMethodFullUrl(url, paramsNoEmptyVal);
MblUtils.executeOnAsyncThread(new Runnable() {
@Override
public void run() {
MblDatabaseCache existingCache = null;
if (isCacheEnabled) {
existingCache = MblDatabaseCache.get(fullUrl);
boolean shouldReadFromCache =
existingCache != null &&
( !MblUtils.isNetworkConnected() ||
System.currentTimeMillis() - existingCache.getDate() <= cacheDuration );
if (shouldReadFromCache) {
try {
final byte[] data = MblUtils.readCacheFile(getCacheFileName(existingCache));
if (data != null) {
if (callback != null) {
MblUtils.executeOnHandlerThread(fCallbackHandler, new Runnable() {
@Override
public void run() {
callback.onSuccess(new MblResponse()
.setRequest(request)
.setStatusCode(-1)
.setData(data));
}
});
}
return;
}
} catch (IOException e) {
Log.e(TAG, "Cache not exist", e);
}
}
}
try {
HttpClient httpClient = getHttpClient(fullUrl, isIgnoreSSLCertificate);
HttpContext httpContext = new BasicHttpContext();
HttpGet httpGet = new HttpGet(fullUrl);
if (!redirectEnabled) {
disableRedirect(httpGet);
}
httpGet.setHeaders(getHeaderArray(headerParams));
final HttpResponse response = httpClient.execute(httpGet, httpContext);
final int statusCode = response.getStatusLine().getStatusCode();
final String statusCodeReason = response.getStatusLine().getReasonPhrase();
final Map<String, String> headers = new HashMap<String, String>();
for (Header h : response.getAllHeaders()) {
headers.put(h.getName(), h.getValue());
}
final byte[] data = EntityUtils.toByteArray(response.getEntity());
if (!statusCodeValidator.isSuccess(statusCode)) {
if (callback != null) {
MblUtils.executeOnHandlerThread(fCallbackHandler, new Runnable() {
@Override
public void run() {
callback.onFailure(new MblResponse()
.setRequest(request)
.setStatusCode(statusCode)
.setStatusCodeReason(statusCodeReason)
.setHeaders(headers)
.setData(data));
}
});
}
return;
}
if (isCacheEnabled) {
saveCache(fullUrl, data);
}
if (callback != null) {
MblUtils.executeOnHandlerThread(fCallbackHandler, new Runnable() {
@Override
public void run() {
callback.onSuccess(new MblResponse()
.setRequest(request)
.setStatusCode(statusCode)
.setStatusCodeReason(statusCodeReason)
.setHeaders(headers)
.setData(data));
}
});
}
} catch (final Exception e) {
Log.e(TAG, "GET request failed due to unexpected exception", e);
if (callback != null) {
MblUtils.executeOnHandlerThread(fCallbackHandler, new Runnable() {
@Override
public void run() {
callback.onFailure(new MblResponse()
.setRequest(request)
.setStatusCode(-1)
.setStatusCodeReason("Unexpected exception: " + e.getMessage()));
}
});
}
}
}
});
}
/**
* <pre>
* Get absolute path to cache file of a URL.
* </pre>
* @param url starts with "http://" or "https://"
* @param params {key,value} containing request parameters (combined with "url" to generate full URL)
* @return
*/
@SuppressWarnings("unchecked")
public static String getCacheFilePath(String url, Map<String, ? extends Object> params) {
String fullUrl = generateGetMethodFullUrl(url, getParamsIgnoreEmptyValues(params));
MblDatabaseCache existingCache = MblDatabaseCache.get(fullUrl);
if (existingCache != null) {
String cacheFileName = getCacheFileName(existingCache);
if (!MblUtils.isEmpty(cacheFileName)) {
String path = MblUtils.getCacheAsbPath(cacheFileName);
File file = new File(path);
if (file.exists() && file.length() > 0) {
return path;
}
}
}
return null;
}
public static enum Method {
GET,
POST,
PUT,
DELETE;
public HttpEntityEnclosingRequestBase getHttpRequest(String url) {
if (this == POST) {
return new HttpPost(url);
}
if (this == PUT) {
return new HttpPut(url);
}
if (this == DELETE) {
return new HttpDeleteWithBody(url);
}
return null;
}
}
@SuppressWarnings("unchecked")
private static void sendRequestWithBody(
final Method method,
final String url,
Map<String, ? extends Object> params,
final Map<String, String> headerParams,
final boolean isIgnoreSSLCertificate,
final MblApiCallback callback,
Handler callbackHandler,
final MblStatusCodeValidator statusCodeValidator,
final String data,
final boolean redirectEnabled,
final MblRequest request) {
Assert.assertNotNull(method);
final Map<String, ? extends Object> paramsNoEmptyVal = getParamsIgnoreEmptyValues(params);
final Handler fCallbackHandler;
if (callbackHandler != null) {
fCallbackHandler = callbackHandler;
} else {
fCallbackHandler = MblUtils.getMainThreadHandler();
}
boolean isMultipart = false;
if (!MblUtils.isEmpty(paramsNoEmptyVal)) {
for (String key : paramsNoEmptyVal.keySet()) {
Object val = paramsNoEmptyVal.get(key);
if (val instanceof InputStream) {
isMultipart = true;
continue;
}
if (val instanceof File) {
isMultipart = true;
continue;
}
if (val instanceof String) {
continue;
}
if (val instanceof Long) {
continue;
}
if (val instanceof Integer) {
continue;
}
if (val instanceof Double) {
continue;
}
if (val instanceof Float) {
continue;
}
final String message = "params " + key + " must be String, Long, Integer, Double, Float, InputStream or File, current value is " + val.getClass().getSimpleName();
Log.e(TAG, method.name() + " '" + url + "': " + message);
if (callback != null) {
MblUtils.executeOnHandlerThread(fCallbackHandler, new Runnable() {
@Override
public void run() {
callback.onFailure(new MblResponse()
.setRequest(request)
.setStatusCode(-1)
.setStatusCodeReason(message));
}
});
}
return;
}
}
final boolean fIsMultipart = isMultipart;
MblUtils.executeOnAsyncThread(new Runnable() {
@Override
public void run() {
try {
HttpClient httpClient = getHttpClient(url, isIgnoreSSLCertificate);
HttpContext httpContext = new BasicHttpContext();
HttpEntityEnclosingRequestBase httpRequest = method.getHttpRequest(url);
if (!redirectEnabled) {
disableRedirect(httpRequest);
}
if (!MblUtils.isEmpty(paramsNoEmptyVal)) {
if (fIsMultipart) {
MultipartEntity multipartContent = new MultipartEntity();
for (String key : paramsNoEmptyVal.keySet()) {
Object val = paramsNoEmptyVal.get(key);
if (val instanceof InputStream) {
multipartContent.addPart(key, new InputStreamBody((InputStream)val, key));
} else if (val instanceof File) {
File file = (File)val;
FileInputStream fis = new FileInputStream(file);
multipartContent.addPart(key, new InputStreamBody(fis, file.getName()));
} else if (val instanceof String){
multipartContent.addPart(key, new StringBody((String)val, CHARSET_UTF8));
} else {
multipartContent.addPart(key, new StringBody(String.valueOf(val), CHARSET_UTF8));
}
}
httpRequest.setEntity(multipartContent);
} else {
List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2);
for (String key : paramsNoEmptyVal.keySet()) {
nameValuePairs.add(new BasicNameValuePair(key, paramsNoEmptyVal.get(key).toString()));
}
httpRequest.setEntity(new UrlEncodedFormEntity(nameValuePairs, UTF8));
}
} else if (!MblUtils.isEmpty(data)) {
httpRequest.setEntity(new StringEntity(data, UTF8));
}
httpRequest.setHeaders(getHeaderArray(headerParams));
final HttpResponse response = httpClient.execute(httpRequest, httpContext);
final int statusCode = response.getStatusLine().getStatusCode();
final String statusCodeReason = response.getStatusLine().getReasonPhrase();
final Map<String, String> headers = new HashMap<String, String>();
for (Header h : response.getAllHeaders()) {
headers.put(h.getName(), h.getValue());
}
final byte[] data = EntityUtils.toByteArray(response.getEntity());
if (!statusCodeValidator.isSuccess(statusCode)) {
if (callback != null) {
MblUtils.executeOnHandlerThread(fCallbackHandler, new Runnable() {
@Override
public void run() {
callback.onFailure(new MblResponse()
.setRequest(request)
.setStatusCode(statusCode)
.setStatusCodeReason(statusCodeReason)
.setHeaders(headers)
.setData(data));
}
});
}
return;
}
if (callback != null) {
MblUtils.executeOnHandlerThread(fCallbackHandler, new Runnable() {
@Override
public void run() {
callback.onSuccess(new MblResponse()
.setRequest(request)
.setStatusCode(statusCode)
.setStatusCodeReason(statusCodeReason)
.setHeaders(headers)
.setData(data));
}
});
}
} catch (final Exception e) {
Log.e(TAG, method.name() + " request failed due to unexpected exception", e);
if (callback != null) {
MblUtils.executeOnHandlerThread(fCallbackHandler, new Runnable() {
@Override
public void run() {
callback.onFailure(new MblResponse()
.setRequest(request)
.setStatusCode(-1)
.setStatusCodeReason("Unexpected exception: " + e.getMessage()));
}
});
}
}
}
});
}
@SuppressWarnings("unused")
private static class HttpDeleteWithBody extends HttpEntityEnclosingRequestBase {
@Override
public String getMethod() {
return "DELETE";
}
public HttpDeleteWithBody(final String uri) {
super();
setURI(URI.create(uri));
}
public HttpDeleteWithBody(final URI uri) {
super();
setURI(uri);
}
public HttpDeleteWithBody() {
super();
}
}
private static HttpClient getHttpClient(String url, boolean ignoreSSLCertificate) {
if (MblSSLCertificateUtils.isHttpsUrl(url) && ignoreSSLCertificate) {
return MblSSLCertificateUtils.getHttpClientIgnoreSSLCertificate();
} else {
return new DefaultHttpClient();
}
}
private static void saveCache(String fullUrl, byte[] data) {
try {
MblDatabaseCache c = new MblDatabaseCache(fullUrl, System.currentTimeMillis());
MblDatabaseCache.upsert(c);
MblUtils.saveCacheFile(data, getCacheFileName(c));
} catch (Exception e) {
Log.e(TAG, "Failed to cache url: " + fullUrl, e);
}
}
private static String generateGetMethodFullUrl(String url, Map<String, ? extends Object> params) {
if (!MblUtils.isEmpty(params)) {
Uri.Builder builder = Uri.parse(url).buildUpon();
for (String key : params.keySet()) {
builder.appendQueryParameter(key, params.get(key).toString());
}
return builder.build().toString();
} else {
return url;
}
}
private static Header[] getHeaderArray(Map<String, String> headerParams) {
Header[] headers = null;
if (!MblUtils.isEmpty(headerParams)) {
headers = new Header[headerParams.keySet().size()];
int i = 0;
for (final String key : headerParams.keySet()) {
final String val = headerParams.get(key);
headers[i++] = new Header() {
@Override
public HeaderElement[] getElements() throws ParseException { return null; }
@Override
public String getName() {
return key;
}
@Override
public String getValue() {
return val;
}
};
}
}
return headers;
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private static Map getParamsIgnoreEmptyValues(Map params) {
if (MblUtils.isEmpty(params)) {
return params;
}
Map ret = new HashMap();
for (Object key : params.keySet()) {
Object val = params.get(key);
if (!shouldIgnoreParamValue(val)) {
ret.put(key, val);
}
}
return ret;
}
private static boolean shouldIgnoreParamValue(Object val) {
if (val == null) {
return true;
}
if (val instanceof String && MblUtils.isEmpty((String)val)) {
return true;
}
return false;
}
/**
* <pre>
* Clear cache of all GET requests.
* </pre>
*/
public static void clearCache() {
// delete cache file
List<MblDatabaseCache> caches = MblDatabaseCache.getAll();
for (MblDatabaseCache c : caches) {
String path = MblUtils.getCacheAsbPath(getCacheFileName(c));
if (!MblUtils.isEmpty(path)) {
new File(path).delete();
}
}
// delete cache records
MblDatabaseCache.deleteAll();
}
private static String getCacheFileName(MblDatabaseCache c) {
if (c != null && !MblUtils.isEmpty(c.getKey())) {
return MblUtils.md5(c.getKey());
} else {
return null;
}
}
private static void disableRedirect(HttpRequest httpRequest) {
HttpParams params = new BasicHttpParams();
params.setParameter(ClientPNames.HANDLE_REDIRECTS, false);
httpRequest.setParams(params);
}
}