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 getExecutor().execute(new Runnable() {
175 startOnExecutorThread();
180 private void startOnExecutorThread() {
181 boolean readingResponse = false;
183 synchronized (mLock) {
189 URL url = new URL(mUrl);
190 mConnection = (HttpURLConnection)url.openConnection();
191 mConnection.setConnectTimeout(CONNECT_TIMEOUT);
192 mConnection.setReadTimeout(READ_TIMEOUT);
193 mConnection.setInstanceFollowRedirects(true);
194 if (mHeaders != null) {
195 for (Entry<String, String> header : mHeaders.entrySet()) {
196 mConnection.setRequestProperty(header.getKey(),
202 mConnection.setRequestProperty("Range",
203 "bytes=" + mOffset + "-");
206 if (mConnection.getRequestProperty("User-Agent") == null) {
207 mConnection.setRequestProperty("User-Agent",
208 UserAgent.from(mContext));
211 if (mPostData != null || mPostDataChannel != null) {
215 InputStream stream = null;
217 // We need to open the stream before asking for the response
219 stream = mConnection.getInputStream();
220 } catch (FileNotFoundException ex) {
221 // Ignore - the response has no body.
224 mHttpStatusCode = mConnection.getResponseCode();
225 mContentType = mConnection.getContentType();
226 mContentLength = mConnection.getContentLength();
227 if (mContentLengthLimit > 0 && mContentLength > mContentLengthLimit
228 && mCancelIfContentLengthOverLimit) {
229 onContentLengthOverLimit();
233 mListener.onResponseStarted(this);
235 mResponseStream = isError(mHttpStatusCode) ? mConnection
239 if (mResponseStream != null
240 && "gzip".equals(mConnection.getContentEncoding())) {
241 mResponseStream = new GZIPInputStream(mResponseStream);
246 // The server may ignore the request for a byte range.
247 if (mHttpStatusCode == HttpStatus.SC_OK) {
248 if (mContentLength != -1) {
249 mContentLength -= mOffset;
251 mSkippingToOffset = true;
257 if (mResponseStream != null) {
258 readingResponse = true;
261 } catch (IOException e) {
264 if (mPostDataChannel != null) {
266 mPostDataChannel.close();
267 } catch (IOException e) {
272 // Don't call onRequestComplete yet if we are reading the response
273 // on a separate thread
274 if (!readingResponse) {
275 mListener.onRequestComplete(this);
280 private void uploadData() throws IOException {
281 mConnection.setDoOutput(true);
282 if (!TextUtils.isEmpty(mPostContentType)) {
283 mConnection.setRequestProperty("Content-Type", mPostContentType);
286 OutputStream uploadStream = null;
288 if (mPostData != null) {
289 mConnection.setFixedLengthStreamingMode(mPostData.length);
290 uploadStream = mConnection.getOutputStream();
291 uploadStream.write(mPostData);
293 mConnection.setChunkedStreamingMode(MAX_CHUNK_SIZE);
294 uploadStream = mConnection.getOutputStream();
295 byte[] bytes = new byte[MAX_CHUNK_SIZE];
296 ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
297 while (mPostDataChannel.read(byteBuffer) > 0) {
299 uploadStream.write(bytes, 0, byteBuffer.limit());
304 if (uploadStream != null) {
305 uploadStream.close();
310 private void readResponseAsync() {
311 getExecutor().execute(new Runnable() {
319 private void readResponse() {
321 if (mResponseStream != null) {
322 readResponseStream();
324 } catch (IOException e) {
328 mConnection.disconnect();
329 } catch (ArrayIndexOutOfBoundsException t) {
335 } catch (IOException e) {
336 if (mException == null) {
341 mListener.onRequestComplete(this);
344 private void readResponseStream() throws IOException {
345 byte[] buffer = new byte[MAX_CHUNK_SIZE];
347 while (!isCanceled() && (size = mResponseStream.read(buffer)) != -1) {
351 if (mSkippingToOffset) {
352 if (mSize <= mOffset) {
355 mSkippingToOffset = false;
356 start = (int)(mOffset - (mSize - size));
361 if (mContentLengthLimit != 0 && mSize > mContentLengthLimit) {
362 count -= (int)(mSize - mContentLengthLimit);
364 mSink.write(ByteBuffer.wrap(buffer, start, count));
366 onContentLengthOverLimit();
370 mSink.write(ByteBuffer.wrap(buffer, start, count));
375 public void cancel() {
376 synchronized (mLock) {
386 public boolean isCanceled() {
387 synchronized (mLock) {
393 public int getHttpStatusCode() {
394 int httpStatusCode = mHttpStatusCode;
396 // If we have been able to successfully resume a previously interrupted
398 // the status code will be 206, not 200. Since the rest of the
400 // expecting 200 to indicate success, we need to fake it.
401 if (httpStatusCode == HttpStatus.SC_PARTIAL_CONTENT) {
402 httpStatusCode = HttpStatus.SC_OK;
404 return httpStatusCode;
408 public IOException getException() {
409 if (mException == null && mContentLengthOverLimit) {
410 mException = new ResponseTooLargeException();
415 private void onContentLengthOverLimit() {
416 mContentLengthOverLimit = true;
420 private static boolean isError(int statusCode) {
421 return (statusCode / 100) != 2;
425 * Returns the response as a ByteBuffer.
428 public ByteBuffer getByteBuffer() {
429 return ((ChunkedWritableByteChannel)mSink).getByteBuffer();
433 public byte[] getResponseAsBytes() {
434 return ((ChunkedWritableByteChannel)mSink).getBytes();
438 public long getContentLength() {
439 return mContentLength;
443 public String getContentType() {
449 public String getHeader(String name) {
450 if (mConnection == null) {
451 throw new IllegalStateException("Response headers not available");
453 return mConnection.getHeaderField(name);
456 private void validateNotStarted() {
458 throw new IllegalStateException("Request already started");