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