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();
185 if (!("PUT".equals(method) || "POST".equals(method))) {
186 throw new IllegalArgumentException(
187 "Only PUT and POST are allowed.");
193 public void start() {
194 getExecutor().execute(new Runnable() {
197 startOnExecutorThread();
202 private void startOnExecutorThread() {
203 boolean readingResponse = false;
205 synchronized (mLock) {
211 URL url = new URL(mUrl);
212 mConnection = (HttpURLConnection)url.openConnection();
213 // If configured, use the provided http verb.
214 if (mMethod != null) {
216 mConnection.setRequestMethod(mMethod);
217 } catch (ProtocolException e) {
218 // Since request hasn't started earlier, it
219 // must be an illegal HTTP verb.
220 throw new IllegalArgumentException(e);
223 mConnection.setConnectTimeout(CONNECT_TIMEOUT);
224 mConnection.setReadTimeout(READ_TIMEOUT);
225 mConnection.setInstanceFollowRedirects(true);
226 if (mHeaders != null) {
227 for (Entry<String, String> header : mHeaders.entrySet()) {
228 mConnection.setRequestProperty(header.getKey(),
234 mConnection.setRequestProperty("Range",
235 "bytes=" + mOffset + "-");
238 if (mConnection.getRequestProperty("User-Agent") == null) {
239 mConnection.setRequestProperty("User-Agent",
240 UserAgent.from(mContext));
243 if (mPostData != null || mPostDataChannel != null) {
247 InputStream stream = null;
249 // We need to open the stream before asking for the response
251 stream = mConnection.getInputStream();
252 } catch (FileNotFoundException ex) {
253 // Ignore - the response has no body.
256 mHttpStatusCode = mConnection.getResponseCode();
257 mContentType = mConnection.getContentType();
258 mContentLength = mConnection.getContentLength();
259 if (mContentLengthLimit > 0 && mContentLength > mContentLengthLimit
260 && mCancelIfContentLengthOverLimit) {
261 onContentLengthOverLimit();
265 mListener.onResponseStarted(this);
267 mResponseStream = isError(mHttpStatusCode) ? mConnection
271 if (mResponseStream != null
272 && "gzip".equals(mConnection.getContentEncoding())) {
273 mResponseStream = new GZIPInputStream(mResponseStream);
278 // The server may ignore the request for a byte range.
279 if (mHttpStatusCode == HttpStatus.SC_OK) {
280 if (mContentLength != -1) {
281 mContentLength -= mOffset;
283 mSkippingToOffset = true;
289 if (mResponseStream != null) {
290 readingResponse = true;
293 } catch (IOException e) {
296 if (mPostDataChannel != null) {
298 mPostDataChannel.close();
299 } catch (IOException e) {
304 // Don't call onRequestComplete yet if we are reading the response
305 // on a separate thread
306 if (!readingResponse) {
307 mListener.onRequestComplete(this);
312 private void uploadData() throws IOException {
313 mConnection.setDoOutput(true);
314 if (!TextUtils.isEmpty(mPostContentType)) {
315 mConnection.setRequestProperty("Content-Type", mPostContentType);
318 OutputStream uploadStream = null;
320 if (mPostData != null) {
321 mConnection.setFixedLengthStreamingMode(mPostData.length);
322 uploadStream = mConnection.getOutputStream();
323 uploadStream.write(mPostData);
325 mConnection.setFixedLengthStreamingMode(mUploadContentLength);
326 uploadStream = mConnection.getOutputStream();
327 byte[] bytes = new byte[MAX_CHUNK_SIZE];
328 ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
329 while (mPostDataChannel.read(byteBuffer) > 0) {
331 uploadStream.write(bytes, 0, byteBuffer.limit());
336 if (uploadStream != null) {
337 uploadStream.close();
342 private void readResponseAsync() {
343 getExecutor().execute(new Runnable() {
351 private void readResponse() {
353 if (mResponseStream != null) {
354 readResponseStream();
356 } catch (IOException e) {
360 mConnection.disconnect();
361 } catch (ArrayIndexOutOfBoundsException t) {
367 } catch (IOException e) {
368 if (mException == null) {
373 mListener.onRequestComplete(this);
376 private void readResponseStream() throws IOException {
377 byte[] buffer = new byte[MAX_CHUNK_SIZE];
379 while (!isCanceled() && (size = mResponseStream.read(buffer)) != -1) {
383 if (mSkippingToOffset) {
384 if (mSize <= mOffset) {
387 mSkippingToOffset = false;
388 start = (int)(mOffset - (mSize - size));
393 if (mContentLengthLimit != 0 && mSize > mContentLengthLimit) {
394 count -= (int)(mSize - mContentLengthLimit);
396 mSink.write(ByteBuffer.wrap(buffer, start, count));
398 onContentLengthOverLimit();
402 mSink.write(ByteBuffer.wrap(buffer, start, count));
407 public void cancel() {
408 synchronized (mLock) {
418 public boolean isCanceled() {
419 synchronized (mLock) {
425 public int getHttpStatusCode() {
426 int httpStatusCode = mHttpStatusCode;
428 // If we have been able to successfully resume a previously interrupted
430 // the status code will be 206, not 200. Since the rest of the
432 // expecting 200 to indicate success, we need to fake it.
433 if (httpStatusCode == HttpStatus.SC_PARTIAL_CONTENT) {
434 httpStatusCode = HttpStatus.SC_OK;
436 return httpStatusCode;
440 public IOException getException() {
441 if (mException == null && mContentLengthOverLimit) {
442 mException = new ResponseTooLargeException();
447 private void onContentLengthOverLimit() {
448 mContentLengthOverLimit = true;
452 private static boolean isError(int statusCode) {
453 return (statusCode / 100) != 2;
457 * Returns the response as a ByteBuffer.
460 public ByteBuffer getByteBuffer() {
461 return ((ChunkedWritableByteChannel)mSink).getByteBuffer();
465 public byte[] getResponseAsBytes() {
466 return ((ChunkedWritableByteChannel)mSink).getBytes();
470 public long getContentLength() {
471 return mContentLength;
475 public String getContentType() {
480 public String getHeader(String name) {
481 if (mConnection == null) {
482 throw new IllegalStateException("Response headers not available");
484 Map<String, List<String>> headerFields = mConnection.getHeaderFields();
485 if (headerFields != null) {
486 List<String> headerValues = headerFields.get(name);
487 if (headerValues != null) {
488 return TextUtils.join(", ", headerValues);
495 public Map<String, List<String>> getAllHeaders() {
496 if (mConnection == null) {
497 throw new IllegalStateException("Response headers not available");
499 return mConnection.getHeaderFields();
502 private void validateNotStarted() {
504 throw new IllegalStateException("Request already started");