/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the Common Development
* and Distribution License (the "License").
* You may not use this file except in compliance with the License.
*
* You can obtain a copy of the license at
* src/com/vodafone360/people/VODAFONE.LICENSE.txt or
* http://github.com/360/360-Engine-for-Android
* See the License for the specific language governing permissions and
* limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each file and
* include the License file at src/com/vodafone360/people/VODAFONE.LICENSE.txt.
* If applicable, add the following below this CDDL HEADER, with the fields
* enclosed by brackets "[]" replaced with your own identifying information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
* Copyright 2010 Vodafone Sales & Services Ltd. All rights reserved.
* Use is subject to license terms.
*/
package com.vodafone360.people.service.transport.http;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import android.util.Log;
import com.vodafone360.people.Settings;
import com.vodafone360.people.SettingsManager;
import com.vodafone360.people.engine.EngineManager.EngineId;
import com.vodafone360.people.engine.login.LoginEngine;
import com.vodafone360.people.service.io.QueueManager;
import com.vodafone360.people.service.io.Request;
import com.vodafone360.people.service.io.ResponseQueue;
import com.vodafone360.people.service.io.ResponseQueue.DecodedResponse;
import com.vodafone360.people.service.transport.DecoderThread;
import com.vodafone360.people.service.transport.DecoderThread.RawResponse;
import com.vodafone360.people.service.transport.IConnection;
import com.vodafone360.people.service.transport.http.authentication.AuthenticationManager;
import com.vodafone360.people.service.utils.hessian.HessianUtils;
import com.vodafone360.people.utils.LogUtils;
/**
* HTTP connection thread handles issuing of RPG requests over HTTP to server.
*/
public class HttpConnectionThread implements Runnable, IConnection {
private Thread mThread;
private volatile boolean mIsConnectionRunning;
private PollThread mPollThread;
private DecoderThread mDecoder;
private HttpClient mHttpClient;
private int mRetryCount;
private URI mRpgUrl;
private boolean mIsPolling, mIsFirstTimePoll;
//protected static final int E_HTTP_PROTOCOL = 2;
private final Object mSendLock = new Object();
private final Object mRunLock = new Object();
public HttpConnectionThread(DecoderThread decoder) {
super();
mIsPolling = false;
mIsFirstTimePoll = true;
mDecoder = decoder;
}
/**
* Starts the RPG connection as a thread and also launches the polling
* thread.
*/
public synchronized void startThread() {
logI("RpgHttpConnectionThread.startThread()", "Starting Thread");
setHttpClient();
mIsConnectionRunning = true;
mThread = new Thread(this);
mThread.start();
}
/**
* Sets HTTP settings.
*/
public void setHttpClient() {
int connectionTimeout = Settings.HTTP_CONNECTION_TIMEOUT;
HttpParams myHttpParams = new BasicHttpParams();
HttpConnectionParams.setConnectionTimeout(myHttpParams, connectionTimeout);
HttpConnectionParams.setSoTimeout(myHttpParams, connectionTimeout);
mHttpClient = new DefaultHttpClient(myHttpParams); // get http
}
/**
* Stops the current thread.
*/
public synchronized void stopThread() {
mIsConnectionRunning = false;
notifyOfItemInRequestQueue();// notify to make sure the thread exits if
// it's waiting
if (mPollThread != null) {
mPollThread.stopConnection();
}
if (mHttpClient != null) {
mHttpClient.getConnectionManager().shutdown();
mHttpClient = null;
}
}
/**
* <p>
* Sends out synchronous requests (for authentication) to the API and
* asynchronous calls to the RPG as soon as there are requests on the
* request queue.
* </p>
* <p>
* If there are no requests the thread is set to wait().
* </p>
*/
public void run() {
AuthenticationManager authMgr = null;
authMgr = new AuthenticationManager(this);
while (mIsConnectionRunning) { // loops through requests and sends them
// out as needed
authMgr.handleAuthRequests(); // TODO move this out. this should be
// done by the LoginEngine directly
if (null != mPollThread) {
if (mIsFirstTimePoll) {
try {
mPollThread.invokePoll(PollThread.SHORT_POLLING_INTERVAL,
PollThread.DEFAULT_BATCHSIZE, PollThread.ACTIVE_MODE);
} catch (Exception e) {
// we do not do anything here as it is not a critical
// error
logI("RpgHttpConnection.run()", "Exception while doing 1st time poll!!");
} finally {
mIsFirstTimePoll = false;
}
}
List<Request> requests = QueueManager.getInstance().getApiRequests();
if ((requests.size() > 0) && mPollThread.getHasCoverage()) {
if (null == mRpgUrl) { // TODO move this out of the loop
// once we have a proper authMgr
try {
mRpgUrl = new URL(SettingsManager.getProperty(Settings.RPG_SERVER_KEY)
+ LoginEngine.getSession().userID).toURI();
} catch (Exception e) {
logE("RpgHttpConnectionThread.run()", "Could not set up URL", e);
}
}
mRetryCount = 0;
List<Integer> reqIds = new ArrayList<Integer>();
try {
byte[] reqData = prepareRPGRequests(requests, reqIds);
if (null != LoginEngine.getSession()) {
synchronized (mSendLock) {
if (mIsConnectionRunning) {
if (Settings.sEnableProtocolTrace) {
HttpConnectionThread.logI("RpgTcpConnectionThread.run()",
"\n \n \nSending a request: >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"
+ HessianUtils.getInHessian(
new ByteArrayInputStream(reqData),
true));
}
HttpResponse response = postHTTPRequest(reqData, mRpgUrl,
Settings.HTTP_HEADER_CONTENT_TYPE);
if (mIsConnectionRunning
&& SettingsManager
.getBooleanProperty(Settings.ENABLE_RPG_KEY)
&& handleRpgResponse(response, reqIds)) {
mPollThread.startRpgPolling();
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
addErrorToResponseQueue(reqIds);// add error to the
// response queue
}
}
}
try {
synchronized (mRunLock) {
mRunLock.wait();
}
} catch (InterruptedException ie) {
LogUtils.logE("HttpConnectionThread.run() Wait was interrupted", ie);
}
}
}
/**
* Takes all requests objects and writes its serialized data to a byte array
* for further posting to the RPG.
*
* @param requests A list of requests to serialize.
* @param reqIds
* @return The serialized requests with RPG headers. Returns NULL id list of requests is NULL.
*/
private byte[] prepareRPGRequests(List<Request> requests, List<Integer> reqIds) {
if (null == requests) {
return null;
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
for (Request request : requests) {
request.writeToOutputStream(baos, true);
request.setActive(true);
reqIds.add(request.getRequestId());
}
baos.flush();
} catch (IOException ioe) {
LogUtils.logE("HttpConnectionThread.prepareRPGRequests() Failed writing to BAOS", ioe);
} finally {
try {
baos.close();
} catch (IOException ioe) {
LogUtils.logE("HttpConnectionThread.prepareRPGRequests() Failed closing BAOS", ioe);
}
}
byte[] reqData = baos.toByteArray();
return reqData;
}
/**
* Posts the serialized data to the RPG and synchronously grabs the
* response.
*
* @param postData The post data to send to the RPG.
* @param uri The URL to send the request to.
* @param contentType The content type to send as, usually
* "application/binary)
* @return The response data as a byte array.
* @throws An exception if the request went wrong after HTTP_MAX_RETRY_COUNT
* retries.
*/
public HttpResponse postHTTPRequest(byte[] postData, URI uri, String contentType)
throws Exception {
HttpResponse response = null;
if (null == postData) {
return response;
}
mRetryCount++;
if (uri != null) {
logI("RpgHttpConnectionThread.postHTTPRequest()", "HTTP Requesting URI " + uri.toString()
+ " " + contentType);
HttpPost httpPost = new HttpPost(uri);
httpPost.addHeader("Content-Type", contentType);
httpPost.addHeader("User-Agent", "PeopleAndroidClient/1.0");
httpPost.addHeader("Cache-Control", "no-cache");
httpPost.setEntity(new ByteArrayEntity(postData));
try {
response = mHttpClient.execute(httpPost);
} catch (Exception e) {
e.printStackTrace();
// repeat the request N times
if (mRetryCount < Settings.HTTP_MAX_RETRY_COUNT) {
return postHTTPRequest(postData, uri, contentType);
} else {
throw new Exception("Could not post request " + e);
}
}
}
return response;
}
/**
* Checks if the response to an RPG request was fired off correctly.
* Basically this method only checks whether the response is returned under
* a HTTP 200 status.
*
* @param response The response to check for.
* @param reqIds The request IDs for the response.
* @return True if the RPG response returned correctly.
* @throws Exception Thrown if the response was null or the status line could
* not be fetched.
*/
private boolean handleRpgResponse(HttpResponse response, List<Integer> reqIds) throws Exception {
boolean ret = false;
if (null != response) {
if (null != response.getStatusLine()) {
int respCode = response.getStatusLine().getStatusCode();
logI("RpgHttpConnectionThread.handleRpgResponse()",
"HTTP BINARY Got response status: " + respCode);
switch (respCode) {
case HttpStatus.SC_OK:
case HttpStatus.SC_CONTINUE:
case HttpStatus.SC_CREATED:
case HttpStatus.SC_ACCEPTED:
case HttpStatus.SC_NON_AUTHORITATIVE_INFORMATION:
case HttpStatus.SC_NO_CONTENT:
case HttpStatus.SC_RESET_CONTENT:
case HttpStatus.SC_PARTIAL_CONTENT:
case HttpStatus.SC_MULTI_STATUS:
ret = true;
break;
default:
addErrorToResponseQueue(reqIds);
}
finishResponse(response);
} else {
throw new Exception("Status line of response was null.");
}
} else {
throw new Exception("Response was null.");
}
return ret;
}
/**
* Finishes reading the response in order to unblock the current connection.
*
* @param response The response to finish reading on.
*/
private void finishResponse(HttpResponse response) {
HttpEntity entity = response.getEntity();
if (entity != null) {
try {
// this is important! otherwise the connection remains
// unusable!!
entity.consumeContent();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* Handles the synchronous responses for the authentication calls which go
* against the API directly by adding it to the queue and checking if the
* response code was a HTTP 200. TODO: this should be refactored into a
* AuthenticationManager class.
*
* @param response The response to add to the decoder.
* @param reqIds The request IDs the response is to be decoded for.
* @throws Exception Thrown if the status line could not be read or the
* response is null.
*/
public void handleApiResponse(HttpResponse response, List<Integer> reqIds) throws Exception {
byte[] ret = null;
if (null != response) {
if (null != response.getStatusLine()) {
int respCode = response.getStatusLine().getStatusCode();
logI("RpgHttpConnectionThread.handleApiResponse()", "HTTP Got response status: "
+ respCode);
switch (respCode) {
case HttpStatus.SC_OK:
case HttpStatus.SC_CONTINUE:
case HttpStatus.SC_CREATED:
case HttpStatus.SC_ACCEPTED:
case HttpStatus.SC_NON_AUTHORITATIVE_INFORMATION:
case HttpStatus.SC_NO_CONTENT:
case HttpStatus.SC_RESET_CONTENT:
case HttpStatus.SC_PARTIAL_CONTENT:
case HttpStatus.SC_MULTI_STATUS:
HttpEntity entity = response.getEntity();
if (null != entity) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
InputStream is = entity.getContent();
if (null != is) {
int nextByte = 0;
while ((nextByte = is.read()) != -1) {
baos.write(nextByte);
}
baos.flush();
ret = baos.toByteArray();
baos.close();
baos = null;
}
entity.consumeContent();
}
if (Settings.sEnableProtocolTrace) {
int length = 0;
if (ret != null) {
length = ret.length;
}
HttpConnectionThread.logI("ResponseReader.handleApiResponse()",
"\n \n \n"
+ "Response with length "
+ length
+ " bytes received "
+ "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"
+ (length == 0 ? "" : HessianUtils.getInHessian(
new ByteArrayInputStream(ret), false)));
}
addToDecoder(ret, reqIds);
break;
default:
addErrorToResponseQueue(reqIds);
}
} else {
throw new Exception("Status line of response was null.");
}
} else {
throw new Exception("Response was null.");
}
}
/**
* Adds a response to the response decoder.
*
* @param input The data of the response.
* @param reqIds The request IDs that a response was received for.
*/
private void addToDecoder(byte[] input, List<Integer> reqIds) {
if (input != null && mDecoder != null) {
int reqId = reqIds.size() > 0 ? reqIds.get(0) : 0;
mDecoder.addToDecode(new RawResponse(reqId, input, false, false));
logI("RpgHttpConnectionThread.handleApiResponse()", "Added response(s) to decoder: "
+ reqIds.toString());
}
}
/**
* Adds errors to the response queue whenever there is an HTTP error on the
* backend.
*
* @param reqIds The request IDs the error happened for.
*/
public void addErrorToResponseQueue(List<Integer> reqIds) {
EngineId source = null;
QueueManager requestQueue = QueueManager.getInstance();
ResponseQueue responseQueue = ResponseQueue.getInstance();
for (Integer reqId : reqIds) {
// attempt to get type from request
Request req = requestQueue.getRequest(reqId);
if (req != null)
source = req.mEngineId;
responseQueue.addToResponseQueue(new DecodedResponse(reqId, null, source, DecodedResponse.ResponseType.SERVER_ERROR.ordinal()));
}
}
/**
* Kicks the request queue as soon as there are more requests on the queue.
*/
@Override
public void notifyOfItemInRequestQueue() {
HttpConnectionThread.logI("HttpConnectionThread.notifyOfItemInRequestQueue()",
"NEW REQUEST AVAILABLE!");
synchronized (mRunLock) {
mRunLock.notify();
}
}
/**
* Called whenever the device regains network coverage. It will kick the
* request queue to see if there are more requests to send.
*/
@Override
public void notifyOfRegainedNetworkCoverage() {
synchronized (mRunLock) {
mRunLock.notify();
}
}
/**
* This method is called when log-in is detected. Polling should not
* normally start before we are logged in as the backend needs a correct
* user id.
*/
private synchronized void startPollThread() {
if (null == mPollThread) {
mPollThread = new PollThread(this);
}
if (mIsConnectionRunning) {
mIsPolling = true;
mPollThread.startConnection(mDecoder);
}
}
/**
* Stops the polling thread. This method is implemented from the
* IQueueListener-interface. It is called when log-out of the account is
* detected but a connection is still ongoing. In this case polling needs to
* stop.
*/
private synchronized void stopPollThread() {
if (mPollThread != null) {
mIsPolling = false;
mPollThread.stopConnection();
}
// clear all requests in RequestQueue
QueueManager.getInstance().clearActiveRequests(false);
}
@Override
public void onLoginStateChanged(boolean isLoggedIn) {
if (!isLoggedIn && mIsConnectionRunning) {
logI("RpgHttpConnectionThread.onLoginStateChanged()", "Stopping to poll.");
stopPollThread();
mIsPolling = false;
} else if (!mIsConnectionRunning) {
startThread();
} else if (isLoggedIn && !mIsPolling) { // if connected but not Polling
if (mIsConnectionRunning) {
logI("RpgHttpConnectionThread.onLoginStateChanged()", "Starting Poll");
startPollThread();
mIsPolling = true;
}
}
}
@Override
public boolean getIsConnected() {
return mIsConnectionRunning;
}
public static void logE(String tag, String message, Throwable error) {
if (Settings.sEnableProtocolTrace) {
Thread t = Thread.currentThread();
if (null != error) {
Log.e("(PROTOCOL)",
"\n \n \n ################################################## \n" + tag
+ "[" + t.getName() + "]" + " : " + message, error);
} else {
Log.e("(PROTOCOL)",
"\n \n \n ################################################## \n" + tag
+ "[" + t.getName() + "]" + " : " + message);
}
}
if (null != error) {
LogUtils.logE(message + " : " + error.toString());
} else {
LogUtils.logE(message
+ " (Note: Settings.ENABLED_TRANSPORT_TRACE might give you more details!)");
}
}
public static void logW(String tag, String message) {
if (Settings.sEnableProtocolTrace) {
Thread t = Thread.currentThread();
Log.w("(PROTOCOL)", tag + "[" + t.getName() + "] ################### " + " : "
+ message);
}
}
public static void logI(String tag, String message) {
if (Settings.sEnableProtocolTrace) {
Thread t = Thread.currentThread();
Log.i("(PROTOCOL)", tag + "[" + t.getName() + "]" + " : " + message);
}
}
public static void logD(String tag, String message) {
if (Settings.sEnableProtocolTrace) {
Thread t = Thread.currentThread();
Log.d("(PROTOCOL)", tag + "[" + t.getName() + "]" + " : " + message);
}
}
public static void logV(String tag, String message) {
if (Settings.sEnableProtocolTrace) {
Thread t = Thread.currentThread();
Log.v("(PROTOCOL)", tag + "[" + t.getName() + "]" + " : " + message);
}
}
@Override
public boolean getIsRpgConnectionActive() {
return mIsPolling;
}
@Override
public void notifyOfUiActivity() {
// TODO Auto-generated method stub
}
}