Upstream version 9.38.198.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         if (!("PUT".equals(method) || "POST".equals(method))) {
186             throw new IllegalArgumentException(
187                 "Only PUT and POST are allowed.");
188         }
189         mMethod = method;
190     }
191
192     @Override
193     public void start() {
194         getExecutor().execute(new Runnable() {
195             @Override
196             public void run() {
197                 startOnExecutorThread();
198             }
199         });
200     }
201
202     private void startOnExecutorThread() {
203         boolean readingResponse = false;
204         try {
205             synchronized (mLock) {
206                 if (mCanceled) {
207                     return;
208                 }
209             }
210
211             URL url = new URL(mUrl);
212             mConnection = (HttpURLConnection)url.openConnection();
213             // If configured, use the provided http verb.
214             if (mMethod != null) {
215                 try {
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);
221                 }
222             }
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(),
229                             header.getValue());
230                 }
231             }
232
233             if (mOffset != 0) {
234                 mConnection.setRequestProperty("Range",
235                         "bytes=" + mOffset + "-");
236             }
237
238             if (mConnection.getRequestProperty("User-Agent") == null) {
239                 mConnection.setRequestProperty("User-Agent",
240                         UserAgent.from(mContext));
241             }
242
243             if (mPostData != null || mPostDataChannel != null) {
244                 uploadData();
245             }
246
247             InputStream stream = null;
248             try {
249                 // We need to open the stream before asking for the response
250                 // code.
251                 stream = mConnection.getInputStream();
252             } catch (FileNotFoundException ex) {
253                 // Ignore - the response has no body.
254             }
255
256             mHttpStatusCode = mConnection.getResponseCode();
257             mContentType = mConnection.getContentType();
258             mContentLength = mConnection.getContentLength();
259             if (mContentLengthLimit > 0 && mContentLength > mContentLengthLimit
260                     && mCancelIfContentLengthOverLimit) {
261                 onContentLengthOverLimit();
262                 return;
263             }
264
265             mListener.onResponseStarted(this);
266
267             mResponseStream = isError(mHttpStatusCode) ? mConnection
268                     .getErrorStream()
269                     : stream;
270
271             if (mResponseStream != null
272                     && "gzip".equals(mConnection.getContentEncoding())) {
273                 mResponseStream = new GZIPInputStream(mResponseStream);
274                 mContentLength = -1;
275             }
276
277             if (mOffset != 0) {
278                 // The server may ignore the request for a byte range.
279                 if (mHttpStatusCode == HttpStatus.SC_OK) {
280                     if (mContentLength != -1) {
281                         mContentLength -= mOffset;
282                     }
283                     mSkippingToOffset = true;
284                 } else {
285                     mSize = mOffset;
286                 }
287             }
288
289             if (mResponseStream != null) {
290                 readingResponse = true;
291                 readResponseAsync();
292             }
293         } catch (IOException e) {
294             mException = e;
295         } finally {
296             if (mPostDataChannel != null) {
297                 try {
298                     mPostDataChannel.close();
299                 } catch (IOException e) {
300                     // Ignore
301                 }
302             }
303
304             // Don't call onRequestComplete yet if we are reading the response
305             // on a separate thread
306             if (!readingResponse) {
307                 mListener.onRequestComplete(this);
308             }
309         }
310     }
311
312     private void uploadData() throws IOException {
313         mConnection.setDoOutput(true);
314         if (!TextUtils.isEmpty(mPostContentType)) {
315             mConnection.setRequestProperty("Content-Type", mPostContentType);
316         }
317
318         OutputStream uploadStream = null;
319         try {
320             if (mPostData != null) {
321                 mConnection.setFixedLengthStreamingMode(mPostData.length);
322                 uploadStream = mConnection.getOutputStream();
323                 uploadStream.write(mPostData);
324             } else {
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) {
330                     byteBuffer.flip();
331                     uploadStream.write(bytes, 0, byteBuffer.limit());
332                     byteBuffer.clear();
333                 }
334             }
335         } finally {
336             if (uploadStream != null) {
337                 uploadStream.close();
338             }
339         }
340     }
341
342     private void readResponseAsync() {
343         getExecutor().execute(new Runnable() {
344             @Override
345             public void run() {
346                 readResponse();
347             }
348         });
349     }
350
351     private void readResponse() {
352         try {
353             if (mResponseStream != null) {
354                 readResponseStream();
355             }
356         } catch (IOException e) {
357             mException = e;
358         } finally {
359             try {
360                 mConnection.disconnect();
361             } catch (ArrayIndexOutOfBoundsException t) {
362                 // Ignore it.
363             }
364
365             try {
366                 mSink.close();
367             } catch (IOException e) {
368                 if (mException == null) {
369                     mException = e;
370                 }
371             }
372         }
373         mListener.onRequestComplete(this);
374     }
375
376     private void readResponseStream() throws IOException {
377         byte[] buffer = new byte[MAX_CHUNK_SIZE];
378         int size;
379         while (!isCanceled() && (size = mResponseStream.read(buffer)) != -1) {
380             int start = 0;
381             int count = size;
382             mSize += size;
383             if (mSkippingToOffset) {
384                 if (mSize <= mOffset) {
385                     continue;
386                 } else {
387                     mSkippingToOffset = false;
388                     start = (int)(mOffset - (mSize - size));
389                     count -= start;
390                 }
391             }
392
393             if (mContentLengthLimit != 0 && mSize > mContentLengthLimit) {
394                 count -= (int)(mSize - mContentLengthLimit);
395                 if (count > 0) {
396                     mSink.write(ByteBuffer.wrap(buffer, start, count));
397                 }
398                 onContentLengthOverLimit();
399                 return;
400             }
401
402             mSink.write(ByteBuffer.wrap(buffer, start, count));
403         }
404     }
405
406     @Override
407     public void cancel() {
408         synchronized (mLock) {
409             if (mCanceled) {
410                 return;
411             }
412
413             mCanceled = true;
414         }
415     }
416
417     @Override
418     public boolean isCanceled() {
419         synchronized (mLock) {
420             return mCanceled;
421         }
422     }
423
424     @Override
425     public int getHttpStatusCode() {
426         int httpStatusCode = mHttpStatusCode;
427
428         // If we have been able to successfully resume a previously interrupted
429         // download,
430         // the status code will be 206, not 200. Since the rest of the
431         // application is
432         // expecting 200 to indicate success, we need to fake it.
433         if (httpStatusCode == HttpStatus.SC_PARTIAL_CONTENT) {
434             httpStatusCode = HttpStatus.SC_OK;
435         }
436         return httpStatusCode;
437     }
438
439     @Override
440     public IOException getException() {
441         if (mException == null && mContentLengthOverLimit) {
442             mException = new ResponseTooLargeException();
443         }
444         return mException;
445     }
446
447     private void onContentLengthOverLimit() {
448         mContentLengthOverLimit = true;
449         cancel();
450     }
451
452     private static boolean isError(int statusCode) {
453         return (statusCode / 100) != 2;
454     }
455
456     /**
457      * Returns the response as a ByteBuffer.
458      */
459     @Override
460     public ByteBuffer getByteBuffer() {
461         return ((ChunkedWritableByteChannel)mSink).getByteBuffer();
462     }
463
464     @Override
465     public byte[] getResponseAsBytes() {
466         return ((ChunkedWritableByteChannel)mSink).getBytes();
467     }
468
469     @Override
470     public long getContentLength() {
471         return mContentLength;
472     }
473
474     @Override
475     public String getContentType() {
476         return mContentType;
477     }
478
479     @Override
480     public String getHeader(String name) {
481         if (mConnection == null) {
482             throw new IllegalStateException("Response headers not available");
483         }
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);
489             }
490         }
491         return null;
492     }
493
494     @Override
495     public Map<String, List<String>> getAllHeaders() {
496         if (mConnection == null) {
497             throw new IllegalStateException("Response headers not available");
498         }
499         return mConnection.getHeaderFields();
500     }
501
502     private void validateNotStarted() {
503         if (mStarted) {
504             throw new IllegalStateException("Request already started");
505         }
506     }
507 }