• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 org.apache.http.conn.ConnectTimeoutException;
8 import org.chromium.base.CalledByNative;
9 import org.chromium.base.JNINamespace;
10 
11 import java.io.IOException;
12 import java.net.MalformedURLException;
13 import java.net.URL;
14 import java.net.UnknownHostException;
15 import java.nio.ByteBuffer;
16 import java.nio.channels.ReadableByteChannel;
17 import java.nio.channels.WritableByteChannel;
18 import java.util.HashMap;
19 import java.util.Map;
20 import java.util.Map.Entry;
21 import java.util.concurrent.Semaphore;
22 
23 /**
24  * Network request using the native http stack implementation.
25  */
26 @JNINamespace("cronet")
27 public 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      */
UrlRequest(UrlRequestContext requestContext, String url, int priority, Map<String, String> headers, WritableByteChannel sink)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      */
addHeader(String header, String value)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      */
setUploadData(String contentType, byte[] data)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      */
setUploadChannel(String contentType, ReadableByteChannel channel)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 
getSink()136     public WritableByteChannel getSink() {
137         return mSink;
138     }
139 
start()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      */
uploadFromChannel(ReadableByteChannel channel)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 
cancel()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 
isCanceled()248     public boolean isCanceled() {
249         synchronized (mLock) {
250             return mCanceled;
251         }
252     }
253 
isRecycled()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      */
getException()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 
getHttpStatusCode()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      */
getContentLength()303     public long getContentLength() {
304         return mContentLength;
305     }
306 
getContentType()307     public String getContentType() {
308         return mContentType;
309     }
310 
getHeader(String name)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
onAppendChunkCompleted()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
onResponseStarted()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      */
onRequestComplete()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
onBytesRead(ByteBuffer byteBuffer)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
finish()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 
validateNotRecycled()386     private void validateNotRecycled() {
387         if (mRecycled) {
388             throw new IllegalStateException("Accessing recycled request");
389         }
390     }
391 
validateNotStarted()392     private void validateNotStarted() {
393         if (mStarted) {
394             throw new IllegalStateException("Request already started");
395         }
396     }
397 
validatePostBodyNotSet()398     private void validatePostBodyNotSet() {
399         if (mPostBodySet) {
400             throw new IllegalStateException("Post Body already set");
401         }
402     }
403 
404 
validateHeadersAvailable()405     private void validateHeadersAvailable() {
406         if (!mHeadersAvailable) {
407             throw new IllegalStateException("Response headers not available");
408         }
409     }
410 
getUrl()411     public String getUrl() {
412         return mUrl;
413     }
414 
nativeCreateRequestPeer(long urlRequestContextPeer, String url, int priority)415     private native long nativeCreateRequestPeer(long urlRequestContextPeer,
416             String url, int priority);
417 
nativeAddHeader(long urlRequestPeer, String name, String value)418     private native void nativeAddHeader(long urlRequestPeer, String name,
419             String value);
420 
nativeSetPostData(long urlRequestPeer, String contentType, byte[] content)421     private native void nativeSetPostData(long urlRequestPeer,
422             String contentType, byte[] content);
423 
nativeBeginChunkedUpload(long urlRequestPeer, String contentType)424     private native void nativeBeginChunkedUpload(long urlRequestPeer,
425             String contentType);
426 
nativeAppendChunk(long urlRequestPeer, ByteBuffer chunk, int chunkSize, boolean isLastChunk)427     private native void nativeAppendChunk(long urlRequestPeer, ByteBuffer chunk,
428             int chunkSize, boolean isLastChunk);
429 
nativeStart(long urlRequestPeer)430     private native void nativeStart(long urlRequestPeer);
431 
nativeCancel(long urlRequestPeer)432     private native void nativeCancel(long urlRequestPeer);
433 
nativeDestroyRequestPeer(long urlRequestPeer)434     private native void nativeDestroyRequestPeer(long urlRequestPeer);
435 
nativeGetErrorCode(long urlRequestPeer)436     private native int nativeGetErrorCode(long urlRequestPeer);
437 
nativeGetHttpStatusCode(long urlRequestPeer)438     private native int nativeGetHttpStatusCode(long urlRequestPeer);
439 
nativeGetErrorString(long urlRequestPeer)440     private native String nativeGetErrorString(long urlRequestPeer);
441 
nativeGetContentType(long urlRequestPeer)442     private native String nativeGetContentType(long urlRequestPeer);
443 
nativeGetContentLength(long urlRequestPeer)444     private native long nativeGetContentLength(long urlRequestPeer);
445 
nativeGetHeader(long urlRequestPeer, String name)446     private native String nativeGetHeader(long urlRequestPeer, String name);
447 }
448