1 /* 2 * Copyright (C) 2008 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 17 package android.net.http; 18 19 import org.apache.http.Header; 20 21 import org.apache.http.HttpConnection; 22 import org.apache.http.HttpClientConnection; 23 import org.apache.http.HttpConnectionMetrics; 24 import org.apache.http.HttpEntity; 25 import org.apache.http.HttpEntityEnclosingRequest; 26 import org.apache.http.HttpException; 27 import org.apache.http.HttpInetConnection; 28 import org.apache.http.HttpRequest; 29 import org.apache.http.HttpResponse; 30 import org.apache.http.HttpResponseFactory; 31 import org.apache.http.NoHttpResponseException; 32 import org.apache.http.StatusLine; 33 import org.apache.http.entity.BasicHttpEntity; 34 import org.apache.http.entity.ContentLengthStrategy; 35 import org.apache.http.impl.DefaultHttpResponseFactory; 36 import org.apache.http.impl.HttpConnectionMetricsImpl; 37 import org.apache.http.impl.entity.EntitySerializer; 38 import org.apache.http.impl.entity.StrictContentLengthStrategy; 39 import org.apache.http.impl.io.ChunkedInputStream; 40 import org.apache.http.impl.io.ContentLengthInputStream; 41 import org.apache.http.impl.io.HttpRequestWriter; 42 import org.apache.http.impl.io.IdentityInputStream; 43 import org.apache.http.impl.io.SocketInputBuffer; 44 import org.apache.http.impl.io.SocketOutputBuffer; 45 import org.apache.http.io.HttpMessageWriter; 46 import org.apache.http.io.SessionInputBuffer; 47 import org.apache.http.io.SessionOutputBuffer; 48 import org.apache.http.message.BasicLineParser; 49 import org.apache.http.message.ParserCursor; 50 import org.apache.http.params.CoreConnectionPNames; 51 import org.apache.http.params.HttpConnectionParams; 52 import org.apache.http.params.HttpParams; 53 import org.apache.http.ParseException; 54 import org.apache.http.util.CharArrayBuffer; 55 56 import java.io.IOException; 57 import java.net.InetAddress; 58 import java.net.Socket; 59 import java.net.SocketException; 60 61 /** 62 * A alternate class for (@link DefaultHttpClientConnection). 63 * It has better performance than DefaultHttpClientConnection 64 * 65 * {@hide} 66 */ 67 public class AndroidHttpClientConnection 68 implements HttpInetConnection, HttpConnection { 69 70 private SessionInputBuffer inbuffer = null; 71 private SessionOutputBuffer outbuffer = null; 72 private int maxHeaderCount; 73 // store CoreConnectionPNames.MAX_LINE_LENGTH for performance 74 private int maxLineLength; 75 76 private final EntitySerializer entityserializer; 77 78 private HttpMessageWriter requestWriter = null; 79 private HttpConnectionMetricsImpl metrics = null; 80 private volatile boolean open; 81 private Socket socket = null; 82 AndroidHttpClientConnection()83 public AndroidHttpClientConnection() { 84 this.entityserializer = new EntitySerializer( 85 new StrictContentLengthStrategy()); 86 } 87 88 /** 89 * Bind socket and set HttpParams to AndroidHttpClientConnection 90 * @param socket outgoing socket 91 * @param params HttpParams 92 * @throws IOException 93 */ bind( final Socket socket, final HttpParams params)94 public void bind( 95 final Socket socket, 96 final HttpParams params) throws IOException { 97 if (socket == null) { 98 throw new IllegalArgumentException("Socket may not be null"); 99 } 100 if (params == null) { 101 throw new IllegalArgumentException("HTTP parameters may not be null"); 102 } 103 assertNotOpen(); 104 socket.setTcpNoDelay(HttpConnectionParams.getTcpNoDelay(params)); 105 socket.setSoTimeout(HttpConnectionParams.getSoTimeout(params)); 106 107 int linger = HttpConnectionParams.getLinger(params); 108 if (linger >= 0) { 109 socket.setSoLinger(linger > 0, linger); 110 } 111 this.socket = socket; 112 113 int buffersize = HttpConnectionParams.getSocketBufferSize(params); 114 this.inbuffer = new SocketInputBuffer(socket, buffersize, params); 115 this.outbuffer = new SocketOutputBuffer(socket, buffersize, params); 116 117 maxHeaderCount = params.getIntParameter( 118 CoreConnectionPNames.MAX_HEADER_COUNT, -1); 119 maxLineLength = params.getIntParameter( 120 CoreConnectionPNames.MAX_LINE_LENGTH, -1); 121 122 this.requestWriter = new HttpRequestWriter(outbuffer, null, params); 123 124 this.metrics = new HttpConnectionMetricsImpl( 125 inbuffer.getMetrics(), 126 outbuffer.getMetrics()); 127 128 this.open = true; 129 } 130 131 @Override toString()132 public String toString() { 133 StringBuilder buffer = new StringBuilder(); 134 buffer.append(getClass().getSimpleName()).append("["); 135 if (isOpen()) { 136 buffer.append(getRemotePort()); 137 } else { 138 buffer.append("closed"); 139 } 140 buffer.append("]"); 141 return buffer.toString(); 142 } 143 144 assertNotOpen()145 private void assertNotOpen() { 146 if (this.open) { 147 throw new IllegalStateException("Connection is already open"); 148 } 149 } 150 assertOpen()151 private void assertOpen() { 152 if (!this.open) { 153 throw new IllegalStateException("Connection is not open"); 154 } 155 } 156 isOpen()157 public boolean isOpen() { 158 // to make this method useful, we want to check if the socket is connected 159 return (this.open && this.socket != null && this.socket.isConnected()); 160 } 161 getLocalAddress()162 public InetAddress getLocalAddress() { 163 if (this.socket != null) { 164 return this.socket.getLocalAddress(); 165 } else { 166 return null; 167 } 168 } 169 getLocalPort()170 public int getLocalPort() { 171 if (this.socket != null) { 172 return this.socket.getLocalPort(); 173 } else { 174 return -1; 175 } 176 } 177 getRemoteAddress()178 public InetAddress getRemoteAddress() { 179 if (this.socket != null) { 180 return this.socket.getInetAddress(); 181 } else { 182 return null; 183 } 184 } 185 getRemotePort()186 public int getRemotePort() { 187 if (this.socket != null) { 188 return this.socket.getPort(); 189 } else { 190 return -1; 191 } 192 } 193 setSocketTimeout(int timeout)194 public void setSocketTimeout(int timeout) { 195 assertOpen(); 196 if (this.socket != null) { 197 try { 198 this.socket.setSoTimeout(timeout); 199 } catch (SocketException ignore) { 200 // It is not quite clear from the original documentation if there are any 201 // other legitimate cases for a socket exception to be thrown when setting 202 // SO_TIMEOUT besides the socket being already closed 203 } 204 } 205 } 206 getSocketTimeout()207 public int getSocketTimeout() { 208 if (this.socket != null) { 209 try { 210 return this.socket.getSoTimeout(); 211 } catch (SocketException ignore) { 212 return -1; 213 } 214 } else { 215 return -1; 216 } 217 } 218 shutdown()219 public void shutdown() throws IOException { 220 this.open = false; 221 Socket tmpsocket = this.socket; 222 if (tmpsocket != null) { 223 tmpsocket.close(); 224 } 225 } 226 close()227 public void close() throws IOException { 228 if (!this.open) { 229 return; 230 } 231 this.open = false; 232 doFlush(); 233 try { 234 try { 235 this.socket.shutdownOutput(); 236 } catch (IOException ignore) { 237 } 238 try { 239 this.socket.shutdownInput(); 240 } catch (IOException ignore) { 241 } 242 } catch (UnsupportedOperationException ignore) { 243 // if one isn't supported, the other one isn't either 244 } 245 this.socket.close(); 246 } 247 248 /** 249 * Sends the request line and all headers over the connection. 250 * @param request the request whose headers to send. 251 * @throws HttpException 252 * @throws IOException 253 */ sendRequestHeader(final HttpRequest request)254 public void sendRequestHeader(final HttpRequest request) 255 throws HttpException, IOException { 256 if (request == null) { 257 throw new IllegalArgumentException("HTTP request may not be null"); 258 } 259 assertOpen(); 260 this.requestWriter.write(request); 261 this.metrics.incrementRequestCount(); 262 } 263 264 /** 265 * Sends the request entity over the connection. 266 * @param request the request whose entity to send. 267 * @throws HttpException 268 * @throws IOException 269 */ sendRequestEntity(final HttpEntityEnclosingRequest request)270 public void sendRequestEntity(final HttpEntityEnclosingRequest request) 271 throws HttpException, IOException { 272 if (request == null) { 273 throw new IllegalArgumentException("HTTP request may not be null"); 274 } 275 assertOpen(); 276 if (request.getEntity() == null) { 277 return; 278 } 279 this.entityserializer.serialize( 280 this.outbuffer, 281 request, 282 request.getEntity()); 283 } 284 doFlush()285 protected void doFlush() throws IOException { 286 this.outbuffer.flush(); 287 } 288 flush()289 public void flush() throws IOException { 290 assertOpen(); 291 doFlush(); 292 } 293 294 /** 295 * Parses the response headers and adds them to the 296 * given {@code headers} object, and returns the response StatusLine 297 * @param headers store parsed header to headers. 298 * @throws IOException 299 * @return StatusLine 300 * @see HttpClientConnection#receiveResponseHeader() 301 */ parseResponseHeader(Headers headers)302 public StatusLine parseResponseHeader(Headers headers) 303 throws IOException, ParseException { 304 assertOpen(); 305 306 CharArrayBuffer current = new CharArrayBuffer(64); 307 308 if (inbuffer.readLine(current) == -1) { 309 throw new NoHttpResponseException("The target server failed to respond"); 310 } 311 312 // Create the status line from the status string 313 StatusLine statusline = BasicLineParser.DEFAULT.parseStatusLine( 314 current, new ParserCursor(0, current.length())); 315 316 if (HttpLog.LOGV) HttpLog.v("read: " + statusline); 317 int statusCode = statusline.getStatusCode(); 318 319 // Parse header body 320 CharArrayBuffer previous = null; 321 int headerNumber = 0; 322 while(true) { 323 if (current == null) { 324 current = new CharArrayBuffer(64); 325 } else { 326 // This must be he buffer used to parse the status 327 current.clear(); 328 } 329 int l = inbuffer.readLine(current); 330 if (l == -1 || current.length() < 1) { 331 break; 332 } 333 // Parse the header name and value 334 // Check for folded headers first 335 // Detect LWS-char see HTTP/1.0 or HTTP/1.1 Section 2.2 336 // discussion on folded headers 337 char first = current.charAt(0); 338 if ((first == ' ' || first == '\t') && previous != null) { 339 // we have continuation folded header 340 // so append value 341 int start = 0; 342 int length = current.length(); 343 while (start < length) { 344 char ch = current.charAt(start); 345 if (ch != ' ' && ch != '\t') { 346 break; 347 } 348 start++; 349 } 350 if (maxLineLength > 0 && 351 previous.length() + 1 + current.length() - start > 352 maxLineLength) { 353 throw new IOException("Maximum line length limit exceeded"); 354 } 355 previous.append(' '); 356 previous.append(current, start, current.length() - start); 357 } else { 358 if (previous != null) { 359 headers.parseHeader(previous); 360 } 361 headerNumber++; 362 previous = current; 363 current = null; 364 } 365 if (maxHeaderCount > 0 && headerNumber >= maxHeaderCount) { 366 throw new IOException("Maximum header count exceeded"); 367 } 368 } 369 370 if (previous != null) { 371 headers.parseHeader(previous); 372 } 373 374 if (statusCode >= 200) { 375 this.metrics.incrementResponseCount(); 376 } 377 return statusline; 378 } 379 380 /** 381 * Return the next response entity. 382 * @param headers contains values for parsing entity 383 * @see HttpClientConnection#receiveResponseEntity(HttpResponse response) 384 */ receiveResponseEntity(final Headers headers)385 public HttpEntity receiveResponseEntity(final Headers headers) { 386 assertOpen(); 387 BasicHttpEntity entity = new BasicHttpEntity(); 388 389 long len = determineLength(headers); 390 if (len == ContentLengthStrategy.CHUNKED) { 391 entity.setChunked(true); 392 entity.setContentLength(-1); 393 entity.setContent(new ChunkedInputStream(inbuffer)); 394 } else if (len == ContentLengthStrategy.IDENTITY) { 395 entity.setChunked(false); 396 entity.setContentLength(-1); 397 entity.setContent(new IdentityInputStream(inbuffer)); 398 } else { 399 entity.setChunked(false); 400 entity.setContentLength(len); 401 entity.setContent(new ContentLengthInputStream(inbuffer, len)); 402 } 403 404 String contentTypeHeader = headers.getContentType(); 405 if (contentTypeHeader != null) { 406 entity.setContentType(contentTypeHeader); 407 } 408 String contentEncodingHeader = headers.getContentEncoding(); 409 if (contentEncodingHeader != null) { 410 entity.setContentEncoding(contentEncodingHeader); 411 } 412 413 return entity; 414 } 415 determineLength(final Headers headers)416 private long determineLength(final Headers headers) { 417 long transferEncoding = headers.getTransferEncoding(); 418 // We use Transfer-Encoding if present and ignore Content-Length. 419 // RFC2616, 4.4 item number 3 420 if (transferEncoding < Headers.NO_TRANSFER_ENCODING) { 421 return transferEncoding; 422 } else { 423 long contentlen = headers.getContentLength(); 424 if (contentlen > Headers.NO_CONTENT_LENGTH) { 425 return contentlen; 426 } else { 427 return ContentLengthStrategy.IDENTITY; 428 } 429 } 430 } 431 432 /** 433 * Checks whether this connection has gone down. 434 * Network connections may get closed during some time of inactivity 435 * for several reasons. The next time a read is attempted on such a 436 * connection it will throw an IOException. 437 * This method tries to alleviate this inconvenience by trying to 438 * find out if a connection is still usable. Implementations may do 439 * that by attempting a read with a very small timeout. Thus this 440 * method may block for a small amount of time before returning a result. 441 * It is therefore an <i>expensive</i> operation. 442 * 443 * @return <code>true</code> if attempts to use this connection are 444 * likely to succeed, or <code>false</code> if they are likely 445 * to fail and this connection should be closed 446 */ isStale()447 public boolean isStale() { 448 assertOpen(); 449 try { 450 this.inbuffer.isDataAvailable(1); 451 return false; 452 } catch (IOException ex) { 453 return true; 454 } 455 } 456 457 /** 458 * Returns a collection of connection metrcis 459 * @return HttpConnectionMetrics 460 */ getMetrics()461 public HttpConnectionMetrics getMetrics() { 462 return this.metrics; 463 } 464 } 465