| 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 |