Request.java revision c319c69b1228f5eee2f9d71a71ad021f3d8ba82b
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 has been cancelled */
71    volatile boolean mCancelled = false;
72
73    int mFailCount = 0;
74
75    private InputStream mBodyProvider;
76    private int mBodyLength;
77
78    private final static String HOST_HEADER = "Host";
79    private final static String ACCEPT_ENCODING_HEADER = "Accept-Encoding";
80    private final static String CONTENT_LENGTH_HEADER = "content-length";
81
82    /* Used to synchronize waitUntilComplete() requests */
83    private final Object mClientResource = new Object();
84
85    /**
86     * Processor used to set content-length and transfer-encoding
87     * headers.
88     */
89    private static RequestContent requestContentProcessor =
90            new RequestContent();
91
92    /**
93     * Instantiates a new Request.
94     * @param method GET/POST/PUT
95     * @param host The server that will handle this request
96     * @param path path part of URI
97     * @param bodyProvider InputStream providing HTTP body, null if none
98     * @param bodyLength length of body, must be 0 if bodyProvider is null
99     * @param eventHandler request will make progress callbacks on
100     * this interface
101     * @param headers reqeust headers
102     */
103    Request(String method, HttpHost host, HttpHost proxyHost, String path,
104            InputStream bodyProvider, int bodyLength,
105            EventHandler eventHandler,
106            Map<String, String> headers) {
107        mEventHandler = eventHandler;
108        mHost = host;
109        mProxyHost = proxyHost;
110        mPath = path;
111        mBodyProvider = bodyProvider;
112        mBodyLength = bodyLength;
113
114        if (bodyProvider == null && !"POST".equalsIgnoreCase(method)) {
115            mHttpRequest = new BasicHttpRequest(method, getUri());
116        } else {
117            mHttpRequest = new BasicHttpEntityEnclosingRequest(
118                    method, getUri());
119            // it is ok to have null entity for BasicHttpEntityEnclosingRequest.
120            // By using BasicHttpEntityEnclosingRequest, it will set up the
121            // correct content-length, content-type and content-encoding.
122            if (bodyProvider != null) {
123                setBodyProvider(bodyProvider, bodyLength);
124            }
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            byte[] buf = null;
259            int count = 0;
260            try {
261                if (contentEncoding != null &&
262                    contentEncoding.getValue().equals("gzip")) {
263                    nis = new GZIPInputStream(is);
264                } else {
265                    nis = is;
266                }
267
268                /* accumulate enough data to make it worth pushing it
269                 * up the stack */
270                buf = mConnection.getBuf();
271                int len = 0;
272                int lowWater = buf.length / 2;
273                while (len != -1) {
274                    len = nis.read(buf, count, buf.length - count);
275                    if (len != -1) {
276                        count += len;
277                    }
278                    if (len == -1 || count >= lowWater) {
279                        if (HttpLog.LOGV) HttpLog.v("Request.readResponse() " + count);
280                        mEventHandler.data(buf, count);
281                        count = 0;
282                    }
283                }
284            } catch (EOFException e) {
285                /* InflaterInputStream throws an EOFException when the
286                   server truncates gzipped content.  Handle this case
287                   as we do truncated non-gzipped content: no error */
288                if (count > 0) {
289                    // if there is uncommited content, we should commit them
290                    mEventHandler.data(buf, count);
291                }
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 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    }
422
423    /**
424     * Supply an InputStream that provides the body of a request.  It's
425     * not great that the caller must also provide the length of the data
426     * returned by that InputStream, but the client needs to know up
427     * front, and I'm not sure how to get this out of the InputStream
428     * itself without a costly readthrough.  I'm not sure skip() would
429     * do what we want.  If you know a better way, please let me know.
430     */
431    private void setBodyProvider(InputStream bodyProvider, int bodyLength) {
432        if (!bodyProvider.markSupported()) {
433            throw new IllegalArgumentException(
434                    "bodyProvider must support mark()");
435        }
436        // Mark beginning of stream
437        bodyProvider.mark(Integer.MAX_VALUE);
438
439        ((BasicHttpEntityEnclosingRequest)mHttpRequest).setEntity(
440                new InputStreamEntity(bodyProvider, bodyLength));
441    }
442
443
444    /**
445     * Handles SSL error(s) on the way down from the user (the user
446     * has already provided their feedback).
447     */
448    public void handleSslErrorResponse(boolean proceed) {
449        HttpsConnection connection = (HttpsConnection)(mConnection);
450        if (connection != null) {
451            connection.restartConnection(proceed);
452        }
453    }
454
455    /**
456     * Helper: calls error() on eventhandler with appropriate message
457     * This should not be called before the mConnection is set.
458     */
459    void error(int errorId, int resourceId) {
460        mEventHandler.error(
461                errorId,
462                mConnection.mContext.getText(
463                        resourceId).toString());
464    }
465
466}
467