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