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