aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/com/android/volley/toolbox
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/android/volley/toolbox')
-rw-r--r--src/main/java/com/android/volley/toolbox/AdaptedHttpStack.java81
-rw-r--r--src/main/java/com/android/volley/toolbox/BaseHttpStack.java93
-rw-r--r--src/main/java/com/android/volley/toolbox/BasicNetwork.java194
-rw-r--r--src/main/java/com/android/volley/toolbox/DiskBasedCache.java60
-rw-r--r--src/main/java/com/android/volley/toolbox/HttpClientStack.java4
-rw-r--r--src/main/java/com/android/volley/toolbox/HttpHeaderParser.java66
-rw-r--r--src/main/java/com/android/volley/toolbox/HttpResponse.java81
-rw-r--r--src/main/java/com/android/volley/toolbox/HttpStack.java5
-rw-r--r--src/main/java/com/android/volley/toolbox/HurlStack.java72
-rw-r--r--src/main/java/com/android/volley/toolbox/Volley.java58
10 files changed, 570 insertions, 144 deletions
diff --git a/src/main/java/com/android/volley/toolbox/AdaptedHttpStack.java b/src/main/java/com/android/volley/toolbox/AdaptedHttpStack.java
new file mode 100644
index 0000000..e5dc62b
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/AdaptedHttpStack.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.volley.toolbox;
+
+import com.android.volley.AuthFailureError;
+import com.android.volley.Header;
+import com.android.volley.Request;
+
+import org.apache.http.conn.ConnectTimeoutException;
+
+import java.io.IOException;
+import java.net.SocketTimeoutException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * {@link BaseHttpStack} implementation wrapping a {@link HttpStack}.
+ *
+ * <p>{@link BasicNetwork} uses this if it is provided a {@link HttpStack} at construction time,
+ * allowing it to have one implementation based atop {@link BaseHttpStack}.
+ */
+@SuppressWarnings("deprecation")
+class AdaptedHttpStack extends BaseHttpStack {
+
+ private final HttpStack mHttpStack;
+
+ AdaptedHttpStack(HttpStack httpStack) {
+ mHttpStack = httpStack;
+ }
+
+ @Override
+ public HttpResponse executeRequest(
+ Request<?> request, Map<String, String> additionalHeaders)
+ throws IOException, AuthFailureError {
+ org.apache.http.HttpResponse apacheResp;
+ try {
+ apacheResp = mHttpStack.performRequest(request, additionalHeaders);
+ } catch (ConnectTimeoutException e) {
+ // BasicNetwork won't know that this exception should be retried like a timeout, since
+ // it's an Apache-specific error, so wrap it in a standard timeout exception.
+ throw new SocketTimeoutException(e.getMessage());
+ }
+
+ int statusCode = apacheResp.getStatusLine().getStatusCode();
+
+ org.apache.http.Header[] headers = apacheResp.getAllHeaders();
+ List<Header> headerList = new ArrayList<>(headers.length);
+ for (org.apache.http.Header header : headers) {
+ headerList.add(new Header(header.getName(), header.getValue()));
+ }
+
+ if (apacheResp.getEntity() == null) {
+ return new HttpResponse(statusCode, headerList);
+ }
+
+ long contentLength = apacheResp.getEntity().getContentLength();
+ if ((int) contentLength != contentLength) {
+ throw new IOException("Response too large: " + contentLength);
+ }
+
+ return new HttpResponse(
+ statusCode,
+ headerList,
+ (int) apacheResp.getEntity().getContentLength(),
+ apacheResp.getEntity().getContent());
+ }
+}
diff --git a/src/main/java/com/android/volley/toolbox/BaseHttpStack.java b/src/main/java/com/android/volley/toolbox/BaseHttpStack.java
new file mode 100644
index 0000000..257f75c
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/BaseHttpStack.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.volley.toolbox;
+
+import com.android.volley.AuthFailureError;
+import com.android.volley.Header;
+import com.android.volley.Request;
+
+import org.apache.http.ProtocolVersion;
+import org.apache.http.StatusLine;
+import org.apache.http.entity.BasicHttpEntity;
+import org.apache.http.message.BasicHeader;
+import org.apache.http.message.BasicHttpResponse;
+import org.apache.http.message.BasicStatusLine;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.SocketTimeoutException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/** An HTTP stack abstraction. */
+@SuppressWarnings("deprecation") // for HttpStack
+public abstract class BaseHttpStack implements HttpStack {
+
+ /**
+ * Performs an HTTP request with the given parameters.
+ *
+ * <p>A GET request is sent if request.getPostBody() == null. A POST request is sent otherwise,
+ * and the Content-Type header is set to request.getPostBodyContentType().
+ *
+ * @param request the request to perform
+ * @param additionalHeaders additional headers to be sent together with
+ * {@link Request#getHeaders()}
+ * @return the {@link HttpResponse}
+ * @throws SocketTimeoutException if the request times out
+ * @throws IOException if another I/O error occurs during the request
+ * @throws AuthFailureError if an authentication failure occurs during the request
+ */
+ public abstract HttpResponse executeRequest(
+ Request<?> request, Map<String, String> additionalHeaders)
+ throws IOException, AuthFailureError;
+
+ /**
+ * @deprecated use {@link #executeRequest} instead to avoid a dependency on the deprecated
+ * Apache HTTP library. Nothing in Volley's own source calls this method. However, since
+ * {@link BasicNetwork#mHttpStack} is exposed to subclasses, we provide this implementation in
+ * case legacy client apps are dependent on that field. This method may be removed in a future
+ * release of Volley.
+ */
+ @Deprecated
+ @Override
+ public final org.apache.http.HttpResponse performRequest(
+ Request<?> request, Map<String, String> additionalHeaders)
+ throws IOException, AuthFailureError {
+ HttpResponse response = executeRequest(request, additionalHeaders);
+
+ ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1);
+ StatusLine statusLine = new BasicStatusLine(
+ protocolVersion, response.getStatusCode(), "" /* reasonPhrase */);
+ BasicHttpResponse apacheResponse = new BasicHttpResponse(statusLine);
+
+ List<org.apache.http.Header> headers = new ArrayList<>();
+ for (Header header : response.getHeaders()) {
+ headers.add(new BasicHeader(header.getName(), header.getValue()));
+ }
+ apacheResponse.setHeaders(headers.toArray(new org.apache.http.Header[headers.size()]));
+
+ InputStream responseStream = response.getContent();
+ if (responseStream != null) {
+ BasicHttpEntity entity = new BasicHttpEntity();
+ entity.setContent(responseStream);
+ entity.setContentLength(response.getContentLength());
+ apacheResponse.setEntity(entity);
+ }
+
+ return apacheResponse;
+ }
+}
diff --git a/src/main/java/com/android/volley/toolbox/BasicNetwork.java b/src/main/java/com/android/volley/toolbox/BasicNetwork.java
index 96fb66e..5330733 100644
--- a/src/main/java/com/android/volley/toolbox/BasicNetwork.java
+++ b/src/main/java/com/android/volley/toolbox/BasicNetwork.java
@@ -22,6 +22,7 @@ import com.android.volley.AuthFailureError;
import com.android.volley.Cache;
import com.android.volley.Cache.Entry;
import com.android.volley.ClientError;
+import com.android.volley.Header;
import com.android.volley.Network;
import com.android.volley.NetworkError;
import com.android.volley.NetworkResponse;
@@ -33,23 +34,19 @@ import com.android.volley.TimeoutError;
import com.android.volley.VolleyError;
import com.android.volley.VolleyLog;
-import org.apache.http.Header;
-import org.apache.http.HttpEntity;
-import org.apache.http.HttpResponse;
-import org.apache.http.HttpStatus;
-import org.apache.http.StatusLine;
-import org.apache.http.conn.ConnectTimeoutException;
-import org.apache.http.impl.cookie.DateUtils;
-
import java.io.IOException;
import java.io.InputStream;
+import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
+import java.util.ArrayList;
import java.util.Collections;
-import java.util.Date;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.TreeMap;
+import java.util.TreeSet;
/**
* A network performing Volley requests over an {@link HttpStack}.
@@ -61,13 +58,23 @@ public class BasicNetwork implements Network {
private static final int DEFAULT_POOL_SIZE = 4096;
+ /**
+ * @deprecated Should never have been exposed in the API. This field may be removed in a future
+ * release of Volley.
+ */
+ @Deprecated
protected final HttpStack mHttpStack;
+ private final BaseHttpStack mBaseHttpStack;
+
protected final ByteArrayPool mPool;
/**
* @param httpStack HTTP stack to be used
+ * @deprecated use {@link #BasicNetwork(BaseHttpStack)} instead to avoid depending on Apache
+ * HTTP. This method may be removed in a future release of Volley.
*/
+ @Deprecated
public BasicNetwork(HttpStack httpStack) {
// If a pool isn't passed in, then build a small default pool that will give us a lot of
// benefit and not use too much memory.
@@ -77,9 +84,36 @@ public class BasicNetwork implements Network {
/**
* @param httpStack HTTP stack to be used
* @param pool a buffer pool that improves GC performance in copy operations
+ * @deprecated use {@link #BasicNetwork(BaseHttpStack, ByteArrayPool)} instead to avoid
+ * depending on Apache HTTP. This method may be removed in a future release of
+ * Volley.
*/
+ @Deprecated
public BasicNetwork(HttpStack httpStack, ByteArrayPool pool) {
mHttpStack = httpStack;
+ mBaseHttpStack = new AdaptedHttpStack(httpStack);
+ mPool = pool;
+ }
+
+ /**
+ * @param httpStack HTTP stack to be used
+ */
+ public BasicNetwork(BaseHttpStack httpStack) {
+ // If a pool isn't passed in, then build a small default pool that will give us a lot of
+ // benefit and not use too much memory.
+ this(httpStack, new ByteArrayPool(DEFAULT_POOL_SIZE));
+ }
+
+ /**
+ * @param httpStack HTTP stack to be used
+ * @param pool a buffer pool that improves GC performance in copy operations
+ */
+ public BasicNetwork(BaseHttpStack httpStack, ByteArrayPool pool) {
+ mBaseHttpStack = httpStack;
+ // Populate mHttpStack for backwards compatibility, since it is a protected field. However,
+ // we won't use it directly here, so clients which don't access it directly won't need to
+ // depend on Apache HTTP.
+ mHttpStack = httpStack;
mPool = pool;
}
@@ -89,39 +123,33 @@ public class BasicNetwork implements Network {
while (true) {
HttpResponse httpResponse = null;
byte[] responseContents = null;
- Map<String, String> responseHeaders = Collections.emptyMap();
+ List<Header> responseHeaders = Collections.emptyList();
try {
// Gather headers.
- Map<String, String> headers = new HashMap<String, String>();
- addCacheHeaders(headers, request.getCacheEntry());
- httpResponse = mHttpStack.performRequest(request, headers);
- StatusLine statusLine = httpResponse.getStatusLine();
- int statusCode = statusLine.getStatusCode();
+ Map<String, String> additionalRequestHeaders =
+ getCacheHeaders(request.getCacheEntry());
+ httpResponse = mBaseHttpStack.executeRequest(request, additionalRequestHeaders);
+ int statusCode = httpResponse.getStatusCode();
- responseHeaders = convertHeaders(httpResponse.getAllHeaders());
+ responseHeaders = httpResponse.getHeaders();
// Handle cache validation.
- if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
-
+ if (statusCode == HttpURLConnection.HTTP_NOT_MODIFIED) {
Entry entry = request.getCacheEntry();
if (entry == null) {
- return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, null,
- responseHeaders, true,
- SystemClock.elapsedRealtime() - requestStart);
+ return new NetworkResponse(HttpURLConnection.HTTP_NOT_MODIFIED, null, true,
+ SystemClock.elapsedRealtime() - requestStart, responseHeaders);
}
-
- // A HTTP 304 response does not have all header fields. We
- // have to use the header fields from the cache entry plus
- // the new ones from the response.
- // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
- entry.responseHeaders.putAll(responseHeaders);
- return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, entry.data,
- entry.responseHeaders, true,
- SystemClock.elapsedRealtime() - requestStart);
+ // Combine cached and response headers so the response will be complete.
+ List<Header> combinedHeaders = combineHeaders(responseHeaders, entry);
+ return new NetworkResponse(HttpURLConnection.HTTP_NOT_MODIFIED, entry.data,
+ true, SystemClock.elapsedRealtime() - requestStart, combinedHeaders);
}
// Some responses such as 204s do not have content. We must check.
- if (httpResponse.getEntity() != null) {
- responseContents = entityToBytes(httpResponse.getEntity());
+ InputStream inputStream = httpResponse.getContent();
+ if (inputStream != null) {
+ responseContents =
+ inputStreamToBytes(inputStream, httpResponse.getContentLength());
} else {
// Add 0 byte response as a way of honestly representing a
// no-content request.
@@ -130,33 +158,31 @@ public class BasicNetwork implements Network {
// if the request is slow, log it.
long requestLifetime = SystemClock.elapsedRealtime() - requestStart;
- logSlowRequests(requestLifetime, request, responseContents, statusLine);
+ logSlowRequests(requestLifetime, request, responseContents, statusCode);
if (statusCode < 200 || statusCode > 299) {
throw new IOException();
}
- return new NetworkResponse(statusCode, responseContents, responseHeaders, false,
- SystemClock.elapsedRealtime() - requestStart);
+ return new NetworkResponse(statusCode, responseContents, false,
+ SystemClock.elapsedRealtime() - requestStart, responseHeaders);
} catch (SocketTimeoutException e) {
attemptRetryOnException("socket", request, new TimeoutError());
- } catch (ConnectTimeoutException e) {
- attemptRetryOnException("connection", request, new TimeoutError());
} catch (MalformedURLException e) {
throw new RuntimeException("Bad URL " + request.getUrl(), e);
} catch (IOException e) {
int statusCode;
if (httpResponse != null) {
- statusCode = httpResponse.getStatusLine().getStatusCode();
+ statusCode = httpResponse.getStatusCode();
} else {
throw new NoConnectionError(e);
}
VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl());
NetworkResponse networkResponse;
if (responseContents != null) {
- networkResponse = new NetworkResponse(statusCode, responseContents,
- responseHeaders, false, SystemClock.elapsedRealtime() - requestStart);
- if (statusCode == HttpStatus.SC_UNAUTHORIZED ||
- statusCode == HttpStatus.SC_FORBIDDEN) {
+ networkResponse = new NetworkResponse(statusCode, responseContents, false,
+ SystemClock.elapsedRealtime() - requestStart, responseHeaders);
+ if (statusCode == HttpURLConnection.HTTP_UNAUTHORIZED ||
+ statusCode == HttpURLConnection.HTTP_FORBIDDEN) {
attemptRetryOnException("auth",
request, new AuthFailureError(networkResponse));
} else if (statusCode >= 400 && statusCode <= 499) {
@@ -184,12 +210,12 @@ public class BasicNetwork implements Network {
* Logs requests that took over SLOW_REQUEST_THRESHOLD_MS to complete.
*/
private void logSlowRequests(long requestLifetime, Request<?> request,
- byte[] responseContents, StatusLine statusLine) {
+ byte[] responseContents, int statusCode) {
if (DEBUG || requestLifetime > SLOW_REQUEST_THRESHOLD_MS) {
VolleyLog.d("HTTP response for request=<%s> [lifetime=%d], [size=%s], " +
"[rc=%d], [retryCount=%s]", request, requestLifetime,
responseContents != null ? responseContents.length : "null",
- statusLine.getStatusCode(), request.getRetryPolicy().getCurrentRetryCount());
+ statusCode, request.getRetryPolicy().getCurrentRetryCount());
}
}
@@ -213,20 +239,24 @@ public class BasicNetwork implements Network {
request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix, oldTimeout));
}
- private void addCacheHeaders(Map<String, String> headers, Cache.Entry entry) {
+ private Map<String, String> getCacheHeaders(Cache.Entry entry) {
// If there's no cache entry, we're done.
if (entry == null) {
- return;
+ return Collections.emptyMap();
}
+ Map<String, String> headers = new HashMap<>();
+
if (entry.etag != null) {
headers.put("If-None-Match", entry.etag);
}
if (entry.lastModified > 0) {
- Date refTime = new Date(entry.lastModified);
- headers.put("If-Modified-Since", DateUtils.formatDate(refTime));
+ headers.put("If-Modified-Since",
+ HttpHeaderParser.formatEpochAsRfc1123(entry.lastModified));
}
+
+ return headers;
}
protected void logError(String what, String url, long start) {
@@ -234,13 +264,13 @@ public class BasicNetwork implements Network {
VolleyLog.v("HTTP ERROR(%s) %d ms to fetch %s", what, (now - start), url);
}
- /** Reads the contents of HttpEntity into a byte[]. */
- private byte[] entityToBytes(HttpEntity entity) throws IOException, ServerError {
+ /** Reads the contents of an InputStream into a byte[]. */
+ private byte[] inputStreamToBytes(InputStream in, int contentLength)
+ throws IOException, ServerError {
PoolingByteArrayOutputStream bytes =
- new PoolingByteArrayOutputStream(mPool, (int) entity.getContentLength());
+ new PoolingByteArrayOutputStream(mPool, contentLength);
byte[] buffer = null;
try {
- InputStream in = entity.getContent();
if (in == null) {
throw new ServerError();
}
@@ -253,11 +283,13 @@ public class BasicNetwork implements Network {
} finally {
try {
// Close the InputStream and release the resources by "consuming the content".
- entity.consumeContent();
+ if (in != null) {
+ in.close();
+ }
} catch (IOException e) {
- // This can happen if there was an exception above that left the entity in
+ // This can happen if there was an exception above that left the stream in
// an invalid state.
- VolleyLog.v("Error occurred when calling consumingContent");
+ VolleyLog.v("Error occurred when closing InputStream");
}
mPool.returnBuf(buffer);
bytes.close();
@@ -266,12 +298,62 @@ public class BasicNetwork implements Network {
/**
* Converts Headers[] to Map&lt;String, String&gt;.
+ *
+ * @deprecated Should never have been exposed in the API. This method may be removed in a future
+ * release of Volley.
*/
+ @Deprecated
protected static Map<String, String> convertHeaders(Header[] headers) {
- Map<String, String> result = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
+ Map<String, String> result = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
for (int i = 0; i < headers.length; i++) {
result.put(headers[i].getName(), headers[i].getValue());
}
return result;
}
+
+ /**
+ * Combine cache headers with network response headers for an HTTP 304 response.
+ *
+ * <p>An HTTP 304 response does not have all header fields. We have to use the header fields
+ * from the cache entry plus the new ones from the response. See also:
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
+ *
+ * @param responseHeaders Headers from the network response.
+ * @param entry The cached response.
+ * @return The combined list of headers.
+ */
+ private static List<Header> combineHeaders(List<Header> responseHeaders, Entry entry) {
+ // First, create a case-insensitive set of header names from the network
+ // response.
+ Set<String> headerNamesFromNetworkResponse =
+ new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
+ if (!responseHeaders.isEmpty()) {
+ for (Header header : responseHeaders) {
+ headerNamesFromNetworkResponse.add(header.getName());
+ }
+ }
+
+ // Second, add headers from the cache entry to the network response as long as
+ // they didn't appear in the network response, which should take precedence.
+ List<Header> combinedHeaders = new ArrayList<>(responseHeaders);
+ if (entry.allResponseHeaders != null) {
+ if (!entry.allResponseHeaders.isEmpty()) {
+ for (Header header : entry.allResponseHeaders) {
+ if (!headerNamesFromNetworkResponse.contains(header.getName())) {
+ combinedHeaders.add(header);
+ }
+ }
+ }
+ } else {
+ // Legacy caches only have entry.responseHeaders.
+ if (!entry.responseHeaders.isEmpty()) {
+ for (Map.Entry<String, String> header : entry.responseHeaders.entrySet()) {
+ if (!headerNamesFromNetworkResponse.contains(header.getKey())) {
+ combinedHeaders.add(new Header(header.getKey(), header.getValue()));
+ }
+ }
+ }
+ }
+ return combinedHeaders;
+ }
}
diff --git a/src/main/java/com/android/volley/toolbox/DiskBasedCache.java b/src/main/java/com/android/volley/toolbox/DiskBasedCache.java
index 0e65183..a6cd960 100644
--- a/src/main/java/com/android/volley/toolbox/DiskBasedCache.java
+++ b/src/main/java/com/android/volley/toolbox/DiskBasedCache.java
@@ -20,6 +20,7 @@ import android.os.SystemClock;
import android.text.TextUtils;
import com.android.volley.Cache;
+import com.android.volley.Header;
import com.android.volley.VolleyLog;
import java.io.BufferedInputStream;
@@ -34,15 +35,18 @@ import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.util.ArrayList;
import java.util.Collections;
-import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
/**
* Cache implementation that caches files directly onto the hard disk in the specified
* directory. The default disk usage size is 5MB, but is configurable.
+ *
+ * <p>This cache supports the {@link Entry#allResponseHeaders} headers field.
*/
public class DiskBasedCache implements Cache {
@@ -379,30 +383,40 @@ public class DiskBasedCache implements Cache {
final long softTtl;
/** Headers from the response resulting in this cache entry. */
- final Map<String, String> responseHeaders;
+ final List<Header> allResponseHeaders;
private CacheHeader(String key, String etag, long serverDate, long lastModified, long ttl,
- long softTtl, Map<String, String> responseHeaders) {
+ long softTtl, List<Header> allResponseHeaders) {
this.key = key;
this.etag = ("".equals(etag)) ? null : etag;
this.serverDate = serverDate;
this.lastModified = lastModified;
this.ttl = ttl;
this.softTtl = softTtl;
- this.responseHeaders = responseHeaders;
+ this.allResponseHeaders = allResponseHeaders;
}
/**
- * Instantiates a new CacheHeader object
+ * Instantiates a new CacheHeader object.
* @param key The key that identifies the cache entry
* @param entry The cache entry.
*/
CacheHeader(String key, Entry entry) {
this(key, entry.etag, entry.serverDate, entry.lastModified, entry.ttl, entry.softTtl,
- entry.responseHeaders);
+ getAllResponseHeaders(entry));
size = entry.data.length;
}
+ private static List<Header> getAllResponseHeaders(Entry entry) {
+ // If the entry contains all the response headers, use that field directly.
+ if (entry.allResponseHeaders != null) {
+ return entry.allResponseHeaders;
+ }
+
+ // Legacy fallback - copy headers from the map.
+ return HttpHeaderParser.toAllHeaderList(entry.responseHeaders);
+ }
+
/**
* Reads the header from a CountingInputStream and returns a CacheHeader object.
* @param is The InputStream to read from.
@@ -420,9 +434,9 @@ public class DiskBasedCache implements Cache {
long lastModified = readLong(is);
long ttl = readLong(is);
long softTtl = readLong(is);
- Map<String, String> responseHeaders = readStringStringMap(is);
+ List<Header> allResponseHeaders = readHeaderList(is);
return new CacheHeader(
- key, etag, serverDate, lastModified, ttl, softTtl, responseHeaders);
+ key, etag, serverDate, lastModified, ttl, softTtl, allResponseHeaders);
}
/**
@@ -436,11 +450,11 @@ public class DiskBasedCache implements Cache {
e.lastModified = lastModified;
e.ttl = ttl;
e.softTtl = softTtl;
- e.responseHeaders = responseHeaders;
+ e.responseHeaders = HttpHeaderParser.toHeaderMap(allResponseHeaders);
+ e.allResponseHeaders = Collections.unmodifiableList(allResponseHeaders);
return e;
}
-
/**
* Writes the contents of this CacheHeader to the specified OutputStream.
*/
@@ -453,7 +467,7 @@ public class DiskBasedCache implements Cache {
writeLong(os, lastModified);
writeLong(os, ttl);
writeLong(os, softTtl);
- writeStringStringMap(responseHeaders, os);
+ writeHeaderList(allResponseHeaders, os);
os.flush();
return true;
} catch (IOException e) {
@@ -574,27 +588,27 @@ public class DiskBasedCache implements Cache {
return new String(b, "UTF-8");
}
- static void writeStringStringMap(Map<String, String> map, OutputStream os) throws IOException {
- if (map != null) {
- writeInt(os, map.size());
- for (Map.Entry<String, String> entry : map.entrySet()) {
- writeString(os, entry.getKey());
- writeString(os, entry.getValue());
+ static void writeHeaderList(List<Header> headers, OutputStream os) throws IOException {
+ if (headers != null) {
+ writeInt(os, headers.size());
+ for (Header header : headers) {
+ writeString(os, header.getName());
+ writeString(os, header.getValue());
}
} else {
writeInt(os, 0);
}
}
- static Map<String, String> readStringStringMap(CountingInputStream cis) throws IOException {
+ static List<Header> readHeaderList(CountingInputStream cis) throws IOException {
int size = readInt(cis);
- Map<String, String> result = (size == 0)
- ? Collections.<String, String>emptyMap()
- : new HashMap<String, String>(size);
+ List<Header> result = (size == 0)
+ ? Collections.<Header>emptyList()
+ : new ArrayList<Header>(size);
for (int i = 0; i < size; i++) {
- String key = readString(cis).intern();
+ String name = readString(cis).intern();
String value = readString(cis).intern();
- result.put(key, value);
+ result.add(new Header(name, value));
}
return result;
}
diff --git a/src/main/java/com/android/volley/toolbox/HttpClientStack.java b/src/main/java/com/android/volley/toolbox/HttpClientStack.java
index 377110e..023ee21 100644
--- a/src/main/java/com/android/volley/toolbox/HttpClientStack.java
+++ b/src/main/java/com/android/volley/toolbox/HttpClientStack.java
@@ -46,7 +46,11 @@ import java.util.Map;
/**
* An HttpStack that performs request over an {@link HttpClient}.
+ *
+ * @deprecated The Apache HTTP library on Android is deprecated. Use {@link HurlStack} or another
+ * {@link BaseHttpStack} implementation.
*/
+@Deprecated
public class HttpClientStack implements HttpStack {
protected final HttpClient mClient;
diff --git a/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java b/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
index f53063c..211c329 100644
--- a/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
+++ b/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
@@ -17,19 +17,31 @@
package com.android.volley.toolbox;
import com.android.volley.Cache;
+import com.android.volley.Header;
import com.android.volley.NetworkResponse;
-
-import org.apache.http.impl.cookie.DateParseException;
-import org.apache.http.impl.cookie.DateUtils;
-import org.apache.http.protocol.HTTP;
-
+import com.android.volley.VolleyLog;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
import java.util.Map;
+import java.util.TimeZone;
+import java.util.TreeMap;
/**
* Utility methods for parsing HTTP headers.
*/
public class HttpHeaderParser {
+ static final String HEADER_CONTENT_TYPE = "Content-Type";
+
+ private static final String DEFAULT_CONTENT_CHARSET = "ISO-8859-1";
+
+ private static final String RFC1123_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz";
+
/**
* Extracts a {@link com.android.volley.Cache.Entry} from a {@link NetworkResponse}.
*
@@ -116,6 +128,7 @@ public class HttpHeaderParser {
entry.serverDate = serverDate;
entry.lastModified = lastModified;
entry.responseHeaders = headers;
+ entry.allResponseHeaders = response.allHeaders;
return entry;
}
@@ -126,13 +139,26 @@ public class HttpHeaderParser {
public static long parseDateAsEpoch(String dateStr) {
try {
// Parse date in RFC1123 format if this header contains one
- return DateUtils.parseDate(dateStr).getTime();
- } catch (DateParseException e) {
+ return newRfc1123Formatter().parse(dateStr).getTime();
+ } catch (ParseException e) {
// Date in invalid format, fallback to 0
+ VolleyLog.e(e, "Unable to parse dateStr: %s, falling back to 0", dateStr);
return 0;
}
}
+ /** Format an epoch date in RFC1123 format. */
+ static String formatEpochAsRfc1123(long epoch) {
+ return newRfc1123Formatter().format(new Date(epoch));
+ }
+
+ private static SimpleDateFormat newRfc1123Formatter() {
+ SimpleDateFormat formatter =
+ new SimpleDateFormat(RFC1123_FORMAT, Locale.US);
+ formatter.setTimeZone(TimeZone.getTimeZone("GMT"));
+ return formatter;
+ }
+
/**
* Retrieve a charset from headers
*
@@ -142,7 +168,7 @@ public class HttpHeaderParser {
* or the defaultCharset if none can be found.
*/
public static String parseCharset(Map<String, String> headers, String defaultCharset) {
- String contentType = headers.get(HTTP.CONTENT_TYPE);
+ String contentType = headers.get(HEADER_CONTENT_TYPE);
if (contentType != null) {
String[] params = contentType.split(";");
for (int i = 1; i < params.length; i++) {
@@ -163,6 +189,28 @@ public class HttpHeaderParser {
* or the HTTP default (ISO-8859-1) if none can be found.
*/
public static String parseCharset(Map<String, String> headers) {
- return parseCharset(headers, HTTP.DEFAULT_CONTENT_CHARSET);
+ return parseCharset(headers, DEFAULT_CONTENT_CHARSET);
+ }
+
+ // Note - these are copied from NetworkResponse to avoid making them public (as needed to access
+ // them from the .toolbox package), which would mean they'd become part of the Volley API.
+ // TODO: Consider obfuscating official releases so we can share utility methods between Volley
+ // and Toolbox without making them public APIs.
+
+ static Map<String, String> toHeaderMap(List<Header> allHeaders) {
+ Map<String, String> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ // Later elements in the list take precedence.
+ for (Header header : allHeaders) {
+ headers.put(header.getName(), header.getValue());
+ }
+ return headers;
+ }
+
+ static List<Header> toAllHeaderList(Map<String, String> headers) {
+ List<Header> allHeaders = new ArrayList<>(headers.size());
+ for (Map.Entry<String, String> header : headers.entrySet()) {
+ allHeaders.add(new Header(header.getKey(), header.getValue()));
+ }
+ return allHeaders;
}
}
diff --git a/src/main/java/com/android/volley/toolbox/HttpResponse.java b/src/main/java/com/android/volley/toolbox/HttpResponse.java
new file mode 100644
index 0000000..db719bc
--- /dev/null
+++ b/src/main/java/com/android/volley/toolbox/HttpResponse.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.volley.toolbox;
+
+import com.android.volley.Header;
+
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.List;
+
+/** A response from an HTTP server. */
+public final class HttpResponse {
+
+ private final int mStatusCode;
+ private final List<Header> mHeaders;
+ private final int mContentLength;
+ private final InputStream mContent;
+
+ /**
+ * Construct a new HttpResponse for an empty response body.
+ *
+ * @param statusCode the HTTP status code of the response
+ * @param headers the response headers
+ */
+ public HttpResponse(int statusCode, List<Header> headers) {
+ this(statusCode, headers, -1 /* contentLength */, null /* content */);
+ }
+
+ /**
+ * Construct a new HttpResponse.
+ *
+ * @param statusCode the HTTP status code of the response
+ * @param headers the response headers
+ * @param contentLength the length of the response content. Ignored if there is no content.
+ * @param content an {@link InputStream} of the response content. May be null to indicate that
+ * the response has no content.
+ */
+ public HttpResponse(
+ int statusCode, List<Header> headers, int contentLength, InputStream content) {
+ mStatusCode = statusCode;
+ mHeaders = headers;
+ mContentLength = contentLength;
+ mContent = content;
+ }
+
+ /** Returns the HTTP status code of the response. */
+ public final int getStatusCode() {
+ return mStatusCode;
+ }
+
+ /** Returns the response headers. Must not be mutated directly. */
+ public final List<Header> getHeaders() {
+ return Collections.unmodifiableList(mHeaders);
+ }
+
+ /** Returns the length of the content. Only valid if {@link #getContent} is non-null. */
+ public final int getContentLength() {
+ return mContentLength;
+ }
+
+ /**
+ * Returns an {@link InputStream} of the response content. May be null to indicate that the
+ * response has no content.
+ */
+ public final InputStream getContent() {
+ return mContent;
+ }
+}
diff --git a/src/main/java/com/android/volley/toolbox/HttpStack.java b/src/main/java/com/android/volley/toolbox/HttpStack.java
index 06f6017..5d34b44 100644
--- a/src/main/java/com/android/volley/toolbox/HttpStack.java
+++ b/src/main/java/com/android/volley/toolbox/HttpStack.java
@@ -26,7 +26,12 @@ import java.util.Map;
/**
* An HTTP stack abstraction.
+ *
+ * @deprecated This interface should be avoided as it depends on the deprecated Apache HTTP library.
+ * Use {@link BaseHttpStack} to avoid this dependency. This class may be removed in a future
+ * release of Volley.
*/
+@Deprecated
public interface HttpStack {
/**
* Performs an HTTP request with the given parameters.
diff --git a/src/main/java/com/android/volley/toolbox/HurlStack.java b/src/main/java/com/android/volley/toolbox/HurlStack.java
index 66f441d..a975a71 100644
--- a/src/main/java/com/android/volley/toolbox/HurlStack.java
+++ b/src/main/java/com/android/volley/toolbox/HurlStack.java
@@ -17,29 +17,19 @@
package com.android.volley.toolbox;
import com.android.volley.AuthFailureError;
+import com.android.volley.Header;
import com.android.volley.Request;
import com.android.volley.Request.Method;
-import org.apache.http.Header;
-import org.apache.http.HttpEntity;
-import org.apache.http.HttpResponse;
-import org.apache.http.HttpStatus;
-import org.apache.http.ProtocolVersion;
-import org.apache.http.StatusLine;
-import org.apache.http.entity.BasicHttpEntity;
-import org.apache.http.message.BasicHeader;
-import org.apache.http.message.BasicHttpResponse;
-import org.apache.http.message.BasicStatusLine;
-
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Map.Entry;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
@@ -47,9 +37,9 @@ import javax.net.ssl.SSLSocketFactory;
/**
* An {@link HttpStack} based on {@link HttpURLConnection}.
*/
-public class HurlStack implements HttpStack {
+public class HurlStack extends BaseHttpStack {
- private static final String HEADER_CONTENT_TYPE = "Content-Type";
+ private static final int HTTP_CONTINUE = 100;
/**
* An interface for transforming URLs before use.
@@ -86,10 +76,10 @@ public class HurlStack implements HttpStack {
}
@Override
- public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
+ public HttpResponse executeRequest(Request<?> request, Map<String, String> additionalHeaders)
throws IOException, AuthFailureError {
String url = request.getUrl();
- HashMap<String, String> map = new HashMap<String, String>();
+ HashMap<String, String> map = new HashMap<>();
map.putAll(request.getHeaders());
map.putAll(additionalHeaders);
if (mUrlRewriter != null) {
@@ -106,26 +96,34 @@ public class HurlStack implements HttpStack {
}
setConnectionParametersForRequest(connection, request);
// Initialize HttpResponse with data from the HttpURLConnection.
- ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1);
int responseCode = connection.getResponseCode();
if (responseCode == -1) {
// -1 is returned by getResponseCode() if the response code could not be retrieved.
// Signal to the caller that something was wrong with the connection.
throw new IOException("Could not retrieve response code from HttpUrlConnection.");
}
- StatusLine responseStatus = new BasicStatusLine(protocolVersion,
- connection.getResponseCode(), connection.getResponseMessage());
- BasicHttpResponse response = new BasicHttpResponse(responseStatus);
- if (hasResponseBody(request.getMethod(), responseStatus.getStatusCode())) {
- response.setEntity(entityFromConnection(connection));
+
+ if (!hasResponseBody(request.getMethod(), responseCode)) {
+ return new HttpResponse(responseCode, convertHeaders(connection.getHeaderFields()));
}
- for (Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) {
- if (header.getKey() != null) {
- Header h = new BasicHeader(header.getKey(), header.getValue().get(0));
- response.addHeader(h);
+
+ return new HttpResponse(responseCode, convertHeaders(connection.getHeaderFields()),
+ connection.getContentLength(), inputStreamFromConnection(connection));
+ }
+
+ // VisibleForTesting
+ static List<Header> convertHeaders(Map<String, List<String>> responseHeaders) {
+ List<Header> headerList = new ArrayList<>(responseHeaders.size());
+ for (Map.Entry<String, List<String>> entry : responseHeaders.entrySet()) {
+ // HttpUrlConnection includes the status line as a header with a null key; omit it here
+ // since it's not really a header and the rest of Volley assumes non-null keys.
+ if (entry.getKey() != null) {
+ for (String value : entry.getValue()) {
+ headerList.add(new Header(entry.getKey(), value));
+ }
}
}
- return response;
+ return headerList;
}
/**
@@ -137,29 +135,24 @@ public class HurlStack implements HttpStack {
*/
private static boolean hasResponseBody(int requestMethod, int responseCode) {
return requestMethod != Request.Method.HEAD
- && !(HttpStatus.SC_CONTINUE <= responseCode && responseCode < HttpStatus.SC_OK)
- && responseCode != HttpStatus.SC_NO_CONTENT
- && responseCode != HttpStatus.SC_NOT_MODIFIED;
+ && !(HTTP_CONTINUE <= responseCode && responseCode < HttpURLConnection.HTTP_OK)
+ && responseCode != HttpURLConnection.HTTP_NO_CONTENT
+ && responseCode != HttpURLConnection.HTTP_NOT_MODIFIED;
}
/**
- * Initializes an {@link HttpEntity} from the given {@link HttpURLConnection}.
+ * Initializes an {@link InputStream} from the given {@link HttpURLConnection}.
* @param connection
* @return an HttpEntity populated with data from <code>connection</code>.
*/
- private static HttpEntity entityFromConnection(HttpURLConnection connection) {
- BasicHttpEntity entity = new BasicHttpEntity();
+ private static InputStream inputStreamFromConnection(HttpURLConnection connection) {
InputStream inputStream;
try {
inputStream = connection.getInputStream();
} catch (IOException ioe) {
inputStream = connection.getErrorStream();
}
- entity.setContent(inputStream);
- entity.setContentLength(connection.getContentLength());
- entity.setContentEncoding(connection.getContentEncoding());
- entity.setContentType(connection.getContentType());
- return entity;
+ return inputStream;
}
/**
@@ -261,7 +254,8 @@ public class HurlStack implements HttpStack {
// since this is handled by HttpURLConnection using the size of the prepared
// output stream.
connection.setDoOutput(true);
- connection.addRequestProperty(HEADER_CONTENT_TYPE, request.getBodyContentType());
+ connection.addRequestProperty(
+ HttpHeaderParser.HEADER_CONTENT_TYPE, request.getBodyContentType());
DataOutputStream out = new DataOutputStream(connection.getOutputStream());
out.write(body);
out.close();
diff --git a/src/main/java/com/android/volley/toolbox/Volley.java b/src/main/java/com/android/volley/toolbox/Volley.java
index 0e04e87..6ec08b1 100644
--- a/src/main/java/com/android/volley/toolbox/Volley.java
+++ b/src/main/java/com/android/volley/toolbox/Volley.java
@@ -36,35 +36,59 @@ public class Volley {
* Creates a default instance of the worker pool and calls {@link RequestQueue#start()} on it.
*
* @param context A {@link Context} to use for creating the cache dir.
- * @param stack An {@link HttpStack} to use for the network, or null for default.
+ * @param stack A {@link BaseHttpStack} to use for the network, or null for default.
* @return A started {@link RequestQueue} instance.
*/
- public static RequestQueue newRequestQueue(Context context, HttpStack stack) {
- File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
-
- String userAgent = "volley/0";
- try {
- String packageName = context.getPackageName();
- PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
- userAgent = packageName + "/" + info.versionCode;
- } catch (NameNotFoundException e) {
- }
-
+ public static RequestQueue newRequestQueue(Context context, BaseHttpStack stack) {
+ BasicNetwork network;
if (stack == null) {
if (Build.VERSION.SDK_INT >= 9) {
- stack = new HurlStack();
+ network = new BasicNetwork(new HurlStack());
} else {
// Prior to Gingerbread, HttpUrlConnection was unreliable.
// See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html
- stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
+ // At some point in the future we'll move our minSdkVersion past Froyo and can
+ // delete this fallback (along with all Apache HTTP code).
+ String userAgent = "volley/0";
+ try {
+ String packageName = context.getPackageName();
+ PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
+ userAgent = packageName + "/" + info.versionCode;
+ } catch (NameNotFoundException e) {
+ }
+
+ network = new BasicNetwork(
+ new HttpClientStack(AndroidHttpClient.newInstance(userAgent)));
}
+ } else {
+ network = new BasicNetwork(stack);
}
- Network network = new BasicNetwork(stack);
+ return newRequestQueue(context, network);
+ }
+
+ /**
+ * Creates a default instance of the worker pool and calls {@link RequestQueue#start()} on it.
+ *
+ * @param context A {@link Context} to use for creating the cache dir.
+ * @param stack An {@link HttpStack} to use for the network, or null for default.
+ * @return A started {@link RequestQueue} instance.
+ * @deprecated Use {@link #newRequestQueue(Context, BaseHttpStack)} instead to avoid depending
+ * on Apache HTTP. This method may be removed in a future release of Volley.
+ */
+ @Deprecated
+ @SuppressWarnings("deprecation")
+ public static RequestQueue newRequestQueue(Context context, HttpStack stack) {
+ if (stack == null) {
+ return newRequestQueue(context, (BaseHttpStack) null);
+ }
+ return newRequestQueue(context, new BasicNetwork(stack));
+ }
+ private static RequestQueue newRequestQueue(Context context, Network network) {
+ File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
queue.start();
-
return queue;
}
@@ -75,6 +99,6 @@ public class Volley {
* @return A started {@link RequestQueue} instance.
*/
public static RequestQueue newRequestQueue(Context context) {
- return newRequestQueue(context, null);
+ return newRequestQueue(context, (BaseHttpStack) null);
}
}