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