blob: 76d7bb9521a351f363374ca866f4f5edf3042f75 [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;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080029import org.apache.http.HttpEntity;
30import org.apache.http.HttpEntityEnclosingRequest;
31import org.apache.http.HttpException;
32import org.apache.http.HttpHost;
33import org.apache.http.HttpRequest;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080034import org.apache.http.HttpStatus;
35import org.apache.http.ParseException;
36import org.apache.http.ProtocolVersion;
37
38import org.apache.http.StatusLine;
39import org.apache.http.message.BasicHttpRequest;
40import org.apache.http.message.BasicHttpEntityEnclosingRequest;
41import org.apache.http.protocol.RequestContent;
42
43/**
44 * Represents an HTTP request for a given host.
45 *
46 * {@hide}
47 */
48
49class Request {
50
51 /** The eventhandler to call as the request progresses */
52 EventHandler mEventHandler;
53
54 private Connection mConnection;
55
56 /** The Apache http request */
57 BasicHttpRequest mHttpRequest;
58
59 /** The path component of this request */
60 String mPath;
61
62 /** Host serving this request */
63 HttpHost mHost;
64
65 /** Set if I'm using a proxy server */
66 HttpHost mProxyHost;
67
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080068 /** True if request has been cancelled */
69 volatile boolean mCancelled = false;
70
71 int mFailCount = 0;
72
Grace Kloba52e41582010-04-21 23:57:43 -070073 // This will be used to set the Range field if we retry a connection. This
74 // is http/1.1 feature.
75 private int mReceivedBytes = 0;
76
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080077 private InputStream mBodyProvider;
78 private int mBodyLength;
79
80 private final static String HOST_HEADER = "Host";
81 private final static String ACCEPT_ENCODING_HEADER = "Accept-Encoding";
82 private final static String CONTENT_LENGTH_HEADER = "content-length";
83
84 /* Used to synchronize waitUntilComplete() requests */
85 private final Object mClientResource = new Object();
86
Ben Murdoch5b494c12010-03-12 20:45:32 +000087 /** True if loading should be paused **/
88 private boolean mLoadingPaused = false;
89
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080090 /**
91 * Processor used to set content-length and transfer-encoding
92 * headers.
93 */
94 private static RequestContent requestContentProcessor =
95 new RequestContent();
96
97 /**
98 * Instantiates a new Request.
99 * @param method GET/POST/PUT
100 * @param host The server that will handle this request
101 * @param path path part of URI
102 * @param bodyProvider InputStream providing HTTP body, null if none
103 * @param bodyLength length of body, must be 0 if bodyProvider is null
104 * @param eventHandler request will make progress callbacks on
105 * this interface
106 * @param headers reqeust headers
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800107 */
108 Request(String method, HttpHost host, HttpHost proxyHost, String path,
109 InputStream bodyProvider, int bodyLength,
110 EventHandler eventHandler,
Patrick Scottfe4fec72009-07-14 15:54:30 -0400111 Map<String, String> headers) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800112 mEventHandler = eventHandler;
113 mHost = host;
114 mProxyHost = proxyHost;
115 mPath = path;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800116 mBodyProvider = bodyProvider;
117 mBodyLength = bodyLength;
118
Grace Kloba45efe692009-05-08 19:21:55 -0700119 if (bodyProvider == null && !"POST".equalsIgnoreCase(method)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800120 mHttpRequest = new BasicHttpRequest(method, getUri());
121 } else {
122 mHttpRequest = new BasicHttpEntityEnclosingRequest(
123 method, getUri());
Grace Kloba45efe692009-05-08 19:21:55 -0700124 // it is ok to have null entity for BasicHttpEntityEnclosingRequest.
125 // By using BasicHttpEntityEnclosingRequest, it will set up the
126 // correct content-length, content-type and content-encoding.
127 if (bodyProvider != null) {
128 setBodyProvider(bodyProvider, bodyLength);
129 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800130 }
131 addHeader(HOST_HEADER, getHostPort());
132
133 /* FIXME: if webcore will make the root document a
134 high-priority request, we can ask for gzip encoding only on
135 high priority reqs (saving the trouble for images, etc) */
136 addHeader(ACCEPT_ENCODING_HEADER, "gzip");
137 addHeaders(headers);
138 }
139
140 /**
Ben Murdoch5b494c12010-03-12 20:45:32 +0000141 * @param pause True if the load should be paused.
142 */
143 synchronized void setLoadingPaused(boolean pause) {
144 mLoadingPaused = pause;
145
146 // Wake up the paused thread if we're unpausing the load.
147 if (!mLoadingPaused) {
148 notify();
149 }
150 }
151
152 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800153 * @param connection Request served by this connection
154 */
155 void setConnection(Connection connection) {
156 mConnection = connection;
157 }
158
159 /* package */ EventHandler getEventHandler() {
160 return mEventHandler;
161 }
162
163 /**
164 * Add header represented by given pair to request. Header will
165 * be formatted in request as "name: value\r\n".
166 * @param name of header
167 * @param value of header
168 */
169 void addHeader(String name, String value) {
170 if (name == null) {
171 String damage = "Null http header name";
172 HttpLog.e(damage);
173 throw new NullPointerException(damage);
174 }
175 if (value == null || value.length() == 0) {
176 String damage = "Null or empty value for header \"" + name + "\"";
177 HttpLog.e(damage);
178 throw new RuntimeException(damage);
179 }
180 mHttpRequest.addHeader(name, value);
181 }
182
183 /**
184 * Add all headers in given map to this request. This is a helper
185 * method: it calls addHeader for each pair in the map.
186 */
187 void addHeaders(Map<String, String> headers) {
188 if (headers == null) {
189 return;
190 }
191
192 Entry<String, String> entry;
193 Iterator<Entry<String, String>> i = headers.entrySet().iterator();
194 while (i.hasNext()) {
195 entry = i.next();
196 addHeader(entry.getKey(), entry.getValue());
197 }
198 }
199
200 /**
201 * Send the request line and headers
202 */
203 void sendRequest(AndroidHttpClientConnection httpClientConnection)
204 throws HttpException, IOException {
205
206 if (mCancelled) return; // don't send cancelled requests
207
208 if (HttpLog.LOGV) {
209 HttpLog.v("Request.sendRequest() " + mHost.getSchemeName() + "://" + getHostPort());
210 // HttpLog.v(mHttpRequest.getRequestLine().toString());
211 if (false) {
212 Iterator i = mHttpRequest.headerIterator();
213 while (i.hasNext()) {
214 Header header = (Header)i.next();
215 HttpLog.v(header.getName() + ": " + header.getValue());
216 }
217 }
218 }
219
220 requestContentProcessor.process(mHttpRequest,
221 mConnection.getHttpContext());
222 httpClientConnection.sendRequestHeader(mHttpRequest);
223 if (mHttpRequest instanceof HttpEntityEnclosingRequest) {
224 httpClientConnection.sendRequestEntity(
225 (HttpEntityEnclosingRequest) mHttpRequest);
226 }
227
228 if (HttpLog.LOGV) {
229 HttpLog.v("Request.requestSent() " + mHost.getSchemeName() + "://" + getHostPort() + mPath);
230 }
231 }
232
233
234 /**
235 * Receive a single http response.
236 *
237 * @param httpClientConnection the request to receive the response for.
238 */
239 void readResponse(AndroidHttpClientConnection httpClientConnection)
240 throws IOException, ParseException {
241
242 if (mCancelled) return; // don't send cancelled requests
243
244 StatusLine statusLine = null;
245 boolean hasBody = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800246 httpClientConnection.flush();
247 int statusCode = 0;
248
249 Headers header = new Headers();
250 do {
251 statusLine = httpClientConnection.parseResponseHeader(header);
252 statusCode = statusLine.getStatusCode();
253 } while (statusCode < HttpStatus.SC_OK);
254 if (HttpLog.LOGV) HttpLog.v(
255 "Request.readResponseStatus() " +
256 statusLine.toString().length() + " " + statusLine);
257
258 ProtocolVersion v = statusLine.getProtocolVersion();
259 mEventHandler.status(v.getMajor(), v.getMinor(),
260 statusCode, statusLine.getReasonPhrase());
261 mEventHandler.headers(header);
262 HttpEntity entity = null;
263 hasBody = canResponseHaveBody(mHttpRequest, statusCode);
264
265 if (hasBody)
266 entity = httpClientConnection.receiveResponseEntity(header);
267
Grace Kloba6ead8f62010-04-23 15:57:58 -0700268 // restrict the range request to the servers claiming that they are
269 // accepting ranges in bytes
270 boolean supportPartialContent = "bytes".equalsIgnoreCase(header
271 .getAcceptRanges());
Grace Kloba52e41582010-04-21 23:57:43 -0700272
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800273 if (entity != null) {
274 InputStream is = entity.getContent();
275
276 // process gzip content encoding
277 Header contentEncoding = entity.getContentEncoding();
278 InputStream nis = null;
Grace Kloba7cd64bd2009-06-09 16:42:45 -0700279 byte[] buf = null;
280 int count = 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800281 try {
282 if (contentEncoding != null &&
283 contentEncoding.getValue().equals("gzip")) {
284 nis = new GZIPInputStream(is);
285 } else {
286 nis = is;
287 }
288
289 /* accumulate enough data to make it worth pushing it
290 * up the stack */
Grace Kloba7cd64bd2009-06-09 16:42:45 -0700291 buf = mConnection.getBuf();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800292 int len = 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800293 int lowWater = buf.length / 2;
294 while (len != -1) {
Ben Murdoch5b494c12010-03-12 20:45:32 +0000295 synchronized(this) {
296 while (mLoadingPaused) {
297 // Put this (network loading) thread to sleep if WebCore
298 // has asked us to. This can happen with plugins for
299 // example, if we are streaming data but the plugin has
300 // filled its internal buffers.
301 try {
302 wait();
303 } catch (InterruptedException e) {
304 HttpLog.e("Interrupted exception whilst "
305 + "network thread paused at WebCore's request."
306 + " " + e.getMessage());
307 }
308 }
309 }
310
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800311 len = nis.read(buf, count, buf.length - count);
Ben Murdoch5b494c12010-03-12 20:45:32 +0000312
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800313 if (len != -1) {
314 count += len;
Grace Kloba52e41582010-04-21 23:57:43 -0700315 if (supportPartialContent) mReceivedBytes += len;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800316 }
317 if (len == -1 || count >= lowWater) {
318 if (HttpLog.LOGV) HttpLog.v("Request.readResponse() " + count);
319 mEventHandler.data(buf, count);
320 count = 0;
321 }
322 }
323 } catch (EOFException e) {
324 /* InflaterInputStream throws an EOFException when the
325 server truncates gzipped content. Handle this case
326 as we do truncated non-gzipped content: no error */
Grace Kloba7cd64bd2009-06-09 16:42:45 -0700327 if (count > 0) {
328 // if there is uncommited content, we should commit them
329 mEventHandler.data(buf, count);
330 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800331 if (HttpLog.LOGV) HttpLog.v( "readResponse() handling " + e);
332 } catch(IOException e) {
333 // don't throw if we have a non-OK status code
Grace Kloba52e41582010-04-21 23:57:43 -0700334 if (statusCode == HttpStatus.SC_OK
335 || statusCode == HttpStatus.SC_PARTIAL_CONTENT) {
336 if (supportPartialContent && count > 0) {
337 // if there is uncommited content, we should commit them
338 // as we will continue the request
339 mEventHandler.data(buf, count);
340 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800341 throw e;
342 }
343 } finally {
344 if (nis != null) {
345 nis.close();
346 }
347 }
348 }
349 mConnection.setCanPersist(entity, statusLine.getProtocolVersion(),
350 header.getConnectionType());
351 mEventHandler.endData();
352 complete();
353
354 if (HttpLog.LOGV) HttpLog.v("Request.readResponse(): done " +
355 mHost.getSchemeName() + "://" + getHostPort() + mPath);
356 }
357
358 /**
359 * Data will not be sent to or received from server after cancel()
360 * call. Does not close connection--use close() below for that.
361 *
362 * Called by RequestHandle from non-network thread
363 */
Ben Murdoch5b494c12010-03-12 20:45:32 +0000364 synchronized void cancel() {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800365 if (HttpLog.LOGV) {
366 HttpLog.v("Request.cancel(): " + getUri());
367 }
Ben Murdoch5b494c12010-03-12 20:45:32 +0000368
369 // Ensure that the network thread is not blocked by a hanging request from WebCore to
370 // pause the load.
371 mLoadingPaused = false;
372 notify();
373
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800374 mCancelled = true;
375 if (mConnection != null) {
376 mConnection.cancel();
377 }
378 }
379
380 String getHostPort() {
381 String myScheme = mHost.getSchemeName();
382 int myPort = mHost.getPort();
383
384 // Only send port when we must... many servers can't deal with it
385 if (myPort != 80 && myScheme.equals("http") ||
386 myPort != 443 && myScheme.equals("https")) {
387 return mHost.toHostString();
388 } else {
389 return mHost.getHostName();
390 }
391 }
392
393 String getUri() {
394 if (mProxyHost == null ||
395 mHost.getSchemeName().equals("https")) {
396 return mPath;
397 }
398 return mHost.getSchemeName() + "://" + getHostPort() + mPath;
399 }
400
401 /**
402 * for debugging
403 */
404 public String toString() {
Patrick Scottfe4fec72009-07-14 15:54:30 -0400405 return mPath;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800406 }
407
408
409 /**
410 * If this request has been sent once and failed, it must be reset
411 * before it can be sent again.
412 */
413 void reset() {
414 /* clear content-length header */
415 mHttpRequest.removeHeaders(CONTENT_LENGTH_HEADER);
416
417 if (mBodyProvider != null) {
418 try {
419 mBodyProvider.reset();
420 } catch (IOException ex) {
421 if (HttpLog.LOGV) HttpLog.v(
422 "failed to reset body provider " +
423 getUri());
424 }
425 setBodyProvider(mBodyProvider, mBodyLength);
426 }
Grace Kloba52e41582010-04-21 23:57:43 -0700427
428 if (mReceivedBytes > 0) {
429 // reset the fail count as we continue the request
430 mFailCount = 0;
431 // set the "Range" header to indicate that the retry will continue
432 // instead of restarting the request
433 HttpLog.v("*** Request.reset() to range:" + mReceivedBytes);
434 mHttpRequest.setHeader("Range", "bytes=" + mReceivedBytes + "-");
435 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800436 }
437
438 /**
439 * Pause thread request completes. Used for synchronous requests,
440 * and testing
441 */
442 void waitUntilComplete() {
443 synchronized (mClientResource) {
444 try {
445 if (HttpLog.LOGV) HttpLog.v("Request.waitUntilComplete()");
446 mClientResource.wait();
447 if (HttpLog.LOGV) HttpLog.v("Request.waitUntilComplete() done waiting");
448 } catch (InterruptedException e) {
449 }
450 }
451 }
452
453 void complete() {
454 synchronized (mClientResource) {
455 mClientResource.notifyAll();
456 }
457 }
458
459 /**
460 * Decide whether a response comes with an entity.
461 * The implementation in this class is based on RFC 2616.
462 * Unknown methods and response codes are supposed to
463 * indicate responses with an entity.
464 * <br/>
465 * Derived executors can override this method to handle
466 * methods and response codes not specified in RFC 2616.
467 *
468 * @param request the request, to obtain the executed method
469 * @param response the response, to obtain the status code
470 */
471
472 private static boolean canResponseHaveBody(final HttpRequest request,
473 final int status) {
474
475 if ("HEAD".equalsIgnoreCase(request.getRequestLine().getMethod())) {
476 return false;
477 }
478 return status >= HttpStatus.SC_OK
479 && status != HttpStatus.SC_NO_CONTENT
Patrick Scottc319c692009-07-20 15:38:17 -0400480 && status != HttpStatus.SC_NOT_MODIFIED;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800481 }
482
483 /**
484 * Supply an InputStream that provides the body of a request. It's
485 * not great that the caller must also provide the length of the data
486 * returned by that InputStream, but the client needs to know up
487 * front, and I'm not sure how to get this out of the InputStream
488 * itself without a costly readthrough. I'm not sure skip() would
489 * do what we want. If you know a better way, please let me know.
490 */
491 private void setBodyProvider(InputStream bodyProvider, int bodyLength) {
492 if (!bodyProvider.markSupported()) {
493 throw new IllegalArgumentException(
494 "bodyProvider must support mark()");
495 }
496 // Mark beginning of stream
497 bodyProvider.mark(Integer.MAX_VALUE);
498
499 ((BasicHttpEntityEnclosingRequest)mHttpRequest).setEntity(
500 new InputStreamEntity(bodyProvider, bodyLength));
501 }
502
503
504 /**
505 * Handles SSL error(s) on the way down from the user (the user
506 * has already provided their feedback).
507 */
508 public void handleSslErrorResponse(boolean proceed) {
509 HttpsConnection connection = (HttpsConnection)(mConnection);
510 if (connection != null) {
511 connection.restartConnection(proceed);
512 }
513 }
514
515 /**
516 * Helper: calls error() on eventhandler with appropriate message
517 * This should not be called before the mConnection is set.
518 */
519 void error(int errorId, int resourceId) {
520 mEventHandler.error(
521 errorId,
522 mConnection.mContext.getText(
523 resourceId).toString());
524 }
525
526}