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