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