Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / components / cronet / android / java / src / org / chromium / net / HttpUrlConnectionUrlRequest.java
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.
4
5 package org.chromium.net;
6
7 import android.content.Context;
8 import android.text.TextUtils;
9
10 import org.apache.http.HttpStatus;
11
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;
18 import java.net.URL;
19 import java.nio.ByteBuffer;
20 import java.nio.channels.ReadableByteChannel;
21 import java.nio.channels.WritableByteChannel;
22 import java.util.List;
23 import java.util.Map;
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;
30
31 /**
32  * Network request using the HttpUrlConnection implementation.
33  */
34 class HttpUrlConnectionUrlRequest implements HttpUrlRequest {
35
36     private static final int MAX_CHUNK_SIZE = 8192;
37
38     private static final int CONNECT_TIMEOUT = 3000;
39
40     private static final int READ_TIMEOUT = 90000;
41
42     private final Context mContext;
43
44     private final String mUrl;
45
46     private final Map<String, String> mHeaders;
47
48     private final WritableByteChannel mSink;
49
50     private final HttpUrlRequestListener mListener;
51
52     private IOException mException;
53
54     private HttpURLConnection mConnection;
55
56     private long mOffset;
57
58     private int mContentLength;
59
60     private int mUploadContentLength;
61
62     private long mContentLengthLimit;
63
64     private boolean mCancelIfContentLengthOverLimit;
65
66     private boolean mContentLengthOverLimit;
67
68     private boolean mSkippingToOffset;
69
70     private long mSize;
71
72     private String mPostContentType;
73
74     private byte[] mPostData;
75
76     private ReadableByteChannel mPostDataChannel;
77
78     private String mContentType;
79
80     private int mHttpStatusCode;
81
82     private boolean mStarted;
83
84     private boolean mCanceled;
85
86     private String mMethod;
87
88     private InputStream mResponseStream;
89
90     private final Object mLock;
91
92     private static ExecutorService sExecutorService;
93
94     private static final Object sExecutorServiceLock = new Object();
95
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);
101     }
102
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");
108         }
109         if (url == null) {
110             throw new NullPointerException("URL is required");
111         }
112         mContext = context;
113         mUrl = url;
114         mHeaders = headers;
115         mSink = sink;
116         mListener = listener;
117         mLock = new Object();
118     }
119
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);
125
126                         @Override
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);
134                         return thread;
135                     }
136                 };
137                 sExecutorService = Executors.newCachedThreadPool(threadFactory);
138             }
139             return sExecutorService;
140         }
141     }
142
143     @Override
144     public String getUrl() {
145         return mUrl;
146     }
147
148     @Override
149     public void setOffset(long offset) {
150         mOffset = offset;
151     }
152
153     @Override
154     public void setContentLengthLimit(long limit, boolean cancelEarly) {
155         mContentLengthLimit = limit;
156         mCancelIfContentLengthOverLimit = cancelEarly;
157     }
158
159     @Override
160     public void setUploadData(String contentType, byte[] data) {
161         validateNotStarted();
162         mPostContentType = contentType;
163         mPostData = data;
164         mPostDataChannel = null;
165     }
166
167     @Override
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.");
174         }
175         mUploadContentLength = (int) contentLength;
176         mPostContentType = contentType;
177         mPostDataChannel = channel;
178         mPostData = null;
179     }
180
181
182     @Override
183     public void setHttpMethod(String method) {
184         validateNotStarted();
185         mMethod = method;
186     }
187
188     @Override
189     public void start() {
190         getExecutor().execute(new Runnable() {
191             @Override
192             public void run() {
193                 startOnExecutorThread();
194             }
195         });
196     }
197
198     private void startOnExecutorThread() {
199         boolean readingResponse = false;
200         try {
201             synchronized (mLock) {
202                 if (mCanceled) {
203                     return;
204                 }
205             }
206
207             URL url = new URL(mUrl);
208             mConnection = (HttpURLConnection) url.openConnection();
209             // If configured, use the provided http verb.
210             if (mMethod != null) {
211                 try {
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);
217                 }
218             }
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(),
225                             header.getValue());
226                 }
227             }
228
229             if (mOffset != 0) {
230                 mConnection.setRequestProperty("Range",
231                         "bytes=" + mOffset + "-");
232             }
233
234             if (mConnection.getRequestProperty("User-Agent") == null) {
235                 mConnection.setRequestProperty("User-Agent",
236                         UserAgent.from(mContext));
237             }
238
239             if (mPostData != null || mPostDataChannel != null) {
240                 uploadData();
241             }
242
243             InputStream stream = null;
244             try {
245                 // We need to open the stream before asking for the response
246                 // code.
247                 stream = mConnection.getInputStream();
248             } catch (FileNotFoundException ex) {
249                 // Ignore - the response has no body.
250             }
251
252             mHttpStatusCode = mConnection.getResponseCode();
253             mContentType = mConnection.getContentType();
254             mContentLength = mConnection.getContentLength();
255             if (mContentLengthLimit > 0 && mContentLength > mContentLengthLimit
256                     && mCancelIfContentLengthOverLimit) {
257                 onContentLengthOverLimit();
258                 return;
259             }
260
261             mListener.onResponseStarted(this);
262
263             mResponseStream = isError(mHttpStatusCode) ? mConnection
264                     .getErrorStream()
265                     : stream;
266
267             if (mResponseStream != null
268                     && "gzip".equals(mConnection.getContentEncoding())) {
269                 mResponseStream = new GZIPInputStream(mResponseStream);
270                 mContentLength = -1;
271             }
272
273             if (mOffset != 0) {
274                 // The server may ignore the request for a byte range.
275                 if (mHttpStatusCode == HttpStatus.SC_OK) {
276                     if (mContentLength != -1) {
277                         mContentLength -= mOffset;
278                     }
279                     mSkippingToOffset = true;
280                 } else {
281                     mSize = mOffset;
282                 }
283             }
284
285             if (mResponseStream != null) {
286                 readingResponse = true;
287                 readResponseAsync();
288             }
289         } catch (IOException e) {
290             mException = e;
291         } finally {
292             if (mPostDataChannel != null) {
293                 try {
294                     mPostDataChannel.close();
295                 } catch (IOException e) {
296                     // Ignore
297                 }
298             }
299
300             // Don't call onRequestComplete yet if we are reading the response
301             // on a separate thread
302             if (!readingResponse) {
303                 mListener.onRequestComplete(this);
304             }
305         }
306     }
307
308     private void uploadData() throws IOException {
309         mConnection.setDoOutput(true);
310         if (!TextUtils.isEmpty(mPostContentType)) {
311             mConnection.setRequestProperty("Content-Type", mPostContentType);
312         }
313
314         OutputStream uploadStream = null;
315         try {
316             if (mPostData != null) {
317                 mConnection.setFixedLengthStreamingMode(mPostData.length);
318                 uploadStream = mConnection.getOutputStream();
319                 uploadStream.write(mPostData);
320             } else {
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) {
326                     byteBuffer.flip();
327                     uploadStream.write(bytes, 0, byteBuffer.limit());
328                     byteBuffer.clear();
329                 }
330             }
331         } finally {
332             if (uploadStream != null) {
333                 uploadStream.close();
334             }
335         }
336     }
337
338     private void readResponseAsync() {
339         getExecutor().execute(new Runnable() {
340             @Override
341             public void run() {
342                 readResponse();
343             }
344         });
345     }
346
347     private void readResponse() {
348         try {
349             if (mResponseStream != null) {
350                 readResponseStream();
351             }
352         } catch (IOException e) {
353             mException = e;
354         } finally {
355             try {
356                 mConnection.disconnect();
357             } catch (ArrayIndexOutOfBoundsException t) {
358                 // Ignore it.
359             }
360
361             try {
362                 mSink.close();
363             } catch (IOException e) {
364                 if (mException == null) {
365                     mException = e;
366                 }
367             }
368         }
369         mListener.onRequestComplete(this);
370     }
371
372     private void readResponseStream() throws IOException {
373         byte[] buffer = new byte[MAX_CHUNK_SIZE];
374         int size;
375         while (!isCanceled() && (size = mResponseStream.read(buffer)) != -1) {
376             int start = 0;
377             int count = size;
378             mSize += size;
379             if (mSkippingToOffset) {
380                 if (mSize <= mOffset) {
381                     continue;
382                 } else {
383                     mSkippingToOffset = false;
384                     start = (int) (mOffset - (mSize - size));
385                     count -= start;
386                 }
387             }
388
389             if (mContentLengthLimit != 0 && mSize > mContentLengthLimit) {
390                 count -= (int) (mSize - mContentLengthLimit);
391                 if (count > 0) {
392                     mSink.write(ByteBuffer.wrap(buffer, start, count));
393                 }
394                 onContentLengthOverLimit();
395                 return;
396             }
397
398             mSink.write(ByteBuffer.wrap(buffer, start, count));
399         }
400     }
401
402     @Override
403     public void cancel() {
404         synchronized (mLock) {
405             if (mCanceled) {
406                 return;
407             }
408
409             mCanceled = true;
410         }
411     }
412
413     @Override
414     public boolean isCanceled() {
415         synchronized (mLock) {
416             return mCanceled;
417         }
418     }
419
420     @Override
421     public String getNegotiatedProtocol() {
422         return "";
423     }
424
425     @Override
426     public int getHttpStatusCode() {
427         int httpStatusCode = mHttpStatusCode;
428
429         // If we have been able to successfully resume a previously interrupted
430         // download,
431         // the status code will be 206, not 200. Since the rest of the
432         // application is
433         // expecting 200 to indicate success, we need to fake it.
434         if (httpStatusCode == HttpStatus.SC_PARTIAL_CONTENT) {
435             httpStatusCode = HttpStatus.SC_OK;
436         }
437         return httpStatusCode;
438     }
439
440     @Override
441     public IOException getException() {
442         if (mException == null && mContentLengthOverLimit) {
443             mException = new ResponseTooLargeException();
444         }
445         return mException;
446     }
447
448     private void onContentLengthOverLimit() {
449         mContentLengthOverLimit = true;
450         cancel();
451     }
452
453     private static boolean isError(int statusCode) {
454         return (statusCode / 100) != 2;
455     }
456
457     /**
458      * Returns the response as a ByteBuffer.
459      */
460     @Override
461     public ByteBuffer getByteBuffer() {
462         return ((ChunkedWritableByteChannel) mSink).getByteBuffer();
463     }
464
465     @Override
466     public byte[] getResponseAsBytes() {
467         return ((ChunkedWritableByteChannel) mSink).getBytes();
468     }
469
470     @Override
471     public long getContentLength() {
472         return mContentLength;
473     }
474
475     @Override
476     public String getContentType() {
477         return mContentType;
478     }
479
480     @Override
481     public String getHeader(String name) {
482         if (mConnection == null) {
483             throw new IllegalStateException("Response headers not available");
484         }
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);
490             }
491         }
492         return null;
493     }
494
495     @Override
496     public Map<String, List<String>> getAllHeaders() {
497         if (mConnection == null) {
498             throw new IllegalStateException("Response headers not available");
499         }
500         return mConnection.getHeaderFields();
501     }
502
503     private void validateNotStarted() {
504         if (mStarted) {
505             throw new IllegalStateException("Request already started");
506         }
507     }
508 }