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 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 */ 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 */ 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 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 */ 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 136 public WritableByteChannel getSink() { 137 return mSink; 138 } 139 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 */ 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 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 248 public boolean isCanceled() { 249 synchronized (mLock) { 250 return mCanceled; 251 } 252 } 253 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 */ 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 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 */ 303 public long getContentLength() { 304 return mContentLength; 305 } 306 307 public String getContentType() { 308 return mContentType; 309 } 310 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 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 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 */ 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 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 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 386 private void validateNotRecycled() { 387 if (mRecycled) { 388 throw new IllegalStateException("Accessing recycled request"); 389 } 390 } 391 392 private void validateNotStarted() { 393 if (mStarted) { 394 throw new IllegalStateException("Request already started"); 395 } 396 } 397 398 private void validatePostBodyNotSet() { 399 if (mPostBodySet) { 400 throw new IllegalStateException("Post Body already set"); 401 } 402 } 403 404 405 private void validateHeadersAvailable() { 406 if (!mHeadersAvailable) { 407 throw new IllegalStateException("Response headers not available"); 408 } 409 } 410 411 public String getUrl() { 412 return mUrl; 413 } 414 415 private native long nativeCreateRequestPeer(long urlRequestContextPeer, 416 String url, int priority); 417 418 private native void nativeAddHeader(long urlRequestPeer, String name, 419 String value); 420 421 private native void nativeSetPostData(long urlRequestPeer, 422 String contentType, byte[] content); 423 424 private native void nativeBeginChunkedUpload(long urlRequestPeer, 425 String contentType); 426 427 private native void nativeAppendChunk(long urlRequestPeer, ByteBuffer chunk, 428 int chunkSize, boolean isLastChunk); 429 430 private native void nativeStart(long urlRequestPeer); 431 432 private native void nativeCancel(long urlRequestPeer); 433 434 private native void nativeDestroyRequestPeer(long urlRequestPeer); 435 436 private native int nativeGetErrorCode(long urlRequestPeer); 437 438 private native int nativeGetHttpStatusCode(long urlRequestPeer); 439 440 private native String nativeGetErrorString(long urlRequestPeer); 441 442 private native String nativeGetContentType(long urlRequestPeer); 443 444 private native long nativeGetContentLength(long urlRequestPeer); 445 446 private native String nativeGetHeader(long urlRequestPeer, String name); 447} 448