package com.koushikdutta.async.http;
import android.os.Handler;
import com.koushikdutta.async.AsyncSSLException;
import com.koushikdutta.async.AsyncServer;
import com.koushikdutta.async.AsyncSocket;
import com.koushikdutta.async.ByteBufferList;
import com.koushikdutta.async.DataEmitter;
import com.koushikdutta.async.NullDataCallback;
import com.koushikdutta.async.callback.CompletedCallback;
import com.koushikdutta.async.callback.ConnectCallback;
import com.koushikdutta.async.future.Cancellable;
import com.koushikdutta.async.future.Future;
import com.koushikdutta.async.future.FutureCallback;
import com.koushikdutta.async.future.SimpleFuture;
import com.koushikdutta.async.http.AsyncHttpClientMiddleware.OnRequestCompleteData;
import com.koushikdutta.async.http.callback.HttpConnectCallback;
import com.koushikdutta.async.http.callback.RequestCallback;
import com.koushikdutta.async.http.libcore.RawHeaders;
import com.koushikdutta.async.parser.AsyncParser;
import com.koushikdutta.async.parser.ByteBufferListParser;
import com.koushikdutta.async.parser.JSONObjectParser;
import com.koushikdutta.async.parser.StringParser;
import com.koushikdutta.async.stream.OutputStreamDataCallback;
import org.json.JSONObject;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.util.ArrayList;
import java.util.concurrent.TimeoutException;
public class AsyncHttpClient {
private static AsyncHttpClient mDefaultInstance;
public static AsyncHttpClient getDefaultInstance() {
if (mDefaultInstance == null)
mDefaultInstance = new AsyncHttpClient(AsyncServer.getDefault());
return mDefaultInstance;
}
ArrayList<AsyncHttpClientMiddleware> mMiddleware = new ArrayList<AsyncHttpClientMiddleware>();
public ArrayList<AsyncHttpClientMiddleware> getMiddleware() {
return mMiddleware;
}
public void insertMiddleware(AsyncHttpClientMiddleware middleware) {
mMiddleware.add(0, middleware);
}
AsyncSSLSocketMiddleware sslSocketMiddleware;
AsyncSocketMiddleware socketMiddleware;
AsyncServer mServer;
public AsyncHttpClient(AsyncServer server) {
mServer = server;
insertMiddleware(socketMiddleware = new AsyncSocketMiddleware(this));
insertMiddleware(sslSocketMiddleware = new AsyncSSLSocketMiddleware(this));
}
public AsyncSocketMiddleware getSocketMiddleware() {
return socketMiddleware;
}
public AsyncSSLSocketMiddleware getSSLSocketMiddleware() {
return sslSocketMiddleware;
}
public Future<AsyncHttpResponse> execute(final AsyncHttpRequest request, final HttpConnectCallback callback) {
FutureAsyncHttpResponse ret;
execute(request, 0, ret = new FutureAsyncHttpResponse(), callback);
return ret;
}
private static final String LOGTAG = "AsyncHttp";
private class FutureAsyncHttpResponse extends SimpleFuture<AsyncHttpResponse> {
public AsyncSocket socket;
public Object scheduled;
public Runnable timeoutRunnable;
@Override
public boolean cancel() {
if (!super.cancel())
return false;
if (socket != null)
socket.close();
if (scheduled != null)
mServer.removeAllCallbacks(scheduled);
return true;
}
}
private void reportConnectedCompleted(FutureAsyncHttpResponse cancel, Exception ex, AsyncHttpResponseImpl response, AsyncHttpRequest request, final HttpConnectCallback callback) {
assert callback != null;
boolean complete;
if (ex != null) {
request.loge("Connection error", ex);
complete = cancel.setComplete(ex);
}
else {
request.logd("Connection successful");
complete = cancel.setComplete(response);
}
if (complete) {
callback.onConnectCompleted(ex, response);
assert ex != null || response.getSocket() == null || response.getDataCallback() != null;
return;
}
if (response != null) {
// the request was cancelled, so close up shop, and eat any pending data
response.setDataCallback(new NullDataCallback());
response.close();
}
}
private void execute(final AsyncHttpRequest request, final int redirectCount, final FutureAsyncHttpResponse cancel, final HttpConnectCallback callback) {
if (mServer.isAffinityThread()) {
executeAffinity(request, redirectCount, cancel, callback);
}
else {
mServer.post(new Runnable() {
@Override
public void run() {
executeAffinity(request, redirectCount, cancel, callback);
}
});
}
}
private static long getTimeoutRemaining(AsyncHttpRequest request) {
// need a better way to calculate this.
// a timer of sorts that stops/resumes.
return request.getTimeout();
}
private void executeAffinity(final AsyncHttpRequest request, final int redirectCount, final FutureAsyncHttpResponse cancel, final HttpConnectCallback callback) {
assert mServer.isAffinityThread();
if (redirectCount > 15) {
reportConnectedCompleted(cancel, new Exception("too many redirects"), null, request, callback);
return;
}
final URI uri = request.getUri();
final OnRequestCompleteData data = new OnRequestCompleteData();
request.executionTime = System.currentTimeMillis();
data.request = request;
request.logd("Executing request.");
// flow:
// 1) set a connect timeout
// 2) wait for connect
// 3) on connect, cancel timeout
// 4) wait for request to be sent fully
// 5) after request is sent, set a header timeout
// 6) wait for headers
// 7) on headers, cancel timeout
// 8) TODO: response can take as long as it wants to arrive?
if (request.getTimeout() > 0) {
// set connect timeout
cancel.timeoutRunnable = new Runnable() {
@Override
public void run() {
// we've timed out, kill the connections
if (data.socketCancellable != null) {
data.socketCancellable.cancel();
if (data.socket != null)
data.socket.close();
}
reportConnectedCompleted(cancel, new TimeoutException(), null, request, callback);
}
};
cancel.scheduled = mServer.postDelayed(cancel.timeoutRunnable, getTimeoutRemaining(request));
}
// 2) wait for a connect
data.connectCallback = new ConnectCallback() {
@Override
public void onConnectCompleted(Exception ex, AsyncSocket socket) {
if (cancel.isCancelled()) {
if (socket != null)
socket.close();
return;
}
// 3) on connect, cancel timeout
if (cancel.timeoutRunnable != null)
mServer.removeAllCallbacks(cancel.scheduled);
data.socket = socket;
for (AsyncHttpClientMiddleware middleware: mMiddleware) {
middleware.onSocket(data);
}
cancel.socket = socket;
if (ex != null) {
reportConnectedCompleted(cancel, ex, null, request, callback);
return;
}
// 4) wait for request to be sent fully
// and
// 6) wait for headers
final AsyncHttpResponseImpl ret = new AsyncHttpResponseImpl(request) {
@Override
protected void onRequestCompleted(Exception ex) {
if (cancel.isCancelled())
return;
// 5) after request is sent, set a header timeout
if (cancel.timeoutRunnable != null && data.headers == null) {
mServer.removeAllCallbacks(cancel.scheduled);
cancel.scheduled = mServer.postDelayed(cancel.timeoutRunnable, getTimeoutRemaining(request));
}
}
@Override
public void setDataEmitter(DataEmitter emitter) {
data.bodyEmitter = emitter;
for (AsyncHttpClientMiddleware middleware: mMiddleware) {
middleware.onBodyDecoder(data);
}
mHeaders = data.headers;
super.setDataEmitter(data.bodyEmitter);
RawHeaders headers = mHeaders.getHeaders();
if ((headers.getResponseCode() == HttpURLConnection.HTTP_MOVED_PERM || headers.getResponseCode() == HttpURLConnection.HTTP_MOVED_TEMP) && request.getFollowRedirect()) {
URI redirect = URI.create(headers.get("Location"));
if (redirect == null || redirect.getScheme() == null) {
redirect = URI.create(uri.toString().substring(0, uri.toString().length() - uri.getPath().length()) + headers.get("Location"));
}
AsyncHttpRequest newReq = new AsyncHttpRequest(redirect, request.getMethod());
newReq.executionTime = request.executionTime;
newReq.logLevel = request.logLevel;
newReq.LOGTAG = request.LOGTAG;
request.logi("Redirecting");
newReq.logi("Redirected");
execute(newReq, redirectCount + 1, cancel, callback);
setDataCallback(new NullDataCallback());
return;
}
request.logv("Final (post cache response) headers: " + mHeaders.getHeaders().toHeaderString());
// at this point the headers are done being modified
reportConnectedCompleted(cancel, null, this, request, callback);
}
protected void onHeadersReceived() {
try {
if (cancel.isCancelled())
return;
// 7) on headers, cancel timeout
if (cancel.timeoutRunnable != null)
mServer.removeAllCallbacks(cancel.scheduled);
// allow the middleware to massage the headers before the body is decoded
request.logv("Received headers: " + mHeaders.getHeaders().toHeaderString());
data.headers = mHeaders;
for (AsyncHttpClientMiddleware middleware: mMiddleware) {
middleware.onHeadersReceived(data);
}
mHeaders = data.headers;
// drop through, and setDataEmitter will be called for the body decoder.
// headers will be further massaged in there.
}
catch (Exception ex) {
reportConnectedCompleted(cancel, ex, null, request, callback);
}
}
@Override
protected void report(Exception ex) {
if (cancel.isCancelled())
return;
if (ex instanceof AsyncSSLException) {
request.loge("SSL Exception", ex);
AsyncSSLException ase = (AsyncSSLException)ex;
request.onHandshakeException(ase);
if (ase.getIgnore())
return;
}
final AsyncSocket socket = getSocket();
if (socket == null)
return;
super.report(ex);
if (!socket.isOpen() || ex != null) {
if (getHeaders() == null && ex != null)
reportConnectedCompleted(cancel, ex, null, request, callback);
}
data.exception = ex;
for (AsyncHttpClientMiddleware middleware: mMiddleware) {
middleware.onRequestComplete(data);
}
}
@Override
public AsyncSocket detachSocket() {
request.logd("Detaching socket");
AsyncSocket socket = getSocket();
if (socket == null)
return null;
socket.setWriteableCallback(null);
socket.setClosedCallback(null);
socket.setEndCallback(null);
socket.setDataCallback(null);
setSocket(null);
return socket;
}
};
ret.setSocket(socket);
}
};
for (AsyncHttpClientMiddleware middleware: mMiddleware) {
Cancellable socketCancellable = middleware.getSocket(data);
if (socketCancellable != null) {
data.socketCancellable = socketCancellable;
cancel.setParent(socketCancellable);
return;
}
}
assert false;
}
public Future<AsyncHttpResponse> execute(URI uri, final HttpConnectCallback callback) {
return execute(new AsyncHttpGet(uri), callback);
}
public Future<AsyncHttpResponse> execute(String uri, final HttpConnectCallback callback) {
return execute(new AsyncHttpGet(URI.create(uri)), callback);
}
public static abstract class RequestCallbackBase<T> implements RequestCallback<T> {
@Override
public void onProgress(AsyncHttpResponse response, int downloaded, int total) {
}
@Override
public void onConnect(AsyncHttpResponse response) {
}
}
public static abstract class DownloadCallback extends RequestCallbackBase<ByteBufferList> {
}
public static abstract class StringCallback extends RequestCallbackBase<String> {
}
public static abstract class JSONObjectCallback extends RequestCallbackBase<JSONObject> {
}
public static abstract class FileCallback extends RequestCallbackBase<File> {
}
@Deprecated
public Future<ByteBufferList> get(String uri, DownloadCallback callback) {
return getByteBufferList(uri, callback);
}
public Future<ByteBufferList> getByteBufferList(String uri) {
return getByteBufferList(uri, null);
}
public Future<ByteBufferList> getByteBufferList(String uri, DownloadCallback callback) {
return executeByteBufferList(new AsyncHttpGet(uri), callback);
}
public Future<ByteBufferList> executeByteBufferList(AsyncHttpRequest request, DownloadCallback callback) {
return execute(request, new ByteBufferListParser(), callback);
}
@Deprecated
public Future<String> get(String uri, final StringCallback callback) {
return executeString(new AsyncHttpGet(uri), callback);
}
@Deprecated
public Future<String> execute(AsyncHttpRequest req, final StringCallback callback) {
return executeString(req, callback);
}
public Future<String> getString(String uri) {
return executeString(new AsyncHttpGet(uri), null);
}
public Future<String> getString(String uri, final StringCallback callback) {
return executeString(new AsyncHttpGet(uri), callback);
}
public Future<String> executeString(AsyncHttpRequest req) {
return executeString(req, null);
}
public Future<String> executeString(AsyncHttpRequest req, final StringCallback callback) {
return execute(req, new StringParser(), callback);
}
@Deprecated
public Future<JSONObject> get(String uri, final JSONObjectCallback callback) {
return executeJSONObject(new AsyncHttpGet(uri), callback);
}
@Deprecated
public Future<JSONObject> execute(AsyncHttpRequest req, final JSONObjectCallback callback) {
return executeJSONObject(req, callback);
}
public Future<JSONObject> getJSONObject(String uri) {
return getJSONObject(uri, null);
}
public Future<JSONObject> getJSONObject(String uri, final JSONObjectCallback callback) {
return executeJSONObject(new AsyncHttpGet(uri), callback);
}
public Future<JSONObject> executeJSONObject(AsyncHttpRequest req) {
return executeJSONObject(req, null);
}
public Future<JSONObject> executeJSONObject(AsyncHttpRequest req, final JSONObjectCallback callback) {
return execute(req, new JSONObjectParser(), callback);
}
private <T> void invokeWithAffinity(final RequestCallback<T> callback, SimpleFuture<T> future, final AsyncHttpResponse response, final Exception e, final T result) {
boolean complete;
if (e != null)
complete = future.setComplete(e);
else
complete = future.setComplete(result);
if (!complete)
return;
if (callback != null)
callback.onCompleted(e, response, result);
}
private <T> void invoke(Handler handler, final RequestCallback<T> callback, final SimpleFuture<T> future, final AsyncHttpResponse response, final Exception e, final T result) {
Runnable runnable = new Runnable() {
@Override
public void run() {
invokeWithAffinity(callback, future, response, e, result);
}
};
if (handler == null)
mServer.post(runnable);
else
AsyncServer.post(handler, runnable);
}
private void invokeProgress(final RequestCallback callback, final AsyncHttpResponse response, final int downloaded, final int total) {
if (callback != null)
callback.onProgress(response, downloaded, total);
}
private void invokeConnect(final RequestCallback callback, final AsyncHttpResponse response) {
if (callback != null)
callback.onConnect(response);
}
@Deprecated
public Future<File> get(String uri, final String filename, final FileCallback callback) {
return executeFile(new AsyncHttpGet(uri), filename, callback);
}
@Deprecated
public Future<File> execute(AsyncHttpRequest req, final String filename, final FileCallback callback) {
return executeFile(req, filename, callback);
}
public Future<File> getFile(String uri, final String filename) {
return getFile(uri, filename, null);
}
public Future<File> getFile(String uri, final String filename, final FileCallback callback) {
return executeFile(new AsyncHttpGet(uri), filename, callback);
}
public Future<File> executeFile(AsyncHttpRequest req, final String filename) {
return executeFile(req, filename, null);
}
public Future<File> executeFile(AsyncHttpRequest req, final String filename, final FileCallback callback) {
final Handler handler = req.getHandler();
final File file = new File(filename);
file.getParentFile().mkdirs();
final OutputStream fout;
try {
fout = new BufferedOutputStream(new FileOutputStream(file), 8192);
}
catch (FileNotFoundException e) {
SimpleFuture<File> ret = new SimpleFuture<File>();
ret.setComplete(e);
return ret;
}
final FutureAsyncHttpResponse cancel = new FutureAsyncHttpResponse();
final SimpleFuture<File> ret = new SimpleFuture<File>() {
@Override
public void cancelCleanup() {
try {
cancel.get().setDataCallback(new NullDataCallback());
cancel.get().close();
}
catch (Exception e) {
}
try {
fout.close();
}
catch (Exception e) {
}
file.delete();
}
};
ret.setParent(cancel);
execute(req, 0, cancel, new HttpConnectCallback() {
int mDownloaded = 0;
@Override
public void onConnectCompleted(Exception ex, final AsyncHttpResponse response) {
if (ex != null) {
try {
fout.close();
}
catch (IOException e) {
}
file.delete();
invoke(handler, callback, ret, response, ex, null);
return;
}
invokeConnect(callback, response);
final int contentLength = response.getHeaders().getContentLength();
response.setDataCallback(new OutputStreamDataCallback(fout) {
@Override
public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {
mDownloaded += bb.remaining();
super.onDataAvailable(emitter, bb);
invokeProgress(callback, response, mDownloaded, contentLength);
}
});
response.setEndCallback(new CompletedCallback() {
@Override
public void onCompleted(Exception ex) {
try {
fout.close();
}
catch (IOException e) {
ex = e;
}
if (ex != null) {
file.delete();
invoke(handler, callback, ret, response, ex, null);
}
else {
invoke(handler, callback, ret, response, null, file);
}
}
});
}
});
return ret;
}
private <T> SimpleFuture<T> execute(AsyncHttpRequest req, final AsyncParser<T> parser, final RequestCallback<T> callback) {
final FutureAsyncHttpResponse cancel = new FutureAsyncHttpResponse();
final SimpleFuture<T> ret = new SimpleFuture<T>();
final Handler handler = req.getHandler();
execute(req, 0, cancel, new HttpConnectCallback() {
@Override
public void onConnectCompleted(Exception ex, final AsyncHttpResponse response) {
if (ex != null) {
invoke(handler, callback, ret, response, ex, null);
return;
}
invokeConnect(callback, response);
final int contentLength = response.getHeaders().getContentLength();
Future<T> parsed = parser.parse(response)
.setCallback(new FutureCallback<T>() {
@Override
public void onCompleted(Exception e, T result) {
invoke(handler, callback, ret, response, e, result);
}
});
// reparent to the new parser future
ret.setParent(parsed);
}
});
ret.setParent(cancel);
return ret;
}
public static interface WebSocketConnectCallback {
public void onCompleted(Exception ex, WebSocket webSocket);
}
public Future<WebSocket> websocket(final AsyncHttpRequest req, String protocol, final WebSocketConnectCallback callback) {
WebSocketImpl.addWebSocketUpgradeHeaders(req, protocol);
final SimpleFuture<WebSocket> ret = new SimpleFuture<WebSocket>();
Cancellable connect = execute(req, new HttpConnectCallback() {
@Override
public void onConnectCompleted(Exception ex, AsyncHttpResponse response) {
if (ex != null) {
if (ret.setComplete(ex)) {
if (callback != null)
callback.onCompleted(ex, null);
}
return;
}
WebSocket ws = WebSocketImpl.finishHandshake(req.getHeaders().getHeaders(), response);
if (ws == null) {
if (!ret.setComplete(new Exception("Unable to complete websocket handshake")))
return;
}
else {
if (!ret.setComplete(ws))
return;
}
if (callback != null)
callback.onCompleted(ex, ws);
}
});
ret.setParent(connect);
return ret;
}
public Future<WebSocket> websocket(String uri, String protocol, final WebSocketConnectCallback callback) {
assert callback != null;
final AsyncHttpGet get = new AsyncHttpGet(uri.replace("ws://", "http://").replace("wss://", "https://"));
return websocket(get, protocol, callback);
}
public AsyncServer getServer() {
return mServer;
}
}