Home | History | Annotate | Download | only in net
      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 }
    509