| /* |
| * 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; |
| } |
| } |