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