package com.dataart.android.devicehive.network;
import java.io.IOException;
import java.util.Map;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpVersion;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpDelete;
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.methods.HttpUriRequest;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.util.EntityUtils;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Parcelable;
import android.os.ResultReceiver;
import android.util.Log;
import com.dataart.android.devicehive.DeviceHive;
/**
* Common base class for all service commands.
*/
public abstract class NetworkCommand implements Parcelable {
protected final static ClassLoader CLASS_LOADER = NetworkCommand.class
.getClassLoader();
private final static String NAMESPACE = NetworkCommand.class.getName();
private final static String KEY_TAG = NAMESPACE.concat(".KEY_TAG");
private final static String KEY_STATUS_CODE = NAMESPACE
.concat(".KEY_STATUS_CODE");
private final static String KEY_EXCEPTION = NAMESPACE
.concat(".KEY_EXCEPTION");
private final static String KEY_COMMAND = NAMESPACE.concat(".KEY_COMMAND");
private NetworkCommandConfig config;
private static final HttpClient client = getClient();
// timeouts
private static final int DEFAULT_CONNECTION_TIMEOUT_MILLIS = 40000;
private static final int DEFAULT_SOCKET_TIMEOUT_MILLIS = 40000;
/**
* Http request type.
*/
protected static enum RequestType {
GET, POST, PUT, DELETE
}
/**
* Start this command using given {@link Context} and {@link ResultReceiver}
* .
*
* @param context
* {@link Context} instance which is used to start command.
* @param config
* {@link NetworkCommandConfig} instance which contains configuration data for given command.
*/
public void start(final Context context, final NetworkCommandConfig config) {
final Intent intent = new Intent(context, DeviceHiveApiService.class);
intent.putExtra(DeviceHiveApiService.EXTRA_COMMAND, this);
intent.putExtra(DeviceHiveApiService.EXTRA_COMMAND_CONFIG, config);
intent.putExtra(DeviceHiveApiService.EXTRA_COMMAND_SERIAL, isSerial());
context.startService(intent);
}
/* package */ void setConfig(NetworkCommandConfig config) {
this.config = config;
}
/**
* Override this method in order to force commands to be executed in a
* "serial" mode, i.e. in order they submitted (in single executor). Default
* implementation returns <code>false</code>.
*
* @return true, if command should be executed in "serial" mode, otherwise
* return false.
*/
protected boolean isSerial() {
return false;
}
/**
* This convenient method is used to ensure the bundle has correct
* command-tag.
*
* @return Tagged {@link Bundle} instance.
*/
protected final Bundle getTaggedBundle() {
return getTaggedBundle(0);
}
/**
* Create a {@link Bundle} instance and ensure the bundle has correct
* command-tag.
*
* @param capacity
* {@link Bundle} initial capacity.
* @return Tagged {@link Bundle} instance.
*/
protected final Bundle getTaggedBundle(final int capacity) {
final Bundle bundle = capacity > 0 ? new Bundle(capacity + 1)
: new Bundle();
bundle.putString(KEY_TAG, getClass().getName());
return bundle;
}
/**
* Get Http request type.
*
* @returnm {@link RequestType}
*/
protected abstract RequestType getRequestType();
/**
* Get request path component.
*
* @return Request path component.
*/
protected abstract String getRequestPath();
/**
* Get request headers.
*
* @return Request headers.
*/
protected abstract Map<String, String> getHeaders();
/**
* Get {@link HttpEntity} for the request. Override this method to return
* appropriate entity for POST and PUT requests.
*
* @return {@link HttpEntity} implementer instance.
*/
protected HttpEntity getRequestEntity() {
return null;
}
/**
* Check if given status code is considered as successful for this request.
* Can be overridden in descendants.
*
* @param statusCode
* Response status code.
*/
protected boolean isSuccessStatusCode(int statusCode) {
return statusCode == 200 || statusCode == 201;
}
/**
* Handle response.
* @param response String representation of the response body.
* @param resultData {@link Bundle} which is used to put and pass the results of command execution to the sender.
* @param context Current {@link Context} object.
* @return Status code.
* @see {@link DeviceHiveResultReceiver}.
*/
protected int handleResponse(final String response,
final Bundle resultData, final Context context) {
return -1;
}
/**
* Execute command. Called by the service on the dedicated thread.
* @param context Current {@link Context} object.
*/
protected void execute(Context context) {
logD("Request started");
final long start = System.currentTimeMillis();
final Bundle resultData = getTaggedBundle();
resultData.putParcelable(KEY_COMMAND, this);
send(DeviceHiveResultReceiver.MSG_EXECUTE_REQUEST, resultData);
HttpUriRequest httpRequest = toHttpRequest();
addHeaders(httpRequest);
final UsernamePasswordCredentials creds = config.getBasicAuthorisation();
if (creds != null) {
addBasicAuthenticationHeader(httpRequest, creds);
}
try {
HttpResponse responce = client.execute(httpRequest);
final int statusCode = responce.getStatusLine().getStatusCode();
String responceString = EntityUtils.toString(responce.getEntity());
logD("Responce status code: " + statusCode);
logD("Responce body: " + responceString);
resultData.putInt(KEY_STATUS_CODE, statusCode);
if (!isSuccessStatusCode(statusCode)) {
logD("Responce code is not OK. Responce body: "
+ responceString);
send(DeviceHiveResultReceiver.MSG_STATUS_FAILURE, resultData);
} else {
final int resultCode = handleResponse(responceString,
resultData, context);
if (resultCode > 0) {
send(resultCode, resultData);
}
}
} catch (ClientProtocolException e) {
Log.e(DeviceHive.TAG, "Failed to execute request: ", e);
resultData.putSerializable(KEY_EXCEPTION, e);
send(DeviceHiveResultReceiver.MSG_EXCEPTION, resultData);
} catch (IOException e) {
Log.e(DeviceHive.TAG, "Failed to execute request: ", e);
resultData.putSerializable(KEY_EXCEPTION, e);
send(DeviceHiveResultReceiver.MSG_EXCEPTION, resultData);
} finally {
send(DeviceHiveResultReceiver.MSG_COMPLETE_REQUEST, resultData);
long end = System.currentTimeMillis();
logD(String.format("Request to %s completed: ", getTargetUrl())
+ (end - start) + "ms");
}
}
private HttpUriRequest addBasicAuthenticationHeader(HttpUriRequest request,
UsernamePasswordCredentials creds) {
Header hdr = BasicScheme.authenticate(creds, "utf-8", false);
request.addHeader(hdr);
return request;
}
private void send(final int resultCode, Bundle resultData) {
if (config != null && config.resultReceiver != null) {
if (resultData == null) {
resultData = getTaggedBundle();
} else if (!resultData.containsKey(KEY_TAG)) {
resultData.putString(KEY_TAG, getClass().getName());
}
config.resultReceiver.send(resultCode, resultData);
}
}
private String getTargetUrl() {
return config.baseUrl + "/" + getRequestPath();
}
private void addHeaders(HttpUriRequest request) {
Map<String, String> headers = getHeaders();
for (Map.Entry<String, String> header : headers.entrySet()) {
request.addHeader(header.getKey(), header.getValue());
}
}
private HttpUriRequest toHttpRequest() {
String requestUrl = getTargetUrl();
switch (getRequestType()) {
case POST: {
HttpPost request = new HttpPost(requestUrl);
HttpEntity entity = getRequestEntity();
if (entity != null) {
request.setEntity(entity);
}
return request;
}
case GET: {
return new HttpGet(requestUrl);
}
case PUT: {
HttpPut request = new HttpPut(requestUrl);
HttpEntity entity = getRequestEntity();
if (entity != null) {
request.setEntity(entity);
}
return request;
}
case DELETE: {
return new HttpDelete(requestUrl);
}
default:
logD("Unrecognized request type: " + getRequestType());
return null;
}
}
private static DefaultHttpClient getClient() {
HttpParams params = getDefaultHttpParams();
SchemeRegistry registry = getDefaultSchemeRegistry();
ThreadSafeClientConnManager manager = new ThreadSafeClientConnManager(
params, registry);
return new DefaultHttpClient(manager, params);
}
private static SchemeRegistry getDefaultSchemeRegistry() {
SchemeRegistry registry = new SchemeRegistry();
registry.register(new Scheme("http", PlainSocketFactory
.getSocketFactory(), 80));
final SSLSocketFactory sslSocketFactory = SSLSocketFactory
.getSocketFactory();
sslSocketFactory
.setHostnameVerifier(SSLSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
registry.register(new Scheme("https", sslSocketFactory, 443));
return registry;
}
private static HttpParams getDefaultHttpParams() {
HttpParams params = new BasicHttpParams();
HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
HttpProtocolParams.setContentCharset(params, "utf-8");
params.setBooleanParameter("http.protocol.expect-continue", false);
HttpConnectionParams.setConnectionTimeout(params,
DEFAULT_CONNECTION_TIMEOUT_MILLIS);
HttpConnectionParams
.setSoTimeout(params, DEFAULT_SOCKET_TIMEOUT_MILLIS);
return params;
}
private void logD(final String message) {
if (config.isDebugLoggingEnabled) {
Log.d(DeviceHive.TAG, message);
}
}
/**
* Get command tag from tagged {@link Bundle}.
* @param resultData Tagged {@link Bundle}.
* @return Command tag.
*/
public final static String getCommandTag(final Bundle resultData) {
return resultData.getString(KEY_TAG);
}
/**
* Get {@link Throwable} from tagged {@link Bundle}.
* @param resultData Tagged {@link Bundle}.
* @return {@link Throwable} if any exceptions occur during command execution, otherwise return null.
*/
public final static Throwable getThrowable(final Bundle resultData) {
return (Throwable) resultData.getSerializable(KEY_EXCEPTION);
}
/**
* Get {@link NetworkCommand} from tagged {@link Bundle}.
* @param resultData Tagged {@link Bundle}.
* @return Current {@link NetworkCommand} instance.
*/
public final static NetworkCommand getCommand(final Bundle resultData) {
return (NetworkCommand) resultData.getParcelable(KEY_COMMAND);
}
/**
* Get status code from tagged {@link Bundle}.
* @param resultData Tagged {@link Bundle}.
* @return Status code.
*/
public final static int getStatusCode(final Bundle resultData) {
return resultData.getInt(KEY_STATUS_CODE);
}
}