blob: 9aef0086d32a0271492f7e3cd55a8d0db49f1583 [file] [log] [blame]
// Copyright 2014 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.CalledByNative;
import org.chromium.base.JNINamespace;
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;
/**
* Network request using the native http stack implementation.
*/
@JNINamespace("cronet")
public class UrlRequest {
private static final class ContextLock {
}
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 String mPostBodyContentType;
private String mMethod;
private byte[] mPostBody;
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 boolean mHeadersAvailable;
private String mContentType;
private long mContentLength;
private long mUploadContentLength;
private final ContextLock mLock;
/**
* Native peer object, owned by UrlRequest.
*/
private long mUrlRequestPeer;
/**
* 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) {
if (requestContext == null) {
throw new NullPointerException("Context is required");
}
if (url == null) {
throw new NullPointerException("URL is required");
}
mRequestContext = requestContext;
mUrl = url;
mPriority = priority;
mHeaders = headers;
mSink = sink;
mLock = new ContextLock();
mUrlRequestPeer = nativeCreateRequestPeer(
mRequestContext.getUrlRequestContextPeer(), mUrl, mPriority);
}
/**
* 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();
mPostBodyContentType = contentType;
mPostBody = data;
mPostBodyChannel = null;
}
}
/**
* 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.
* @param contentLength The length of data to upload.
*/
public void setUploadChannel(String contentType,
ReadableByteChannel channel, long contentLength) {
synchronized (mLock) {
validateNotStarted();
mPostBodyContentType = contentType;
mPostBodyChannel = channel;
mUploadContentLength = contentLength;
mPostBody = null;
}
}
public void setHttpMethod(String method) {
validateNotStarted();
if (!("PUT".equals(method) || "POST".equals(method))) {
throw new IllegalArgumentException("Only PUT and POST are allowed.");
}
mMethod = method;
}
public WritableByteChannel getSink() {
return mSink;
}
public void start() {
synchronized (mLock) {
if (mCanceled) {
return;
}
validateNotStarted();
validateNotRecycled();
mStarted = true;
String method = mMethod;
if (method == null &&
((mPostBody != null && mPostBody.length > 0) ||
mPostBodyChannel != null)) {
// Default to POST if there is data to upload but no method was
// specified.
method = "POST";
}
if (method != null) {
nativeSetMethod(mUrlRequestPeer, method);
}
if (mHeaders != null && !mHeaders.isEmpty()) {
for (Entry<String, String> entry : mHeaders.entrySet()) {
nativeAddHeader(mUrlRequestPeer, entry.getKey(),
entry.getValue());
}
}
if (mAdditionalHeaders != null) {
for (Entry<String, String> entry :
mAdditionalHeaders.entrySet()) {
nativeAddHeader(mUrlRequestPeer, entry.getKey(),
entry.getValue());
}
}
if (mPostBody != null && mPostBody.length > 0) {
nativeSetUploadData(mUrlRequestPeer, mPostBodyContentType,
mPostBody);
} else if (mPostBodyChannel != null) {
nativeSetUploadChannel(mUrlRequestPeer, mPostBodyContentType,
mPostBodyChannel, mUploadContentLength);
}
nativeStart(mUrlRequestPeer);
}
}
public void cancel() {
synchronized (mLock) {
if (mCanceled) {
return;
}
mCanceled = true;
if (!mRecycled) {
nativeCancel(mUrlRequestPeer);
}
}
}
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(mUrlRequestPeer);
switch (errorCode) {
case UrlRequestError.SUCCESS:
return null;
case UrlRequestError.UNKNOWN:
return new IOException(nativeGetErrorString(mUrlRequestPeer));
case UrlRequestError.MALFORMED_URL:
return new MalformedURLException("Malformed URL: " + mUrl);
case UrlRequestError.CONNECTION_TIMED_OUT:
return new ConnectTimeoutException("Connection timed out");
case UrlRequestError.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 int getHttpStatusCode() {
return nativeGetHttpStatusCode(mUrlRequestPeer);
}
/**
* 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;
}
public String getHeader(String name) {
validateHeadersAvailable();
return nativeGetHeader(mUrlRequestPeer, name);
}
/**
* A callback invoked when the first chunk of the response has arrived.
*/
@CalledByNative
protected void onResponseStarted() {
mContentType = nativeGetContentType(mUrlRequestPeer);
mContentLength = nativeGetContentLength(mUrlRequestPeer);
mHeadersAvailable = true;
}
/**
* 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 (mRecycled) {
return;
}
try {
mSink.close();
} catch (IOException e) {
// Ignore
}
onRequestComplete();
nativeDestroyRequestPeer(mUrlRequestPeer);
mUrlRequestPeer = 0;
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 validateHeadersAvailable() {
if (!mHeadersAvailable) {
throw new IllegalStateException("Response headers not available");
}
}
public String getUrl() {
return mUrl;
}
private native long nativeCreateRequestPeer(long urlRequestContextPeer,
String url, int priority);
private native void nativeAddHeader(long urlRequestPeer, String name,
String value);
private native void nativeSetMethod(long urlRequestPeer, String method);
private native void nativeSetUploadData(long urlRequestPeer,
String contentType, byte[] content);
private native void nativeSetUploadChannel(long urlRequestPeer,
String contentType, ReadableByteChannel content,
long contentLength);
private native void nativeStart(long urlRequestPeer);
private native void nativeCancel(long urlRequestPeer);
private native void nativeDestroyRequestPeer(long urlRequestPeer);
private native int nativeGetErrorCode(long urlRequestPeer);
private native int nativeGetHttpStatusCode(long urlRequestPeer);
private native String nativeGetErrorString(long urlRequestPeer);
private native String nativeGetContentType(long urlRequestPeer);
private native long nativeGetContentLength(long urlRequestPeer);
private native String nativeGetHeader(long urlRequestPeer, String name);
}