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