Request.java revision 9066cfe9886ac131c34d59ed0e2d287b0e3c0087
1/* 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; 37import org.apache.http.ParseException; 38import org.apache.http.ProtocolVersion; 39 40import org.apache.http.StatusLine; 41import org.apache.http.message.BasicHttpRequest; 42import org.apache.http.message.BasicHttpEntityEnclosingRequest; 43import org.apache.http.protocol.RequestContent; 44 45/** 46 * Represents an HTTP request for a given host. 47 * 48 * {@hide} 49 */ 50 51class Request { 52 53 /** The eventhandler to call as the request progresses */ 54 EventHandler mEventHandler; 55 56 private Connection mConnection; 57 58 /** The Apache http request */ 59 BasicHttpRequest mHttpRequest; 60 61 /** The path component of this request */ 62 String mPath; 63 64 /** Host serving this request */ 65 HttpHost mHost; 66 67 /** Set if I'm using a proxy server */ 68 HttpHost mProxyHost; 69 70 /** True if request is .html, .js, .css */ 71 boolean mHighPriority; 72 73 /** True if request has been cancelled */ 74 volatile boolean mCancelled = false; 75 76 int mFailCount = 0; 77 78 private InputStream mBodyProvider; 79 private int mBodyLength; 80 81 private final static String HOST_HEADER = "Host"; 82 private final static String ACCEPT_ENCODING_HEADER = "Accept-Encoding"; 83 private final static String CONTENT_LENGTH_HEADER = "content-length"; 84 85 /* Used to synchronize waitUntilComplete() requests */ 86 private final Object mClientResource = new Object(); 87 88 /** 89 * Processor used to set content-length and transfer-encoding 90 * headers. 91 */ 92 private static RequestContent requestContentProcessor = 93 new RequestContent(); 94 95 /** 96 * Instantiates a new Request. 97 * @param method GET/POST/PUT 98 * @param host The server that will handle this request 99 * @param path path part of URI 100 * @param bodyProvider InputStream providing HTTP body, null if none 101 * @param bodyLength length of body, must be 0 if bodyProvider is null 102 * @param eventHandler request will make progress callbacks on 103 * this interface 104 * @param headers reqeust headers 105 * @param highPriority true for .html, css, .cs 106 */ 107 Request(String method, HttpHost host, HttpHost proxyHost, String path, 108 InputStream bodyProvider, int bodyLength, 109 EventHandler eventHandler, 110 Map<String, String> headers, boolean highPriority) { 111 mEventHandler = eventHandler; 112 mHost = host; 113 mProxyHost = proxyHost; 114 mPath = path; 115 mHighPriority = highPriority; 116 mBodyProvider = bodyProvider; 117 mBodyLength = bodyLength; 118 119 if (bodyProvider == null) { 120 mHttpRequest = new BasicHttpRequest(method, getUri()); 121 } else { 122 mHttpRequest = new BasicHttpEntityEnclosingRequest( 123 method, getUri()); 124 setBodyProvider(bodyProvider, bodyLength); 125 } 126 addHeader(HOST_HEADER, getHostPort()); 127 128 /* FIXME: if webcore will make the root document a 129 high-priority request, we can ask for gzip encoding only on 130 high priority reqs (saving the trouble for images, etc) */ 131 addHeader(ACCEPT_ENCODING_HEADER, "gzip"); 132 addHeaders(headers); 133 } 134 135 /** 136 * @param connection Request served by this connection 137 */ 138 void setConnection(Connection connection) { 139 mConnection = connection; 140 } 141 142 /* package */ EventHandler getEventHandler() { 143 return mEventHandler; 144 } 145 146 /** 147 * Add header represented by given pair to request. Header will 148 * be formatted in request as "name: value\r\n". 149 * @param name of header 150 * @param value of header 151 */ 152 void addHeader(String name, String value) { 153 if (name == null) { 154 String damage = "Null http header name"; 155 HttpLog.e(damage); 156 throw new NullPointerException(damage); 157 } 158 if (value == null || value.length() == 0) { 159 String damage = "Null or empty value for header \"" + name + "\""; 160 HttpLog.e(damage); 161 throw new RuntimeException(damage); 162 } 163 mHttpRequest.addHeader(name, value); 164 } 165 166 /** 167 * Add all headers in given map to this request. This is a helper 168 * method: it calls addHeader for each pair in the map. 169 */ 170 void addHeaders(Map<String, String> headers) { 171 if (headers == null) { 172 return; 173 } 174 175 Entry<String, String> entry; 176 Iterator<Entry<String, String>> i = headers.entrySet().iterator(); 177 while (i.hasNext()) { 178 entry = i.next(); 179 addHeader(entry.getKey(), entry.getValue()); 180 } 181 } 182 183 /** 184 * Send the request line and headers 185 */ 186 void sendRequest(AndroidHttpClientConnection httpClientConnection) 187 throws HttpException, IOException { 188 189 if (mCancelled) return; // don't send cancelled requests 190 191 if (HttpLog.LOGV) { 192 HttpLog.v("Request.sendRequest() " + mHost.getSchemeName() + "://" + getHostPort()); 193 // HttpLog.v(mHttpRequest.getRequestLine().toString()); 194 if (false) { 195 Iterator i = mHttpRequest.headerIterator(); 196 while (i.hasNext()) { 197 Header header = (Header)i.next(); 198 HttpLog.v(header.getName() + ": " + header.getValue()); 199 } 200 } 201 } 202 203 requestContentProcessor.process(mHttpRequest, 204 mConnection.getHttpContext()); 205 httpClientConnection.sendRequestHeader(mHttpRequest); 206 if (mHttpRequest instanceof HttpEntityEnclosingRequest) { 207 httpClientConnection.sendRequestEntity( 208 (HttpEntityEnclosingRequest) mHttpRequest); 209 } 210 211 if (HttpLog.LOGV) { 212 HttpLog.v("Request.requestSent() " + mHost.getSchemeName() + "://" + getHostPort() + mPath); 213 } 214 } 215 216 217 /** 218 * Receive a single http response. 219 * 220 * @param httpClientConnection the request to receive the response for. 221 */ 222 void readResponse(AndroidHttpClientConnection httpClientConnection) 223 throws IOException, ParseException { 224 225 if (mCancelled) return; // don't send cancelled requests 226 227 StatusLine statusLine = null; 228 boolean hasBody = false; 229 boolean reuse = false; 230 httpClientConnection.flush(); 231 int statusCode = 0; 232 233 Headers header = new Headers(); 234 do { 235 statusLine = httpClientConnection.parseResponseHeader(header); 236 statusCode = statusLine.getStatusCode(); 237 } while (statusCode < HttpStatus.SC_OK); 238 if (HttpLog.LOGV) HttpLog.v( 239 "Request.readResponseStatus() " + 240 statusLine.toString().length() + " " + statusLine); 241 242 ProtocolVersion v = statusLine.getProtocolVersion(); 243 mEventHandler.status(v.getMajor(), v.getMinor(), 244 statusCode, statusLine.getReasonPhrase()); 245 mEventHandler.headers(header); 246 HttpEntity entity = null; 247 hasBody = canResponseHaveBody(mHttpRequest, statusCode); 248 249 if (hasBody) 250 entity = httpClientConnection.receiveResponseEntity(header); 251 252 if (entity != null) { 253 InputStream is = entity.getContent(); 254 255 // process gzip content encoding 256 Header contentEncoding = entity.getContentEncoding(); 257 InputStream nis = null; 258 try { 259 if (contentEncoding != null && 260 contentEncoding.getValue().equals("gzip")) { 261 nis = new GZIPInputStream(is); 262 } else { 263 nis = is; 264 } 265 266 /* accumulate enough data to make it worth pushing it 267 * up the stack */ 268 byte[] buf = mConnection.getBuf(); 269 int len = 0; 270 int count = 0; 271 int lowWater = buf.length / 2; 272 while (len != -1) { 273 len = nis.read(buf, count, buf.length - count); 274 if (len != -1) { 275 count += len; 276 } 277 if (len == -1 || count >= lowWater) { 278 if (HttpLog.LOGV) HttpLog.v("Request.readResponse() " + count); 279 mEventHandler.data(buf, count); 280 count = 0; 281 } 282 } 283 } catch (EOFException e) { 284 /* InflaterInputStream throws an EOFException when the 285 server truncates gzipped content. Handle this case 286 as we do truncated non-gzipped content: no error */ 287 if (HttpLog.LOGV) HttpLog.v( "readResponse() handling " + e); 288 } catch(IOException e) { 289 // don't throw if we have a non-OK status code 290 if (statusCode == HttpStatus.SC_OK) { 291 throw e; 292 } 293 } finally { 294 if (nis != null) { 295 nis.close(); 296 } 297 } 298 } 299 mConnection.setCanPersist(entity, statusLine.getProtocolVersion(), 300 header.getConnectionType()); 301 mEventHandler.endData(); 302 complete(); 303 304 if (HttpLog.LOGV) HttpLog.v("Request.readResponse(): done " + 305 mHost.getSchemeName() + "://" + getHostPort() + mPath); 306 } 307 308 /** 309 * Data will not be sent to or received from server after cancel() 310 * call. Does not close connection--use close() below for that. 311 * 312 * Called by RequestHandle from non-network thread 313 */ 314 void cancel() { 315 if (HttpLog.LOGV) { 316 HttpLog.v("Request.cancel(): " + getUri()); 317 } 318 mCancelled = true; 319 if (mConnection != null) { 320 mConnection.cancel(); 321 } 322 } 323 324 String getHostPort() { 325 String myScheme = mHost.getSchemeName(); 326 int myPort = mHost.getPort(); 327 328 // Only send port when we must... many servers can't deal with it 329 if (myPort != 80 && myScheme.equals("http") || 330 myPort != 443 && myScheme.equals("https")) { 331 return mHost.toHostString(); 332 } else { 333 return mHost.getHostName(); 334 } 335 } 336 337 String getUri() { 338 if (mProxyHost == null || 339 mHost.getSchemeName().equals("https")) { 340 return mPath; 341 } 342 return mHost.getSchemeName() + "://" + getHostPort() + mPath; 343 } 344 345 /** 346 * for debugging 347 */ 348 public String toString() { 349 return (mHighPriority ? "P*" : "") + mPath; 350 } 351 352 353 /** 354 * If this request has been sent once and failed, it must be reset 355 * before it can be sent again. 356 */ 357 void reset() { 358 /* clear content-length header */ 359 mHttpRequest.removeHeaders(CONTENT_LENGTH_HEADER); 360 361 if (mBodyProvider != null) { 362 try { 363 mBodyProvider.reset(); 364 } catch (IOException ex) { 365 if (HttpLog.LOGV) HttpLog.v( 366 "failed to reset body provider " + 367 getUri()); 368 } 369 setBodyProvider(mBodyProvider, mBodyLength); 370 } 371 } 372 373 /** 374 * Pause thread request completes. Used for synchronous requests, 375 * and testing 376 */ 377 void waitUntilComplete() { 378 synchronized (mClientResource) { 379 try { 380 if (HttpLog.LOGV) HttpLog.v("Request.waitUntilComplete()"); 381 mClientResource.wait(); 382 if (HttpLog.LOGV) HttpLog.v("Request.waitUntilComplete() done waiting"); 383 } catch (InterruptedException e) { 384 } 385 } 386 } 387 388 void complete() { 389 synchronized (mClientResource) { 390 mClientResource.notifyAll(); 391 } 392 } 393 394 /** 395 * Decide whether a response comes with an entity. 396 * The implementation in this class is based on RFC 2616. 397 * Unknown methods and response codes are supposed to 398 * indicate responses with an entity. 399 * <br/> 400 * Derived executors can override this method to handle 401 * methods and response codes not specified in RFC 2616. 402 * 403 * @param request the request, to obtain the executed method 404 * @param response the response, to obtain the status code 405 */ 406 407 private static boolean canResponseHaveBody(final HttpRequest request, 408 final int status) { 409 410 if ("HEAD".equalsIgnoreCase(request.getRequestLine().getMethod())) { 411 return false; 412 } 413 return status >= HttpStatus.SC_OK 414 && status != HttpStatus.SC_NO_CONTENT 415 && status != HttpStatus.SC_NOT_MODIFIED 416 && status != HttpStatus.SC_RESET_CONTENT; 417 } 418 419 /** 420 * Supply an InputStream that provides the body of a request. It's 421 * not great that the caller must also provide the length of the data 422 * returned by that InputStream, but the client needs to know up 423 * front, and I'm not sure how to get this out of the InputStream 424 * itself without a costly readthrough. I'm not sure skip() would 425 * do what we want. If you know a better way, please let me know. 426 */ 427 private void setBodyProvider(InputStream bodyProvider, int bodyLength) { 428 if (!bodyProvider.markSupported()) { 429 throw new IllegalArgumentException( 430 "bodyProvider must support mark()"); 431 } 432 // Mark beginning of stream 433 bodyProvider.mark(Integer.MAX_VALUE); 434 435 ((BasicHttpEntityEnclosingRequest)mHttpRequest).setEntity( 436 new InputStreamEntity(bodyProvider, bodyLength)); 437 } 438 439 440 /** 441 * Handles SSL error(s) on the way down from the user (the user 442 * has already provided their feedback). 443 */ 444 public void handleSslErrorResponse(boolean proceed) { 445 HttpsConnection connection = (HttpsConnection)(mConnection); 446 if (connection != null) { 447 connection.restartConnection(proceed); 448 } 449 } 450 451 /** 452 * Helper: calls error() on eventhandler with appropriate message 453 * This should not be called before the mConnection is set. 454 */ 455 void error(int errorId, int resourceId) { 456 mEventHandler.error( 457 errorId, 458 mConnection.mContext.getText( 459 resourceId).toString()); 460 } 461 462} 463