root/net/cronet/android/java/src/org/chromium/net/HttpUrlConnectionUrlRequest.java

/* [<][>][^][v][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. getExecutor
  2. getUrl
  3. setOffset
  4. setContentLengthLimit
  5. setUploadData
  6. setUploadChannel
  7. start
  8. uploadData
  9. readResponseAsync
  10. readResponse
  11. readResponseStream
  12. cancel
  13. isCanceled
  14. getHttpStatusCode
  15. getException
  16. onContentLengthOverLimit
  17. isError
  18. getByteBuffer
  19. getResponseAsBytes
  20. getContentLength
  21. getContentType
  22. validateNotStarted

// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.net;

import android.content.Context;
import android.text.TextUtils;

import org.apache.http.HttpStatus;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.zip.GZIPInputStream;

/**
 * Network request using the HttpUrlConnection implementation.
 */
class HttpUrlConnectionUrlRequest implements HttpUrlRequest {

    private static final int MAX_CHUNK_SIZE = 8192;

    private static final int CONNECT_TIMEOUT = 3000;

    private static final int READ_TIMEOUT = 90000;

    private final Context mContext;

    private final String mUrl;

    private final Map<String, String> mHeaders;

    private final WritableByteChannel mSink;

    private final HttpUrlRequestListener mListener;

    private IOException mException;

    private HttpURLConnection mConnection;

    private long mOffset;

    private int mContentLength;

    private long mContentLengthLimit;

    private boolean mCancelIfContentLengthOverLimit;

    private boolean mContentLengthOverLimit;

    private boolean mSkippingToOffset;

    private long mSize;

    private String mPostContentType;

    private byte[] mPostData;

    private ReadableByteChannel mPostDataChannel;

    private String mContentType;

    private int mHttpStatusCode;

    private boolean mStarted;

    private boolean mCanceled;

    private InputStream mResponseStream;

    private final Object mLock;

    private static ExecutorService sExecutorService;

    private static final Object sExecutorServiceLock = new Object();

    HttpUrlConnectionUrlRequest(Context context, String url,
            int requestPriority, Map<String, String> headers,
            HttpUrlRequestListener listener) {
        this(context, url, requestPriority, headers,
                new ChunkedWritableByteChannel(), listener);
    }

    HttpUrlConnectionUrlRequest(Context context, String url,
            int requestPriority, Map<String, String> headers,
            WritableByteChannel sink, HttpUrlRequestListener listener) {
        if (context == null) {
            throw new NullPointerException("Context is required");
        }
        if (url == null) {
            throw new NullPointerException("URL is required");
        }
        mContext = context;
        mUrl = url;
        mHeaders = headers;
        mSink = sink;
        mListener = listener;
        mLock = new Object();
    }

    private static ExecutorService getExecutor() {
        synchronized (sExecutorServiceLock) {
            if (sExecutorService == null) {
                ThreadFactory threadFactory = new ThreadFactory() {
                    private final AtomicInteger mCount = new AtomicInteger(1);

                        @Override
                    public Thread newThread(Runnable r) {
                        Thread thread = new Thread(r,
                                "HttpUrlConnection #" +
                                mCount.getAndIncrement());
                        // Note that this thread is not doing actual networking.
                        // It's only a controller.
                        thread.setPriority(Thread.NORM_PRIORITY);
                        return thread;
                    }
                };
                sExecutorService = Executors.newCachedThreadPool(threadFactory);
            }
            return sExecutorService;
        }
    }

    @Override
    public String getUrl() {
        return mUrl;
    }

    @Override
    public void setOffset(long offset) {
        mOffset = offset;
    }

    @Override
    public void setContentLengthLimit(long limit, boolean cancelEarly) {
        mContentLengthLimit = limit;
        mCancelIfContentLengthOverLimit = cancelEarly;
    }

    @Override
    public void setUploadData(String contentType, byte[] data) {
        validateNotStarted();
        mPostContentType = contentType;
        mPostData = data;
        mPostDataChannel = null;
    }

    @Override
    public void setUploadChannel(String contentType,
            ReadableByteChannel channel) {
        validateNotStarted();
        mPostContentType = contentType;
        mPostDataChannel = channel;
        mPostData = null;
    }

    @Override
    public void start() {
        boolean readingResponse = false;
        try {
            synchronized (mLock) {
                if (mCanceled) {
                    return;
                }
            }

            URL url = new URL(mUrl);
            mConnection = (HttpURLConnection)url.openConnection();
            mConnection.setConnectTimeout(CONNECT_TIMEOUT);
            mConnection.setReadTimeout(READ_TIMEOUT);
            mConnection.setInstanceFollowRedirects(true);
            if (mHeaders != null) {
                for (Entry<String, String> header : mHeaders.entrySet()) {
                    mConnection.setRequestProperty(header.getKey(),
                            header.getValue());
                }
            }

            if (mOffset != 0) {
                mConnection.setRequestProperty("Range",
                        "bytes=" + mOffset + "-");
            }

            if (mConnection.getRequestProperty("User-Agent") == null) {
                mConnection.setRequestProperty("User-Agent",
                        UserAgent.from(mContext));
            }

            if (mPostData != null || mPostDataChannel != null) {
                uploadData();
            }

            InputStream stream = null;
            try {
                // We need to open the stream before asking for the response
                // code.
                stream = mConnection.getInputStream();
            } catch (FileNotFoundException ex) {
                // Ignore - the response has no body.
            }

            mHttpStatusCode = mConnection.getResponseCode();
            mContentType = mConnection.getContentType();
            mContentLength = mConnection.getContentLength();
            if (mContentLengthLimit > 0 && mContentLength > mContentLengthLimit
                    && mCancelIfContentLengthOverLimit) {
                onContentLengthOverLimit();
                return;
            }

            mResponseStream = isError(mHttpStatusCode) ? mConnection
                    .getErrorStream()
                    : stream;

            if (mResponseStream != null
                    && "gzip".equals(mConnection.getContentEncoding())) {
                mResponseStream = new GZIPInputStream(mResponseStream);
                mContentLength = -1;
            }

            if (mOffset != 0) {
                // The server may ignore the request for a byte range.
                if (mHttpStatusCode == HttpStatus.SC_OK) {
                    if (mContentLength != -1) {
                        mContentLength -= mOffset;
                    }
                    mSkippingToOffset = true;
                } else {
                    mSize = mOffset;
                }
            }

            if (mResponseStream != null) {
                readingResponse = true;
                readResponseAsync();
            }
        } catch (IOException e) {
            mException = e;
        } finally {
            // Don't call onRequestComplete yet if we are reading the response
            // on a separate thread
            if (!readingResponse) {
                mListener.onRequestComplete(this);
            }
        }
    }

    private void uploadData() throws IOException {
        mConnection.setDoOutput(true);
        if (!TextUtils.isEmpty(mPostContentType)) {
            mConnection.setRequestProperty("Content-Type", mPostContentType);
        }

        OutputStream uploadStream = null;
        try {
            if (mPostData != null) {
                mConnection.setFixedLengthStreamingMode(mPostData.length);
                uploadStream = mConnection.getOutputStream();
                uploadStream.write(mPostData);
            } else {
                mConnection.setChunkedStreamingMode(MAX_CHUNK_SIZE);
                uploadStream = mConnection.getOutputStream();
                byte[] bytes = new byte[MAX_CHUNK_SIZE];
                ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
                while (mPostDataChannel.read(byteBuffer) > 0) {
                    byteBuffer.flip();
                    uploadStream.write(bytes, 0, byteBuffer.limit());
                    byteBuffer.clear();
                }
            }
        } finally {
            if (uploadStream != null) {
                uploadStream.close();
            }
        }
    }

    private void readResponseAsync() {
        getExecutor().execute(new Runnable() {
            @Override
            public void run() {
                readResponse();
            }
        });
    }

    private void readResponse() {
        try {
            if (mResponseStream != null) {
                readResponseStream();
            }
        } catch (IOException e) {
            mException = e;
        } finally {
            try {
                mConnection.disconnect();
            } catch (ArrayIndexOutOfBoundsException t) {
                // Ignore it.
            }

            try {
                mSink.close();
            } catch (IOException e) {
                if (mException == null) {
                    mException = e;
                }
            }
        }
        mListener.onRequestComplete(this);
    }

    private void readResponseStream() throws IOException {
        byte[] buffer = new byte[MAX_CHUNK_SIZE];
        int size;
        while (!isCanceled() && (size = mResponseStream.read(buffer)) != -1) {
            int start = 0;
            int count = size;
            mSize += size;
            if (mSkippingToOffset) {
                if (mSize <= mOffset) {
                    continue;
                } else {
                    mSkippingToOffset = false;
                    start = (int)(mOffset - (mSize - size));
                    count -= start;
                }
            }

            if (mContentLengthLimit != 0 && mSize > mContentLengthLimit) {
                count -= (int)(mSize - mContentLengthLimit);
                if (count > 0) {
                    mSink.write(ByteBuffer.wrap(buffer, start, count));
                }
                onContentLengthOverLimit();
                return;
            }

            mSink.write(ByteBuffer.wrap(buffer, start, count));
        }
    }

    @Override
    public void cancel() {
        synchronized (mLock) {
            if (mCanceled) {
                return;
            }

            mCanceled = true;
        }
    }

    @Override
    public boolean isCanceled() {
        synchronized (mLock) {
            return mCanceled;
        }
    }

    @Override
    public int getHttpStatusCode() {
        int httpStatusCode = mHttpStatusCode;

        // If we have been able to successfully resume a previously interrupted
        // download,
        // the status code will be 206, not 200. Since the rest of the
        // application is
        // expecting 200 to indicate success, we need to fake it.
        if (httpStatusCode == HttpStatus.SC_PARTIAL_CONTENT) {
            httpStatusCode = HttpStatus.SC_OK;
        }
        return httpStatusCode;
    }

    @Override
    public IOException getException() {
        if (mException == null && mContentLengthOverLimit) {
            mException = new ResponseTooLargeException();
        }
        return mException;
    }

    private void onContentLengthOverLimit() {
        mContentLengthOverLimit = true;
        cancel();
    }

    private static boolean isError(int statusCode) {
        return (statusCode / 100) != 2;
    }

    /**
     * Returns the response as a ByteBuffer.
     */
    @Override
    public ByteBuffer getByteBuffer() {
        return ((ChunkedWritableByteChannel)mSink).getByteBuffer();
    }

    @Override
    public byte[] getResponseAsBytes() {
        return ((ChunkedWritableByteChannel)mSink).getBytes();
    }

    @Override
    public long getContentLength() {
        return mContentLength;
    }

    @Override
    public String getContentType() {
        return mContentType;
    }

    private void validateNotStarted() {
        if (mStarted) {
            throw new IllegalStateException("Request already started");
        }
    }
}

/* [<][>][^][v][top][bottom][index][help] */