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 |