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.Header;
20
21import org.apache.http.HttpConnection;
22import org.apache.http.HttpClientConnection;
23import org.apache.http.HttpConnectionMetrics;
24import org.apache.http.HttpEntity;
25import org.apache.http.HttpEntityEnclosingRequest;
26import org.apache.http.HttpException;
27import org.apache.http.HttpInetConnection;
28import org.apache.http.HttpRequest;
29import org.apache.http.HttpResponse;
30import org.apache.http.HttpResponseFactory;
31import org.apache.http.NoHttpResponseException;
32import org.apache.http.StatusLine;
33import org.apache.http.entity.BasicHttpEntity;
34import org.apache.http.entity.ContentLengthStrategy;
35import org.apache.http.impl.DefaultHttpResponseFactory;
36import org.apache.http.impl.HttpConnectionMetricsImpl;
37import org.apache.http.impl.entity.EntitySerializer;
38import org.apache.http.impl.entity.StrictContentLengthStrategy;
39import org.apache.http.impl.io.ChunkedInputStream;
40import org.apache.http.impl.io.ContentLengthInputStream;
41import org.apache.http.impl.io.HttpRequestWriter;
42import org.apache.http.impl.io.IdentityInputStream;
43import org.apache.http.impl.io.SocketInputBuffer;
44import org.apache.http.impl.io.SocketOutputBuffer;
45import org.apache.http.io.HttpMessageWriter;
46import org.apache.http.io.SessionInputBuffer;
47import org.apache.http.io.SessionOutputBuffer;
48import org.apache.http.message.BasicLineParser;
49import org.apache.http.message.ParserCursor;
50import org.apache.http.params.CoreConnectionPNames;
51import org.apache.http.params.HttpConnectionParams;
52import org.apache.http.params.HttpParams;
53import org.apache.http.ParseException;
54import org.apache.http.util.CharArrayBuffer;
55
56import java.io.IOException;
57import java.net.InetAddress;
58import java.net.Socket;
59import java.net.SocketException;
60
61/**
62 * A alternate class for (@link DefaultHttpClientConnection).
63 * It has better performance than DefaultHttpClientConnection
64 *
65 * {@hide}
66 */
67public 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