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