/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.net.http; import org.apache.http.Header; import org.apache.http.HttpConnection; import org.apache.http.HttpClientConnection; import org.apache.http.HttpConnectionMetrics; import org.apache.http.HttpEntity; import org.apache.http.HttpEntityEnclosingRequest; import org.apache.http.HttpException; import org.apache.http.HttpInetConnection; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; import org.apache.http.HttpResponseFactory; import org.apache.http.NoHttpResponseException; import org.apache.http.StatusLine; import org.apache.http.entity.BasicHttpEntity; import org.apache.http.entity.ContentLengthStrategy; import org.apache.http.impl.DefaultHttpResponseFactory; import org.apache.http.impl.HttpConnectionMetricsImpl; import org.apache.http.impl.entity.EntitySerializer; import org.apache.http.impl.entity.StrictContentLengthStrategy; import org.apache.http.impl.io.ChunkedInputStream; import org.apache.http.impl.io.ContentLengthInputStream; import org.apache.http.impl.io.HttpRequestWriter; import org.apache.http.impl.io.IdentityInputStream; import org.apache.http.impl.io.SocketInputBuffer; import org.apache.http.impl.io.SocketOutputBuffer; import org.apache.http.io.HttpMessageWriter; import org.apache.http.io.SessionInputBuffer; import org.apache.http.io.SessionOutputBuffer; import org.apache.http.message.BasicLineParser; import org.apache.http.message.ParserCursor; import org.apache.http.params.CoreConnectionPNames; import org.apache.http.params.HttpConnectionParams; import org.apache.http.params.HttpParams; import org.apache.http.ParseException; import org.apache.http.util.CharArrayBuffer; import java.io.IOException; import java.net.InetAddress; import java.net.Socket; import java.net.SocketException; /** * A alternate class for (@link DefaultHttpClientConnection). * It has better performance than DefaultHttpClientConnection * * {@hide} */ public class AndroidHttpClientConnection implements HttpInetConnection, HttpConnection { private SessionInputBuffer inbuffer = null; private SessionOutputBuffer outbuffer = null; private int maxHeaderCount; // store CoreConnectionPNames.MAX_LINE_LENGTH for performance private int maxLineLength; private final EntitySerializer entityserializer; private HttpMessageWriter requestWriter = null; private HttpConnectionMetricsImpl metrics = null; private volatile boolean open; private Socket socket = null; public AndroidHttpClientConnection() { this.entityserializer = new EntitySerializer( new StrictContentLengthStrategy()); } /** * Bind socket and set HttpParams to AndroidHttpClientConnection * @param socket outgoing socket * @param params HttpParams * @throws IOException */ public void bind( final Socket socket, final HttpParams params) throws IOException { if (socket == null) { throw new IllegalArgumentException("Socket may not be null"); } if (params == null) { throw new IllegalArgumentException("HTTP parameters may not be null"); } assertNotOpen(); socket.setTcpNoDelay(HttpConnectionParams.getTcpNoDelay(params)); socket.setSoTimeout(HttpConnectionParams.getSoTimeout(params)); int linger = HttpConnectionParams.getLinger(params); if (linger >= 0) { socket.setSoLinger(linger > 0, linger); } this.socket = socket; int buffersize = HttpConnectionParams.getSocketBufferSize(params); this.inbuffer = new SocketInputBuffer(socket, buffersize, params); this.outbuffer = new SocketOutputBuffer(socket, buffersize, params); maxHeaderCount = params.getIntParameter( CoreConnectionPNames.MAX_HEADER_COUNT, -1); maxLineLength = params.getIntParameter( CoreConnectionPNames.MAX_LINE_LENGTH, -1); this.requestWriter = new HttpRequestWriter(outbuffer, null, params); this.metrics = new HttpConnectionMetricsImpl( inbuffer.getMetrics(), outbuffer.getMetrics()); this.open = true; } @Override public String toString() { StringBuilder buffer = new StringBuilder(); buffer.append(getClass().getSimpleName()).append("["); if (isOpen()) { buffer.append(getRemotePort()); } else { buffer.append("closed"); } buffer.append("]"); return buffer.toString(); } private void assertNotOpen() { if (this.open) { throw new IllegalStateException("Connection is already open"); } } private void assertOpen() { if (!this.open) { throw new IllegalStateException("Connection is not open"); } } public boolean isOpen() { // to make this method useful, we want to check if the socket is connected return (this.open && this.socket != null && this.socket.isConnected()); } public InetAddress getLocalAddress() { if (this.socket != null) { return this.socket.getLocalAddress(); } else { return null; } } public int getLocalPort() { if (this.socket != null) { return this.socket.getLocalPort(); } else { return -1; } } public InetAddress getRemoteAddress() { if (this.socket != null) { return this.socket.getInetAddress(); } else { return null; } } public int getRemotePort() { if (this.socket != null) { return this.socket.getPort(); } else { return -1; } } public void setSocketTimeout(int timeout) { assertOpen(); if (this.socket != null) { try { this.socket.setSoTimeout(timeout); } catch (SocketException ignore) { // It is not quite clear from the original documentation if there are any // other legitimate cases for a socket exception to be thrown when setting // SO_TIMEOUT besides the socket being already closed } } } public int getSocketTimeout() { if (this.socket != null) { try { return this.socket.getSoTimeout(); } catch (SocketException ignore) { return -1; } } else { return -1; } } public void shutdown() throws IOException { this.open = false; Socket tmpsocket = this.socket; if (tmpsocket != null) { tmpsocket.close(); } } public void close() throws IOException { if (!this.open) { return; } this.open = false; doFlush(); try { try { this.socket.shutdownOutput(); } catch (IOException ignore) { } try { this.socket.shutdownInput(); } catch (IOException ignore) { } } catch (UnsupportedOperationException ignore) { // if one isn't supported, the other one isn't either } this.socket.close(); } /** * Sends the request line and all headers over the connection. * @param request the request whose headers to send. * @throws HttpException * @throws IOException */ public void sendRequestHeader(final HttpRequest request) throws HttpException, IOException { if (request == null) { throw new IllegalArgumentException("HTTP request may not be null"); } assertOpen(); this.requestWriter.write(request); this.metrics.incrementRequestCount(); } /** * Sends the request entity over the connection. * @param request the request whose entity to send. * @throws HttpException * @throws IOException */ public void sendRequestEntity(final HttpEntityEnclosingRequest request) throws HttpException, IOException { if (request == null) { throw new IllegalArgumentException("HTTP request may not be null"); } assertOpen(); if (request.getEntity() == null) { return; } this.entityserializer.serialize( this.outbuffer, request, request.getEntity()); } protected void doFlush() throws IOException { this.outbuffer.flush(); } public void flush() throws IOException { assertOpen(); doFlush(); } /** * Parses the response headers and adds them to the * given {@code headers} object, and returns the response StatusLine * @param headers store parsed header to headers. * @throws IOException * @return StatusLine * @see HttpClientConnection#receiveResponseHeader() */ public StatusLine parseResponseHeader(Headers headers) throws IOException, ParseException { assertOpen(); CharArrayBuffer current = new CharArrayBuffer(64); if (inbuffer.readLine(current) == -1) { throw new NoHttpResponseException("The target server failed to respond"); } // Create the status line from the status string StatusLine statusline = BasicLineParser.DEFAULT.parseStatusLine( current, new ParserCursor(0, current.length())); if (HttpLog.LOGV) HttpLog.v("read: " + statusline); int statusCode = statusline.getStatusCode(); // Parse header body CharArrayBuffer previous = null; int headerNumber = 0; while(true) { if (current == null) { current = new CharArrayBuffer(64); } else { // This must be he buffer used to parse the status current.clear(); } int l = inbuffer.readLine(current); if (l == -1 || current.length() < 1) { break; } // Parse the header name and value // Check for folded headers first // Detect LWS-char see HTTP/1.0 or HTTP/1.1 Section 2.2 // discussion on folded headers char first = current.charAt(0); if ((first == ' ' || first == '\t') && previous != null) { // we have continuation folded header // so append value int start = 0; int length = current.length(); while (start < length) { char ch = current.charAt(start); if (ch != ' ' && ch != '\t') { break; } start++; } if (maxLineLength > 0 && previous.length() + 1 + current.length() - start > maxLineLength) { throw new IOException("Maximum line length limit exceeded"); } previous.append(' '); previous.append(current, start, current.length() - start); } else { if (previous != null) { headers.parseHeader(previous); } headerNumber++; previous = current; current = null; } if (maxHeaderCount > 0 && headerNumber >= maxHeaderCount) { throw new IOException("Maximum header count exceeded"); } } if (previous != null) { headers.parseHeader(previous); } if (statusCode >= 200) { this.metrics.incrementResponseCount(); } return statusline; } /** * Return the next response entity. * @param headers contains values for parsing entity * @see HttpClientConnection#receiveResponseEntity(HttpResponse response) */ public HttpEntity receiveResponseEntity(final Headers headers) { assertOpen(); BasicHttpEntity entity = new BasicHttpEntity(); long len = determineLength(headers); if (len == ContentLengthStrategy.CHUNKED) { entity.setChunked(true); entity.setContentLength(-1); entity.setContent(new ChunkedInputStream(inbuffer)); } else if (len == ContentLengthStrategy.IDENTITY) { entity.setChunked(false); entity.setContentLength(-1); entity.setContent(new IdentityInputStream(inbuffer)); } else { entity.setChunked(false); entity.setContentLength(len); entity.setContent(new ContentLengthInputStream(inbuffer, len)); } String contentTypeHeader = headers.getContentType(); if (contentTypeHeader != null) { entity.setContentType(contentTypeHeader); } String contentEncodingHeader = headers.getContentEncoding(); if (contentEncodingHeader != null) { entity.setContentEncoding(contentEncodingHeader); } return entity; } private long determineLength(final Headers headers) { long transferEncoding = headers.getTransferEncoding(); // We use Transfer-Encoding if present and ignore Content-Length. // RFC2616, 4.4 item number 3 if (transferEncoding < Headers.NO_TRANSFER_ENCODING) { return transferEncoding; } else { long contentlen = headers.getContentLength(); if (contentlen > Headers.NO_CONTENT_LENGTH) { return contentlen; } else { return ContentLengthStrategy.IDENTITY; } } } /** * Checks whether this connection has gone down. * Network connections may get closed during some time of inactivity * for several reasons. The next time a read is attempted on such a * connection it will throw an IOException. * This method tries to alleviate this inconvenience by trying to * find out if a connection is still usable. Implementations may do * that by attempting a read with a very small timeout. Thus this * method may block for a small amount of time before returning a result. * It is therefore an <i>expensive</i> operation. * * @return <code>true</code> if attempts to use this connection are * likely to succeed, or <code>false</code> if they are likely * to fail and this connection should be closed */ public boolean isStale() { assertOpen(); try { this.inbuffer.isDataAvailable(1); return false; } catch (IOException ex) { return true; } } /** * Returns a collection of connection metrcis * @return HttpConnectionMetrics */ public HttpConnectionMetrics getMetrics() { return this.metrics; } }