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.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 
     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       */
     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
    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 
    145     private void assertNotOpen() {
    146         if (this.open) {
    147             throw new IllegalStateException("Connection is already open");
    148         }
    149     }
    150 
    151     private void assertOpen() {
    152         if (!this.open) {
    153             throw new IllegalStateException("Connection is not open");
    154         }
    155     }
    156 
    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 
    162     public InetAddress getLocalAddress() {
    163         if (this.socket != null) {
    164             return this.socket.getLocalAddress();
    165         } else {
    166             return null;
    167         }
    168     }
    169 
    170     public int getLocalPort() {
    171         if (this.socket != null) {
    172             return this.socket.getLocalPort();
    173         } else {
    174             return -1;
    175         }
    176     }
    177 
    178     public InetAddress getRemoteAddress() {
    179         if (this.socket != null) {
    180             return this.socket.getInetAddress();
    181         } else {
    182             return null;
    183         }
    184     }
    185 
    186     public int getRemotePort() {
    187         if (this.socket != null) {
    188             return this.socket.getPort();
    189         } else {
    190             return -1;
    191         }
    192     }
    193 
    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 
    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 
    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 
    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      */
    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      */
    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 
    285     protected void doFlush() throws IOException {
    286         this.outbuffer.flush();
    287     }
    288 
    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       */
    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      */
    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 
    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      */
    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      */
    461     public HttpConnectionMetrics getMetrics() {
    462         return this.metrics;
    463     }
    464 }
    465