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