/*
* 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.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.NoHttpResponseException;
import org.apache.http.StatusLine;
import org.apache.http.entity.BasicHttpEntity;
import org.apache.http.entity.ContentLengthStrategy;
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 expensive operation.
*
* @return true
if attempts to use this connection are
* likely to succeed, or false
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;
}
}