Request.java revision 45efe69f8019f9743aef4d2b4eb6acf56ea0551f
1/*
2 * Copyright (C) 2006 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 java.io.EOFException;
20import java.io.InputStream;
21import java.io.IOException;
22import java.util.Iterator;
23import java.util.Map;
24import java.util.Map.Entry;
25import java.util.zip.GZIPInputStream;
26
27import org.apache.http.entity.InputStreamEntity;
28import org.apache.http.Header;
29import org.apache.http.HttpClientConnection;
30import org.apache.http.HttpEntity;
31import org.apache.http.HttpEntityEnclosingRequest;
32import org.apache.http.HttpException;
33import org.apache.http.HttpHost;
34import org.apache.http.HttpRequest;
35import org.apache.http.HttpResponse;
36import org.apache.http.HttpStatus;
37import org.apache.http.ParseException;
38import org.apache.http.ProtocolVersion;
39
40import org.apache.http.StatusLine;
41import org.apache.http.message.BasicHttpRequest;
42import org.apache.http.message.BasicHttpEntityEnclosingRequest;
43import org.apache.http.protocol.RequestContent;
44
45/**
46 * Represents an HTTP request for a given host.
47 *
48 * {@hide}
49 */
50
51class Request {
52
53    /** The eventhandler to call as the request progresses */
54    EventHandler mEventHandler;
55
56    private Connection mConnection;
57
58    /** The Apache http request */
59    BasicHttpRequest mHttpRequest;
60
61    /** The path component of this request */
62    String mPath;
63
64    /** Host serving this request */
65    HttpHost mHost;
66
67    /** Set if I'm using a proxy server */
68    HttpHost mProxyHost;
69
70    /** True if request is .html, .js, .css */
71    boolean mHighPriority;
72
73    /** True if request has been cancelled */
74    volatile boolean mCancelled = false;
75
76    int mFailCount = 0;
77
78    private InputStream mBodyProvider;
79    private int mBodyLength;
80
81    private final static String HOST_HEADER = "Host";
82    private final static String ACCEPT_ENCODING_HEADER = "Accept-Encoding";
83    private final static String CONTENT_LENGTH_HEADER = "content-length";
84
85    /* Used to synchronize waitUntilComplete() requests */
86    private final Object mClientResource = new Object();
87
88    /**
89     * Processor used to set content-length and transfer-encoding
90     * headers.
91     */
92    private static RequestContent requestContentProcessor =
93            new RequestContent();
94
95    /**
96     * Instantiates a new Request.
97     * @param method GET/POST/PUT
98     * @param host The server that will handle this request
99     * @param path path part of URI
100     * @param bodyProvider InputStream providing HTTP body, null if none
101     * @param bodyLength length of body, must be 0 if bodyProvider is null
102     * @param eventHandler request will make progress callbacks on
103     * this interface
104     * @param headers reqeust headers
105     * @param highPriority true for .html, css, .cs
106     */
107    Request(String method, HttpHost host, HttpHost proxyHost, String path,
108            InputStream bodyProvider, int bodyLength,
109            EventHandler eventHandler,
110            Map<String, String> headers, boolean highPriority) {
111        mEventHandler = eventHandler;
112        mHost = host;
113        mProxyHost = proxyHost;
114        mPath = path;
115        mHighPriority = highPriority;
116        mBodyProvider = bodyProvider;
117        mBodyLength = bodyLength;
118
119        if (bodyProvider == null && !"POST".equalsIgnoreCase(method)) {
120            mHttpRequest = new BasicHttpRequest(method, getUri());
121        } else {
122            mHttpRequest = new BasicHttpEntityEnclosingRequest(
123                    method, getUri());
124            // it is ok to have null entity for BasicHttpEntityEnclosingRequest.
125            // By using BasicHttpEntityEnclosingRequest, it will set up the
126            // correct content-length, content-type and content-encoding.
127            if (bodyProvider != null) {
128                setBodyProvider(bodyProvider, bodyLength);
129            }
130        }
131        addHeader(HOST_HEADER, getHostPort());
132
133        /* FIXME: if webcore will make the root document a
134           high-priority request, we can ask for gzip encoding only on
135           high priority reqs (saving the trouble for images, etc) */
136        addHeader(ACCEPT_ENCODING_HEADER, "gzip");
137        addHeaders(headers);
138    }
139
140    /**
141     * @param connection Request served by this connection
142     */
143    void setConnection(Connection connection) {
144        mConnection = connection;
145    }
146
147    /* package */ EventHandler getEventHandler() {
148        return mEventHandler;
149    }
150
151    /**
152     * Add header represented by given pair to request.  Header will
153     * be formatted in request as "name: value\r\n".
154     * @param name of header
155     * @param value of header
156     */
157    void addHeader(String name, String value) {
158        if (name == null) {
159            String damage = "Null http header name";
160            HttpLog.e(damage);
161            throw new NullPointerException(damage);
162        }
163        if (value == null || value.length() == 0) {
164            String damage = "Null or empty value for header \"" + name + "\"";
165            HttpLog.e(damage);
166            throw new RuntimeException(damage);
167        }
168        mHttpRequest.addHeader(name, value);
169    }
170
171    /**
172     * Add all headers in given map to this request.  This is a helper
173     * method: it calls addHeader for each pair in the map.
174     */
175    void addHeaders(Map<String, String> headers) {
176        if (headers == null) {
177            return;
178        }
179
180        Entry<String, String> entry;
181        Iterator<Entry<String, String>> i = headers.entrySet().iterator();
182        while (i.hasNext()) {
183            entry = i.next();
184            addHeader(entry.getKey(), entry.getValue());
185        }
186    }
187
188    /**
189     * Send the request line and headers
190     */
191    void sendRequest(AndroidHttpClientConnection httpClientConnection)
192            throws HttpException, IOException {
193
194        if (mCancelled) return; // don't send cancelled requests
195
196        if (HttpLog.LOGV) {
197            HttpLog.v("Request.sendRequest() " + mHost.getSchemeName() + "://" + getHostPort());
198            // HttpLog.v(mHttpRequest.getRequestLine().toString());
199            if (false) {
200                Iterator i = mHttpRequest.headerIterator();
201                while (i.hasNext()) {
202                    Header header = (Header)i.next();
203                    HttpLog.v(header.getName() + ": " + header.getValue());
204                }
205            }
206        }
207
208        requestContentProcessor.process(mHttpRequest,
209                                        mConnection.getHttpContext());
210        httpClientConnection.sendRequestHeader(mHttpRequest);
211        if (mHttpRequest instanceof HttpEntityEnclosingRequest) {
212            httpClientConnection.sendRequestEntity(
213                    (HttpEntityEnclosingRequest) mHttpRequest);
214        }
215
216        if (HttpLog.LOGV) {
217            HttpLog.v("Request.requestSent() " + mHost.getSchemeName() + "://" + getHostPort() + mPath);
218        }
219    }
220
221
222    /**
223     * Receive a single http response.
224     *
225     * @param httpClientConnection the request to receive the response for.
226     */
227    void readResponse(AndroidHttpClientConnection httpClientConnection)
228            throws IOException, ParseException {
229
230        if (mCancelled) return; // don't send cancelled requests
231
232        StatusLine statusLine = null;
233        boolean hasBody = false;
234        boolean reuse = false;
235        httpClientConnection.flush();
236        int statusCode = 0;
237
238        Headers header = new Headers();
239        do {
240            statusLine = httpClientConnection.parseResponseHeader(header);
241            statusCode = statusLine.getStatusCode();
242        } while (statusCode < HttpStatus.SC_OK);
243        if (HttpLog.LOGV) HttpLog.v(
244                "Request.readResponseStatus() " +
245                statusLine.toString().length() + " " + statusLine);
246
247        ProtocolVersion v = statusLine.getProtocolVersion();
248        mEventHandler.status(v.getMajor(), v.getMinor(),
249                statusCode, statusLine.getReasonPhrase());
250        mEventHandler.headers(header);
251        HttpEntity entity = null;
252        hasBody = canResponseHaveBody(mHttpRequest, statusCode);
253
254        if (hasBody)
255            entity = httpClientConnection.receiveResponseEntity(header);
256
257        if (entity != null) {
258            InputStream is = entity.getContent();
259
260            // process gzip content encoding
261            Header contentEncoding = entity.getContentEncoding();
262            InputStream nis = null;
263            try {
264                if (contentEncoding != null &&
265                    contentEncoding.getValue().equals("gzip")) {
266                    nis = new GZIPInputStream(is);
267                } else {
268                    nis = is;
269                }
270
271                /* accumulate enough data to make it worth pushing it
272                 * up the stack */
273                byte[] buf = mConnection.getBuf();
274                int len = 0;
275                int count = 0;
276                int lowWater = buf.length / 2;
277                while (len != -1) {
278                    len = nis.read(buf, count, buf.length - count);
279                    if (len != -1) {
280                        count += len;
281                    }
282                    if (len == -1 || count >= lowWater) {
283                        if (HttpLog.LOGV) HttpLog.v("Request.readResponse() " + count);
284                        mEventHandler.data(buf, count);
285                        count = 0;
286                    }
287                }
288            } catch (EOFException e) {
289                /* InflaterInputStream throws an EOFException when the
290                   server truncates gzipped content.  Handle this case
291                   as we do truncated non-gzipped content: no error */
292                if (HttpLog.LOGV) HttpLog.v( "readResponse() handling " + e);
293            } catch(IOException e) {
294                // don't throw if we have a non-OK status code
295                if (statusCode == HttpStatus.SC_OK) {
296                    throw e;
297                }
298            } finally {
299                if (nis != null) {
300                    nis.close();
301                }
302            }
303        }
304        mConnection.setCanPersist(entity, statusLine.getProtocolVersion(),
305                header.getConnectionType());
306        mEventHandler.endData();
307        complete();
308
309        if (HttpLog.LOGV) HttpLog.v("Request.readResponse(): done " +
310                                    mHost.getSchemeName() + "://" + getHostPort() + mPath);
311    }
312
313    /**
314     * Data will not be sent to or received from server after cancel()
315     * call.  Does not close connection--use close() below for that.
316     *
317     * Called by RequestHandle from non-network thread
318     */
319    void cancel() {
320        if (HttpLog.LOGV) {
321            HttpLog.v("Request.cancel(): " + getUri());
322        }
323        mCancelled = true;
324        if (mConnection != null) {
325            mConnection.cancel();
326        }
327    }
328
329    String getHostPort() {
330        String myScheme = mHost.getSchemeName();
331        int myPort = mHost.getPort();
332
333        // Only send port when we must... many servers can't deal with it
334        if (myPort != 80 && myScheme.equals("http") ||
335            myPort != 443 && myScheme.equals("https")) {
336            return mHost.toHostString();
337        } else {
338            return mHost.getHostName();
339        }
340    }
341
342    String getUri() {
343        if (mProxyHost == null ||
344            mHost.getSchemeName().equals("https")) {
345            return mPath;
346        }
347        return mHost.getSchemeName() + "://" + getHostPort() + mPath;
348    }
349
350    /**
351     * for debugging
352     */
353    public String toString() {
354        return (mHighPriority ? "P*" : "") + mPath;
355    }
356
357
358    /**
359     * If this request has been sent once and failed, it must be reset
360     * before it can be sent again.
361     */
362    void reset() {
363        /* clear content-length header */
364        mHttpRequest.removeHeaders(CONTENT_LENGTH_HEADER);
365
366        if (mBodyProvider != null) {
367            try {
368                mBodyProvider.reset();
369            } catch (IOException ex) {
370                if (HttpLog.LOGV) HttpLog.v(
371                        "failed to reset body provider " +
372                        getUri());
373            }
374            setBodyProvider(mBodyProvider, mBodyLength);
375        }
376    }
377
378    /**
379     * Pause thread request completes.  Used for synchronous requests,
380     * and testing
381     */
382    void waitUntilComplete() {
383        synchronized (mClientResource) {
384            try {
385                if (HttpLog.LOGV) HttpLog.v("Request.waitUntilComplete()");
386                mClientResource.wait();
387                if (HttpLog.LOGV) HttpLog.v("Request.waitUntilComplete() done waiting");
388            } catch (InterruptedException e) {
389            }
390        }
391    }
392
393    void complete() {
394        synchronized (mClientResource) {
395            mClientResource.notifyAll();
396        }
397    }
398
399    /**
400     * Decide whether a response comes with an entity.
401     * The implementation in this class is based on RFC 2616.
402     * Unknown methods and response codes are supposed to
403     * indicate responses with an entity.
404     * <br/>
405     * Derived executors can override this method to handle
406     * methods and response codes not specified in RFC 2616.
407     *
408     * @param request   the request, to obtain the executed method
409     * @param response  the response, to obtain the status code
410     */
411
412    private static boolean canResponseHaveBody(final HttpRequest request,
413                                               final int status) {
414
415        if ("HEAD".equalsIgnoreCase(request.getRequestLine().getMethod())) {
416            return false;
417        }
418        return status >= HttpStatus.SC_OK
419            && status != HttpStatus.SC_NO_CONTENT
420            && status != HttpStatus.SC_NOT_MODIFIED
421            && status != HttpStatus.SC_RESET_CONTENT;
422    }
423
424    /**
425     * Supply an InputStream that provides the body of a request.  It's
426     * not great that the caller must also provide the length of the data
427     * returned by that InputStream, but the client needs to know up
428     * front, and I'm not sure how to get this out of the InputStream
429     * itself without a costly readthrough.  I'm not sure skip() would
430     * do what we want.  If you know a better way, please let me know.
431     */
432    private void setBodyProvider(InputStream bodyProvider, int bodyLength) {
433        if (!bodyProvider.markSupported()) {
434            throw new IllegalArgumentException(
435                    "bodyProvider must support mark()");
436        }
437        // Mark beginning of stream
438        bodyProvider.mark(Integer.MAX_VALUE);
439
440        ((BasicHttpEntityEnclosingRequest)mHttpRequest).setEntity(
441                new InputStreamEntity(bodyProvider, bodyLength));
442    }
443
444
445    /**
446     * Handles SSL error(s) on the way down from the user (the user
447     * has already provided their feedback).
448     */
449    public void handleSslErrorResponse(boolean proceed) {
450        HttpsConnection connection = (HttpsConnection)(mConnection);
451        if (connection != null) {
452            connection.restartConnection(proceed);
453        }
454    }
455
456    /**
457     * Helper: calls error() on eventhandler with appropriate message
458     * This should not be called before the mConnection is set.
459     */
460    void error(int errorId, int resourceId) {
461        mEventHandler.error(
462                errorId,
463                mConnection.mContext.getText(
464                        resourceId).toString());
465    }
466
467}
468