blob: 8c0d5032d5ce7552d9d190f611a916df7879abe3 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
2 * Copyright (C) 2006 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.net.http;
18
19import java.io.EOFException;
20import java.io.InputStream;
21import java.io.IOException;
22import java.util.Iterator;
23import java.util.Map;
24import java.util.Map.Entry;
25import java.util.zip.GZIPInputStream;
26
27import org.apache.http.entity.InputStreamEntity;
28import org.apache.http.Header;
29import org.apache.http.HttpClientConnection;
30import org.apache.http.HttpEntity;
31import org.apache.http.HttpEntityEnclosingRequest;
32import org.apache.http.HttpException;
33import org.apache.http.HttpHost;
34import org.apache.http.HttpRequest;
35import org.apache.http.HttpResponse;
36import org.apache.http.HttpStatus;
Grace Kloba52e41582010-04-21 23:57:43 -070037import org.apache.http.HttpVersion;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080038import org.apache.http.ParseException;
39import org.apache.http.ProtocolVersion;
40
41import org.apache.http.StatusLine;
42import org.apache.http.message.BasicHttpRequest;
43import org.apache.http.message.BasicHttpEntityEnclosingRequest;
44import org.apache.http.protocol.RequestContent;
45
46/**
47 * Represents an HTTP request for a given host.
48 *
49 * {@hide}
50 */
51
52class Request {
53
54 /** The eventhandler to call as the request progresses */
55 EventHandler mEventHandler;
56
57 private Connection mConnection;
58
59 /** The Apache http request */
60 BasicHttpRequest mHttpRequest;
61
62 /** The path component of this request */
63 String mPath;
64
65 /** Host serving this request */
66 HttpHost mHost;
67
68 /** Set if I'm using a proxy server */
69 HttpHost mProxyHost;
70
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080071 /** True if request has been cancelled */
72 volatile boolean mCancelled = false;
73
74 int mFailCount = 0;
75
Grace Kloba52e41582010-04-21 23:57:43 -070076 // This will be used to set the Range field if we retry a connection. This
77 // is http/1.1 feature.
78 private int mReceivedBytes = 0;
79
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080080 private InputStream mBodyProvider;
81 private int mBodyLength;
82
83 private final static String HOST_HEADER = "Host";
84 private final static String ACCEPT_ENCODING_HEADER = "Accept-Encoding";
85 private final static String CONTENT_LENGTH_HEADER = "content-length";
86
87 /* Used to synchronize waitUntilComplete() requests */
88 private final Object mClientResource = new Object();
89
Ben Murdoch5b494c12010-03-12 20:45:32 +000090 /** True if loading should be paused **/
91 private boolean mLoadingPaused = false;
92
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080093 /**
94 * Processor used to set content-length and transfer-encoding
95 * headers.
96 */
97 private static RequestContent requestContentProcessor =
98 new RequestContent();
99
100 /**
101 * Instantiates a new Request.
102 * @param method GET/POST/PUT
103 * @param host The server that will handle this request
104 * @param path path part of URI
105 * @param bodyProvider InputStream providing HTTP body, null if none
106 * @param bodyLength length of body, must be 0 if bodyProvider is null
107 * @param eventHandler request will make progress callbacks on
108 * this interface
109 * @param headers reqeust headers
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800110 */
111 Request(String method, HttpHost host, HttpHost proxyHost, String path,
112 InputStream bodyProvider, int bodyLength,
113 EventHandler eventHandler,
Patrick Scottfe4fec72009-07-14 15:54:30 -0400114 Map<String, String> headers) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800115 mEventHandler = eventHandler;
116 mHost = host;
117 mProxyHost = proxyHost;
118 mPath = path;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800119 mBodyProvider = bodyProvider;
120 mBodyLength = bodyLength;
121
Grace Kloba45efe692009-05-08 19:21:55 -0700122 if (bodyProvider == null && !"POST".equalsIgnoreCase(method)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800123 mHttpRequest = new BasicHttpRequest(method, getUri());
124 } else {
125 mHttpRequest = new BasicHttpEntityEnclosingRequest(
126 method, getUri());
Grace Kloba45efe692009-05-08 19:21:55 -0700127 // it is ok to have null entity for BasicHttpEntityEnclosingRequest.
128 // By using BasicHttpEntityEnclosingRequest, it will set up the
129 // correct content-length, content-type and content-encoding.
130 if (bodyProvider != null) {
131 setBodyProvider(bodyProvider, bodyLength);
132 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800133 }
134 addHeader(HOST_HEADER, getHostPort());
135
136 /* FIXME: if webcore will make the root document a
137 high-priority request, we can ask for gzip encoding only on
138 high priority reqs (saving the trouble for images, etc) */
139 addHeader(ACCEPT_ENCODING_HEADER, "gzip");
140 addHeaders(headers);
141 }
142
143 /**
Ben Murdoch5b494c12010-03-12 20:45:32 +0000144 * @param pause True if the load should be paused.
145 */
146 synchronized void setLoadingPaused(boolean pause) {
147 mLoadingPaused = pause;
148
149 // Wake up the paused thread if we're unpausing the load.
150 if (!mLoadingPaused) {
151 notify();
152 }
153 }
154
155 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800156 * @param connection Request served by this connection
157 */
158 void setConnection(Connection connection) {
159 mConnection = connection;
160 }
161
162 /* package */ EventHandler getEventHandler() {
163 return mEventHandler;
164 }
165
166 /**
167 * Add header represented by given pair to request. Header will
168 * be formatted in request as "name: value\r\n".
169 * @param name of header
170 * @param value of header
171 */
172 void addHeader(String name, String value) {
173 if (name == null) {
174 String damage = "Null http header name";
175 HttpLog.e(damage);
176 throw new NullPointerException(damage);
177 }
178 if (value == null || value.length() == 0) {
179 String damage = "Null or empty value for header \"" + name + "\"";
180 HttpLog.e(damage);
181 throw new RuntimeException(damage);
182 }
183 mHttpRequest.addHeader(name, value);
184 }
185
186 /**
187 * Add all headers in given map to this request. This is a helper
188 * method: it calls addHeader for each pair in the map.
189 */
190 void addHeaders(Map<String, String> headers) {
191 if (headers == null) {
192 return;
193 }
194
195 Entry<String, String> entry;
196 Iterator<Entry<String, String>> i = headers.entrySet().iterator();
197 while (i.hasNext()) {
198 entry = i.next();
199 addHeader(entry.getKey(), entry.getValue());
200 }
201 }
202
203 /**
204 * Send the request line and headers
205 */
206 void sendRequest(AndroidHttpClientConnection httpClientConnection)
207 throws HttpException, IOException {
208
209 if (mCancelled) return; // don't send cancelled requests
210
211 if (HttpLog.LOGV) {
212 HttpLog.v("Request.sendRequest() " + mHost.getSchemeName() + "://" + getHostPort());
213 // HttpLog.v(mHttpRequest.getRequestLine().toString());
214 if (false) {
215 Iterator i = mHttpRequest.headerIterator();
216 while (i.hasNext()) {
217 Header header = (Header)i.next();
218 HttpLog.v(header.getName() + ": " + header.getValue());
219 }
220 }
221 }
222
223 requestContentProcessor.process(mHttpRequest,
224 mConnection.getHttpContext());
225 httpClientConnection.sendRequestHeader(mHttpRequest);
226 if (mHttpRequest instanceof HttpEntityEnclosingRequest) {
227 httpClientConnection.sendRequestEntity(
228 (HttpEntityEnclosingRequest) mHttpRequest);
229 }
230
231 if (HttpLog.LOGV) {
232 HttpLog.v("Request.requestSent() " + mHost.getSchemeName() + "://" + getHostPort() + mPath);
233 }
234 }
235
236
237 /**
238 * Receive a single http response.
239 *
240 * @param httpClientConnection the request to receive the response for.
241 */
242 void readResponse(AndroidHttpClientConnection httpClientConnection)
243 throws IOException, ParseException {
244
245 if (mCancelled) return; // don't send cancelled requests
246
247 StatusLine statusLine = null;
248 boolean hasBody = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800249 httpClientConnection.flush();
250 int statusCode = 0;
251
252 Headers header = new Headers();
253 do {
254 statusLine = httpClientConnection.parseResponseHeader(header);
255 statusCode = statusLine.getStatusCode();
256 } while (statusCode < HttpStatus.SC_OK);
257 if (HttpLog.LOGV) HttpLog.v(
258 "Request.readResponseStatus() " +
259 statusLine.toString().length() + " " + statusLine);
260
261 ProtocolVersion v = statusLine.getProtocolVersion();
262 mEventHandler.status(v.getMajor(), v.getMinor(),
263 statusCode, statusLine.getReasonPhrase());
264 mEventHandler.headers(header);
265 HttpEntity entity = null;
266 hasBody = canResponseHaveBody(mHttpRequest, statusCode);
267
268 if (hasBody)
269 entity = httpClientConnection.receiveResponseEntity(header);
270
Grace Kloba6ead8f62010-04-23 15:57:58 -0700271 // restrict the range request to the servers claiming that they are
272 // accepting ranges in bytes
273 boolean supportPartialContent = "bytes".equalsIgnoreCase(header
274 .getAcceptRanges());
Grace Kloba52e41582010-04-21 23:57:43 -0700275
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800276 if (entity != null) {
277 InputStream is = entity.getContent();
278
279 // process gzip content encoding
280 Header contentEncoding = entity.getContentEncoding();
281 InputStream nis = null;
Grace Kloba7cd64bd2009-06-09 16:42:45 -0700282 byte[] buf = null;
283 int count = 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800284 try {
285 if (contentEncoding != null &&
286 contentEncoding.getValue().equals("gzip")) {
287 nis = new GZIPInputStream(is);
288 } else {
289 nis = is;
290 }
291
292 /* accumulate enough data to make it worth pushing it
293 * up the stack */
Grace Kloba7cd64bd2009-06-09 16:42:45 -0700294 buf = mConnection.getBuf();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800295 int len = 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800296 int lowWater = buf.length / 2;
297 while (len != -1) {
Ben Murdoch5b494c12010-03-12 20:45:32 +0000298 synchronized(this) {
299 while (mLoadingPaused) {
300 // Put this (network loading) thread to sleep if WebCore
301 // has asked us to. This can happen with plugins for
302 // example, if we are streaming data but the plugin has
303 // filled its internal buffers.
304 try {
305 wait();
306 } catch (InterruptedException e) {
307 HttpLog.e("Interrupted exception whilst "
308 + "network thread paused at WebCore's request."
309 + " " + e.getMessage());
310 }
311 }
312 }
313
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800314 len = nis.read(buf, count, buf.length - count);
Ben Murdoch5b494c12010-03-12 20:45:32 +0000315
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800316 if (len != -1) {
317 count += len;
Grace Kloba52e41582010-04-21 23:57:43 -0700318 if (supportPartialContent) mReceivedBytes += len;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800319 }
320 if (len == -1 || count >= lowWater) {
321 if (HttpLog.LOGV) HttpLog.v("Request.readResponse() " + count);
322 mEventHandler.data(buf, count);
323 count = 0;
324 }
325 }
326 } catch (EOFException e) {
327 /* InflaterInputStream throws an EOFException when the
328 server truncates gzipped content. Handle this case
329 as we do truncated non-gzipped content: no error */
Grace Kloba7cd64bd2009-06-09 16:42:45 -0700330 if (count > 0) {
331 // if there is uncommited content, we should commit them
332 mEventHandler.data(buf, count);
333 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800334 if (HttpLog.LOGV) HttpLog.v( "readResponse() handling " + e);
335 } catch(IOException e) {
336 // don't throw if we have a non-OK status code
Grace Kloba52e41582010-04-21 23:57:43 -0700337 if (statusCode == HttpStatus.SC_OK
338 || statusCode == HttpStatus.SC_PARTIAL_CONTENT) {
339 if (supportPartialContent && count > 0) {
340 // if there is uncommited content, we should commit them
341 // as we will continue the request
342 mEventHandler.data(buf, count);
343 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800344 throw e;
345 }
346 } finally {
347 if (nis != null) {
348 nis.close();
349 }
350 }
351 }
352 mConnection.setCanPersist(entity, statusLine.getProtocolVersion(),
353 header.getConnectionType());
354 mEventHandler.endData();
355 complete();
356
357 if (HttpLog.LOGV) HttpLog.v("Request.readResponse(): done " +
358 mHost.getSchemeName() + "://" + getHostPort() + mPath);
359 }
360
361 /**
362 * Data will not be sent to or received from server after cancel()
363 * call. Does not close connection--use close() below for that.
364 *
365 * Called by RequestHandle from non-network thread
366 */
Ben Murdoch5b494c12010-03-12 20:45:32 +0000367 synchronized void cancel() {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800368 if (HttpLog.LOGV) {
369 HttpLog.v("Request.cancel(): " + getUri());
370 }
Ben Murdoch5b494c12010-03-12 20:45:32 +0000371
372 // Ensure that the network thread is not blocked by a hanging request from WebCore to
373 // pause the load.
374 mLoadingPaused = false;
375 notify();
376
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800377 mCancelled = true;
378 if (mConnection != null) {
379 mConnection.cancel();
380 }
381 }
382
383 String getHostPort() {
384 String myScheme = mHost.getSchemeName();
385 int myPort = mHost.getPort();
386
387 // Only send port when we must... many servers can't deal with it
388 if (myPort != 80 && myScheme.equals("http") ||
389 myPort != 443 && myScheme.equals("https")) {
390 return mHost.toHostString();
391 } else {
392 return mHost.getHostName();
393 }
394 }
395
396 String getUri() {
397 if (mProxyHost == null ||
398 mHost.getSchemeName().equals("https")) {
399 return mPath;
400 }
401 return mHost.getSchemeName() + "://" + getHostPort() + mPath;
402 }
403
404 /**
405 * for debugging
406 */
407 public String toString() {
Patrick Scottfe4fec72009-07-14 15:54:30 -0400408 return mPath;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800409 }
410
411
412 /**
413 * If this request has been sent once and failed, it must be reset
414 * before it can be sent again.
415 */
416 void reset() {
417 /* clear content-length header */
418 mHttpRequest.removeHeaders(CONTENT_LENGTH_HEADER);
419
420 if (mBodyProvider != null) {
421 try {
422 mBodyProvider.reset();
423 } catch (IOException ex) {
424 if (HttpLog.LOGV) HttpLog.v(
425 "failed to reset body provider " +
426 getUri());
427 }
428 setBodyProvider(mBodyProvider, mBodyLength);
429 }
Grace Kloba52e41582010-04-21 23:57:43 -0700430
431 if (mReceivedBytes > 0) {
432 // reset the fail count as we continue the request
433 mFailCount = 0;
434 // set the "Range" header to indicate that the retry will continue
435 // instead of restarting the request
436 HttpLog.v("*** Request.reset() to range:" + mReceivedBytes);
437 mHttpRequest.setHeader("Range", "bytes=" + mReceivedBytes + "-");
438 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800439 }
440
441 /**
442 * Pause thread request completes. Used for synchronous requests,
443 * and testing
444 */
445 void waitUntilComplete() {
446 synchronized (mClientResource) {
447 try {
448 if (HttpLog.LOGV) HttpLog.v("Request.waitUntilComplete()");
449 mClientResource.wait();
450 if (HttpLog.LOGV) HttpLog.v("Request.waitUntilComplete() done waiting");
451 } catch (InterruptedException e) {
452 }
453 }
454 }
455
456 void complete() {
457 synchronized (mClientResource) {
458 mClientResource.notifyAll();
459 }
460 }
461
462 /**
463 * Decide whether a response comes with an entity.
464 * The implementation in this class is based on RFC 2616.
465 * Unknown methods and response codes are supposed to
466 * indicate responses with an entity.
467 * <br/>
468 * Derived executors can override this method to handle
469 * methods and response codes not specified in RFC 2616.
470 *
471 * @param request the request, to obtain the executed method
472 * @param response the response, to obtain the status code
473 */
474
475 private static boolean canResponseHaveBody(final HttpRequest request,
476 final int status) {
477
478 if ("HEAD".equalsIgnoreCase(request.getRequestLine().getMethod())) {
479 return false;
480 }
481 return status >= HttpStatus.SC_OK
482 && status != HttpStatus.SC_NO_CONTENT
Patrick Scottc319c692009-07-20 15:38:17 -0400483 && status != HttpStatus.SC_NOT_MODIFIED;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800484 }
485
486 /**
487 * Supply an InputStream that provides the body of a request. It's
488 * not great that the caller must also provide the length of the data
489 * returned by that InputStream, but the client needs to know up
490 * front, and I'm not sure how to get this out of the InputStream
491 * itself without a costly readthrough. I'm not sure skip() would
492 * do what we want. If you know a better way, please let me know.
493 */
494 private void setBodyProvider(InputStream bodyProvider, int bodyLength) {
495 if (!bodyProvider.markSupported()) {
496 throw new IllegalArgumentException(
497 "bodyProvider must support mark()");
498 }
499 // Mark beginning of stream
500 bodyProvider.mark(Integer.MAX_VALUE);
501
502 ((BasicHttpEntityEnclosingRequest)mHttpRequest).setEntity(
503 new InputStreamEntity(bodyProvider, bodyLength));
504 }
505
506
507 /**
508 * Handles SSL error(s) on the way down from the user (the user
509 * has already provided their feedback).
510 */
511 public void handleSslErrorResponse(boolean proceed) {
512 HttpsConnection connection = (HttpsConnection)(mConnection);
513 if (connection != null) {
514 connection.restartConnection(proceed);
515 }
516 }
517
518 /**
519 * Helper: calls error() on eventhandler with appropriate message
520 * This should not be called before the mConnection is set.
521 */
522 void error(int errorId, int resourceId) {
523 mEventHandler.error(
524 errorId,
525 mConnection.mContext.getText(
526 resourceId).toString());
527 }
528
529}