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 org.apache.http.conn.ConnectTimeoutException;
8import org.chromium.base.CalledByNative;
9import org.chromium.base.JNINamespace;
10
11import java.io.IOException;
12import java.net.MalformedURLException;
13import java.net.URL;
14import java.net.UnknownHostException;
15import java.nio.ByteBuffer;
16import java.nio.channels.ReadableByteChannel;
17import java.nio.channels.WritableByteChannel;
18import java.util.HashMap;
19import java.util.Map;
20import java.util.Map.Entry;
21import java.util.concurrent.Semaphore;
22
23/**
24 * Network request using the native http stack implementation.
25 */
26@JNINamespace("cronet")
27public class UrlRequest {
28    private static final class ContextLock {
29    }
30
31    private static final int UPLOAD_BYTE_BUFFER_SIZE = 32768;
32
33    private final UrlRequestContext mRequestContext;
34    private final String mUrl;
35    private final int mPriority;
36    private final Map<String, String> mHeaders;
37    private final WritableByteChannel mSink;
38    private Map<String, String> mAdditionalHeaders;
39    private boolean mPostBodySet;
40    private ReadableByteChannel mPostBodyChannel;
41    private WritableByteChannel mOutputChannel;
42    private IOException mSinkException;
43    private volatile boolean mStarted;
44    private volatile boolean mCanceled;
45    private volatile boolean mRecycled;
46    private volatile boolean mFinished;
47    private boolean mHeadersAvailable;
48    private String mContentType;
49    private long mContentLength;
50    private Semaphore mAppendChunkSemaphore;
51    private final ContextLock mLock;
52
53    /**
54     * Native peer object, owned by UrlRequest.
55     */
56    private long mUrlRequestPeer;
57
58    /**
59     * Constructor.
60     *
61     * @param requestContext The context.
62     * @param url The URL.
63     * @param priority Request priority, e.g. {@link #REQUEST_PRIORITY_MEDIUM}.
64     * @param headers HTTP headers.
65     * @param sink The output channel into which downloaded content will be
66     *            written.
67     */
68    public UrlRequest(UrlRequestContext requestContext, String url,
69            int priority, Map<String, String> headers,
70            WritableByteChannel sink) {
71        if (requestContext == null) {
72            throw new NullPointerException("Context is required");
73        }
74        if (url == null) {
75            throw new NullPointerException("URL is required");
76        }
77        mRequestContext = requestContext;
78        mUrl = url;
79        mPriority = priority;
80        mHeaders = headers;
81        mSink = sink;
82        mLock = new ContextLock();
83        mUrlRequestPeer = nativeCreateRequestPeer(
84                mRequestContext.getUrlRequestContextPeer(), mUrl, mPriority);
85        mPostBodySet = false;
86    }
87
88    /**
89     * Adds a request header.
90     */
91    public void addHeader(String header, String value) {
92        validateNotStarted();
93        if (mAdditionalHeaders == null) {
94            mAdditionalHeaders = new HashMap<String, String>();
95        }
96        mAdditionalHeaders.put(header, value);
97    }
98
99    /**
100     * Sets data to upload as part of a POST request.
101     *
102     * @param contentType MIME type of the post content or null if this is not a
103     *            POST.
104     * @param data The content that needs to be uploaded if this is a POST
105     *            request.
106     */
107    public void setUploadData(String contentType, byte[] data) {
108        synchronized (mLock) {
109            validateNotStarted();
110            validatePostBodyNotSet();
111            nativeSetPostData(mUrlRequestPeer, contentType, data);
112            mPostBodySet = true;
113        }
114    }
115
116    /**
117     * Sets a readable byte channel to upload as part of a POST request.
118     *
119     * @param contentType MIME type of the post content or null if this is not a
120     *            POST request.
121     * @param channel The channel to read to read upload data from if this is a
122     *            POST request.
123     */
124    public void setUploadChannel(String contentType,
125            ReadableByteChannel channel) {
126        synchronized (mLock) {
127            validateNotStarted();
128            validatePostBodyNotSet();
129            nativeBeginChunkedUpload(mUrlRequestPeer, contentType);
130            mPostBodyChannel = channel;
131            mPostBodySet = true;
132        }
133        mAppendChunkSemaphore = new Semaphore(0);
134    }
135
136    public WritableByteChannel getSink() {
137        return mSink;
138    }
139
140    public void start() {
141        try {
142            synchronized (mLock) {
143                if (mCanceled) {
144                    return;
145                }
146
147                validateNotStarted();
148                validateNotRecycled();
149
150                mStarted = true;
151
152                if (mHeaders != null && !mHeaders.isEmpty()) {
153                    for (Entry<String, String> entry : mHeaders.entrySet()) {
154                        nativeAddHeader(mUrlRequestPeer, entry.getKey(),
155                                entry.getValue());
156                    }
157                }
158
159                if (mAdditionalHeaders != null && !mAdditionalHeaders.isEmpty()) {
160                    for (Entry<String, String> entry :
161                            mAdditionalHeaders.entrySet()) {
162                        nativeAddHeader(mUrlRequestPeer, entry.getKey(),
163                                entry.getValue());
164                    }
165                }
166
167                nativeStart(mUrlRequestPeer);
168            }
169
170            if (mPostBodyChannel != null) {
171                uploadFromChannel(mPostBodyChannel);
172            }
173        } finally {
174            if (mPostBodyChannel != null) {
175                try {
176                    mPostBodyChannel.close();
177                } catch (IOException e) {
178                    // Ignore
179                }
180            }
181        }
182    }
183
184    /**
185     * Uploads data from a {@code ReadableByteChannel} using chunked transfer
186     * encoding. The native call to append a chunk is asynchronous so a
187     * semaphore is used to delay writing into the buffer again until chromium
188     * is finished with it.
189     *
190     * @param channel the channel to read data from.
191     */
192    private void uploadFromChannel(ReadableByteChannel channel) {
193        ByteBuffer buffer = ByteBuffer.allocateDirect(UPLOAD_BYTE_BUFFER_SIZE);
194
195        // The chromium API requires us to specify in advance if a chunk is the
196        // last one. This extra ByteBuffer is needed to peek ahead and check for
197        // the end of the channel.
198        ByteBuffer checkForEnd = ByteBuffer.allocate(1);
199
200        try {
201            boolean lastChunk;
202            do {
203                // First dump in the one byte we read to check for the end of
204                // the channel. (The first time through the loop the checkForEnd
205                // buffer will be empty).
206                checkForEnd.flip();
207                buffer.clear();
208                buffer.put(checkForEnd);
209                checkForEnd.clear();
210
211                channel.read(buffer);
212                lastChunk = channel.read(checkForEnd) <= 0;
213                buffer.flip();
214                nativeAppendChunk(mUrlRequestPeer, buffer, buffer.limit(),
215                        lastChunk);
216
217                if (lastChunk) {
218                    break;
219                }
220
221                // Acquire permit before writing to the buffer again to ensure
222                // chromium is done with it.
223                mAppendChunkSemaphore.acquire();
224            } while (!lastChunk && !mFinished);
225        } catch (IOException e) {
226            mSinkException = e;
227            cancel();
228        } catch (InterruptedException e) {
229            mSinkException = new IOException(e);
230            cancel();
231        }
232    }
233
234    public void cancel() {
235        synchronized (mLock) {
236            if (mCanceled) {
237                return;
238            }
239
240            mCanceled = true;
241
242            if (!mRecycled) {
243                nativeCancel(mUrlRequestPeer);
244            }
245        }
246    }
247
248    public boolean isCanceled() {
249        synchronized (mLock) {
250            return mCanceled;
251        }
252    }
253
254    public boolean isRecycled() {
255        synchronized (mLock) {
256            return mRecycled;
257        }
258    }
259
260    /**
261     * Returns an exception if any, or null if the request was completed
262     * successfully.
263     */
264    public IOException getException() {
265        if (mSinkException != null) {
266            return mSinkException;
267        }
268
269        validateNotRecycled();
270
271        int errorCode = nativeGetErrorCode(mUrlRequestPeer);
272        switch (errorCode) {
273            case UrlRequestError.SUCCESS:
274                return null;
275            case UrlRequestError.UNKNOWN:
276                return new IOException(nativeGetErrorString(mUrlRequestPeer));
277            case UrlRequestError.MALFORMED_URL:
278                return new MalformedURLException("Malformed URL: " + mUrl);
279            case UrlRequestError.CONNECTION_TIMED_OUT:
280                return new ConnectTimeoutException("Connection timed out");
281            case UrlRequestError.UNKNOWN_HOST:
282                String host;
283                try {
284                    host = new URL(mUrl).getHost();
285                } catch (MalformedURLException e) {
286                    host = mUrl;
287                }
288                return new UnknownHostException("Unknown host: " + host);
289            default:
290                throw new IllegalStateException(
291                        "Unrecognized error code: " + errorCode);
292        }
293    }
294
295    public int getHttpStatusCode() {
296        return nativeGetHttpStatusCode(mUrlRequestPeer);
297    }
298
299    /**
300     * Content length as reported by the server. May be -1 or incorrect if the
301     * server returns the wrong number, which happens even with Google servers.
302     */
303    public long getContentLength() {
304        return mContentLength;
305    }
306
307    public String getContentType() {
308        return mContentType;
309    }
310
311    public String getHeader(String name) {
312        validateHeadersAvailable();
313        return nativeGetHeader(mUrlRequestPeer, name);
314    }
315
316    /**
317     * A callback invoked when appending a chunk to the request has completed.
318     */
319    @CalledByNative
320    protected void onAppendChunkCompleted() {
321        mAppendChunkSemaphore.release();
322    }
323
324    /**
325     * A callback invoked when the first chunk of the response has arrived.
326     */
327    @CalledByNative
328    protected void onResponseStarted() {
329        mContentType = nativeGetContentType(mUrlRequestPeer);
330        mContentLength = nativeGetContentLength(mUrlRequestPeer);
331        mHeadersAvailable = true;
332    }
333
334    /**
335     * A callback invoked when the response has been fully consumed.
336     */
337    protected void onRequestComplete() {
338    }
339
340    /**
341     * Consumes a portion of the response.
342     *
343     * @param byteBuffer The ByteBuffer to append. Must be a direct buffer, and
344     *            no references to it may be retained after the method ends, as
345     *            it wraps code managed on the native heap.
346     */
347    @CalledByNative
348    protected void onBytesRead(ByteBuffer byteBuffer) {
349        try {
350            while (byteBuffer.hasRemaining()) {
351                mSink.write(byteBuffer);
352            }
353        } catch (IOException e) {
354            mSinkException = e;
355            cancel();
356        }
357    }
358
359    /**
360     * Notifies the listener, releases native data structures.
361     */
362    @SuppressWarnings("unused")
363    @CalledByNative
364    private void finish() {
365        synchronized (mLock) {
366            mFinished = true;
367            if (mAppendChunkSemaphore != null) {
368                mAppendChunkSemaphore.release();
369            }
370
371            if (mRecycled) {
372                return;
373            }
374            try {
375                mSink.close();
376            } catch (IOException e) {
377                // Ignore
378            }
379            onRequestComplete();
380            nativeDestroyRequestPeer(mUrlRequestPeer);
381            mUrlRequestPeer = 0;
382            mRecycled = true;
383        }
384    }
385
386    private void validateNotRecycled() {
387        if (mRecycled) {
388            throw new IllegalStateException("Accessing recycled request");
389        }
390    }
391
392    private void validateNotStarted() {
393        if (mStarted) {
394            throw new IllegalStateException("Request already started");
395        }
396    }
397
398    private void validatePostBodyNotSet() {
399        if (mPostBodySet) {
400            throw new IllegalStateException("Post Body already set");
401        }
402    }
403
404
405    private void validateHeadersAvailable() {
406        if (!mHeadersAvailable) {
407            throw new IllegalStateException("Response headers not available");
408        }
409    }
410
411    public String getUrl() {
412        return mUrl;
413    }
414
415    private native long nativeCreateRequestPeer(long urlRequestContextPeer,
416            String url, int priority);
417
418    private native void nativeAddHeader(long urlRequestPeer, String name,
419            String value);
420
421    private native void nativeSetPostData(long urlRequestPeer,
422            String contentType, byte[] content);
423
424    private native void nativeBeginChunkedUpload(long urlRequestPeer,
425            String contentType);
426
427    private native void nativeAppendChunk(long urlRequestPeer, ByteBuffer chunk,
428            int chunkSize, boolean isLastChunk);
429
430    private native void nativeStart(long urlRequestPeer);
431
432    private native void nativeCancel(long urlRequestPeer);
433
434    private native void nativeDestroyRequestPeer(long urlRequestPeer);
435
436    private native int nativeGetErrorCode(long urlRequestPeer);
437
438    private native int nativeGetHttpStatusCode(long urlRequestPeer);
439
440    private native String nativeGetErrorString(long urlRequestPeer);
441
442    private native String nativeGetContentType(long urlRequestPeer);
443
444    private native long nativeGetContentLength(long urlRequestPeer);
445
446    private native String nativeGetHeader(long urlRequestPeer, String name);
447}
448