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
5package org.chromium.net;
6
7import android.content.Context;
8import android.text.TextUtils;
9
10import org.apache.http.HttpStatus;
11
12import java.io.FileNotFoundException;
13import java.io.IOException;
14import java.io.InputStream;
15import java.io.OutputStream;
16import java.net.HttpURLConnection;
17import java.net.URL;
18import java.nio.ByteBuffer;
19import java.nio.channels.ReadableByteChannel;
20import java.nio.channels.WritableByteChannel;
21import java.util.Map;
22import java.util.Map.Entry;
23import java.util.concurrent.ExecutorService;
24import java.util.concurrent.Executors;
25import java.util.concurrent.ThreadFactory;
26import java.util.concurrent.atomic.AtomicInteger;
27import java.util.zip.GZIPInputStream;
28
29/**
30 * Network request using the HttpUrlConnection implementation.
31 */
32class 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}
462