Upstream version 7.35.144.0
[platform/framework/web/crosswalk.git] / src / net / 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.URL;
18 import java.nio.ByteBuffer;
19 import java.nio.channels.ReadableByteChannel;
20 import java.nio.channels.WritableByteChannel;
21 import java.util.Map;
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;
28
29 /**
30  * Network request using the HttpUrlConnection implementation.
31  */
32 class HttpUrlConnectionUrlRequest implements HttpUrlRequest {
33
34     private static final int MAX_CHUNK_SIZE = 8192;
35
36     private static final int CONNECT_TIMEOUT = 3000;
37
38     private static final int READ_TIMEOUT = 90000;
39
40     private final Context mContext;
41
42     private final String mUrl;
43
44     private final Map<String, String> mHeaders;
45
46     private final WritableByteChannel mSink;
47
48     private final HttpUrlRequestListener mListener;
49
50     private IOException mException;
51
52     private HttpURLConnection mConnection;
53
54     private long mOffset;
55
56     private int mContentLength;
57
58     private long mContentLengthLimit;
59
60     private boolean mCancelIfContentLengthOverLimit;
61
62     private boolean mContentLengthOverLimit;
63
64     private boolean mSkippingToOffset;
65
66     private long mSize;
67
68     private String mPostContentType;
69
70     private byte[] mPostData;
71
72     private ReadableByteChannel mPostDataChannel;
73
74     private String mContentType;
75
76     private int mHttpStatusCode;
77
78     private boolean mStarted;
79
80     private boolean mCanceled;
81
82     private InputStream mResponseStream;
83
84     private final Object mLock;
85
86     private static ExecutorService sExecutorService;
87
88     private static final Object sExecutorServiceLock = new Object();
89
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);
95     }
96
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");
102         }
103         if (url == null) {
104             throw new NullPointerException("URL is required");
105         }
106         mContext = context;
107         mUrl = url;
108         mHeaders = headers;
109         mSink = sink;
110         mListener = listener;
111         mLock = new Object();
112     }
113
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);
119
120                         @Override
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);
128                         return thread;
129                     }
130                 };
131                 sExecutorService = Executors.newCachedThreadPool(threadFactory);
132             }
133             return sExecutorService;
134         }
135     }
136
137     @Override
138     public String getUrl() {
139         return mUrl;
140     }
141
142     @Override
143     public void setOffset(long offset) {
144         mOffset = offset;
145     }
146
147     @Override
148     public void setContentLengthLimit(long limit, boolean cancelEarly) {
149         mContentLengthLimit = limit;
150         mCancelIfContentLengthOverLimit = cancelEarly;
151     }
152
153     @Override
154     public void setUploadData(String contentType, byte[] data) {
155         validateNotStarted();
156         mPostContentType = contentType;
157         mPostData = data;
158         mPostDataChannel = null;
159     }
160
161     @Override
162     public void setUploadChannel(String contentType,
163             ReadableByteChannel channel) {
164         validateNotStarted();
165         mPostContentType = contentType;
166         mPostDataChannel = channel;
167         mPostData = null;
168     }
169
170     @Override
171     public void start() {
172         boolean readingResponse = false;
173         try {
174             synchronized (mLock) {
175                 if (mCanceled) {
176                     return;
177                 }
178             }
179
180             URL url = new URL(mUrl);
181             mConnection = (HttpURLConnection)url.openConnection();
182             mConnection.setConnectTimeout(CONNECT_TIMEOUT);
183             mConnection.setReadTimeout(READ_TIMEOUT);
184             mConnection.setInstanceFollowRedirects(true);
185             if (mHeaders != null) {
186                 for (Entry<String, String> header : mHeaders.entrySet()) {
187                     mConnection.setRequestProperty(header.getKey(),
188                             header.getValue());
189                 }
190             }
191
192             if (mOffset != 0) {
193                 mConnection.setRequestProperty("Range",
194                         "bytes=" + mOffset + "-");
195             }
196
197             if (mConnection.getRequestProperty("User-Agent") == null) {
198                 mConnection.setRequestProperty("User-Agent",
199                         UserAgent.from(mContext));
200             }
201
202             if (mPostData != null || mPostDataChannel != null) {
203                 uploadData();
204             }
205
206             InputStream stream = null;
207             try {
208                 // We need to open the stream before asking for the response
209                 // code.
210                 stream = mConnection.getInputStream();
211             } catch (FileNotFoundException ex) {
212                 // Ignore - the response has no body.
213             }
214
215             mHttpStatusCode = mConnection.getResponseCode();
216             mContentType = mConnection.getContentType();
217             mContentLength = mConnection.getContentLength();
218             if (mContentLengthLimit > 0 && mContentLength > mContentLengthLimit
219                     && mCancelIfContentLengthOverLimit) {
220                 onContentLengthOverLimit();
221                 return;
222             }
223
224             mResponseStream = isError(mHttpStatusCode) ? mConnection
225                     .getErrorStream()
226                     : stream;
227
228             if (mResponseStream != null
229                     && "gzip".equals(mConnection.getContentEncoding())) {
230                 mResponseStream = new GZIPInputStream(mResponseStream);
231                 mContentLength = -1;
232             }
233
234             if (mOffset != 0) {
235                 // The server may ignore the request for a byte range.
236                 if (mHttpStatusCode == HttpStatus.SC_OK) {
237                     if (mContentLength != -1) {
238                         mContentLength -= mOffset;
239                     }
240                     mSkippingToOffset = true;
241                 } else {
242                     mSize = mOffset;
243                 }
244             }
245
246             if (mResponseStream != null) {
247                 readingResponse = true;
248                 readResponseAsync();
249             }
250         } catch (IOException e) {
251             mException = e;
252         } finally {
253             // Don't call onRequestComplete yet if we are reading the response
254             // on a separate thread
255             if (!readingResponse) {
256                 mListener.onRequestComplete(this);
257             }
258         }
259     }
260
261     private void uploadData() throws IOException {
262         mConnection.setDoOutput(true);
263         if (!TextUtils.isEmpty(mPostContentType)) {
264             mConnection.setRequestProperty("Content-Type", mPostContentType);
265         }
266
267         OutputStream uploadStream = null;
268         try {
269             if (mPostData != null) {
270                 mConnection.setFixedLengthStreamingMode(mPostData.length);
271                 uploadStream = mConnection.getOutputStream();
272                 uploadStream.write(mPostData);
273             } else {
274                 mConnection.setChunkedStreamingMode(MAX_CHUNK_SIZE);
275                 uploadStream = mConnection.getOutputStream();
276                 byte[] bytes = new byte[MAX_CHUNK_SIZE];
277                 ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
278                 while (mPostDataChannel.read(byteBuffer) > 0) {
279                     byteBuffer.flip();
280                     uploadStream.write(bytes, 0, byteBuffer.limit());
281                     byteBuffer.clear();
282                 }
283             }
284         } finally {
285             if (uploadStream != null) {
286                 uploadStream.close();
287             }
288         }
289     }
290
291     private void readResponseAsync() {
292         getExecutor().execute(new Runnable() {
293             @Override
294             public void run() {
295                 readResponse();
296             }
297         });
298     }
299
300     private void readResponse() {
301         try {
302             if (mResponseStream != null) {
303                 readResponseStream();
304             }
305         } catch (IOException e) {
306             mException = e;
307         } finally {
308             try {
309                 mConnection.disconnect();
310             } catch (ArrayIndexOutOfBoundsException t) {
311                 // Ignore it.
312             }
313
314             try {
315                 mSink.close();
316             } catch (IOException e) {
317                 if (mException == null) {
318                     mException = e;
319                 }
320             }
321         }
322         mListener.onRequestComplete(this);
323     }
324
325     private void readResponseStream() throws IOException {
326         byte[] buffer = new byte[MAX_CHUNK_SIZE];
327         int size;
328         while (!isCanceled() && (size = mResponseStream.read(buffer)) != -1) {
329             int start = 0;
330             int count = size;
331             mSize += size;
332             if (mSkippingToOffset) {
333                 if (mSize <= mOffset) {
334                     continue;
335                 } else {
336                     mSkippingToOffset = false;
337                     start = (int)(mOffset - (mSize - size));
338                     count -= start;
339                 }
340             }
341
342             if (mContentLengthLimit != 0 && mSize > mContentLengthLimit) {
343                 count -= (int)(mSize - mContentLengthLimit);
344                 if (count > 0) {
345                     mSink.write(ByteBuffer.wrap(buffer, start, count));
346                 }
347                 onContentLengthOverLimit();
348                 return;
349             }
350
351             mSink.write(ByteBuffer.wrap(buffer, start, count));
352         }
353     }
354
355     @Override
356     public void cancel() {
357         synchronized (mLock) {
358             if (mCanceled) {
359                 return;
360             }
361
362             mCanceled = true;
363         }
364     }
365
366     @Override
367     public boolean isCanceled() {
368         synchronized (mLock) {
369             return mCanceled;
370         }
371     }
372
373     @Override
374     public int getHttpStatusCode() {
375         int httpStatusCode = mHttpStatusCode;
376
377         // If we have been able to successfully resume a previously interrupted
378         // download,
379         // the status code will be 206, not 200. Since the rest of the
380         // application is
381         // expecting 200 to indicate success, we need to fake it.
382         if (httpStatusCode == HttpStatus.SC_PARTIAL_CONTENT) {
383             httpStatusCode = HttpStatus.SC_OK;
384         }
385         return httpStatusCode;
386     }
387
388     @Override
389     public IOException getException() {
390         if (mException == null && mContentLengthOverLimit) {
391             mException = new ResponseTooLargeException();
392         }
393         return mException;
394     }
395
396     private void onContentLengthOverLimit() {
397         mContentLengthOverLimit = true;
398         cancel();
399     }
400
401     private static boolean isError(int statusCode) {
402         return (statusCode / 100) != 2;
403     }
404
405     /**
406      * Returns the response as a ByteBuffer.
407      */
408     @Override
409     public ByteBuffer getByteBuffer() {
410         return ((ChunkedWritableByteChannel)mSink).getByteBuffer();
411     }
412
413     @Override
414     public byte[] getResponseAsBytes() {
415         return ((ChunkedWritableByteChannel)mSink).getBytes();
416     }
417
418     @Override
419     public long getContentLength() {
420         return mContentLength;
421     }
422
423     @Override
424     public String getContentType() {
425         return mContentType;
426     }
427
428     private void validateNotStarted() {
429         if (mStarted) {
430             throw new IllegalStateException("Request already started");
431         }
432     }
433 }