1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 package org.chromium.net;
7 import android.content.Context;
8 import android.text.TextUtils;
10 import org.apache.http.HttpStatus;
12 import java.io.FileNotFoundException;
13 import java.io.IOException;
14 import java.io.InputStream;
15 import java.io.OutputStream;
16 import java.net.HttpURLConnection;
18 import java.nio.ByteBuffer;
19 import java.nio.channels.ReadableByteChannel;
20 import java.nio.channels.WritableByteChannel;
22 import java.util.Map.Entry;
23 import java.util.concurrent.ExecutorService;
24 import java.util.concurrent.Executors;
25 import java.util.concurrent.ThreadFactory;
26 import java.util.concurrent.atomic.AtomicInteger;
27 import java.util.zip.GZIPInputStream;
30 * Network request using the HttpUrlConnection implementation.
32 class HttpUrlConnectionUrlRequest implements HttpUrlRequest {
34 private static final int MAX_CHUNK_SIZE = 8192;
36 private static final int CONNECT_TIMEOUT = 3000;
38 private static final int READ_TIMEOUT = 90000;
40 private final Context mContext;
42 private final String mUrl;
44 private final Map<String, String> mHeaders;
46 private final WritableByteChannel mSink;
48 private final HttpUrlRequestListener mListener;
50 private IOException mException;
52 private HttpURLConnection mConnection;
56 private int mContentLength;
58 private long mContentLengthLimit;
60 private boolean mCancelIfContentLengthOverLimit;
62 private boolean mContentLengthOverLimit;
64 private boolean mSkippingToOffset;
68 private String mPostContentType;
70 private byte[] mPostData;
72 private ReadableByteChannel mPostDataChannel;
74 private String mContentType;
76 private int mHttpStatusCode;
78 private boolean mStarted;
80 private boolean mCanceled;
82 private InputStream mResponseStream;
84 private final Object mLock;
86 private static ExecutorService sExecutorService;
88 private static final Object sExecutorServiceLock = new Object();
90 HttpUrlConnectionUrlRequest(Context context, String url,
91 int requestPriority, Map<String, String> headers,
92 HttpUrlRequestListener listener) {
93 this(context, url, requestPriority, headers,
94 new ChunkedWritableByteChannel(), listener);
97 HttpUrlConnectionUrlRequest(Context context, String url,
98 int requestPriority, Map<String, String> headers,
99 WritableByteChannel sink, HttpUrlRequestListener listener) {
100 if (context == null) {
101 throw new NullPointerException("Context is required");
104 throw new NullPointerException("URL is required");
110 mListener = listener;
111 mLock = new Object();
114 private static ExecutorService getExecutor() {
115 synchronized (sExecutorServiceLock) {
116 if (sExecutorService == null) {
117 ThreadFactory threadFactory = new ThreadFactory() {
118 private final AtomicInteger mCount = new AtomicInteger(1);
121 public Thread newThread(Runnable r) {
122 Thread thread = new Thread(r,
123 "HttpUrlConnection #" +
124 mCount.getAndIncrement());
125 // Note that this thread is not doing actual networking.
126 // It's only a controller.
127 thread.setPriority(Thread.NORM_PRIORITY);
131 sExecutorService = Executors.newCachedThreadPool(threadFactory);
133 return sExecutorService;
138 public String getUrl() {
143 public void setOffset(long offset) {
148 public void setContentLengthLimit(long limit, boolean cancelEarly) {
149 mContentLengthLimit = limit;
150 mCancelIfContentLengthOverLimit = cancelEarly;
154 public void setUploadData(String contentType, byte[] data) {
155 validateNotStarted();
156 mPostContentType = contentType;
158 mPostDataChannel = null;
162 public void setUploadChannel(String contentType,
163 ReadableByteChannel channel) {
164 validateNotStarted();
165 mPostContentType = contentType;
166 mPostDataChannel = channel;
171 public void start() {
172 boolean readingResponse = false;
174 synchronized (mLock) {
180 URL url = new URL(mUrl);
181 mConnection = (HttpURLConnection)url.openConnection();
182 mConnection.setConnectTimeout(CONNECT_TIMEOUT);
183 mConnection.setReadTimeout(READ_TIMEOUT);
184 mConnection.setInstanceFollowRedirects(true);
185 if (mHeaders != null) {
186 for (Entry<String, String> header : mHeaders.entrySet()) {
187 mConnection.setRequestProperty(header.getKey(),
193 mConnection.setRequestProperty("Range",
194 "bytes=" + mOffset + "-");
197 if (mConnection.getRequestProperty("User-Agent") == null) {
198 mConnection.setRequestProperty("User-Agent",
199 UserAgent.from(mContext));
202 if (mPostData != null || mPostDataChannel != null) {
206 InputStream stream = null;
208 // We need to open the stream before asking for the response
210 stream = mConnection.getInputStream();
211 } catch (FileNotFoundException ex) {
212 // Ignore - the response has no body.
215 mHttpStatusCode = mConnection.getResponseCode();
216 mContentType = mConnection.getContentType();
217 mContentLength = mConnection.getContentLength();
218 if (mContentLengthLimit > 0 && mContentLength > mContentLengthLimit
219 && mCancelIfContentLengthOverLimit) {
220 onContentLengthOverLimit();
224 mResponseStream = isError(mHttpStatusCode) ? mConnection
228 if (mResponseStream != null
229 && "gzip".equals(mConnection.getContentEncoding())) {
230 mResponseStream = new GZIPInputStream(mResponseStream);
235 // The server may ignore the request for a byte range.
236 if (mHttpStatusCode == HttpStatus.SC_OK) {
237 if (mContentLength != -1) {
238 mContentLength -= mOffset;
240 mSkippingToOffset = true;
246 if (mResponseStream != null) {
247 readingResponse = true;
250 } catch (IOException e) {
253 // Don't call onRequestComplete yet if we are reading the response
254 // on a separate thread
255 if (!readingResponse) {
256 mListener.onRequestComplete(this);
261 private void uploadData() throws IOException {
262 mConnection.setDoOutput(true);
263 if (!TextUtils.isEmpty(mPostContentType)) {
264 mConnection.setRequestProperty("Content-Type", mPostContentType);
267 OutputStream uploadStream = null;
269 if (mPostData != null) {
270 mConnection.setFixedLengthStreamingMode(mPostData.length);
271 uploadStream = mConnection.getOutputStream();
272 uploadStream.write(mPostData);
274 mConnection.setChunkedStreamingMode(MAX_CHUNK_SIZE);
275 uploadStream = mConnection.getOutputStream();
276 byte[] bytes = new byte[MAX_CHUNK_SIZE];
277 ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
278 while (mPostDataChannel.read(byteBuffer) > 0) {
280 uploadStream.write(bytes, 0, byteBuffer.limit());
285 if (uploadStream != null) {
286 uploadStream.close();
291 private void readResponseAsync() {
292 getExecutor().execute(new Runnable() {
300 private void readResponse() {
302 if (mResponseStream != null) {
303 readResponseStream();
305 } catch (IOException e) {
309 mConnection.disconnect();
310 } catch (ArrayIndexOutOfBoundsException t) {
316 } catch (IOException e) {
317 if (mException == null) {
322 mListener.onRequestComplete(this);
325 private void readResponseStream() throws IOException {
326 byte[] buffer = new byte[MAX_CHUNK_SIZE];
328 while (!isCanceled() && (size = mResponseStream.read(buffer)) != -1) {
332 if (mSkippingToOffset) {
333 if (mSize <= mOffset) {
336 mSkippingToOffset = false;
337 start = (int)(mOffset - (mSize - size));
342 if (mContentLengthLimit != 0 && mSize > mContentLengthLimit) {
343 count -= (int)(mSize - mContentLengthLimit);
345 mSink.write(ByteBuffer.wrap(buffer, start, count));
347 onContentLengthOverLimit();
351 mSink.write(ByteBuffer.wrap(buffer, start, count));
356 public void cancel() {
357 synchronized (mLock) {
367 public boolean isCanceled() {
368 synchronized (mLock) {
374 public int getHttpStatusCode() {
375 int httpStatusCode = mHttpStatusCode;
377 // If we have been able to successfully resume a previously interrupted
379 // the status code will be 206, not 200. Since the rest of the
381 // expecting 200 to indicate success, we need to fake it.
382 if (httpStatusCode == HttpStatus.SC_PARTIAL_CONTENT) {
383 httpStatusCode = HttpStatus.SC_OK;
385 return httpStatusCode;
389 public IOException getException() {
390 if (mException == null && mContentLengthOverLimit) {
391 mException = new ResponseTooLargeException();
396 private void onContentLengthOverLimit() {
397 mContentLengthOverLimit = true;
401 private static boolean isError(int statusCode) {
402 return (statusCode / 100) != 2;
406 * Returns the response as a ByteBuffer.
409 public ByteBuffer getByteBuffer() {
410 return ((ChunkedWritableByteChannel)mSink).getByteBuffer();
414 public byte[] getResponseAsBytes() {
415 return ((ChunkedWritableByteChannel)mSink).getBytes();
419 public long getContentLength() {
420 return mContentLength;
424 public String getContentType() {
428 private void validateNotStarted() {
430 throw new IllegalStateException("Request already started");