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.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 }
    462