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;
17 import java.net.ProtocolException;
19 import java.nio.ByteBuffer;
20 import java.nio.channels.ReadableByteChannel;
21 import java.nio.channels.WritableByteChannel;
22 import java.util.List;
24 import java.util.Map.Entry;
25 import java.util.concurrent.ExecutorService;
26 import java.util.concurrent.Executors;
27 import java.util.concurrent.ThreadFactory;
28 import java.util.concurrent.atomic.AtomicInteger;
29 import java.util.zip.GZIPInputStream;
32 * Network request using the HttpUrlConnection implementation.
34 class HttpUrlConnectionUrlRequest implements HttpUrlRequest {
36 private static final int MAX_CHUNK_SIZE = 8192;
38 private static final int CONNECT_TIMEOUT = 3000;
40 private static final int READ_TIMEOUT = 90000;
42 private final Context mContext;
44 private final String mUrl;
46 private final Map<String, String> mHeaders;
48 private final WritableByteChannel mSink;
50 private final HttpUrlRequestListener mListener;
52 private IOException mException;
54 private HttpURLConnection mConnection;
58 private int mContentLength;
60 private int mUploadContentLength;
62 private long mContentLengthLimit;
64 private boolean mCancelIfContentLengthOverLimit;
66 private boolean mContentLengthOverLimit;
68 private boolean mSkippingToOffset;
72 private String mPostContentType;
74 private byte[] mPostData;
76 private ReadableByteChannel mPostDataChannel;
78 private String mContentType;
80 private int mHttpStatusCode;
82 private boolean mStarted;
84 private boolean mCanceled;
86 private String mMethod;
88 private InputStream mResponseStream;
90 private final Object mLock;
92 private static ExecutorService sExecutorService;
94 private static final Object sExecutorServiceLock = new Object();
96 HttpUrlConnectionUrlRequest(Context context, String url,
97 int requestPriority, Map<String, String> headers,
98 HttpUrlRequestListener listener) {
99 this(context, url, requestPriority, headers,
100 new ChunkedWritableByteChannel(), listener);
103 HttpUrlConnectionUrlRequest(Context context, String url,
104 int requestPriority, Map<String, String> headers,
105 WritableByteChannel sink, HttpUrlRequestListener listener) {
106 if (context == null) {
107 throw new NullPointerException("Context is required");
110 throw new NullPointerException("URL is required");
116 mListener = listener;
117 mLock = new Object();
120 private static ExecutorService getExecutor() {
121 synchronized (sExecutorServiceLock) {
122 if (sExecutorService == null) {
123 ThreadFactory threadFactory = new ThreadFactory() {
124 private final AtomicInteger mCount = new AtomicInteger(1);
127 public Thread newThread(Runnable r) {
128 Thread thread = new Thread(r,
129 "HttpUrlConnection #" +
130 mCount.getAndIncrement());
131 // Note that this thread is not doing actual networking.
132 // It's only a controller.
133 thread.setPriority(Thread.NORM_PRIORITY);
137 sExecutorService = Executors.newCachedThreadPool(threadFactory);
139 return sExecutorService;
144 public String getUrl() {
149 public void setOffset(long offset) {
154 public void setContentLengthLimit(long limit, boolean cancelEarly) {
155 mContentLengthLimit = limit;
156 mCancelIfContentLengthOverLimit = cancelEarly;
160 public void setUploadData(String contentType, byte[] data) {
161 validateNotStarted();
162 mPostContentType = contentType;
164 mPostDataChannel = null;
168 public void setUploadChannel(String contentType,
169 ReadableByteChannel channel, long contentLength) {
170 validateNotStarted();
171 if (contentLength > Integer.MAX_VALUE) {
172 throw new IllegalArgumentException(
173 "Upload contentLength is too big.");
175 mUploadContentLength = (int) contentLength;
176 mPostContentType = contentType;
177 mPostDataChannel = channel;
183 public void setHttpMethod(String method) {
184 validateNotStarted();
189 public void start() {
190 getExecutor().execute(new Runnable() {
193 startOnExecutorThread();
198 private void startOnExecutorThread() {
199 boolean readingResponse = false;
201 synchronized (mLock) {
207 URL url = new URL(mUrl);
208 mConnection = (HttpURLConnection) url.openConnection();
209 // If configured, use the provided http verb.
210 if (mMethod != null) {
212 mConnection.setRequestMethod(mMethod);
213 } catch (ProtocolException e) {
214 // Since request hasn't started earlier, it
215 // must be an illegal HTTP verb.
216 throw new IllegalArgumentException(e);
219 mConnection.setConnectTimeout(CONNECT_TIMEOUT);
220 mConnection.setReadTimeout(READ_TIMEOUT);
221 mConnection.setInstanceFollowRedirects(true);
222 if (mHeaders != null) {
223 for (Entry<String, String> header : mHeaders.entrySet()) {
224 mConnection.setRequestProperty(header.getKey(),
230 mConnection.setRequestProperty("Range",
231 "bytes=" + mOffset + "-");
234 if (mConnection.getRequestProperty("User-Agent") == null) {
235 mConnection.setRequestProperty("User-Agent",
236 UserAgent.from(mContext));
239 if (mPostData != null || mPostDataChannel != null) {
243 InputStream stream = null;
245 // We need to open the stream before asking for the response
247 stream = mConnection.getInputStream();
248 } catch (FileNotFoundException ex) {
249 // Ignore - the response has no body.
252 mHttpStatusCode = mConnection.getResponseCode();
253 mContentType = mConnection.getContentType();
254 mContentLength = mConnection.getContentLength();
255 if (mContentLengthLimit > 0 && mContentLength > mContentLengthLimit
256 && mCancelIfContentLengthOverLimit) {
257 onContentLengthOverLimit();
261 mListener.onResponseStarted(this);
263 mResponseStream = isError(mHttpStatusCode) ? mConnection
267 if (mResponseStream != null
268 && "gzip".equals(mConnection.getContentEncoding())) {
269 mResponseStream = new GZIPInputStream(mResponseStream);
274 // The server may ignore the request for a byte range.
275 if (mHttpStatusCode == HttpStatus.SC_OK) {
276 if (mContentLength != -1) {
277 mContentLength -= mOffset;
279 mSkippingToOffset = true;
285 if (mResponseStream != null) {
286 readingResponse = true;
289 } catch (IOException e) {
292 if (mPostDataChannel != null) {
294 mPostDataChannel.close();
295 } catch (IOException e) {
300 // Don't call onRequestComplete yet if we are reading the response
301 // on a separate thread
302 if (!readingResponse) {
303 mListener.onRequestComplete(this);
308 private void uploadData() throws IOException {
309 mConnection.setDoOutput(true);
310 if (!TextUtils.isEmpty(mPostContentType)) {
311 mConnection.setRequestProperty("Content-Type", mPostContentType);
314 OutputStream uploadStream = null;
316 if (mPostData != null) {
317 mConnection.setFixedLengthStreamingMode(mPostData.length);
318 uploadStream = mConnection.getOutputStream();
319 uploadStream.write(mPostData);
321 mConnection.setFixedLengthStreamingMode(mUploadContentLength);
322 uploadStream = mConnection.getOutputStream();
323 byte[] bytes = new byte[MAX_CHUNK_SIZE];
324 ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
325 while (mPostDataChannel.read(byteBuffer) > 0) {
327 uploadStream.write(bytes, 0, byteBuffer.limit());
332 if (uploadStream != null) {
333 uploadStream.close();
338 private void readResponseAsync() {
339 getExecutor().execute(new Runnable() {
347 private void readResponse() {
349 if (mResponseStream != null) {
350 readResponseStream();
352 } catch (IOException e) {
356 mConnection.disconnect();
357 } catch (ArrayIndexOutOfBoundsException t) {
363 } catch (IOException e) {
364 if (mException == null) {
369 mListener.onRequestComplete(this);
372 private void readResponseStream() throws IOException {
373 byte[] buffer = new byte[MAX_CHUNK_SIZE];
375 while (!isCanceled() && (size = mResponseStream.read(buffer)) != -1) {
379 if (mSkippingToOffset) {
380 if (mSize <= mOffset) {
383 mSkippingToOffset = false;
384 start = (int) (mOffset - (mSize - size));
389 if (mContentLengthLimit != 0 && mSize > mContentLengthLimit) {
390 count -= (int) (mSize - mContentLengthLimit);
392 mSink.write(ByteBuffer.wrap(buffer, start, count));
394 onContentLengthOverLimit();
398 mSink.write(ByteBuffer.wrap(buffer, start, count));
403 public void cancel() {
404 synchronized (mLock) {
414 public boolean isCanceled() {
415 synchronized (mLock) {
421 public String getNegotiatedProtocol() {
426 public int getHttpStatusCode() {
427 int httpStatusCode = mHttpStatusCode;
429 // If we have been able to successfully resume a previously interrupted
431 // the status code will be 206, not 200. Since the rest of the
433 // expecting 200 to indicate success, we need to fake it.
434 if (httpStatusCode == HttpStatus.SC_PARTIAL_CONTENT) {
435 httpStatusCode = HttpStatus.SC_OK;
437 return httpStatusCode;
441 public IOException getException() {
442 if (mException == null && mContentLengthOverLimit) {
443 mException = new ResponseTooLargeException();
448 private void onContentLengthOverLimit() {
449 mContentLengthOverLimit = true;
453 private static boolean isError(int statusCode) {
454 return (statusCode / 100) != 2;
458 * Returns the response as a ByteBuffer.
461 public ByteBuffer getByteBuffer() {
462 return ((ChunkedWritableByteChannel) mSink).getByteBuffer();
466 public byte[] getResponseAsBytes() {
467 return ((ChunkedWritableByteChannel) mSink).getBytes();
471 public long getContentLength() {
472 return mContentLength;
476 public String getContentType() {
481 public String getHeader(String name) {
482 if (mConnection == null) {
483 throw new IllegalStateException("Response headers not available");
485 Map<String, List<String>> headerFields = mConnection.getHeaderFields();
486 if (headerFields != null) {
487 List<String> headerValues = headerFields.get(name);
488 if (headerValues != null) {
489 return TextUtils.join(", ", headerValues);
496 public Map<String, List<String>> getAllHeaders() {
497 if (mConnection == null) {
498 throw new IllegalStateException("Response headers not available");
500 return mConnection.getHeaderFields();
503 private void validateNotStarted() {
505 throw new IllegalStateException("Request already started");