OLD | NEW |
| (Empty) |
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.ArrayList; | |
19 import java.util.HashMap; | |
20 import java.util.List; | |
21 import java.util.Map; | |
22 import java.util.Map.Entry; | |
23 | |
24 /** | |
25 * Network request using the native http stack implementation. | |
26 */ | |
27 @JNINamespace("cronet") | |
28 public class UrlRequest { | |
29 private static final class ContextLock { | |
30 } | |
31 | |
32 private final UrlRequestContext mRequestContext; | |
33 private final String mUrl; | |
34 private final int mPriority; | |
35 private final Map<String, String> mHeaders; | |
36 private final WritableByteChannel mSink; | |
37 private Map<String, String> mAdditionalHeaders; | |
38 private String mUploadContentType; | |
39 private String mMethod; | |
40 private byte[] mUploadData; | |
41 private ReadableByteChannel mUploadChannel; | |
42 private WritableByteChannel mOutputChannel; | |
43 private IOException mSinkException; | |
44 private volatile boolean mStarted; | |
45 private volatile boolean mCanceled; | |
46 private volatile boolean mRecycled; | |
47 private volatile boolean mFinished; | |
48 private boolean mHeadersAvailable; | |
49 private String mContentType; | |
50 private long mContentLength; | |
51 private long mUploadContentLength; | |
52 private final ContextLock mLock; | |
53 | |
54 /** | |
55 * Native adapter object, owned by UrlRequest. | |
56 */ | |
57 private long mUrlRequestAdapter; | |
58 | |
59 /** | |
60 * Constructor. | |
61 * | |
62 * @param requestContext The context. | |
63 * @param url The URL. | |
64 * @param priority Request priority, e.g. {@link #REQUEST_PRIORITY_MEDIUM}. | |
65 * @param headers HTTP headers. | |
66 * @param sink The output channel into which downloaded content will be | |
67 * written. | |
68 */ | |
69 public UrlRequest(UrlRequestContext requestContext, String url, | |
70 int priority, Map<String, String> headers, | |
71 WritableByteChannel sink) { | |
72 if (requestContext == null) { | |
73 throw new NullPointerException("Context is required"); | |
74 } | |
75 if (url == null) { | |
76 throw new NullPointerException("URL is required"); | |
77 } | |
78 mRequestContext = requestContext; | |
79 mUrl = url; | |
80 mPriority = priority; | |
81 mHeaders = headers; | |
82 mSink = sink; | |
83 mLock = new ContextLock(); | |
84 mUrlRequestAdapter = nativeCreateRequestAdapter( | |
85 mRequestContext.getUrlRequestContextAdapter(), mUrl, mPriority); | |
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 mUploadContentType = contentType; | |
111 mUploadData = data; | |
112 mUploadChannel = null; | |
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 * @param contentLength The length of data to upload. | |
124 */ | |
125 public void setUploadChannel(String contentType, | |
126 ReadableByteChannel channel, long contentLength) { | |
127 synchronized (mLock) { | |
128 validateNotStarted(); | |
129 mUploadContentType = contentType; | |
130 mUploadChannel = channel; | |
131 mUploadContentLength = contentLength; | |
132 mUploadData = null; | |
133 } | |
134 } | |
135 | |
136 public void setHttpMethod(String method) { | |
137 validateNotStarted(); | |
138 if (!("PUT".equals(method) || "POST".equals(method))) { | |
139 throw new IllegalArgumentException("Only PUT or POST are allowed."); | |
140 } | |
141 mMethod = method; | |
142 } | |
143 | |
144 public WritableByteChannel getSink() { | |
145 return mSink; | |
146 } | |
147 | |
148 public void start() { | |
149 synchronized (mLock) { | |
150 if (mCanceled) { | |
151 return; | |
152 } | |
153 | |
154 validateNotStarted(); | |
155 validateNotRecycled(); | |
156 | |
157 mStarted = true; | |
158 | |
159 String method = mMethod; | |
160 if (method == null && | |
161 ((mUploadData != null && mUploadData.length > 0) || | |
162 mUploadChannel != null)) { | |
163 // Default to POST if there is data to upload but no method was | |
164 // specified. | |
165 method = "POST"; | |
166 } | |
167 | |
168 if (method != null) { | |
169 nativeSetMethod(mUrlRequestAdapter, method); | |
170 } | |
171 | |
172 if (mHeaders != null && !mHeaders.isEmpty()) { | |
173 for (Entry<String, String> entry : mHeaders.entrySet()) { | |
174 nativeAddHeader(mUrlRequestAdapter, entry.getKey(), | |
175 entry.getValue()); | |
176 } | |
177 } | |
178 | |
179 if (mAdditionalHeaders != null) { | |
180 for (Entry<String, String> entry : | |
181 mAdditionalHeaders.entrySet()) { | |
182 nativeAddHeader(mUrlRequestAdapter, entry.getKey(), | |
183 entry.getValue()); | |
184 } | |
185 } | |
186 | |
187 if (mUploadData != null && mUploadData.length > 0) { | |
188 nativeSetUploadData(mUrlRequestAdapter, mUploadContentType, | |
189 mUploadData); | |
190 } else if (mUploadChannel != null) { | |
191 nativeSetUploadChannel(mUrlRequestAdapter, mUploadContentType, | |
192 mUploadContentLength); | |
193 } | |
194 | |
195 nativeStart(mUrlRequestAdapter); | |
196 } | |
197 } | |
198 | |
199 public void cancel() { | |
200 synchronized (mLock) { | |
201 if (mCanceled) { | |
202 return; | |
203 } | |
204 | |
205 mCanceled = true; | |
206 | |
207 if (!mRecycled) { | |
208 nativeCancel(mUrlRequestAdapter); | |
209 } | |
210 } | |
211 } | |
212 | |
213 public boolean isCanceled() { | |
214 synchronized (mLock) { | |
215 return mCanceled; | |
216 } | |
217 } | |
218 | |
219 public boolean isRecycled() { | |
220 synchronized (mLock) { | |
221 return mRecycled; | |
222 } | |
223 } | |
224 | |
225 /** | |
226 * Returns an exception if any, or null if the request was completed | |
227 * successfully. | |
228 */ | |
229 public IOException getException() { | |
230 if (mSinkException != null) { | |
231 return mSinkException; | |
232 } | |
233 | |
234 validateNotRecycled(); | |
235 | |
236 int errorCode = nativeGetErrorCode(mUrlRequestAdapter); | |
237 switch (errorCode) { | |
238 case UrlRequestError.SUCCESS: | |
239 return null; | |
240 case UrlRequestError.UNKNOWN: | |
241 return new IOException( | |
242 nativeGetErrorString(mUrlRequestAdapter)); | |
243 case UrlRequestError.MALFORMED_URL: | |
244 return new MalformedURLException("Malformed URL: " + mUrl); | |
245 case UrlRequestError.CONNECTION_TIMED_OUT: | |
246 return new ConnectTimeoutException("Connection timed out"); | |
247 case UrlRequestError.UNKNOWN_HOST: | |
248 String host; | |
249 try { | |
250 host = new URL(mUrl).getHost(); | |
251 } catch (MalformedURLException e) { | |
252 host = mUrl; | |
253 } | |
254 return new UnknownHostException("Unknown host: " + host); | |
255 default: | |
256 throw new IllegalStateException( | |
257 "Unrecognized error code: " + errorCode); | |
258 } | |
259 } | |
260 | |
261 public int getHttpStatusCode() { | |
262 return nativeGetHttpStatusCode(mUrlRequestAdapter); | |
263 } | |
264 | |
265 /** | |
266 * Content length as reported by the server. May be -1 or incorrect if the | |
267 * server returns the wrong number, which happens even with Google servers. | |
268 */ | |
269 public long getContentLength() { | |
270 return mContentLength; | |
271 } | |
272 | |
273 public String getContentType() { | |
274 return mContentType; | |
275 } | |
276 | |
277 public String getHeader(String name) { | |
278 validateHeadersAvailable(); | |
279 return nativeGetHeader(mUrlRequestAdapter, name); | |
280 } | |
281 | |
282 // All response headers. | |
283 public Map<String, List<String>> getAllHeaders() { | |
284 validateHeadersAvailable(); | |
285 ResponseHeadersMap result = new ResponseHeadersMap(); | |
286 nativeGetAllHeaders(mUrlRequestAdapter, result); | |
287 return result; | |
288 } | |
289 | |
290 /** | |
291 * A callback invoked when the first chunk of the response has arrived. | |
292 */ | |
293 @CalledByNative | |
294 protected void onResponseStarted() { | |
295 mContentType = nativeGetContentType(mUrlRequestAdapter); | |
296 mContentLength = nativeGetContentLength(mUrlRequestAdapter); | |
297 mHeadersAvailable = true; | |
298 } | |
299 | |
300 /** | |
301 * A callback invoked when the response has been fully consumed. | |
302 */ | |
303 protected void onRequestComplete() { | |
304 } | |
305 | |
306 /** | |
307 * Consumes a portion of the response. | |
308 * | |
309 * @param byteBuffer The ByteBuffer to append. Must be a direct buffer, and | |
310 * no references to it may be retained after the method ends, as | |
311 * it wraps code managed on the native heap. | |
312 */ | |
313 @CalledByNative | |
314 protected void onBytesRead(ByteBuffer byteBuffer) { | |
315 try { | |
316 while (byteBuffer.hasRemaining()) { | |
317 mSink.write(byteBuffer); | |
318 } | |
319 } catch (IOException e) { | |
320 mSinkException = e; | |
321 cancel(); | |
322 } | |
323 } | |
324 | |
325 /** | |
326 * Notifies the listener, releases native data structures. | |
327 */ | |
328 @SuppressWarnings("unused") | |
329 @CalledByNative | |
330 private void finish() { | |
331 synchronized (mLock) { | |
332 mFinished = true; | |
333 | |
334 if (mRecycled) { | |
335 return; | |
336 } | |
337 try { | |
338 mSink.close(); | |
339 } catch (IOException e) { | |
340 // Ignore | |
341 } | |
342 onRequestComplete(); | |
343 nativeDestroyRequestAdapter(mUrlRequestAdapter); | |
344 mUrlRequestAdapter = 0; | |
345 mRecycled = true; | |
346 } | |
347 } | |
348 | |
349 /** | |
350 * Appends header |name| with value |value| to |headersMap|. | |
351 */ | |
352 @SuppressWarnings("unused") | |
353 @CalledByNative | |
354 private void onAppendResponseHeader(ResponseHeadersMap headersMap, | |
355 String name, String value) { | |
356 if (!headersMap.containsKey(name)) { | |
357 headersMap.put(name, new ArrayList<String>()); | |
358 } | |
359 headersMap.get(name).add(value); | |
360 } | |
361 | |
362 /** | |
363 * Reads a sequence of bytes from upload channel into the given buffer. | |
364 * @param dest The buffer into which bytes are to be transferred. | |
365 * @return Returns number of bytes read (could be 0) or -1 and closes | |
366 * the channel if error occured. | |
367 */ | |
368 @SuppressWarnings("unused") | |
369 @CalledByNative | |
370 private int readFromUploadChannel(ByteBuffer dest) { | |
371 if (mUploadChannel == null || !mUploadChannel.isOpen()) | |
372 return -1; | |
373 try { | |
374 int result = mUploadChannel.read(dest); | |
375 if (result < 0) { | |
376 mUploadChannel.close(); | |
377 return 0; | |
378 } | |
379 return result; | |
380 } catch (IOException e) { | |
381 mSinkException = e; | |
382 try { | |
383 mUploadChannel.close(); | |
384 } catch (IOException ignored) { | |
385 // Ignore this exception. | |
386 } | |
387 cancel(); | |
388 return -1; | |
389 } | |
390 } | |
391 | |
392 private void validateNotRecycled() { | |
393 if (mRecycled) { | |
394 throw new IllegalStateException("Accessing recycled request"); | |
395 } | |
396 } | |
397 | |
398 private void validateNotStarted() { | |
399 if (mStarted) { | |
400 throw new IllegalStateException("Request already started"); | |
401 } | |
402 } | |
403 | |
404 private void validateHeadersAvailable() { | |
405 if (!mHeadersAvailable) { | |
406 throw new IllegalStateException("Response headers not available"); | |
407 } | |
408 } | |
409 | |
410 public String getUrl() { | |
411 return mUrl; | |
412 } | |
413 | |
414 private native long nativeCreateRequestAdapter( | |
415 long urlRequestContextAdapter, String url, int priority); | |
416 | |
417 private native void nativeAddHeader(long urlRequestAdapter, String name, | |
418 String value); | |
419 | |
420 private native void nativeSetMethod(long urlRequestAdapter, String method); | |
421 | |
422 private native void nativeSetUploadData(long urlRequestAdapter, | |
423 String contentType, byte[] content); | |
424 | |
425 private native void nativeSetUploadChannel(long urlRequestAdapter, | |
426 String contentType, long contentLength); | |
427 | |
428 private native void nativeStart(long urlRequestAdapter); | |
429 | |
430 private native void nativeCancel(long urlRequestAdapter); | |
431 | |
432 private native void nativeDestroyRequestAdapter(long urlRequestAdapter); | |
433 | |
434 private native int nativeGetErrorCode(long urlRequestAdapter); | |
435 | |
436 private native int nativeGetHttpStatusCode(long urlRequestAdapter); | |
437 | |
438 private native String nativeGetErrorString(long urlRequestAdapter); | |
439 | |
440 private native String nativeGetContentType(long urlRequestAdapter); | |
441 | |
442 private native long nativeGetContentLength(long urlRequestAdapter); | |
443 | |
444 private native String nativeGetHeader(long urlRequestAdapter, String name); | |
445 | |
446 private native void nativeGetAllHeaders(long urlRequestAdapter, | |
447 ResponseHeadersMap headers); | |
448 | |
449 // Explicit class to work around JNI-generator generics confusion. | |
450 private class ResponseHeadersMap extends HashMap<String, List<String>> { | |
451 } | |
452 } | |
OLD | NEW |