Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright 2014 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 package org.chromium.net.urlconnection; | 5 package org.chromium.net.urlconnection; |
| 6 | 6 |
| 7 import android.util.Log; | |
| 7 import android.util.Pair; | 8 import android.util.Pair; |
| 8 | 9 |
| 9 import org.chromium.net.ExtendedResponseInfo; | 10 import org.chromium.net.ExtendedResponseInfo; |
| 10 import org.chromium.net.ResponseInfo; | 11 import org.chromium.net.ResponseInfo; |
| 12 import org.chromium.net.UploadDataProvider; | |
| 11 import org.chromium.net.UrlRequest; | 13 import org.chromium.net.UrlRequest; |
| 12 import org.chromium.net.UrlRequestContext; | 14 import org.chromium.net.UrlRequestContext; |
| 13 import org.chromium.net.UrlRequestException; | 15 import org.chromium.net.UrlRequestException; |
| 14 import org.chromium.net.UrlRequestListener; | 16 import org.chromium.net.UrlRequestListener; |
| 15 | 17 |
| 16 import java.io.FileNotFoundException; | 18 import java.io.FileNotFoundException; |
| 17 import java.io.IOException; | 19 import java.io.IOException; |
| 18 import java.io.InputStream; | 20 import java.io.InputStream; |
| 21 import java.io.OutputStream; | |
| 19 import java.net.HttpURLConnection; | 22 import java.net.HttpURLConnection; |
| 20 import java.net.MalformedURLException; | 23 import java.net.MalformedURLException; |
| 24 import java.net.ProtocolException; | |
| 21 import java.net.URL; | 25 import java.net.URL; |
| 22 import java.nio.ByteBuffer; | 26 import java.nio.ByteBuffer; |
| 23 import java.util.ArrayList; | 27 import java.util.ArrayList; |
| 24 import java.util.Collections; | 28 import java.util.Collections; |
| 25 import java.util.List; | 29 import java.util.List; |
| 26 import java.util.Map; | 30 import java.util.Map; |
| 27 import java.util.TreeMap; | 31 import java.util.TreeMap; |
| 28 | 32 |
| 29 /** | 33 /** |
| 30 * An implementation of HttpURLConnection that uses Cronet to send requests and | 34 * An implementation of HttpURLConnection that uses Cronet to send requests and |
| 31 * receive response. This class inherits a {@code connected} field from the | 35 * receive response. This class inherits a {@code connected} field from the |
| 32 * superclass. That field indicates whether a connection has ever been | 36 * superclass. That field indicates whether a connection has ever been |
| 33 * attempted. | 37 * attempted. |
| 34 */ | 38 */ |
| 35 public class CronetHttpURLConnection extends HttpURLConnection { | 39 public class CronetHttpURLConnection extends HttpURLConnection { |
| 40 private static final String TAG = "CronetHttpURLConnection"; | |
| 36 private final UrlRequestContext mUrlRequestContext; | 41 private final UrlRequestContext mUrlRequestContext; |
| 37 private final MessageLoop mMessageLoop; | 42 private final MessageLoop mMessageLoop; |
| 38 private final UrlRequest mRequest; | 43 private final UrlRequest mRequest; |
| 39 private final List<Pair<String, String>> mRequestHeaders; | 44 private final List<Pair<String, String>> mRequestHeaders; |
| 40 | 45 |
| 41 private CronetInputStream mInputStream; | 46 private CronetInputStream mInputStream; |
| 47 private OutputStream mOutputStream; | |
| 42 private ResponseInfo mResponseInfo; | 48 private ResponseInfo mResponseInfo; |
| 43 private UrlRequestException mException; | 49 private UrlRequestException mException; |
| 44 private ByteBuffer mResponseByteBuffer; | 50 private ByteBuffer mResponseByteBuffer; |
| 45 private boolean mOnRedirectCalled = false; | 51 private boolean mOnRedirectCalled = false; |
| 52 private boolean mHasResponse = false; | |
| 46 | 53 |
| 47 protected CronetHttpURLConnection(URL url, | 54 public CronetHttpURLConnection(URL url, |
| 48 UrlRequestContext urlRequestContext) { | 55 UrlRequestContext urlRequestContext) { |
| 49 super(url); | 56 super(url); |
| 50 mUrlRequestContext = urlRequestContext; | 57 mUrlRequestContext = urlRequestContext; |
| 51 mMessageLoop = new MessageLoop(); | 58 mMessageLoop = new MessageLoop(); |
| 52 mRequest = mUrlRequestContext.createRequest(url.toString(), | 59 mRequest = mUrlRequestContext.createRequest(url.toString(), |
| 53 new CronetUrlRequestListener(), mMessageLoop); | 60 new CronetUrlRequestListener(), mMessageLoop); |
| 54 mInputStream = new CronetInputStream(this); | 61 mInputStream = new CronetInputStream(this); |
| 55 mRequestHeaders = new ArrayList<Pair<String, String>>(); | 62 mRequestHeaders = new ArrayList<Pair<String, String>>(); |
| 56 } | 63 } |
| 57 | 64 |
| 58 /** | 65 /** |
| 59 * Opens a connection to the resource. If the connect method is called when | 66 * Opens a connection to the resource. If the connect method is called when |
| 60 * the connection has already been opened (indicated by the connected field | 67 * the connection has already been opened (indicated by the connected field |
| 61 * having the value true), the call is ignored unless an exception is thrown | 68 * having the value true), the call is ignored. |
| 62 * previously, in which case, the exception will be rethrown. | |
| 63 */ | 69 */ |
| 64 @Override | 70 @Override |
| 65 public void connect() throws IOException { | 71 public void connect() throws IOException { |
| 66 if (connected) { | 72 startRequest(); |
| 67 checkHasResponse(); | |
| 68 return; | |
| 69 } | |
| 70 connected = true; | |
| 71 for (Pair<String, String> requestHeader : mRequestHeaders) { | |
| 72 mRequest.addHeader(requestHeader.first, requestHeader.second); | |
| 73 } | |
| 74 if (!getUseCaches()) { | |
| 75 mRequest.disableCache(); | |
| 76 } | |
| 77 mRequest.start(); | |
| 78 // Blocks until onResponseStarted or onFailed is called. | |
| 79 mMessageLoop.loop(); | |
| 80 checkHasResponse(); | |
| 81 } | 73 } |
| 82 | 74 |
| 83 /** | 75 /** |
| 84 * Releases this connection so that its resources may be either reused or | 76 * Releases this connection so that its resources may be either reused or |
| 85 * closed. | 77 * closed. |
| 86 */ | 78 */ |
| 87 @Override | 79 @Override |
| 88 public void disconnect() { | 80 public void disconnect() { |
| 89 // Disconnect before connection is made should have no effect. | 81 // Disconnect before connection is made should have no effect. |
| 90 if (connected) { | 82 if (connected && mInputStream != null) { |
| 91 try { | 83 try { |
| 92 mInputStream.close(); | 84 mInputStream.close(); |
| 93 } catch (IOException e) { | 85 } catch (IOException e) { |
| 94 e.printStackTrace(); | 86 e.printStackTrace(); |
| 95 } | 87 } |
| 96 mInputStream = null; | 88 mInputStream = null; |
| 97 mRequest.cancel(); | 89 mRequest.cancel(); |
| 98 } | 90 } |
| 99 } | 91 } |
| 100 | 92 |
| 101 /** | 93 /** |
| 102 * Returns the response message returned by the remote HTTP server. | 94 * Returns the response message returned by the remote HTTP server. |
| 103 */ | 95 */ |
| 104 @Override | 96 @Override |
| 105 public String getResponseMessage() throws IOException { | 97 public String getResponseMessage() throws IOException { |
| 106 connect(); | 98 getResponse(); |
| 107 return mResponseInfo.getHttpStatusText(); | 99 return mResponseInfo.getHttpStatusText(); |
| 108 } | 100 } |
| 109 | 101 |
| 110 /** | 102 /** |
| 111 * Returns the response code returned by the remote HTTP server. | 103 * Returns the response code returned by the remote HTTP server. |
| 112 */ | 104 */ |
| 113 @Override | 105 @Override |
| 114 public int getResponseCode() throws IOException { | 106 public int getResponseCode() throws IOException { |
| 115 connect(); | 107 getResponse(); |
| 116 return mResponseInfo.getHttpStatusCode(); | 108 return mResponseInfo.getHttpStatusCode(); |
| 117 } | 109 } |
| 118 | 110 |
| 119 /** | 111 /** |
| 120 * Returns an unmodifiable map of the response-header fields and values. | 112 * Returns an unmodifiable map of the response-header fields and values. |
| 121 */ | 113 */ |
| 122 @Override | 114 @Override |
| 123 public Map<String, List<String>> getHeaderFields() { | 115 public Map<String, List<String>> getHeaderFields() { |
| 124 try { | 116 try { |
| 125 connect(); | 117 getResponse(); |
| 126 } catch (IOException e) { | 118 } catch (IOException e) { |
| 127 return Collections.emptyMap(); | 119 return Collections.emptyMap(); |
| 128 } | 120 } |
| 129 return mResponseInfo.getAllHeaders(); | 121 return mResponseInfo.getAllHeaders(); |
| 130 } | 122 } |
| 131 | 123 |
| 132 /** | 124 /** |
| 133 * Returns the value of the named header field. If called on a connection | 125 * Returns the value of the named header field. If called on a connection |
| 134 * that sets the same header multiple times with possibly different values, | 126 * that sets the same header multiple times with possibly different values, |
| 135 * only the last value is returned. | 127 * only the last value is returned. |
| 136 */ | 128 */ |
| 137 @Override | 129 @Override |
| 138 public final String getHeaderField(String fieldName) { | 130 public final String getHeaderField(String fieldName) { |
| 139 try { | 131 try { |
| 140 connect(); | 132 getResponse(); |
| 141 } catch (IOException e) { | 133 } catch (IOException e) { |
| 142 return null; | 134 return null; |
| 143 } | 135 } |
| 144 Map<String, List<String>> map = mResponseInfo.getAllHeaders(); | 136 Map<String, List<String>> map = mResponseInfo.getAllHeaders(); |
| 145 if (!map.containsKey(fieldName)) { | 137 if (!map.containsKey(fieldName)) { |
| 146 return null; | 138 return null; |
| 147 } | 139 } |
| 148 List<String> values = map.get(fieldName); | 140 List<String> values = map.get(fieldName); |
| 149 return values.get(values.size() - 1); | 141 return values.get(values.size() - 1); |
| 150 } | 142 } |
| (...skipping 28 matching lines...) Expand all Loading... | |
| 179 * Returns an InputStream for reading data from the resource pointed by this | 171 * Returns an InputStream for reading data from the resource pointed by this |
| 180 * URLConnection. | 172 * URLConnection. |
| 181 * @throws FileNotFoundException if http response code is equal or greater | 173 * @throws FileNotFoundException if http response code is equal or greater |
| 182 * than {@link HTTP_BAD_REQUEST}. | 174 * than {@link HTTP_BAD_REQUEST}. |
| 183 * @throws IOException If the request gets a network error or HTTP error | 175 * @throws IOException If the request gets a network error or HTTP error |
| 184 * status code, or if the caller tried to read the response body | 176 * status code, or if the caller tried to read the response body |
| 185 * of a redirect when redirects are disabled. | 177 * of a redirect when redirects are disabled. |
| 186 */ | 178 */ |
| 187 @Override | 179 @Override |
| 188 public InputStream getInputStream() throws IOException { | 180 public InputStream getInputStream() throws IOException { |
| 189 connect(); | 181 getResponse(); |
| 190 if (!instanceFollowRedirects && mOnRedirectCalled) { | 182 if (!instanceFollowRedirects && mOnRedirectCalled) { |
| 191 throw new IOException("Cannot read response body of a redirect."); | 183 throw new IOException("Cannot read response body of a redirect."); |
| 192 } | 184 } |
| 193 // Emulate default implementation's behavior to throw | 185 // Emulate default implementation's behavior to throw |
| 194 // FileNotFoundException when we get a 400 and above. | 186 // FileNotFoundException when we get a 400 and above. |
| 195 if (mResponseInfo.getHttpStatusCode() >= HTTP_BAD_REQUEST) { | 187 if (mResponseInfo.getHttpStatusCode() >= HTTP_BAD_REQUEST) { |
| 196 throw new FileNotFoundException(url.toString()); | 188 throw new FileNotFoundException(url.toString()); |
| 197 } | 189 } |
| 198 return mInputStream; | 190 return mInputStream; |
| 199 } | 191 } |
| 200 | 192 |
| 193 @Override | |
| 194 public OutputStream getOutputStream() throws IOException { | |
| 195 if (mOutputStream == null) { | |
| 196 if (connected) { | |
| 197 throw new ProtocolException( | |
| 198 "Cannot write to OutputStream after receiving response." ); | |
| 199 } | |
| 200 long fixedStreamingModeContentLength = getStreamingModeContentLength (); | |
| 201 if (fixedStreamingModeContentLength != -1) { | |
| 202 mOutputStream = new CronetFixedModeOutputStream(this, | |
| 203 fixedStreamingModeContentLength, mMessageLoop); | |
| 204 // Start the request now since all headers can be sent. | |
|
mef
2015/04/06 16:14:55
why is this true?
xunjieli
2015/04/06 18:09:29
Because the user sets fixedLengthStreamingMode, so
mef
2015/04/06 18:37:18
Yes, but why couldn't user call setRequestProperty
xunjieli
2015/04/06 21:03:45
getOutputStream() should try to establish a connec
| |
| 205 startRequest(); | |
| 206 } else { | |
| 207 // For the buffered case, start the request only when | |
| 208 // content-length bytes are received, or when a | |
| 209 // connect action is initiated by the consumer. | |
| 210 Log.d(TAG, "Outputstream is being buffered in memory."); | |
| 211 String length = getRequestProperty("Content-Length"); | |
| 212 if (length == null) { | |
| 213 mOutputStream = new CronetBufferedOutputStream(this); | |
| 214 } else { | |
| 215 long lengthParsed = Long.parseLong(length); | |
|
mef
2015/04/06 16:14:54
what if it is over 2gb?
xunjieli
2015/04/06 18:09:29
CronetBufferedOutputStream's constructor will thro
| |
| 216 mOutputStream = new CronetBufferedOutputStream(this, lengthP arsed); | |
| 217 } | |
| 218 } | |
| 219 } | |
| 220 return mOutputStream; | |
| 221 } | |
| 222 | |
| 223 /** | |
| 224 * Helper method to get content length passed in by | |
| 225 * {@link #setFixedLengthStreamingMode} | |
| 226 */ | |
| 227 private long getStreamingModeContentLength() { | |
| 228 long contentLength = fixedContentLength; | |
| 229 // Use reflection to see whether fixedContentLengthLong (only added | |
| 230 // in API 19) is inherited. | |
| 231 try { | |
| 232 Class<?> parent = this.getClass(); | |
| 233 long superFixedContentLengthLong = | |
| 234 parent.getField("fixedContentLengthLong").getLong(this); | |
| 235 if (superFixedContentLengthLong != -1) { | |
| 236 contentLength = superFixedContentLengthLong; | |
| 237 } | |
| 238 } catch (Exception e) { | |
| 239 // Ignored. | |
| 240 } | |
| 241 return contentLength; | |
| 242 } | |
| 243 | |
| 244 /** | |
| 245 * Starts the request if {@code connected} is false. | |
| 246 */ | |
| 247 private void startRequest() throws IOException { | |
| 248 if (connected) { | |
| 249 return; | |
| 250 } | |
| 251 if (doOutput) { | |
| 252 if (mOutputStream != null) { | |
| 253 mRequest.setUploadDataProvider( | |
| 254 (UploadDataProvider) mOutputStream, mMessageLoop); | |
| 255 if (getRequestProperty("Content-Length") == null) { | |
| 256 addRequestProperty("Content-Length", | |
| 257 Long.toString(((UploadDataProvider) mOutputStream).g etLength())); | |
| 258 } | |
| 259 if (mOutputStream instanceof CronetBufferedOutputStream) { | |
| 260 // Disallow the embedder to write more data, and prepare | |
| 261 // internal buffer for reading. | |
| 262 ((CronetBufferedOutputStream) mOutputStream).setConnected(); | |
|
mef
2015/04/06 16:14:54
can mOutputStream be a common base class like 'Cro
xunjieli
2015/04/06 18:09:29
Only CronetBufferedOutputStream needs this method.
| |
| 263 } | |
| 264 } else { | |
| 265 if (getRequestProperty("Content-Length") == null) { | |
|
mef
2015/04/06 16:14:55
suggest string constant for "Content-Length" and "
xunjieli
2015/04/06 18:09:30
Done.
| |
| 266 addRequestProperty("Content-Length", "0"); | |
| 267 } | |
| 268 } | |
| 269 // Default Content-Type to application/x-www-form-urlencoded | |
| 270 if (getRequestProperty("Content-Type") == null) { | |
| 271 addRequestProperty("Content-Type", | |
| 272 "application/x-www-form-urlencoded"); | |
| 273 } | |
| 274 } | |
| 275 for (Pair<String, String> requestHeader : mRequestHeaders) { | |
| 276 mRequest.addHeader(requestHeader.first, requestHeader.second); | |
| 277 } | |
| 278 if (!getUseCaches()) { | |
| 279 mRequest.disableCache(); | |
| 280 } | |
| 281 connected = true; | |
| 282 // Start the request. | |
| 283 mRequest.start(); | |
| 284 } | |
| 285 | |
| 201 /** | 286 /** |
| 202 * Returns an input stream from the server in the case of an error such as | 287 * Returns an input stream from the server in the case of an error such as |
| 203 * the requested file has not been found on the remote server. | 288 * the requested file has not been found on the remote server. |
| 204 */ | 289 */ |
| 205 @Override | 290 @Override |
| 206 public InputStream getErrorStream() { | 291 public InputStream getErrorStream() { |
| 207 try { | 292 try { |
| 208 connect(); | 293 getResponse(); |
| 209 } catch (IOException e) { | 294 } catch (IOException e) { |
| 210 return null; | 295 return null; |
| 211 } | 296 } |
| 212 if (mResponseInfo.getHttpStatusCode() >= HTTP_BAD_REQUEST) { | 297 if (mResponseInfo.getHttpStatusCode() >= HTTP_BAD_REQUEST) { |
| 213 return mInputStream; | 298 return mInputStream; |
| 214 } | 299 } |
| 215 return null; | 300 return null; |
| 216 } | 301 } |
| 217 | 302 |
| 218 /** | 303 /** |
| (...skipping 75 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 294 /** | 379 /** |
| 295 * Returns whether this connection uses a proxy server. | 380 * Returns whether this connection uses a proxy server. |
| 296 */ | 381 */ |
| 297 @Override | 382 @Override |
| 298 public boolean usingProxy() { | 383 public boolean usingProxy() { |
| 299 // TODO(xunjieli): implement this. | 384 // TODO(xunjieli): implement this. |
| 300 return false; | 385 return false; |
| 301 } | 386 } |
| 302 | 387 |
| 303 /** | 388 /** |
| 389 * Sets chunked streaming mode. | |
| 390 */ | |
| 391 @Override | |
| 392 public void setChunkedStreamingMode(int chunklen) { | |
| 393 // TODO(xunjieli): implement this. | |
| 394 throw new UnsupportedOperationException("Chunked mode not supported yet" ); | |
| 395 } | |
| 396 | |
| 397 /** | |
| 304 * Used by {@link CronetInputStream} to get more data from the network | 398 * Used by {@link CronetInputStream} to get more data from the network |
| 305 * stack. This should only be called after the request has started. Note | 399 * stack. This should only be called after the request has started. Note |
| 306 * that this call might block if there isn't any more data to be read. | 400 * that this call might block if there isn't any more data to be read. |
| 307 */ | 401 */ |
| 308 ByteBuffer getMoreData() throws IOException { | 402 ByteBuffer getMoreData() throws IOException { |
| 309 mResponseByteBuffer = null; | 403 mResponseByteBuffer = null; |
| 310 mMessageLoop.loop(); | 404 mMessageLoop.loop(); |
| 311 return mResponseByteBuffer; | 405 return mResponseByteBuffer; |
| 312 } | 406 } |
| 313 | 407 |
| (...skipping 73 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 387 */ | 481 */ |
| 388 private void setResponseDataCompleted() { | 482 private void setResponseDataCompleted() { |
| 389 if (mInputStream != null) { | 483 if (mInputStream != null) { |
| 390 mInputStream.setResponseDataCompleted(); | 484 mInputStream.setResponseDataCompleted(); |
| 391 } | 485 } |
| 392 mMessageLoop.postQuitTask(); | 486 mMessageLoop.postQuitTask(); |
| 393 } | 487 } |
| 394 } | 488 } |
| 395 | 489 |
| 396 /** | 490 /** |
| 491 * Blocks until the respone headers are received. | |
| 492 */ | |
| 493 private void getResponse() throws IOException { | |
| 494 // Check to see if enough data has been received. CronetBufferedOutputSt ream's | |
| 495 // case is checked in CronetBufferedOutputStream#setConnected(). | |
| 496 if (mOutputStream != null && mOutputStream instanceof CronetFixedModeOut putStream) { | |
| 497 ((CronetFixedModeOutputStream) mOutputStream).checkReceivedEnoughCon tent(); | |
|
mef
2015/04/06 16:14:54
Maybe make checkReceivedEnoughContent a base class
xunjieli
2015/04/06 18:09:29
Only CronetFixedModeOutputStream needs this method
mef
2015/04/06 18:37:18
But it can be an abstract class that extends Outpu
xunjieli
2015/04/06 21:03:45
Done. Yes, you are right. Changed the code to do t
| |
| 498 } | |
| 499 if (!mHasResponse) { | |
| 500 startRequest(); | |
| 501 // Blocks until onResponseStarted or onFailed is called. | |
| 502 mMessageLoop.loop(); | |
| 503 mHasResponse = true; | |
| 504 } | |
| 505 checkHasResponse(); | |
| 506 } | |
| 507 | |
| 508 /** | |
| 397 * Checks whether response headers are received, and throws an exception if | 509 * Checks whether response headers are received, and throws an exception if |
| 398 * an exception occurred before headers received. This method should only be | 510 * an exception occurred before headers received. This method should only be |
| 399 * called after onResponseStarted or onFailed. | 511 * called after onResponseStarted or onFailed. |
| 400 */ | 512 */ |
| 401 private void checkHasResponse() throws IOException { | 513 private void checkHasResponse() throws IOException { |
| 514 if (!mHasResponse) throw new IllegalStateException("No response."); | |
|
mef
2015/04/06 16:14:55
isn't it a duplicate of mResponseInfo == null?
xunjieli
2015/04/06 18:09:30
Nope. mResponseInfo can be null when the request f
| |
| 402 if (mException != null) { | 515 if (mException != null) { |
| 403 throw mException; | 516 throw mException; |
| 404 } else if (mResponseInfo == null) { | 517 } else if (mResponseInfo == null) { |
| 405 throw new NullPointerException( | 518 throw new NullPointerException( |
| 406 "Response info is null when there is no exception."); | 519 "Response info is null when there is no exception."); |
| 407 } | 520 } |
| 408 } | 521 } |
| 409 | 522 |
| 410 /** | 523 /** |
| 411 * Helper method to return the response header field at position pos. | 524 * Helper method to return the response header field at position pos. |
| 412 */ | 525 */ |
| 413 private Pair<String, String> getHeaderFieldPair(int pos) { | 526 private Pair<String, String> getHeaderFieldPair(int pos) { |
| 414 try { | 527 try { |
| 415 connect(); | 528 getResponse(); |
| 416 } catch (IOException e) { | 529 } catch (IOException e) { |
| 417 return null; | 530 return null; |
| 418 } | 531 } |
| 419 List<Pair<String, String>> headers = | 532 List<Pair<String, String>> headers = |
| 420 mResponseInfo.getAllHeadersAsList(); | 533 mResponseInfo.getAllHeadersAsList(); |
| 421 if (pos >= headers.size()) { | 534 if (pos >= headers.size()) { |
| 422 return null; | 535 return null; |
| 423 } | 536 } |
| 424 return headers.get(pos); | 537 return headers.get(pos); |
| 425 } | 538 } |
| 426 } | 539 } |
| OLD | NEW |