1/*
2 * Copyright (C) 2011 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 com.android.volley.toolbox;
18
19import android.os.SystemClock;
20
21import com.android.volley.AuthFailureError;
22import com.android.volley.Cache;
23import com.android.volley.Cache.Entry;
24import com.android.volley.Network;
25import com.android.volley.NetworkError;
26import com.android.volley.NetworkResponse;
27import com.android.volley.NoConnectionError;
28import com.android.volley.Request;
29import com.android.volley.RetryPolicy;
30import com.android.volley.ServerError;
31import com.android.volley.TimeoutError;
32import com.android.volley.VolleyError;
33import com.android.volley.VolleyLog;
34
35import org.apache.http.Header;
36import org.apache.http.HttpEntity;
37import org.apache.http.HttpResponse;
38import org.apache.http.HttpStatus;
39import org.apache.http.StatusLine;
40import org.apache.http.conn.ConnectTimeoutException;
41import org.apache.http.impl.cookie.DateUtils;
42
43import java.io.IOException;
44import java.io.InputStream;
45import java.net.MalformedURLException;
46import java.net.SocketTimeoutException;
47import java.util.Collections;
48import java.util.Date;
49import java.util.HashMap;
50import java.util.Map;
51import java.util.TreeMap;
52
53/**
54 * A network performing Volley requests over an {@link HttpStack}.
55 */
56public class BasicNetwork implements Network {
57    protected static final boolean DEBUG = VolleyLog.DEBUG;
58
59    private static int SLOW_REQUEST_THRESHOLD_MS = 3000;
60
61    private static int DEFAULT_POOL_SIZE = 4096;
62
63    protected final HttpStack mHttpStack;
64
65    protected final ByteArrayPool mPool;
66
67    /**
68     * @param httpStack HTTP stack to be used
69     */
70    public BasicNetwork(HttpStack httpStack) {
71        // If a pool isn't passed in, then build a small default pool that will give us a lot of
72        // benefit and not use too much memory.
73        this(httpStack, new ByteArrayPool(DEFAULT_POOL_SIZE));
74    }
75
76    /**
77     * @param httpStack HTTP stack to be used
78     * @param pool a buffer pool that improves GC performance in copy operations
79     */
80    public BasicNetwork(HttpStack httpStack, ByteArrayPool pool) {
81        mHttpStack = httpStack;
82        mPool = pool;
83    }
84
85    @Override
86    public NetworkResponse performRequest(Request<?> request) throws VolleyError {
87        long requestStart = SystemClock.elapsedRealtime();
88        while (true) {
89            HttpResponse httpResponse = null;
90            byte[] responseContents = null;
91            Map<String, String> responseHeaders = Collections.emptyMap();
92            try {
93                // Gather headers.
94                Map<String, String> headers = new HashMap<String, String>();
95                addCacheHeaders(headers, request.getCacheEntry());
96                httpResponse = mHttpStack.performRequest(request, headers);
97                StatusLine statusLine = httpResponse.getStatusLine();
98                int statusCode = statusLine.getStatusCode();
99
100                responseHeaders = convertHeaders(httpResponse.getAllHeaders());
101                // Handle cache validation.
102                if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
103
104                    Entry entry = request.getCacheEntry();
105                    if (entry == null) {
106                        return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, null,
107                                responseHeaders, true,
108                                SystemClock.elapsedRealtime() - requestStart);
109                    }
110
111                    // A HTTP 304 response does not have all header fields. We
112                    // have to use the header fields from the cache entry plus
113                    // the new ones from the response.
114                    // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
115                    entry.responseHeaders.putAll(responseHeaders);
116                    return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, entry.data,
117                            entry.responseHeaders, true,
118                            SystemClock.elapsedRealtime() - requestStart);
119                }
120
121                // Some responses such as 204s do not have content.  We must check.
122                if (httpResponse.getEntity() != null) {
123                  responseContents = entityToBytes(httpResponse.getEntity());
124                } else {
125                  // Add 0 byte response as a way of honestly representing a
126                  // no-content request.
127                  responseContents = new byte[0];
128                }
129
130                // if the request is slow, log it.
131                long requestLifetime = SystemClock.elapsedRealtime() - requestStart;
132                logSlowRequests(requestLifetime, request, responseContents, statusLine);
133
134                if (statusCode < 200 || statusCode > 299) {
135                    throw new IOException();
136                }
137                return new NetworkResponse(statusCode, responseContents, responseHeaders, false,
138                        SystemClock.elapsedRealtime() - requestStart);
139            } catch (SocketTimeoutException e) {
140                attemptRetryOnException("socket", request, new TimeoutError());
141            } catch (ConnectTimeoutException e) {
142                attemptRetryOnException("connection", request, new TimeoutError());
143            } catch (MalformedURLException e) {
144                throw new RuntimeException("Bad URL " + request.getUrl(), e);
145            } catch (IOException e) {
146                int statusCode = 0;
147                NetworkResponse networkResponse = null;
148                if (httpResponse != null) {
149                    statusCode = httpResponse.getStatusLine().getStatusCode();
150                } else {
151                    throw new NoConnectionError(e);
152                }
153                VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl());
154                if (responseContents != null) {
155                    networkResponse = new NetworkResponse(statusCode, responseContents,
156                            responseHeaders, false, SystemClock.elapsedRealtime() - requestStart);
157                    if (statusCode == HttpStatus.SC_UNAUTHORIZED ||
158                            statusCode == HttpStatus.SC_FORBIDDEN) {
159                        attemptRetryOnException("auth",
160                                request, new AuthFailureError(networkResponse));
161                    } else {
162                        // TODO: Only throw ServerError for 5xx status codes.
163                        throw new ServerError(networkResponse);
164                    }
165                } else {
166                    throw new NetworkError(networkResponse);
167                }
168            }
169        }
170    }
171
172    /**
173     * Logs requests that took over SLOW_REQUEST_THRESHOLD_MS to complete.
174     */
175    private void logSlowRequests(long requestLifetime, Request<?> request,
176            byte[] responseContents, StatusLine statusLine) {
177        if (DEBUG || requestLifetime > SLOW_REQUEST_THRESHOLD_MS) {
178            VolleyLog.d("HTTP response for request=<%s> [lifetime=%d], [size=%s], " +
179                    "[rc=%d], [retryCount=%s]", request, requestLifetime,
180                    responseContents != null ? responseContents.length : "null",
181                    statusLine.getStatusCode(), request.getRetryPolicy().getCurrentRetryCount());
182        }
183    }
184
185    /**
186     * Attempts to prepare the request for a retry. If there are no more attempts remaining in the
187     * request's retry policy, a timeout exception is thrown.
188     * @param request The request to use.
189     */
190    private static void attemptRetryOnException(String logPrefix, Request<?> request,
191            VolleyError exception) throws VolleyError {
192        RetryPolicy retryPolicy = request.getRetryPolicy();
193        int oldTimeout = request.getTimeoutMs();
194
195        try {
196            retryPolicy.retry(exception);
197        } catch (VolleyError e) {
198            request.addMarker(
199                    String.format("%s-timeout-giveup [timeout=%s]", logPrefix, oldTimeout));
200            throw e;
201        }
202        request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix, oldTimeout));
203    }
204
205    private void addCacheHeaders(Map<String, String> headers, Cache.Entry entry) {
206        // If there's no cache entry, we're done.
207        if (entry == null) {
208            return;
209        }
210
211        if (entry.etag != null) {
212            headers.put("If-None-Match", entry.etag);
213        }
214
215        if (entry.lastModified > 0) {
216            Date refTime = new Date(entry.lastModified);
217            headers.put("If-Modified-Since", DateUtils.formatDate(refTime));
218        }
219    }
220
221    protected void logError(String what, String url, long start) {
222        long now = SystemClock.elapsedRealtime();
223        VolleyLog.v("HTTP ERROR(%s) %d ms to fetch %s", what, (now - start), url);
224    }
225
226    /** Reads the contents of HttpEntity into a byte[]. */
227    private byte[] entityToBytes(HttpEntity entity) throws IOException, ServerError {
228        PoolingByteArrayOutputStream bytes =
229                new PoolingByteArrayOutputStream(mPool, (int) entity.getContentLength());
230        byte[] buffer = null;
231        try {
232            InputStream in = entity.getContent();
233            if (in == null) {
234                throw new ServerError();
235            }
236            buffer = mPool.getBuf(1024);
237            int count;
238            while ((count = in.read(buffer)) != -1) {
239                bytes.write(buffer, 0, count);
240            }
241            return bytes.toByteArray();
242        } finally {
243            try {
244                // Close the InputStream and release the resources by "consuming the content".
245                entity.consumeContent();
246            } catch (IOException e) {
247                // This can happen if there was an exception above that left the entity in
248                // an invalid state.
249                VolleyLog.v("Error occured when calling consumingContent");
250            }
251            mPool.returnBuf(buffer);
252            bytes.close();
253        }
254    }
255
256    /**
257     * Converts Headers[] to Map<String, String>.
258     */
259    protected static Map<String, String> convertHeaders(Header[] headers) {
260        Map<String, String> result = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
261        for (int i = 0; i < headers.length; i++) {
262            result.put(headers[i].getName(), headers[i].getValue());
263        }
264        return result;
265    }
266}
267