Index: net/cronet/android/java/src/org/chromium/net/UrlRequest.java |
diff --git a/net/cronet/android/java/src/org/chromium/net/UrlRequest.java b/net/cronet/android/java/src/org/chromium/net/UrlRequest.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..722498f1135b7b28e3d050b601b002b6be48a95b |
--- /dev/null |
+++ b/net/cronet/android/java/src/org/chromium/net/UrlRequest.java |
@@ -0,0 +1,427 @@ |
+// Copyright 2013 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+package org.chromium.net; |
+ |
+import org.apache.http.conn.ConnectTimeoutException; |
+import org.chromium.base.AccessedByNative; |
+import org.chromium.base.CalledByNative; |
+ |
+import java.io.IOException; |
+import java.net.MalformedURLException; |
+import java.net.URL; |
+import java.net.UnknownHostException; |
+import java.nio.ByteBuffer; |
+import java.nio.channels.ReadableByteChannel; |
+import java.nio.channels.WritableByteChannel; |
+import java.util.HashMap; |
+import java.util.Map; |
+import java.util.Map.Entry; |
+import java.util.concurrent.Semaphore; |
+ |
+/** |
+ * Network request using the native http stack implementation. |
+ */ |
+public class UrlRequest { |
+ private static final class ContextLock { |
+ } |
+ |
+ /** |
+ * Must match definitions in the native source. |
+ */ |
+ public static final int REQUEST_PRIORITY_IDLE = 0; |
+ public static final int REQUEST_PRIORITY_LOWEST = 1; |
+ public static final int REQUEST_PRIORITY_LOW = 2; |
+ public static final int REQUEST_PRIORITY_MEDIUM = 3; |
+ public static final int REQUEST_PRIORITY_HIGHEST = 4; |
+ |
+ /** |
+ * Must match definitions in the native source. |
+ */ |
+ private static final int SUCCESS = 0; |
+ private static final int UNKNOWN_ERROR = 1; |
+ private static final int MALFORMED_URL_ERROR = 2; |
+ private static final int CONNECTION_TIMED_OUT = 3; |
+ private static final int UNKNOWN_HOST = 4; |
+ |
+ private static final int UPLOAD_BYTE_BUFFER_SIZE = 32768; |
+ |
+ // TODO(mef) : Rename to follow Google Java style guide: |
+ // In Google Style special prefixes or suffixes, like those seen in the |
+ // examples name_, mName, s_name and kName, are not used. |
+ private final UrlRequestContext mRequestContext; |
+ private final String mUrl; |
+ private final int mPriority; |
+ private final Map<String, String> mHeaders; |
+ private final WritableByteChannel mSink; |
+ |
+ private Map<String, String> mAdditionalHeaders; |
+ private boolean mPostBodySet; |
+ private ReadableByteChannel mPostBodyChannel; |
+ private WritableByteChannel mOutputChannel; |
+ private IOException mSinkException; |
+ private volatile boolean mStarted; |
+ private volatile boolean mCanceled; |
+ private volatile boolean mRecycled; |
+ private volatile boolean mFinished; |
+ private String mContentType; |
+ private long mContentLength; |
+ private Semaphore mAppendChunkSemaphore; |
+ private final ContextLock mLock; |
+ |
+ /** |
+ * This field is accessed exclusively from the native layer. |
+ */ |
+ @AccessedByNative |
+ @SuppressWarnings("unused") |
+ private long mRequest; |
+ |
+ /** |
+ * Constructor. |
+ * |
+ * @param requestContext The context. |
+ * @param url The URL. |
+ * @param priority Request priority, e.g. {@link #REQUEST_PRIORITY_MEDIUM}. |
+ * @param headers HTTP headers. |
+ * @param sink The output channel into which downloaded content will be |
+ * written. |
+ */ |
+ public UrlRequest(UrlRequestContext requestContext, |
+ String url, |
+ int priority, |
+ Map<String, String> headers, |
+ WritableByteChannel sink) { |
+ mRequestContext = requestContext; |
+ mUrl = url; |
+ mPriority = priority; |
+ mHeaders = headers; |
+ mSink = sink; |
+ mLock = new ContextLock(); |
+ nativeInit(mRequestContext, mUrl, mPriority); |
+ mPostBodySet = false; |
+ } |
+ |
+ /** |
+ * Adds a request header. |
+ */ |
+ public void addHeader(String header, String value) { |
+ validateNotStarted(); |
+ if (mAdditionalHeaders == null) { |
+ mAdditionalHeaders = new HashMap<String, String>(); |
+ } |
+ mAdditionalHeaders.put(header, value); |
+ } |
+ |
+ /** |
+ * Sets data to upload as part of a POST request. |
+ * |
+ * @param contentType MIME type of the post content or null if this is not a |
+ * POST. |
+ * @param data The content that needs to be uploaded if this is a POST |
+ * request. |
+ */ |
+ public void setUploadData(String contentType, byte[] data) { |
+ synchronized (mLock) { |
+ validateNotStarted(); |
+ validatePostBodyNotSet(); |
+ nativeSetPostData(contentType, data); |
+ mPostBodySet = true; |
+ } |
+ } |
+ |
+ /** |
+ * Sets a readable byte channel to upload as part of a POST request. |
+ * |
+ * @param contentType MIME type of the post content or null if this is not a |
+ * POST request. |
+ * @param channel The channel to read to read upload data from if this is a |
+ * POST request. |
+ */ |
+ public void setUploadChannel(String contentType, |
+ ReadableByteChannel channel) { |
+ synchronized (mLock) { |
+ validateNotStarted(); |
+ validatePostBodyNotSet(); |
+ nativeBeginChunkedUpload(contentType); |
+ mPostBodyChannel = channel; |
+ mPostBodySet = true; |
+ } |
+ mAppendChunkSemaphore = new Semaphore(0); |
+ } |
+ |
+ public WritableByteChannel getSink() { |
+ return mSink; |
+ } |
+ |
+ public void start() { |
+ synchronized (mLock) { |
+ if (mCanceled) { |
+ return; |
+ } |
+ |
+ validateNotStarted(); |
+ validateNotRecycled(); |
+ |
+ mStarted = true; |
+ |
+ if (mHeaders != null && !mHeaders.isEmpty()) { |
+ for (Entry<String, String> entry : mHeaders.entrySet()) { |
+ nativeAddHeader(entry.getKey(), entry.getValue()); |
+ } |
+ } |
+ |
+ if (mAdditionalHeaders != null && !mAdditionalHeaders.isEmpty()) { |
+ for (Entry<String, String> entry : mAdditionalHeaders.entrySet()) { |
+ nativeAddHeader(entry.getKey(), entry.getValue()); |
+ } |
+ } |
+ |
+ nativeStart(); |
+ } |
+ |
+ if (mPostBodyChannel != null) { |
+ uploadFromChannel(mPostBodyChannel); |
+ } |
+ } |
+ |
+ /** |
+ * Uploads data from a {@code ReadableByteChannel} using chunked transfer |
+ * encoding. |
+ * |
+ * The native call to append a chunk is asynchronous so a semaphore is used |
+ * to delay writing into the buffer again until chromium is finished with it. |
+ * |
+ * @param channel the channel to read data from. |
+ */ |
+ private void uploadFromChannel(ReadableByteChannel channel) { |
+ ByteBuffer buffer = ByteBuffer.allocateDirect(UPLOAD_BYTE_BUFFER_SIZE); |
+ |
+ // The chromium API requires us to specify in advance if a chunk is the last |
+ // one. This extra ByteBuffer is needed to peek ahead and check for the end |
+ // of the channel. |
+ ByteBuffer checkForEnd = ByteBuffer.allocate(1); |
+ |
+ try { |
+ boolean lastChunk; |
+ do { |
+ // First dump in the one byte we read to check for the end of the |
+ // channel. (The first time through the loop the checkForEnd buffer will |
+ // be empty). |
+ checkForEnd.flip(); |
+ buffer.clear(); |
+ buffer.put(checkForEnd); |
+ checkForEnd.clear(); |
+ |
+ channel.read(buffer); |
+ lastChunk = channel.read(checkForEnd) <= 0; |
+ buffer.flip(); |
+ nativeAppendChunk(buffer, buffer.limit(), lastChunk); |
+ |
+ if (lastChunk) { |
+ break; |
+ } |
+ |
+ // Acquire permit before writing to the buffer again to ensure chromium |
+ // is done with it. |
+ mAppendChunkSemaphore.acquire(); |
+ |
+ } while (!lastChunk && !mFinished); |
+ |
+ } catch (IOException e) { |
+ mSinkException = e; |
+ cancel(); |
+ } catch (InterruptedException e) { |
+ mSinkException = new IOException(e); |
+ cancel(); |
+ } finally { |
+ try { |
+ mPostBodyChannel.close(); |
+ } catch (IOException ignore) { |
+ ; |
+ } |
+ } |
+ } |
+ |
+ public void cancel() { |
+ synchronized (mLock) { |
+ if (mCanceled) { |
+ return; |
+ } |
+ |
+ mCanceled = true; |
+ |
+ if (!mRecycled) { |
+ nativeCancel(); |
+ } |
+ } |
+ } |
+ |
+ public boolean isCanceled() { |
+ synchronized (mLock) { |
+ return mCanceled; |
+ } |
+ } |
+ |
+ public boolean isRecycled() { |
+ synchronized (mLock) { |
+ return mRecycled; |
+ } |
+ } |
+ |
+ /** |
+ * Returns an exception if any, or null if the request was completed |
+ * successfully. |
+ */ |
+ public IOException getException() { |
+ if (mSinkException != null) { |
+ return mSinkException; |
+ } |
+ |
+ validateNotRecycled(); |
+ |
+ int errorCode = nativeGetErrorCode(); |
+ switch (errorCode) { |
+ case SUCCESS: |
+ return null; |
+ case UNKNOWN_ERROR: |
+ return new IOException(nativeGetErrorString()); |
+ case MALFORMED_URL_ERROR: |
+ return new MalformedURLException("Malformed URL: " + mUrl); |
+ case CONNECTION_TIMED_OUT: |
+ return new ConnectTimeoutException("Connection timed out"); |
+ case UNKNOWN_HOST: |
+ String host; |
+ try { |
+ host = new URL(mUrl).getHost(); |
+ } catch (MalformedURLException e) { |
+ host = mUrl; |
+ } |
+ return new UnknownHostException("Unknown host: " + host); |
+ default: |
+ throw new IllegalStateException("Unrecognized error code: " + |
+ errorCode); |
+ } |
+ } |
+ |
+ public native int getHttpStatusCode(); |
+ |
+ /** |
+ * Content length as reported by the server. May be -1 or incorrect if the |
+ * server returns the wrong number, which happens even with Google servers. |
+ */ |
+ public long getContentLength() { |
+ return mContentLength; |
+ } |
+ |
+ public String getContentType() { |
+ return mContentType; |
+ } |
+ |
+ /** |
+ * A callback invoked when appending a chunk to the request has completed. |
+ */ |
+ @CalledByNative |
+ protected void onAppendChunkCompleted() { |
+ mAppendChunkSemaphore.release(); |
+ } |
+ |
+ /** |
+ * A callback invoked when the first chunk of the response has arrived. |
+ */ |
+ @CalledByNative |
+ protected void onResponseStarted() { |
+ mContentType = nativeGetContentType(); |
+ mContentLength = nativeGetContentLength(); |
+ } |
+ |
+ /** |
+ * A callback invoked when the response has been fully consumed. |
+ */ |
+ protected void onRequestComplete() { |
+ } |
+ |
+ /** |
+ * Consumes a portion of the response. |
+ * |
+ * @param byteBuffer The ByteBuffer to append. Must be a direct buffer, and |
+ * no references to it may be retained after the method ends, as it |
+ * wraps code managed on the native heap. |
+ */ |
+ @CalledByNative |
+ protected void onBytesRead(ByteBuffer byteBuffer) { |
+ try { |
+ while (byteBuffer.hasRemaining()) { |
+ mSink.write(byteBuffer); |
+ } |
+ } catch (IOException e) { |
+ mSinkException = e; |
+ cancel(); |
+ } |
+ } |
+ |
+ /** |
+ * Notifies the listener, releases native data structures. |
+ */ |
+ @SuppressWarnings("unused") |
+ @CalledByNative |
+ private void finish() { |
+ synchronized (mLock) { |
+ mFinished = true; |
+ if (mAppendChunkSemaphore != null) { |
+ mAppendChunkSemaphore.release(); |
+ } |
+ |
+ if (mRecycled) { |
+ return; |
+ } |
+ try { |
+ mSink.close(); |
+ } catch (IOException e) { |
+ // Ignore |
+ } |
+ onRequestComplete(); |
+ nativeRecycle(); |
+ mRecycled = true; |
+ } |
+ } |
+ |
+ private void validateNotRecycled() { |
+ if (mRecycled) { |
+ throw new IllegalStateException("Accessing recycled request"); |
+ } |
+ } |
+ |
+ private void validateNotStarted() { |
+ if (mStarted) { |
+ throw new IllegalStateException("Request already started"); |
+ } |
+ } |
+ |
+ private void validatePostBodyNotSet() { |
+ if (mPostBodySet) { |
+ throw new IllegalStateException("Post Body already set"); |
+ } |
+ } |
+ |
+ public String getUrl() { |
+ return mUrl; |
+ } |
+ |
+ private native void nativeInit(UrlRequestContext requestContext, |
+ String url, |
+ int priority); |
+ private native void nativeAddHeader(String name, String value); |
+ private native void nativeSetPostData(String contentType, byte[] content); |
+ private native void nativeBeginChunkedUpload(String contentType); |
+ private native void nativeAppendChunk(ByteBuffer chunk, |
+ int chunkSize, |
+ boolean isLastChunk); |
+ private native void nativeStart(); |
+ private native void nativeCancel(); |
+ private native void nativeRecycle(); |
+ private native int nativeGetErrorCode(); |
+ private native String nativeGetErrorString(); |
+ private native String nativeGetContentType(); |
+ private native long nativeGetContentLength(); |
+} |