diff options
author | Koushik Dutta <koushd@gmail.com> | 2013-03-23 02:44:30 -0700 |
---|---|---|
committer | Koushik Dutta <koushd@gmail.com> | 2013-03-23 02:44:30 -0700 |
commit | a9f3cb26f3065b7c931857ad4a117face4e68ad1 (patch) | |
tree | 5c3f63532e193aa115bbb8bfb9aa3e9ff22c1c74 | |
parent | b8349e870e13195cc8c683c9c7d616dc1cf5dd45 (diff) | |
download | AndroidAsync-a9f3cb26f3065b7c931857ad4a117face4e68ad1.tar.gz AndroidAsync-a9f3cb26f3065b7c931857ad4a117face4e68ad1.tar.bz2 AndroidAsync-a9f3cb26f3065b7c931857ad4a117face4e68ad1.zip |
further refactorings to fixup caching. implement conditional caching (304 not modified)
19 files changed, 547 insertions, 256 deletions
diff --git a/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocket.java b/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocket.java index 2d981ff..8ece9bf 100644 --- a/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocket.java +++ b/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocket.java @@ -24,7 +24,7 @@ import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.callback.WritableCallback; -public class AsyncSSLSocket implements WrapperSocket, IAsyncSSLSocket { +public class AsyncSSLSocket implements AsyncSocketWrapper, IAsyncSSLSocket { AsyncSocket mSocket; BufferedDataEmitter mEmitter; BufferedDataSink mSink; @@ -388,6 +388,11 @@ public class AsyncSSLSocket implements WrapperSocket, IAsyncSSLSocket { return mSocket; } + @Override + public DataEmitter getDataEmitter() { + return mSocket; + } + X509Certificate[] peerCertificates; @Override public X509Certificate[] getPeerCertificates() { diff --git a/AndroidAsync/src/com/koushikdutta/async/AsyncSocketWrapper.java b/AndroidAsync/src/com/koushikdutta/async/AsyncSocketWrapper.java new file mode 100644 index 0000000..c8bcdfa --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/AsyncSocketWrapper.java @@ -0,0 +1,5 @@ +package com.koushikdutta.async; + +public interface AsyncSocketWrapper extends AsyncSocket, DataEmitterWrapper { + public AsyncSocket getSocket(); +} diff --git a/AndroidAsync/src/com/koushikdutta/async/DataEmitterWrapper.java b/AndroidAsync/src/com/koushikdutta/async/DataEmitterWrapper.java new file mode 100644 index 0000000..5b9beee --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/DataEmitterWrapper.java @@ -0,0 +1,5 @@ +package com.koushikdutta.async; + +public interface DataEmitterWrapper extends DataEmitter { + public DataEmitter getDataEmitter(); +} diff --git a/AndroidAsync/src/com/koushikdutta/async/DataWrapperSocket.java b/AndroidAsync/src/com/koushikdutta/async/DataWrapperSocket.java index abd676f..6164faf 100644 --- a/AndroidAsync/src/com/koushikdutta/async/DataWrapperSocket.java +++ b/AndroidAsync/src/com/koushikdutta/async/DataWrapperSocket.java @@ -5,7 +5,7 @@ import java.nio.ByteBuffer; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.WritableCallback; -public class DataWrapperSocket extends FilteredDataEmitter implements WrapperSocket { +public class DataWrapperSocket extends FilteredDataEmitter implements AsyncSocketWrapper { private AsyncSocket mSocket; public void setSocket(AsyncSocket socket) { mSocket = socket; diff --git a/AndroidAsync/src/com/koushikdutta/async/FilteredDataEmitter.java b/AndroidAsync/src/com/koushikdutta/async/FilteredDataEmitter.java index 496004a..eac9b5b 100644 --- a/AndroidAsync/src/com/koushikdutta/async/FilteredDataEmitter.java +++ b/AndroidAsync/src/com/koushikdutta/async/FilteredDataEmitter.java @@ -1,10 +1,13 @@ package com.koushikdutta.async; +import junit.framework.Assert; + import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.DataCallback; -public class FilteredDataEmitter implements DataEmitter, DataCallback { +public class FilteredDataEmitter implements DataEmitter, DataCallback, DataEmitterWrapper { DataEmitter mEmitter; + @Override public DataEmitter getDataEmitter() { return mEmitter; } @@ -20,6 +23,8 @@ public class FilteredDataEmitter implements DataEmitter, DataCallback { } mEmitter = emitter; mEmitter.setDataCallback(this); +// mEndCallback = mEmitter.getEndCallback(); +// Assert.assertNull(mEndCallback); mEmitter.setEndCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { diff --git a/AndroidAsync/src/com/koushikdutta/async/SimpleWrapperSocket.java b/AndroidAsync/src/com/koushikdutta/async/SimpleWrapperSocket.java index 6adfb2c..0ed6c32 100644 --- a/AndroidAsync/src/com/koushikdutta/async/SimpleWrapperSocket.java +++ b/AndroidAsync/src/com/koushikdutta/async/SimpleWrapperSocket.java @@ -6,7 +6,7 @@ import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.callback.WritableCallback; -public class SimpleWrapperSocket implements WrapperSocket { +public class SimpleWrapperSocket implements AsyncSocketWrapper { AsyncSocket socket; public void setSocket(AsyncSocket socket) { this.socket = socket; @@ -101,4 +101,9 @@ public class SimpleWrapperSocket implements WrapperSocket { public AsyncSocket getSocket() { return socket; } + + @Override + public DataEmitter getDataEmitter() { + return socket; + } } diff --git a/AndroidAsync/src/com/koushikdutta/async/Util.java b/AndroidAsync/src/com/koushikdutta/async/Util.java index beced7e..13436cf 100644 --- a/AndroidAsync/src/com/koushikdutta/async/Util.java +++ b/AndroidAsync/src/com/koushikdutta/async/Util.java @@ -169,11 +169,22 @@ public class Util { public static AsyncSocket getWrappedSocket(AsyncSocket socket, Class wrappedClass) { if (wrappedClass.isInstance(socket)) return socket; - while (socket instanceof WrapperSocket) { - socket = ((WrapperSocket)socket).getSocket(); + while (socket instanceof AsyncSocketWrapper) { + socket = ((AsyncSocketWrapper)socket).getSocket(); if (wrappedClass.isInstance(socket)) return socket; } return null; } + + public static DataEmitter getWrappedDataEmitter(DataEmitter emitter, Class wrappedClass) { + if (wrappedClass.isInstance(emitter)) + return emitter; + while (emitter instanceof DataEmitterWrapper) { + emitter = ((AsyncSocketWrapper)emitter).getSocket(); + if (wrappedClass.isInstance(emitter)) + return emitter; + } + return null; + } } diff --git a/AndroidAsync/src/com/koushikdutta/async/WrapperSocket.java b/AndroidAsync/src/com/koushikdutta/async/WrapperSocket.java deleted file mode 100644 index 4d4eb39..0000000 --- a/AndroidAsync/src/com/koushikdutta/async/WrapperSocket.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.koushikdutta.async; - -public interface WrapperSocket extends AsyncSocket { - public AsyncSocket getSocket(); -} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java index fef07a3..4cd4b48 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java @@ -8,7 +8,6 @@ import java.net.HttpURLConnection; import java.net.URI; import java.nio.ByteBuffer; import java.util.ArrayList; -import java.util.HashMap; import java.util.concurrent.CancellationException; import java.util.concurrent.TimeoutException; @@ -34,7 +33,9 @@ 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.http.AsyncHttpClientMiddleware.OnRequestCompleteData; import com.koushikdutta.async.http.libcore.RawHeaders; +import com.koushikdutta.async.http.libcore.ResponseHeaders; import com.koushikdutta.async.stream.OutputStreamDataCallback; public class AsyncHttpClient { @@ -54,24 +55,6 @@ public class AsyncHttpClient { mMiddleware.add(0, middleware); } } - - HashMap<String, Integer> protocolPort = new HashMap<String, Integer>(); - - public void setProtocolPort(String protocol, int port) { - protocolPort.put(protocol, port); - } - - public int getProtocolPort(URI uri) { - if (uri.getPort() == -1) { - Integer mapped = protocolPort.get(uri.getScheme()); - if (mapped == null) - return -1; - return mapped; - } - else { - return uri.getPort(); - } - } AsyncServer mServer; public AsyncHttpClient(AsyncServer server) { @@ -125,7 +108,8 @@ public class AsyncHttpClient { return; } final URI uri = request.getUri(); - final Bundle state = new Bundle(); + final OnRequestCompleteData data = new OnRequestCompleteData(); + data.request = request; final InternalConnectCallback socketConnected = new InternalConnectCallback() { Object scheduled; @@ -145,19 +129,37 @@ public class AsyncHttpClient { } @Override - public void onConnectCompleted(Exception ex, AsyncSocket socket) { + public void onConnectCompleted(Exception ex, AsyncSocket _socket) { if (cancel.isCanceled()) { - if (socket != null) - socket.close(); + 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); + } + protected void onHeadersReceived() { try { if (cancel.isCanceled()) @@ -165,11 +167,14 @@ public class AsyncHttpClient { if (scheduled != null) mServer.removeAllCallbacks(scheduled); - RawHeaders headers = getRawHeaders(); + data.headers = mHeaders; for (AsyncHttpClientMiddleware middleware: mMiddleware) { - middleware.onHeadersReceived(state, getSocket(), request, getHeaders()); + middleware.onHeadersReceived(data); } + mHeaders = data.headers; + 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")); @@ -209,8 +214,9 @@ public class AsyncHttpClient { reportConnectedCompleted(cancel, ex, null, callback); } + data.exception = ex; for (AsyncHttpClientMiddleware middleware: mMiddleware) { - middleware.onRequestComplete(state, socket, request, getHeaders(), ex); + middleware.onRequestComplete(data); } } @@ -229,18 +235,13 @@ public class AsyncHttpClient { } }; - for (AsyncHttpClientMiddleware middleware: mMiddleware) { - AsyncSocket newSocket = middleware.onSocket(state, socket, request); - if (newSocket != null) - socket = newSocket; - } - ret.setSocket(socket); } }; - + data.connectCallback = socketConnected; + for (AsyncHttpClientMiddleware middleware: mMiddleware) { - if (null != (cancel.socketCancelable = middleware.getSocket(state, request, socketConnected))) + if (null != (cancel.socketCancelable = middleware.getSocket(data))) return; } Assert.fail(); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClientMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClientMiddleware.java index 8963629..847d57e 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClientMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClientMiddleware.java @@ -4,12 +4,36 @@ import android.os.Bundle; import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.Cancelable; +import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.callback.ConnectCallback; import com.koushikdutta.async.http.libcore.ResponseHeaders; public interface AsyncHttpClientMiddleware { - public Cancelable getSocket(Bundle state, AsyncHttpRequest request, final ConnectCallback callback); - public AsyncSocket onSocket(Bundle state, AsyncSocket socket, AsyncHttpRequest request); - public void onHeadersReceived(Bundle state, AsyncSocket socket, AsyncHttpRequest request, ResponseHeaders headers); - public void onRequestComplete(Bundle state, AsyncSocket socket, AsyncHttpRequest request, ResponseHeaders headers, Exception ex); + public static class GetSocketData { + Bundle state = new Bundle(); + AsyncHttpRequest request; + ConnectCallback connectCallback; + } + + public static class OnSocketData extends GetSocketData { + AsyncSocket socket; + } + + public static class OnHeadersReceivedData extends OnSocketData { + ResponseHeaders headers; + } + + public static class OnBodyData extends OnHeadersReceivedData { + DataEmitter bodyEmitter; + } + + public static class OnRequestCompleteData extends OnBodyData { + Exception exception; + } + + public Cancelable getSocket(GetSocketData data); + public void onSocket(OnSocketData data); + public void onHeadersReceived(OnHeadersReceivedData data); + public void onBodyDecoder(OnBodyData data); + public void onRequestComplete(OnRequestCompleteData data); } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java index 1997e19..151d19c 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java @@ -20,11 +20,6 @@ import com.koushikdutta.async.http.libcore.RawHeaders; import com.koushikdutta.async.http.libcore.ResponseHeaders; abstract class AsyncHttpResponseImpl extends FilteredDataEmitter implements AsyncHttpResponse { - private RawHeaders mRawHeaders = new RawHeaders(); - public RawHeaders getRawHeaders() { - return mRawHeaders; - } - private AsyncHttpRequestBody mWriter; public AsyncSocket getSocket() { @@ -90,13 +85,14 @@ abstract class AsyncHttpResponseImpl extends FilteredDataEmitter implements Asyn protected abstract void onHeadersReceived(); StringCallback mHeaderCallback = new StringCallback() { + private RawHeaders mRawHeaders = new RawHeaders(); @Override public void onStringAvailable(String s) { try { if (mRawHeaders.getStatusLine() == null) { mRawHeaders.setStatusLine(s); } - else if (!"\r".equals(s)){ + else if (!"\r".equals(s)) { mRawHeaders.addLine(s); } else { @@ -137,7 +133,7 @@ abstract class AsyncHttpResponseImpl extends FilteredDataEmitter implements Asyn private AsyncHttpRequest mRequest; private AsyncSocket mSocket; - private ResponseHeaders mHeaders; + ResponseHeaders mHeaders; public AsyncHttpResponseImpl(AsyncHttpRequest request) { mRequest = request; } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncSSLSocketMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncSSLSocketMiddleware.java index c8844c1..1fe5c81 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncSSLSocketMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncSSLSocketMiddleware.java @@ -1,27 +1,28 @@ package com.koushikdutta.async.http; -import android.os.Bundle; +import java.net.URI; import com.koushikdutta.async.AsyncSSLSocket; import com.koushikdutta.async.AsyncSocket; -import com.koushikdutta.async.IAsyncSSLSocket; +import com.koushikdutta.async.callback.ConnectCallback; -public class AsyncSSLSocketMiddleware extends SimpleMiddleware { - AsyncHttpClient mClient; +public class AsyncSSLSocketMiddleware extends AsyncSocketMiddleware { public AsyncSSLSocketMiddleware(AsyncHttpClient client) { - mClient = client; - mClient.setProtocolPort("https", 443); + super(client, "https", 443); } - + @Override - public AsyncSocket onSocket(Bundle state, AsyncSocket socket, AsyncHttpRequest request) { - if (!request.getUri().getScheme().equals("https")) - return super.onSocket(state, socket, request); - - // don't wrap anything that is already an ssl socket - if (com.koushikdutta.async.Util.getWrappedSocket(socket, IAsyncSSLSocket.class) != null) - return super.onSocket(state, socket, request); - - return new AsyncSSLSocket(socket, request.getUri().getHost(), mClient.getProtocolPort(request.getUri())); + protected ConnectCallback wrapCallback(final ConnectCallback callback, final URI uri, final int port) { + return new ConnectCallback() { + @Override + public void onConnectCompleted(Exception ex, AsyncSocket socket) { + if (ex == null) { + callback.onConnectCompleted(ex, new AsyncSSLSocket(socket, uri.getHost(), port)); + } + else { + callback.onConnectCompleted(ex, socket); + } + } + }; } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncSocketMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncSocketMiddleware.java index 87f4a39..cbf7031 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncSocketMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncSocketMiddleware.java @@ -4,21 +4,36 @@ import java.net.URI; import java.util.HashSet; import java.util.Hashtable; -import android.os.Bundle; import android.util.Log; -import com.koushikdutta.async.AsyncNetworkSocket; import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.Cancelable; import com.koushikdutta.async.SimpleCancelable; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.ConnectCallback; -import com.koushikdutta.async.http.libcore.ResponseHeaders; public class AsyncSocketMiddleware extends SimpleMiddleware { - public AsyncSocketMiddleware(AsyncHttpClient client) { + String scheme; + int port; + public AsyncSocketMiddleware(AsyncHttpClient client, String scheme, int port) { mClient = client; - mClient.setProtocolPort("http", 80); + this.scheme = scheme; + this.port = port; + } + + public int getSchemePort(URI uri) { + if (!uri.getScheme().equals(scheme)) + return -1; + if (uri.getPort() == -1) { + return port; + } + else { + return uri.getPort(); + } + } + + public AsyncSocketMiddleware(AsyncHttpClient client) { + this(client, "http", 80); } AsyncHttpClient mClient; @@ -29,13 +44,15 @@ public class AsyncSocketMiddleware extends SimpleMiddleware { } @Override - public Cancelable getSocket(Bundle state, AsyncHttpRequest request, final ConnectCallback callback) { - final URI uri = request.getUri(); - final int port = mClient.getProtocolPort(uri); + public Cancelable getSocket(final GetSocketData data) { + final URI uri = data.request.getUri(); + final int port = getSchemePort(data.request.getUri()); if (port == -1) { return null; } final String lookup = uri.getScheme() + "//" + uri.getHost() + ":" + port; + + data.state.putBoolean(getClass().getCanonicalName() + ".owned", true); HashSet<AsyncSocket> sockets = mSockets.get(lookup); if (sockets != null) { @@ -48,7 +65,7 @@ public class AsyncSocketMiddleware extends SimpleMiddleware { @Override public void run() { Log.i("AsyncHttpSocket", "Reusing socket."); - callback.onConnectCompleted(null, socket); + data.connectCallback.onConnectCompleted(null, socket); } }); // just a noop/dummy, as this can't actually be cancelled. @@ -58,28 +75,30 @@ public class AsyncSocketMiddleware extends SimpleMiddleware { } } - return mClient.getServer().connectSocket(uri.getHost(), port, callback); + return mClient.getServer().connectSocket(uri.getHost(), port, wrapCallback(data.connectCallback, uri, port)); } @Override - public void onRequestComplete(Bundle state, final AsyncSocket socket, AsyncHttpRequest request, ResponseHeaders headers, Exception ex) { - if (com.koushikdutta.async.Util.getWrappedSocket(socket, AsyncNetworkSocket.class) == null) { - Log.i("AsyncHttpSocket", getClass().getCanonicalName() + " Not keeping non-owned socket: " + state.getString("socket.owner")); + public void onRequestComplete(final OnRequestCompleteData data) { + if (!data.state.getBoolean(getClass().getCanonicalName() + ".owned", false)) { + Log.i("AsyncHttpSocket", getClass().getCanonicalName() + " Not keeping non-owned socket: " + data.state.getString("socket.owner")); return; } - if (ex != null || !socket.isOpen()) { - socket.close(); + if (data.exception != null || !data.socket.isOpen()) { + data.socket.close(); return; } - String kas = headers.getConnection(); + String kas = data.headers.getConnection(); if (kas == null || !"keep-alive".toLowerCase().equals(kas.toLowerCase())) { - socket.close(); + data.socket.close(); return; } - final URI uri = request.getUri(); - final int port = mClient.getProtocolPort(uri); + Log.i("AsynchttpSocket", "recycling"); + + final URI uri = data.request.getUri(); + final int port = getSchemePort(data.request.getUri()); final String lookup = uri.getScheme() + "//" + uri.getHost() + ":" + port; HashSet<AsyncSocket> sockets = mSockets.get(lookup); if (sockets == null) { @@ -88,14 +107,14 @@ public class AsyncSocketMiddleware extends SimpleMiddleware { } final HashSet<AsyncSocket> ss = sockets; synchronized (sockets) { - sockets.add(socket); - socket.setClosedCallback(new CompletedCallback() { + sockets.add(data.socket); + data.socket.setClosedCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { synchronized (ss) { - ss.remove(socket); + ss.remove(data.socket); } - socket.setClosedCallback(null); + data.socket.setClosedCallback(null); } }); } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/ResponseCacheMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/ResponseCacheMiddleware.java index 1af857a..4171db0 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/ResponseCacheMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/ResponseCacheMiddleware.java @@ -31,24 +31,26 @@ import java.util.Map; import javax.net.ssl.SSLPeerUnverifiedException; -import junit.framework.Assert; -import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; import android.util.Base64; +import android.util.Log; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.Cancelable; import com.koushikdutta.async.DataEmitter; +import com.koushikdutta.async.DataWrapperSocket; +import com.koushikdutta.async.FilteredDataEmitter; import com.koushikdutta.async.IAsyncSSLSocket; import com.koushikdutta.async.SimpleCancelable; -import com.koushikdutta.async.DataWrapperSocket; import com.koushikdutta.async.callback.CompletedCallback; -import com.koushikdutta.async.callback.ConnectCallback; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.callback.WritableCallback; import com.koushikdutta.async.http.libcore.RawHeaders; import com.koushikdutta.async.http.libcore.ResponseHeaders; +import com.koushikdutta.async.http.libcore.ResponseSource; import com.koushikdutta.async.util.cache.Charsets; import com.koushikdutta.async.util.cache.DiskLruCache; import com.koushikdutta.async.util.cache.StrictLineReader; @@ -72,6 +74,7 @@ public class ResponseCacheMiddleware extends SimpleMiddleware { ResponseCacheMiddleware ret = new ResponseCacheMiddleware(); ret.client = client; ret.cache = DiskLruCache.open(cacheDir, VERSION, ENTRY_COUNT, size); + client.insertMiddleware(ret); return ret; } @@ -105,73 +108,6 @@ public class ResponseCacheMiddleware extends SimpleMiddleware { } } - private static class CachingSocket extends DataWrapperSocket { - CacheRequestImpl cacheRequest; - ByteArrayOutputStream outputStream; - ByteBufferList cached; - - public CachingSocket() { - reset(); - } - @Override - public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { - if (cached != null) { - com.koushikdutta.async.Util.emitAllData(this, cached); - // couldn't emit it all, so just wait for another day... - if (cached.remaining() > 0) - return; - cached = null; - } - - // write to cache... any data not consumed needs to be retained for the next callback - OutputStream outputStream = this.outputStream; - try { - if (outputStream == null && cacheRequest != null) - outputStream = cacheRequest.getBody(); - if (outputStream != null) { - for (ByteBuffer b: bb) { - outputStream.write(b.array(), b.arrayOffset() + b.position(), b.remaining()); - } - } - } - catch (Exception e) { - outputStream = null; - abort(); - } - - super.onDataAvailable(emitter, bb); - - if (outputStream != null && bb.remaining() > 0) { - cached = new ByteBufferList(); - cached.add(bb); - bb.clear(); - } - } - - public void abort() { - if (cacheRequest != null) { - cacheRequest.abort(); - cacheRequest = null; - } - - outputStream = null; - } - - public void commit() { - if (cacheRequest != null) { - try { - cacheRequest.getBody().close(); - } - catch (Exception e) { - } - } - } - - public void reset() { - outputStream = new ByteArrayOutputStream(); - cacheRequest = null; - } - } private class CachedSocket implements AsyncSocket { CacheResponse cacheResponse; @@ -316,15 +252,37 @@ public class ResponseCacheMiddleware extends SimpleMiddleware { } } -// private static final String LOGTAG = "AsyncHttpCache"; + public static class CacheData implements Parcelable { + CacheResponse candidate; + ResponseHeaders cachedResponseHeaders; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + } + + } + + private static final String LOGTAG = "AsyncHttpCache"; + // step 1) see if we can serve request from the cache directly. + // also see if this can be turned into a conditional cache request. @Override - public Cancelable getSocket(Bundle state, AsyncHttpRequest request, final ConnectCallback callback) { + public Cancelable getSocket(final GetSocketData data) { + if (cache == null) + return null; + if (!caching) return null; + if (data.request.getHeaders().isNoCache()) + return null; // Log.i(LOGTAG, "getting cache socket: " + request.getUri().toString()); - String key = uriToKey(request.getUri()); + String key = uriToKey(data.request.getUri()); DiskLruCache.Snapshot snapshot; Entry entry; try { @@ -339,106 +297,302 @@ public class ResponseCacheMiddleware extends SimpleMiddleware { return null; } - if (!entry.matches(request.getUri(), request.getMethod(), request.getHeaders().getHeaders().toMultimap())) { + if (!entry.matches(data.request.getUri(), data.request.getMethod(), data.request.getHeaders().getHeaders().toMultimap())) { snapshot.close(); return null; } + + ResponseSource responseSource = ResponseSource.NETWORK; + + CacheResponse candidate = entry.isHttps() ? new EntrySecureCacheResponse(entry, snapshot) : new EntryCacheResponse(entry, snapshot); + + Map<String, List<String>> responseHeadersMap; + InputStream cachedResponseBody; + try { + responseHeadersMap = candidate.getHeaders(); + cachedResponseBody = candidate.getBody(); + } + catch (Exception e) { + return null; + } + if (responseHeadersMap == null || cachedResponseBody == null) { + try { + cachedResponseBody.close(); + } + catch (Exception e) { + } + return null; + } + + RawHeaders rawResponseHeaders = RawHeaders.fromMultimap(responseHeadersMap); + ResponseHeaders cachedResponseHeaders = new ResponseHeaders(data.request.getUri(), rawResponseHeaders); -// Log.i(LOGTAG, "Serving from cache"); - final CachedSocket socket = entry.isHttps() ? new CachedSSLSocket(new EntrySecureCacheResponse(entry, snapshot)) : new CachedSocket(new EntryCacheResponse(entry, snapshot)); - client.getServer().post(new Runnable() { - @Override - public void run() { - callback.onConnectCompleted(null, socket); - socket.spewInternal(); + //// + if (false) { + final CachedSocket ss = entry.isHttps() ? new CachedSSLSocket((EntrySecureCacheResponse)candidate) : new CachedSocket((EntryCacheResponse)candidate); + + ss.pending.add(ByteBuffer.wrap(rawResponseHeaders.toHeaderString().getBytes())); + + client.getServer().post(new Runnable() { + @Override + public void run() { + data.connectCallback.onConnectCompleted(null, ss); + ss.spewInternal(); + } + }); + + if (true) + return new SimpleCancelable(); + } + /// + + + long now = System.currentTimeMillis(); + responseSource = cachedResponseHeaders.chooseResponseSource(now, data.request.getHeaders()); + + if (responseSource == ResponseSource.CACHE) { + final CachedSocket socket = entry.isHttps() ? new CachedSSLSocket((EntrySecureCacheResponse)candidate) : new CachedSocket((EntryCacheResponse)candidate); + + client.getServer().post(new Runnable() { + @Override + public void run() { + data.connectCallback.onConnectCompleted(null, socket); + socket.spewInternal(); + } + }); + } + else if (responseSource == ResponseSource.CONDITIONAL_CACHE) { + CacheData cacheData = new CacheData(); + cacheData.cachedResponseHeaders = cachedResponseHeaders; + cacheData.candidate = candidate; + data.state.putParcelable("cache-data", cacheData); + + return null; + } + else { + // NETWORK or other + try { + cachedResponseBody.close(); + } + catch (Exception e) { } - }); + return null; + } + return new SimpleCancelable(); } - @Override - public AsyncSocket onSocket(Bundle state, AsyncSocket socket, AsyncHttpRequest request) { - if (!caching) - return socket; + private static class BodyCacher extends FilteredDataEmitter implements Parcelable { + CacheRequestImpl cacheRequest; + ByteBufferList cached; + + @Override + public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { + if (cached != null) { + com.koushikdutta.async.Util.emitAllData(this, cached); + // couldn't emit it all, so just wait for another day... + if (cached.remaining() > 0) + return; + cached = null; + } - // dont cache socket served from cache - if (com.koushikdutta.async.Util.getWrappedSocket(socket, CachedSocket.class) != null) - return socket; + // write to cache... any data not consumed needs to be retained for the next callback + try { + if (cacheRequest != null) { + OutputStream outputStream = cacheRequest.getBody(); + if (outputStream != null) { + for (ByteBuffer b: bb) { + outputStream.write(b.array(), b.arrayOffset() + b.position(), b.remaining()); + } + } + else { + abort(); + } + } + } + catch (Exception e) { + abort(); + } + + super.onDataAvailable(emitter, bb); + + if (cacheRequest != null && bb.remaining() > 0) { + cached = new ByteBufferList(); + cached.add(bb); + bb.clear(); + } + } - if (cache == null) - return socket; + public void abort() { + if (cacheRequest != null) { + cacheRequest.abort(); + cacheRequest = null; + } + } - if (!request.getMethod().equals(AsyncHttpGet.METHOD)) - return socket; + public void commit() { + if (cacheRequest != null) { + try { + cacheRequest.getBody().close(); + } + catch (Exception e) { + } + } + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + } + } + + private static class BodySpewer extends FilteredDataEmitter { + CacheResponse cacheResponse; + + void spewInternal() { + if (pending.remaining() > 0) { + com.koushikdutta.async.Util.emitAllData(BodySpewer.this, pending); + if (pending.remaining() > 0) + return; + } + + // fill pending + try { + while (pending.remaining() == 0) { + ByteBuffer buffer = ByteBuffer.allocate(8192); + int read = cacheResponse.getBody().read(buffer.array()); + if (read == -1) { + allowEnd = true; + report(null); + return; + } + buffer.limit(read); + pending.add(buffer); + com.koushikdutta.async.Util.emitAllData(BodySpewer.this, pending); + } + } + catch (IOException e) { + allowEnd = true; + report(e); + } + } + + ByteBufferList pending = new ByteBufferList(); + void spew() { + getServer().post(new Runnable() { + @Override + public void run() { + spewInternal(); + } + }); + } - try { - CachingSocket ret = new CachingSocket(); - ret.setSocket(socket); - return ret; + boolean paused; + @Override + public void resume() { + paused = false; + spew(); } - catch (Exception e) { - return socket; + + @Override + public boolean isPaused() { + return paused; + } + + boolean allowEnd; + @Override + protected void report(Exception e) { + if (!allowEnd) + return; + super.report(e); } } + + // step 3) if this is a conditional cache request, serve it from the cache if necessary + // otherwise, see if it is cacheable @Override - public void onHeadersReceived(Bundle state, AsyncSocket socket, AsyncHttpRequest request, ResponseHeaders headers) { -// Log.i(LOGTAG, "headers: " + request.getUri().toString()); - CachingSocket caching = (CachingSocket)com.koushikdutta.async.Util.getWrappedSocket(socket, CachingSocket.class); - if (caching == null) + public void onBodyDecoder(OnBodyData data) { + CacheData cacheData = data.state.getParcelable("cache-data"); + if (cacheData != null) { + if (cacheData.cachedResponseHeaders.validate(data.headers)) { + data.headers = cacheData.cachedResponseHeaders.combine(data.headers); + data.headers.getHeaders().setStatusLine(cacheData.cachedResponseHeaders.getHeaders().getStatusLine()); + + BodySpewer bodySpewer = new BodySpewer(); + bodySpewer.cacheResponse = cacheData.candidate; + bodySpewer.setDataEmitter(data.bodyEmitter); + data.bodyEmitter = bodySpewer; + bodySpewer.spew(); + return; + } + + // did not validate, so fall through and cache the response + data.state.remove("cache-data"); + } + + if (!caching) return; - if (!headers.isCacheable(request.getHeaders())) { - caching.abort(); + if (!data.headers.isCacheable(data.request.getHeaders())) return; - } - - String key = uriToKey(request.getUri()); - RawHeaders varyHeaders = request.getHeaders().getHeaders().getAll(headers.getVaryFields()); - Entry entry = new Entry(request.getUri(), varyHeaders, request, headers); + + String key = uriToKey(data.request.getUri()); + RawHeaders varyHeaders = data.request.getHeaders().getHeaders().getAll(data.headers.getVaryFields()); + Entry entry = new Entry(data.request.getUri(), varyHeaders, data.request, data.headers); DiskLruCache.Editor editor = null; + BodyCacher cacher = new BodyCacher(); try { editor = cache.edit(key); if (editor == null) { -// Log.i(LOGTAG, "can't cache"); - caching.outputStream = null; + // Log.i(LOGTAG, "can't cache"); return; } entry.writeTo(editor); - caching.cacheRequest = new CacheRequestImpl(editor); - Assert.assertNotNull(caching.outputStream); - byte[] bytes = caching.outputStream.toByteArray(); - caching.outputStream = null; - caching.cacheRequest.getBody().write(bytes); + + + cacher.cacheRequest = new CacheRequestImpl(editor); + if (cacher.cacheRequest.getBody() == null) + return; +// cacher.cacheData = + cacher.setDataEmitter(data.bodyEmitter); + data.bodyEmitter = cacher; + + data.state.putParcelable("body-cacher", cacher); } catch (Exception e) { -// Log.e(LOGTAG, "error", e); - caching.outputStream = null; - if (caching.cacheRequest != null) - caching.cacheRequest.abort(); - caching.cacheRequest = null; + // Log.e(LOGTAG, "error", e); + if (cacher.cacheRequest != null) + cacher.cacheRequest.abort(); + cacher.cacheRequest = null; } } + @Override - public void onRequestComplete(Bundle state, AsyncSocket socket, AsyncHttpRequest request, ResponseHeaders headers, Exception ex) { - CachingSocket caching = (CachingSocket)com.koushikdutta.async.Util.getWrappedSocket(socket, CachingSocket.class); - if (caching == null) + public void onRequestComplete(OnRequestCompleteData data) { +// CachingSocket caching = (CachingSocket)com.koushikdutta.async.Util.getWrappedSocket(data.socket, CachingSocket.class); +// if (caching == null) +// return; + + BodyCacher cacher = data.state.getParcelable("body-cacher"); + if (cacher == null) return; // Log.i(LOGTAG, "Cache done: " + ex); try { - if (ex != null) - caching.abort(); + if (data.exception != null) + cacher.abort(); else - caching.commit(); + cacher.commit(); } catch (Exception e) { } - - // reset for socket reuse - caching.reset(); } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/SimpleMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/SimpleMiddleware.java index ba60e58..1930016 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/SimpleMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/SimpleMiddleware.java @@ -1,28 +1,32 @@ package com.koushikdutta.async.http; -import android.os.Bundle; - -import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.Cancelable; -import com.koushikdutta.async.callback.ConnectCallback; -import com.koushikdutta.async.http.libcore.ResponseHeaders; public class SimpleMiddleware implements AsyncHttpClientMiddleware { + @Override - public Cancelable getSocket(Bundle state, AsyncHttpRequest request, ConnectCallback callback) { + public Cancelable getSocket(GetSocketData data) { return null; } @Override - public AsyncSocket onSocket(Bundle state, AsyncSocket socket, AsyncHttpRequest request) { - return null; + public void onSocket(OnSocketData data) { + } @Override - public void onHeadersReceived(Bundle state, AsyncSocket socket, AsyncHttpRequest request, ResponseHeaders headers) { + public void onHeadersReceived(OnHeadersReceivedData data) { + } @Override - public void onRequestComplete(Bundle state, AsyncSocket socket, AsyncHttpRequest request, ResponseHeaders headers, Exception ex) { + public void onBodyDecoder(OnBodyData data) { + } + + @Override + public void onRequestComplete(OnRequestCompleteData data) { + + } + } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/Util.java b/AndroidAsync/src/com/koushikdutta/async/http/Util.java index cafe9fc..67cdee7 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/Util.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/Util.java @@ -1,6 +1,8 @@ package com.koushikdutta.async.http; +import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.DataEmitter; +import com.koushikdutta.async.FilteredDataEmitter; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.http.filter.ChunkedInputFilter; import com.koushikdutta.async.http.filter.ContentLengthFilter; @@ -74,15 +76,24 @@ public class Util { chunker.setDataEmitter(emitter); emitter = chunker; } - else if (server) { - // if this is the server, and the client has not indicated a request body, the client is done - emitter.getServer().post(new Runnable() { - @Override - public void run() { - reporter.onCompleted(null); - } - }); - return emitter; + else { + if (server || headers.getStatusLine().contains("HTTP/1.1")) { + // if this is the server, and the client has not indicated a request body, the client is done + final AsyncServer srv = emitter.getServer(); + final FilteredDataEmitter wrapped = new FilteredDataEmitter() { + { + srv.post(new Runnable() { + @Override + public void run() { + report(null); + } + }); + } + }; + wrapped.setDataEmitter(emitter); + emitter = wrapped; + return emitter; + } } if ("gzip".equals(headers.get("Content-Encoding"))) { @@ -96,7 +107,7 @@ public class Util { emitter = inflater; } - // conversely, if this is the client, and the server has not indicated a request body, we do not report + // conversely, if this is the client (http 1.0), and the server has not indicated a request body, we do not report // the close/end event until the server actually closes the connection. return emitter; } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/libcore/ResponseHeaders.java b/AndroidAsync/src/com/koushikdutta/async/http/libcore/ResponseHeaders.java index 8f7bb65..30f0bf3 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/libcore/ResponseHeaders.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/libcore/ResponseHeaders.java @@ -435,15 +435,15 @@ public final class ResponseHeaders { return ResponseSource.CACHE; } - if (lastModified != null) { + if (etag != null) { + request.setIfNoneMatch(etag); + } + else if (lastModified != null) { request.setIfModifiedSince(lastModified); } else if (servedDate != null) { request.setIfModifiedSince(servedDate); } - if (etag != null) { - request.setIfNoneMatch(etag); - } return request.hasConditions() ? ResponseSource.CONDITIONAL_CACHE diff --git a/AndroidAsync/src/com/koushikdutta/async/http/libcore/ResponseSource.java b/AndroidAsync/src/com/koushikdutta/async/http/libcore/ResponseSource.java index d1eaed4..3501b0f 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/libcore/ResponseSource.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/libcore/ResponseSource.java @@ -16,7 +16,7 @@ package com.koushikdutta.async.http.libcore; -enum ResponseSource { +public enum ResponseSource { /** * Return the response from the cache immediately. diff --git a/AndroidAsyncSample/src/com/koushikdutta/async/sample/MainActivity.java b/AndroidAsyncSample/src/com/koushikdutta/async/sample/MainActivity.java index 115f312..4b2fe88 100644 --- a/AndroidAsyncSample/src/com/koushikdutta/async/sample/MainActivity.java +++ b/AndroidAsyncSample/src/com/koushikdutta/async/sample/MainActivity.java @@ -2,16 +2,22 @@ package com.koushikdutta.async.sample; import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; import java.util.ArrayList; import org.apache.http.NameValuePair; import org.apache.http.message.BasicNameValuePair; +import android.annotation.SuppressLint; import android.app.Activity; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.BitmapDrawable; +import android.net.http.HttpResponseCache; import android.os.Bundle; +import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.MenuItem.OnMenuItemClickListener; @@ -35,11 +41,55 @@ public class MainActivity extends Activity { ImageView desksms; ImageView chart; + @SuppressLint("NewApi") @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - + + new Thread() { + public void run() { + try { + HttpResponseCache cache; + try { + File httpCacheDir = new File(getCacheDir(), "http"); + long httpCacheSize = 10 * 1024 * 1024; // 10 MiB + cache = HttpResponseCache.install(httpCacheDir, httpCacheSize); + } + catch (IOException e) { + Log.i("cache", "HTTP response cache installation failed:" + e); + return; + } + URL url = new URL("https://desksms.appspot.com"); + URLConnection conn = url.openConnection(); + for (String header: conn.getRequestProperties().keySet()) { + System.out.println(header + ": "); + for (String value: conn.getRequestProperties().get(header)) { + System.out.println(value); + } + } + for (String header: conn.getHeaderFields().keySet()) { + System.out.println(header + ": " + conn.getHeaderField(header)); + } + InputStream in = conn.getInputStream(); + int count = 0; + while (in.read() != -1) { + count++; + } + in.close(); + System.out.println("count: " + count); + + System.out.println("cache count: " + cache.getHitCount()); + System.out.println("network count: " + cache.getNetworkCount()); + } + catch (Exception e) { + e.printStackTrace(); + } + }; + }.start(); + + if (cacher == null) { + try { cacher = ResponseCacheMiddleware.addCache(AsyncHttpClient.getDefaultInstance(), getFileStreamPath("asynccache"), 1024 * 1024 * 10); cacher.setCaching(false); @@ -149,8 +199,8 @@ public class MainActivity extends Activity { chart.setImageBitmap(null); getFile(rommanager, "https://raw.github.com/koush/AndroidAsync/master/rommanager.png", getFileStreamPath(randomFile()).getAbsolutePath()); - getFile(tether, "https://raw.github.com/koush/AndroidAsync/master/tether.png", getFileStreamPath(randomFile()).getAbsolutePath()); - getFile(desksms, "https://raw.github.com/koush/AndroidAsync/master/desksms.png", getFileStreamPath(randomFile()).getAbsolutePath()); - getChartFile(); +// getFile(tether, "https://raw.github.com/koush/AndroidAsync/master/tether.png", getFileStreamPath(randomFile()).getAbsolutePath()); +// getFile(desksms, "https://raw.github.com/koush/AndroidAsync/master/desksms.png", getFileStreamPath(randomFile()).getAbsolutePath()); +// getChartFile(); } } |