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