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