package com.openxc.sinks; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URI; import java.util.ArrayList; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; 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.message.BasicHeader; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpConnectionParams; import org.apache.http.params.HttpParams; import android.content.Context; import android.util.Log; import com.google.common.base.MoreObjects; import com.openxc.messages.VehicleMessage; import com.openxc.messages.formatters.JsonFormatter; /** * Upload a stream of all incoming vehicle data to a remote HTTP server. * * The remote HTTP server specified should be expecting POST requests with the * data being a JSON array of OpenXC JSON message objects, e.g.: * * [{"name": "steering_wheel_angle", "value": 42}, * {"name": "parking_brake_status", "value": false}] * * The UploaderSink has a small buffer to preserve records across short network * outages, but do not expect it to be especially reliable. No guarantee is * provided about the preservation of records - to do that, a subclass would * need to write the data to a file when no network connection is available. */ public class UploaderSink extends ContextualVehicleDataSink { private final static String TAG = "UploaderSink"; private final static int UPLOAD_BATCH_SIZE = 25; private final static int MAXIMUM_QUEUED_RECORDS = 5000; private final static int HTTP_TIMEOUT = 5000; private URI mUri; private BlockingQueue<VehicleMessage> mRecordQueue = new LinkedBlockingQueue<>(MAXIMUM_QUEUED_RECORDS); private Lock mQueueLock = new ReentrantLock(); private Condition mRecordsQueued = mQueueLock.newCondition(); private UploaderThread mUploader = new UploaderThread(); /** * Initialize and start a new UploaderSink immediately. * * @param uri the URI to send HTTP POST requests to with the JSON data. */ public UploaderSink(Context context, URI uri) { super(context); mUri = uri; } public UploaderSink(Context context, String path) throws DataSinkException { this(context, uriFromString(path)); } @Override public void stop() { mUploader.done(); } @Override public void receive(VehicleMessage message) { mRecordQueue.offer(message); if(mRecordQueue.size() >= UPLOAD_BATCH_SIZE) { try { mQueueLock.lock(); mRecordsQueued.signal(); } finally { mQueueLock.unlock(); } } } /** * Returns true if the path is not null and if it is a valid URI. * * @param path a URI to validate * @return true if path is a valid URI. * */ public static boolean validatePath(String path) { if(path == null) { Log.w(TAG, "Uploading path not set"); return false; } try { uriFromString(path); return true; } catch(DataSinkException e) { return false; } } @Override public String toString() { return MoreObjects.toStringHelper(this) .add("uri", mUri) .add("queuedRecords", mRecordQueue.size()) .toString(); } private static URI uriFromString(String path) throws DataSinkException { try { return new URI(path); } catch(java.net.URISyntaxException e) { throw new UploaderException( "Uploading path in wrong format -- expected: ip:port"); } } private static class UploaderException extends DataSinkException { private static final long serialVersionUID = 7436279598279767619L; public UploaderException() { } public UploaderException(String message) { super(message); } } private class UploaderThread extends Thread { private boolean mRunning = true; public UploaderThread() { start(); } @Override public void run() { while(mRunning) { try { ArrayList<VehicleMessage> records = getRecords(); String data = JsonFormatter.serialize(records); HttpPost request = constructRequest(data); makeRequest(request); } catch(UploaderException e) { Log.w(TAG, "Problem uploading the record", e); } catch(InterruptedException e) { Log.w(TAG, "Uploader was interrupted", e); break; } } } public void done() { mRunning = false; } private HttpPost constructRequest(String data) throws UploaderException { HttpPost request = new HttpPost(mUri); try { ByteArrayEntity entity = new ByteArrayEntity( data.getBytes("UTF8")); entity.setContentEncoding( new BasicHeader("Content-Type", "application/json")); request.setEntity(entity); } catch(UnsupportedEncodingException e) { Log.w(TAG, "Couldn't encode records for uploading", e); throw new UploaderException(); } return request; } private void makeRequest(HttpPost request) throws InterruptedException { HttpParams parameters = new BasicHttpParams(); HttpConnectionParams.setConnectionTimeout(parameters, HTTP_TIMEOUT); HttpConnectionParams.setSoTimeout(parameters, HTTP_TIMEOUT); final HttpClient client = new DefaultHttpClient(parameters); try { HttpResponse response = client.execute(request); final int statusCode = response.getStatusLine().getStatusCode(); if(statusCode != HttpStatus.SC_CREATED) { Log.w(TAG, "Got unexpected status code: " + statusCode); } } catch(IOException e) { Log.w(TAG, "Problem uploading the record", e); try { Thread.sleep(5000); } catch(InterruptedException e2) { Log.w(TAG, "Uploader interrupted after an error", e2); throw e2; } } } private ArrayList<VehicleMessage> getRecords() throws InterruptedException { try { mQueueLock.lock(); while(mRecordQueue.isEmpty()) { // the queue is already thread safe, but we use this lock to get // a condition variable we can use to signal when a batch has // been queued. mRecordsQueued.await(5, TimeUnit.SECONDS); } ArrayList<VehicleMessage> records = new ArrayList<>(); mRecordQueue.drainTo(records, UPLOAD_BATCH_SIZE); return records; } finally { mQueueLock.unlock(); } } } }