package com.koushikdutta.async.http;
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.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.concurrent.TimeoutException;
import junit.framework.Assert;
import org.json.JSONException;
import org.json.JSONObject;
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.DataSink;
import com.koushikdutta.async.NullDataCallback;
import com.koushikdutta.async.callback.CompletedCallback;
import com.koushikdutta.async.callback.ConnectCallback;
import com.koushikdutta.async.callback.DataCallback;
import com.koushikdutta.async.callback.RequestCallback;
import com.koushikdutta.async.future.Cancellable;
import com.koushikdutta.async.future.Future;
import com.koushikdutta.async.future.SimpleCancelable;
import com.koushikdutta.async.future.SimpleFuture;
import com.koushikdutta.async.http.AsyncHttpClientMiddleware.OnRequestCompleteData;
import com.koushikdutta.async.http.libcore.RawHeaders;
import com.koushikdutta.async.stream.OutputStreamDataCallback;
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) {
synchronized (mMiddleware) {
mMiddleware.add(0, middleware);
}
}
AsyncServer mServer;
public AsyncHttpClient(AsyncServer server) {
mServer = server;
insertMiddleware(new AsyncSocketMiddleware(this));
insertMiddleware(new AsyncSSLSocketMiddleware(this));
}
private static abstract class InternalConnectCallback implements ConnectCallback {
boolean reused = false;
}
public Cancellable execute(final AsyncHttpRequest request, final HttpConnectCallback callback) {
CancelableImpl ret;
execute(request, callback, 0, ret = new CancelableImpl());
return ret;
}
private static final String LOGTAG = "AsyncHttp";
private static class CancelableImpl extends SimpleCancelable {
public Cancellable socketCancelable;
public AsyncSocket socket;
@Override
public boolean cancel() {
if (!super.cancel())
return false;
if (socketCancelable != null) {
socketCancelable.cancel();
}
if (socket != null)
socket.close();
return true;
}
}
private void reportConnectedCompleted(CancelableImpl cancel, Exception ex, AsyncHttpResponseImpl response, final HttpConnectCallback callback) {
if (cancel.setComplete()) {
callback.onConnectCompleted(ex, response);
return;
}
// 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 HttpConnectCallback callback, final int redirectCount, final CancelableImpl cancel) {
if (redirectCount > 5) {
reportConnectedCompleted(cancel, new Exception("too many redirects"), null, callback);
return;
}
final URI uri = request.getUri();
final OnRequestCompleteData data = new OnRequestCompleteData();
data.request = request;
final InternalConnectCallback socketConnected = new InternalConnectCallback() {
Object scheduled = null;
{
if (request.getTimeout() > 0) {
scheduled = mServer.postDelayed(new Runnable() {
@Override
public void run() {
if (cancel.cancel())
reportConnectedCompleted(cancel, new TimeoutException(), null, callback);
}
}, request.getTimeout());
}
}
@Override
public void onConnectCompleted(Exception ex, AsyncSocket _socket) {
if (cancel.isCancelled()) {
if (_socket != null)
_socket.close();
return;
}
data.socket = _socket;
for (AsyncHttpClientMiddleware middleware: mMiddleware) {
middleware.onSocket(data);
}
AsyncSocket socket = data.socket;
cancel.socket = socket;
if (ex != null) {
reportConnectedCompleted(cancel, ex, null, callback);
return;
}
final AsyncHttpResponseImpl ret = new AsyncHttpResponseImpl(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());
execute(newReq, callback, redirectCount + 1, cancel);
setDataCallback(new NullDataCallback());
return;
}
// at this point the headers are done being modified
reportConnectedCompleted(cancel, null, this, callback);
}
protected void onHeadersReceived() {
try {
if (cancel.isCancelled())
return;
if (scheduled != null)
mServer.removeAllCallbacks(scheduled);
// allow the middleware to massage the headers before the body is decoded
data.headers = mHeaders;
for (AsyncHttpClientMiddleware middleware: mMiddleware) {
middleware.onHeadersReceived(data);
}
mHeaders = data.headers;
RawHeaders headers = mHeaders.getHeaders();
// 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, callback);
}
};
@Override
protected void report(Exception ex) {
if (cancel.isCancelled())
return;
if (ex instanceof AsyncSSLException) {
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, callback);
}
data.exception = ex;
for (AsyncHttpClientMiddleware middleware: mMiddleware) {
middleware.onRequestComplete(data);
}
}
@Override
public AsyncSocket detachSocket() {
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);
}
};
data.connectCallback = socketConnected;
for (AsyncHttpClientMiddleware middleware: mMiddleware) {
if (null != (cancel.socketCancelable = middleware.getSocket(data)))
return;
}
Assert.fail();
}
public Cancellable execute(URI uri, final HttpConnectCallback callback) {
return execute(new AsyncHttpGet(uri), callback);
}
public Cancellable 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> {
}
private interface ResultConvert<T> {
public T convert(ByteBufferList bb) throws Exception;
}
public Future<ByteBufferList> get(String uri, final DownloadCallback callback) {
return get(uri, callback, new ResultConvert<ByteBufferList>() {
@Override
public ByteBufferList convert(ByteBufferList b) {
return b;
}
});
}
public Future<String> get(String uri, final StringCallback callback) {
return execute(new AsyncHttpGet(uri), callback);
}
public Future<String> execute(AsyncHttpRequest req, final StringCallback callback) {
return execute(req, callback, new ResultConvert<String>() {
@Override
public String convert(ByteBufferList bb) {
StringBuilder builder = new StringBuilder();
for (ByteBuffer b: bb) {
builder.append(new String(b.array(), b.arrayOffset() + b.position(), b.remaining()));
}
return builder.toString();
}
});
}
public Future<JSONObject> get(String uri, final JSONObjectCallback callback) {
return execute(new AsyncHttpGet(uri), callback);
}
public Future<JSONObject> execute(AsyncHttpRequest req, final JSONObjectCallback callback) {
return execute(req, callback, new ResultConvert<JSONObject>() {
@Override
public JSONObject convert(ByteBufferList bb) throws JSONException {
StringBuilder builder = new StringBuilder();
for (ByteBuffer b: bb) {
builder.append(new String(b.array(), b.arrayOffset() + b.position(), b.remaining()));
}
return new JSONObject(builder.toString());
}
});
}
private void invoke(Handler handler, final RequestCallback callback, final AsyncHttpResponse response, final Exception e, final Object result) {
if (callback == null)
return;
if (handler == null) {
mServer.post(new Runnable() {
@Override
public void run() {
callback.onCompleted(e, response, result);
}
});
return;
}
AsyncServer.post(handler, new Runnable() {
@Override
public void run() {
callback.onCompleted(e, response, result);
}
});
}
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);
}
public Future<File> get(String uri, final String filename, final FileCallback callback) {
return execute(new AsyncHttpGet(uri), filename, callback);
}
public Cancellable get(String uri, final DataSink sink, final CompletedCallback callback) {
sink.setClosedCallback(callback);
return execute(new AsyncHttpGet(URI.create(uri)), new HttpConnectCallback() {
@Override
public void onConnectCompleted(Exception ex, AsyncHttpResponse response) {
if (ex != null) {
callback.onCompleted(ex);
return;
}
com.koushikdutta.async.Util.pump(response, sink, callback);
}
});
}
public Future<File> execute(AsyncHttpRequest req, final String filename, final FileCallback callback) {
final Handler handler = req.getHandler();
final File file = new File(filename);
CancelableImpl cancel = new CancelableImpl();
final SimpleFuture<File> ret = new SimpleFuture<File>() {
@Override
public boolean cancel() {
if (!super.cancel())
return false;
file.delete();
return true;
}
};
ret.setParent(cancel);
file.getParentFile().mkdirs();
final OutputStream fout;
try {
fout = new BufferedOutputStream(new FileOutputStream(file), 8192);
}
catch (FileNotFoundException e) {
if (ret.setComplete(e))
invoke(handler, callback, null, e, null);
return ret;
}
execute(req, 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();
if (ret.setComplete(ex))
invoke(handler, callback, 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();
if (ret.setComplete(ex))
invoke(handler, callback, response, ex, null);
}
else if (ret.setComplete(file)) {
invoke(handler, callback, response, null, file);
}
}
});
}
}, 0, cancel);
return ret;
}
private <T> SimpleFuture<T> execute(AsyncHttpRequest req, final RequestCallback callback, final ResultConvert<T> convert) {
final SimpleFuture<T> ret = new SimpleFuture<T>();
final Handler handler = req.getHandler();
final CancelableImpl cancel = new CancelableImpl();
execute(req, new HttpConnectCallback() {
int mDownloaded = 0;
ByteBufferList buffer = new ByteBufferList();
@Override
public void onConnectCompleted(Exception ex, final AsyncHttpResponse response) {
if (ex != null) {
if (ret.setComplete(ex))
invoke(handler, callback, response, ex, null);
return;
}
invokeConnect(callback, response);
final int contentLength = response.getHeaders().getContentLength();
response.setDataCallback(new DataCallback() {
@Override
public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {
mDownloaded += bb.remaining();
buffer.add(bb);
bb.clear();
invokeProgress(callback, response, mDownloaded, contentLength);
}
});
response.setEndCallback(new CompletedCallback() {
@Override
public void onCompleted(Exception ex) {
if (ex == null) {
try {
T value = convert.convert(buffer);
if (ret.setComplete(value))
invoke(handler, callback, response, null, value);
return;
}
catch (Exception e) {
ex = e;
}
}
if (ret.setComplete(ex))
invoke(handler, callback, response, ex, null);
}
});
}
}, 0, cancel);
ret.setParent(cancel);
return ret;
}
private <T> Future<T> get(String uri, final RequestCallback callback, final ResultConvert<T> convert) {
return execute(new AsyncHttpGet(URI.create(uri)), callback, convert);
}
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))
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;
}
callback.onCompleted(ex, ws);
}
});
ret.setParent(connect);
return ret;
}
public Future<WebSocket> websocket(String uri, String protocol, final WebSocketConnectCallback callback) {
final AsyncHttpGet get = new AsyncHttpGet(uri);
return websocket(get, protocol, callback);
}
AsyncServer getServer() {
return mServer;
}
}