Request.java revision fe4fec7c66b0d956f008ead0fd899b588cfacb5d
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 has been cancelled */ 71 volatile boolean mCancelled = false; 72 73 int mFailCount = 0; 74 75 private InputStream mBodyProvider; 76 private int mBodyLength; 77 78 private final static String HOST_HEADER = "Host"; 79 private final static String ACCEPT_ENCODING_HEADER = "Accept-Encoding"; 80 private final static String CONTENT_LENGTH_HEADER = "content-length"; 81 82 /* Used to synchronize waitUntilComplete() requests */ 83 private final Object mClientResource = new Object(); 84 85 /** 86 * Processor used to set content-length and transfer-encoding 87 * headers. 88 */ 89 private static RequestContent requestContentProcessor = 90 new RequestContent(); 91 92 /** 93 * Instantiates a new Request. 94 * @param method GET/POST/PUT 95 * @param host The server that will handle this request 96 * @param path path part of URI 97 * @param bodyProvider InputStream providing HTTP body, null if none 98 * @param bodyLength length of body, must be 0 if bodyProvider is null 99 * @param eventHandler request will make progress callbacks on 100 * this interface 101 * @param headers reqeust headers 102 */ 103 Request(String method, HttpHost host, HttpHost proxyHost, String path, 104 InputStream bodyProvider, int bodyLength, 105 EventHandler eventHandler, 106 Map<String, String> headers) { 107 mEventHandler = eventHandler; 108 mHost = host; 109 mProxyHost = proxyHost; 110 mPath = path; 111 mBodyProvider = bodyProvider; 112 mBodyLength = bodyLength; 113 114 if (bodyProvider == null && !"POST".equalsIgnoreCase(method)) { 115 mHttpRequest = new BasicHttpRequest(method, getUri()); 116 } else { 117 mHttpRequest = new BasicHttpEntityEnclosingRequest( 118 method, getUri()); 119 // it is ok to have null entity for BasicHttpEntityEnclosingRequest. 120 // By using BasicHttpEntityEnclosingRequest, it will set up the 121 // correct content-length, content-type and content-encoding. 122 if (bodyProvider != null) { 123 setBodyProvider(bodyProvider, bodyLength); 124 } 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 byte[] buf = null; 259 int count = 0; 260 try { 261 if (contentEncoding != null && 262 contentEncoding.getValue().equals("gzip")) { 263 nis = new GZIPInputStream(is); 264 } else { 265 nis = is; 266 } 267 268 /* accumulate enough data to make it worth pushing it 269 * up the stack */ 270 buf = mConnection.getBuf(); 271 int len = 0; 272 int lowWater = buf.length / 2; 273 while (len != -1) { 274 len = nis.read(buf, count, buf.length - count); 275 if (len != -1) { 276 count += len; 277 } 278 if (len == -1 || count >= lowWater) { 279 if (HttpLog.LOGV) HttpLog.v("Request.readResponse() " + count); 280 mEventHandler.data(buf, count); 281 count = 0; 282 } 283 } 284 } catch (EOFException e) { 285 /* InflaterInputStream throws an EOFException when the 286 server truncates gzipped content. Handle this case 287 as we do truncated non-gzipped content: no error */ 288 if (count > 0) { 289 // if there is uncommited content, we should commit them 290 mEventHandler.data(buf, count); 291 } 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 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