aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnonymous <no-reply@google.com>2018-06-21 10:06:47 -0700
committerandroid-build-merger <android-build-merger@google.com>2018-06-21 10:06:47 -0700
commitc409124cab8fa87dab8a3e42ce34844f5a4fa7ea (patch)
treec81fa90a1e8f5bca7c7faf978e7e3a6d4402f3ae
parentf6db66b103746d0ff38a29ad492a9a20cd5e24fe (diff)
parent65d9fb8addc5e338cf485811379484b8fd5e3ccc (diff)
downloadvolley-c409124cab8fa87dab8a3e42ce34844f5a4fa7ea.tar.gz
am: 65d9fb8add Change-Id: I5b96728796f5d26f4cb017eff6e51eaf4b1bff88
-rw-r--r--src/main/java/com/android/volley/CacheDispatcher.java4
-rw-r--r--src/main/java/com/android/volley/NetworkDispatcher.java4
-rw-r--r--src/main/java/com/android/volley/Request.java29
-rw-r--r--src/main/java/com/android/volley/RetryPolicy.java22
-rw-r--r--src/main/java/com/android/volley/toolbox/HttpClientStack.java8
-rw-r--r--src/main/java/com/android/volley/toolbox/HurlStack.java16
-rw-r--r--src/main/java/com/android/volley/toolbox/ImageRequest.java4
-rw-r--r--src/main/java/com/android/volley/toolbox/JsonArrayRequest.java12
-rw-r--r--src/main/java/com/android/volley/toolbox/JsonObjectRequest.java13
-rw-r--r--src/main/java/com/android/volley/toolbox/JsonRequest.java8
-rw-r--r--src/main/java/com/android/volley/toolbox/StringRequest.java10
-rw-r--r--src/test/java/com/android/volley/RequestTest.java96
-rw-r--r--src/test/java/com/android/volley/toolbox/HttpStackConformanceTest.java192
13 files changed, 387 insertions, 31 deletions
diff --git a/src/main/java/com/android/volley/CacheDispatcher.java b/src/main/java/com/android/volley/CacheDispatcher.java
index 4ea8a0b..f616285 100644
--- a/src/main/java/com/android/volley/CacheDispatcher.java
+++ b/src/main/java/com/android/volley/CacheDispatcher.java
@@ -98,8 +98,12 @@ public class CacheDispatcher extends Thread {
} catch (InterruptedException e) {
// We may have been interrupted because it was time to quit.
if (mQuit) {
+ Thread.currentThread().interrupt();
return;
}
+ VolleyLog.e(
+ "Ignoring spurious interrupt of CacheDispatcher thread; "
+ + "use quit() to terminate it");
}
}
}
diff --git a/src/main/java/com/android/volley/NetworkDispatcher.java b/src/main/java/com/android/volley/NetworkDispatcher.java
index 6e47465..327afed 100644
--- a/src/main/java/com/android/volley/NetworkDispatcher.java
+++ b/src/main/java/com/android/volley/NetworkDispatcher.java
@@ -91,8 +91,12 @@ public class NetworkDispatcher extends Thread {
} catch (InterruptedException e) {
// We may have been interrupted because it was time to quit.
if (mQuit) {
+ Thread.currentThread().interrupt();
return;
}
+ VolleyLog.e(
+ "Ignoring spurious interrupt of NetworkDispatcher thread; "
+ + "use quit() to terminate it");
}
}
}
diff --git a/src/main/java/com/android/volley/Request.java b/src/main/java/com/android/volley/Request.java
index c958088..cd7290a 100644
--- a/src/main/java/com/android/volley/Request.java
+++ b/src/main/java/com/android/volley/Request.java
@@ -22,6 +22,7 @@ import android.os.Handler;
import android.os.Looper;
import android.support.annotation.CallSuper;
import android.support.annotation.GuardedBy;
+import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.android.volley.VolleyLog.MarkerLog;
import java.io.UnsupportedEncodingException;
@@ -81,6 +82,7 @@ public abstract class Request<T> implements Comparable<Request<T>> {
private final Object mLock = new Object();
/** Listener interface for errors. */
+ @Nullable
@GuardedBy("mLock")
private Response.ErrorListener mErrorListener;
@@ -91,6 +93,7 @@ public abstract class Request<T> implements Comparable<Request<T>> {
private RequestQueue mRequestQueue;
/** Whether or not responses to this request should be cached. */
+ // TODO(#190): Turn this off by default for anything other than GET requests.
private boolean mShouldCache = true;
/** Whether or not this request has been canceled. */
@@ -139,7 +142,7 @@ public abstract class Request<T> implements Comparable<Request<T>> {
* responses is provided by subclasses, who have a better idea of how to deliver an
* already-parsed response.
*/
- public Request(int method, String url, Response.ErrorListener listener) {
+ public Request(int method, String url, @Nullable Response.ErrorListener listener) {
mMethod = method;
mUrl = url;
mErrorListener = listener;
@@ -174,6 +177,7 @@ public abstract class Request<T> implements Comparable<Request<T>> {
}
/** @return this request's {@link com.android.volley.Response.ErrorListener}. */
+ @Nullable
public Response.ErrorListener getErrorListener() {
synchronized (mLock) {
return mErrorListener;
@@ -283,7 +287,18 @@ public abstract class Request<T> implements Comparable<Request<T>> {
/** Returns the cache key for this request. By default, this is the URL. */
public String getCacheKey() {
- return getUrl();
+ String url = getUrl();
+ // If this is a GET request, just use the URL as the key.
+ // For callers using DEPRECATED_GET_OR_POST, we assume the method is GET, which matches
+ // legacy behavior where all methods had the same cache key. We can't determine which method
+ // will be used because doing so requires calling getPostBody() which is expensive and may
+ // throw AuthFailureError.
+ // TODO(#190): Remove support for non-GET methods.
+ int method = getMethod();
+ if (method == Method.GET || method == Method.DEPRECATED_GET_OR_POST) {
+ return url;
+ }
+ return Integer.toString(method) + '-' + url;
}
/**
@@ -458,6 +473,14 @@ public abstract class Request<T> implements Comparable<Request<T>> {
StringBuilder encodedParams = new StringBuilder();
try {
for (Map.Entry<String, String> entry : params.entrySet()) {
+ if (entry.getKey() == null || entry.getValue() == null) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Request#getParams() or Request#getPostParams() returned a map "
+ + "containing a null key or value: (%s, %s). All keys "
+ + "and values must be non-null.",
+ entry.getKey(), entry.getValue()));
+ }
encodedParams.append(URLEncoder.encode(entry.getKey(), paramsEncoding));
encodedParams.append('=');
encodedParams.append(URLEncoder.encode(entry.getValue(), paramsEncoding));
@@ -523,7 +546,7 @@ public abstract class Request<T> implements Comparable<Request<T>> {
* remaining, this will cause delivery of a {@link TimeoutError} error.
*/
public final int getTimeoutMs() {
- return mRetryPolicy.getCurrentTimeout();
+ return getRetryPolicy().getCurrentTimeout();
}
/** Returns the retry policy that should be used for this request. */
diff --git a/src/main/java/com/android/volley/RetryPolicy.java b/src/main/java/com/android/volley/RetryPolicy.java
index aa6af43..3ef26de 100644
--- a/src/main/java/com/android/volley/RetryPolicy.java
+++ b/src/main/java/com/android/volley/RetryPolicy.java
@@ -16,7 +16,27 @@
package com.android.volley;
-/** Retry policy for a request. */
+/**
+ * Retry policy for a request.
+ *
+ * <p>A retry policy can control two parameters:
+ *
+ * <ul>
+ * <li>The number of tries. This can be a simple counter or more complex logic based on the type
+ * of error passed to {@link #retry(VolleyError)}, although {@link #getCurrentRetryCount()}
+ * should always return the current retry count for logging purposes.
+ * <li>The request timeout for each try, via {@link #getCurrentTimeout()}. In the common case that
+ * a request times out before the response has been received from the server, retrying again
+ * with a longer timeout can increase the likelihood of success (at the expense of causing the
+ * user to wait longer, especially if the request still fails).
+ * </ul>
+ *
+ * <p>Note that currently, retries triggered by a retry policy are attempted immediately in sequence
+ * with no delay between them (although the time between tries may increase if the requests are
+ * timing out and {@link #getCurrentTimeout()} is returning increasing values).
+ *
+ * <p>By default, Volley uses {@link DefaultRetryPolicy}.
+ */
public interface RetryPolicy {
/** Returns the current timeout (used for logging). */
diff --git a/src/main/java/com/android/volley/toolbox/HttpClientStack.java b/src/main/java/com/android/volley/toolbox/HttpClientStack.java
index be0918a..1e9e4b0 100644
--- a/src/main/java/com/android/volley/toolbox/HttpClientStack.java
+++ b/src/main/java/com/android/volley/toolbox/HttpClientStack.java
@@ -58,7 +58,7 @@ public class HttpClientStack implements HttpStack {
mClient = client;
}
- private static void addHeaders(HttpUriRequest httpRequest, Map<String, String> headers) {
+ private static void setHeaders(HttpUriRequest httpRequest, Map<String, String> headers) {
for (String key : headers.keySet()) {
httpRequest.setHeader(key, headers.get(key));
}
@@ -77,8 +77,10 @@ public class HttpClientStack implements HttpStack {
public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
throws IOException, AuthFailureError {
HttpUriRequest httpRequest = createHttpRequest(request, additionalHeaders);
- addHeaders(httpRequest, additionalHeaders);
- addHeaders(httpRequest, request.getHeaders());
+ setHeaders(httpRequest, additionalHeaders);
+ // Request.getHeaders() takes precedence over the given additional (cache) headers) and any
+ // headers set by createHttpRequest (like the Content-Type header).
+ setHeaders(httpRequest, request.getHeaders());
onPrepareRequest(httpRequest);
HttpParams httpParams = httpRequest.getParams();
int timeoutMs = request.getTimeoutMs();
diff --git a/src/main/java/com/android/volley/toolbox/HurlStack.java b/src/main/java/com/android/volley/toolbox/HurlStack.java
index dd73759..5af18ef 100644
--- a/src/main/java/com/android/volley/toolbox/HurlStack.java
+++ b/src/main/java/com/android/volley/toolbox/HurlStack.java
@@ -74,8 +74,9 @@ public class HurlStack extends BaseHttpStack {
throws IOException, AuthFailureError {
String url = request.getUrl();
HashMap<String, String> map = new HashMap<>();
- map.putAll(request.getHeaders());
map.putAll(additionalHeaders);
+ // Request.getHeaders() takes precedence over the given additional (cache) headers).
+ map.putAll(request.getHeaders());
if (mUrlRewriter != null) {
String rewritten = mUrlRewriter.rewriteUrl(url);
if (rewritten == null) {
@@ -88,7 +89,7 @@ public class HurlStack extends BaseHttpStack {
boolean keepConnectionOpen = false;
try {
for (String headerName : map.keySet()) {
- connection.addRequestProperty(headerName, map.get(headerName));
+ connection.setRequestProperty(headerName, map.get(headerName));
}
setConnectionParametersForRequest(connection, request);
// Initialize HttpResponse with data from the HttpURLConnection.
@@ -219,6 +220,8 @@ public class HurlStack extends BaseHttpStack {
return connection;
}
+ // NOTE: Any request headers added here (via setRequestProperty or addRequestProperty) should be
+ // checked against the existing properties in the connection and not overridden if already set.
@SuppressWarnings("deprecation")
/* package */ static void setConnectionParametersForRequest(
HttpURLConnection connection, Request<?> request) throws IOException, AuthFailureError {
@@ -276,13 +279,16 @@ public class HurlStack extends BaseHttpStack {
}
private static void addBody(HttpURLConnection connection, Request<?> request, byte[] body)
- throws IOException, AuthFailureError {
+ throws IOException {
// Prepare output. There is no need to set Content-Length explicitly,
// since this is handled by HttpURLConnection using the size of the prepared
// output stream.
connection.setDoOutput(true);
- connection.addRequestProperty(
- HttpHeaderParser.HEADER_CONTENT_TYPE, request.getBodyContentType());
+ // Set the content-type unless it was already set (by Request#getHeaders).
+ if (!connection.getRequestProperties().containsKey(HttpHeaderParser.HEADER_CONTENT_TYPE)) {
+ connection.setRequestProperty(
+ 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/ImageRequest.java b/src/main/java/com/android/volley/toolbox/ImageRequest.java
index c804267..59e468f 100644
--- a/src/main/java/com/android/volley/toolbox/ImageRequest.java
+++ b/src/main/java/com/android/volley/toolbox/ImageRequest.java
@@ -20,6 +20,7 @@ import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.support.annotation.GuardedBy;
+import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.widget.ImageView.ScaleType;
import com.android.volley.DefaultRetryPolicy;
@@ -44,6 +45,7 @@ public class ImageRequest extends Request<Bitmap> {
private final Object mLock = new Object();
@GuardedBy("mLock")
+ @Nullable
private Response.Listener<Bitmap> mListener;
private final Config mDecodeConfig;
@@ -76,7 +78,7 @@ public class ImageRequest extends Request<Bitmap> {
int maxHeight,
ScaleType scaleType,
Config decodeConfig,
- Response.ErrorListener errorListener) {
+ @Nullable Response.ErrorListener errorListener) {
super(Method.GET, url, errorListener);
setRetryPolicy(
new DefaultRetryPolicy(
diff --git a/src/main/java/com/android/volley/toolbox/JsonArrayRequest.java b/src/main/java/com/android/volley/toolbox/JsonArrayRequest.java
index 757c7f9..1abaec7 100644
--- a/src/main/java/com/android/volley/toolbox/JsonArrayRequest.java
+++ b/src/main/java/com/android/volley/toolbox/JsonArrayRequest.java
@@ -16,6 +16,7 @@
package com.android.volley.toolbox;
+import android.support.annotation.Nullable;
import com.android.volley.NetworkResponse;
import com.android.volley.ParseError;
import com.android.volley.Response;
@@ -35,7 +36,8 @@ public class JsonArrayRequest extends JsonRequest<JSONArray> {
* @param listener Listener to receive the JSON response
* @param errorListener Error listener, or null to ignore errors.
*/
- public JsonArrayRequest(String url, Listener<JSONArray> listener, ErrorListener errorListener) {
+ public JsonArrayRequest(
+ String url, Listener<JSONArray> listener, @Nullable ErrorListener errorListener) {
super(Method.GET, url, null, listener, errorListener);
}
@@ -44,17 +46,17 @@ public class JsonArrayRequest extends JsonRequest<JSONArray> {
*
* @param method the HTTP method to use
* @param url URL to fetch the JSON from
- * @param jsonRequest A {@link JSONArray} to post with the request. Null is allowed and
- * indicates no parameters will be posted along with request.
+ * @param jsonRequest A {@link JSONArray} to post with the request. Null indicates no parameters
+ * will be posted along with request.
* @param listener Listener to receive the JSON response
* @param errorListener Error listener, or null to ignore errors.
*/
public JsonArrayRequest(
int method,
String url,
- JSONArray jsonRequest,
+ @Nullable JSONArray jsonRequest,
Listener<JSONArray> listener,
- ErrorListener errorListener) {
+ @Nullable ErrorListener errorListener) {
super(
method,
url,
diff --git a/src/main/java/com/android/volley/toolbox/JsonObjectRequest.java b/src/main/java/com/android/volley/toolbox/JsonObjectRequest.java
index e9dc3d7..cee5efe 100644
--- a/src/main/java/com/android/volley/toolbox/JsonObjectRequest.java
+++ b/src/main/java/com/android/volley/toolbox/JsonObjectRequest.java
@@ -16,6 +16,7 @@
package com.android.volley.toolbox;
+import android.support.annotation.Nullable;
import com.android.volley.NetworkResponse;
import com.android.volley.ParseError;
import com.android.volley.Response;
@@ -36,17 +37,17 @@ public class JsonObjectRequest extends JsonRequest<JSONObject> {
*
* @param method the HTTP method to use
* @param url URL to fetch the JSON from
- * @param jsonRequest A {@link JSONObject} to post with the request. Null is allowed and
- * indicates no parameters will be posted along with request.
+ * @param jsonRequest A {@link JSONObject} to post with the request. Null indicates no
+ * parameters will be posted along with request.
* @param listener Listener to receive the JSON response
* @param errorListener Error listener, or null to ignore errors.
*/
public JsonObjectRequest(
int method,
String url,
- JSONObject jsonRequest,
+ @Nullable JSONObject jsonRequest,
Listener<JSONObject> listener,
- ErrorListener errorListener) {
+ @Nullable ErrorListener errorListener) {
super(
method,
url,
@@ -63,9 +64,9 @@ public class JsonObjectRequest extends JsonRequest<JSONObject> {
*/
public JsonObjectRequest(
String url,
- JSONObject jsonRequest,
+ @Nullable JSONObject jsonRequest,
Listener<JSONObject> listener,
- ErrorListener errorListener) {
+ @Nullable ErrorListener errorListener) {
this(
jsonRequest == null ? Method.GET : Method.POST,
url,
diff --git a/src/main/java/com/android/volley/toolbox/JsonRequest.java b/src/main/java/com/android/volley/toolbox/JsonRequest.java
index fd395dd..c00d3db 100644
--- a/src/main/java/com/android/volley/toolbox/JsonRequest.java
+++ b/src/main/java/com/android/volley/toolbox/JsonRequest.java
@@ -17,6 +17,7 @@
package com.android.volley.toolbox;
import android.support.annotation.GuardedBy;
+import android.support.annotation.Nullable;
import com.android.volley.NetworkResponse;
import com.android.volley.Request;
import com.android.volley.Response;
@@ -42,10 +43,11 @@ public abstract class JsonRequest<T> extends Request<T> {
/** Lock to guard mListener as it is cleared on cancel() and read on delivery. */
private final Object mLock = new Object();
+ @Nullable
@GuardedBy("mLock")
private Listener<T> mListener;
- private final String mRequestBody;
+ @Nullable private final String mRequestBody;
/**
* Deprecated constructor for a JsonRequest which defaults to GET unless {@link #getPostBody()}
@@ -62,9 +64,9 @@ public abstract class JsonRequest<T> extends Request<T> {
public JsonRequest(
int method,
String url,
- String requestBody,
+ @Nullable String requestBody,
Listener<T> listener,
- ErrorListener errorListener) {
+ @Nullable ErrorListener errorListener) {
super(method, url, errorListener);
mListener = listener;
mRequestBody = requestBody;
diff --git a/src/main/java/com/android/volley/toolbox/StringRequest.java b/src/main/java/com/android/volley/toolbox/StringRequest.java
index 0fbab14..c4c89b5 100644
--- a/src/main/java/com/android/volley/toolbox/StringRequest.java
+++ b/src/main/java/com/android/volley/toolbox/StringRequest.java
@@ -17,6 +17,7 @@
package com.android.volley.toolbox;
import android.support.annotation.GuardedBy;
+import android.support.annotation.Nullable;
import com.android.volley.NetworkResponse;
import com.android.volley.Request;
import com.android.volley.Response;
@@ -30,6 +31,7 @@ public class StringRequest extends Request<String> {
/** Lock to guard mListener as it is cleared on cancel() and read on delivery. */
private final Object mLock = new Object();
+ @Nullable
@GuardedBy("mLock")
private Listener<String> mListener;
@@ -42,7 +44,10 @@ public class StringRequest extends Request<String> {
* @param errorListener Error listener, or null to ignore errors
*/
public StringRequest(
- int method, String url, Listener<String> listener, ErrorListener errorListener) {
+ int method,
+ String url,
+ Listener<String> listener,
+ @Nullable ErrorListener errorListener) {
super(method, url, errorListener);
mListener = listener;
}
@@ -54,7 +59,8 @@ public class StringRequest extends Request<String> {
* @param listener Listener to receive the String response
* @param errorListener Error listener, or null to ignore errors
*/
- public StringRequest(String url, Listener<String> listener, ErrorListener errorListener) {
+ public StringRequest(
+ String url, Listener<String> listener, @Nullable ErrorListener errorListener) {
this(Method.GET, url, listener, errorListener);
}
diff --git a/src/test/java/com/android/volley/RequestTest.java b/src/test/java/com/android/volley/RequestTest.java
index e2dd655..382d9da 100644
--- a/src/test/java/com/android/volley/RequestTest.java
+++ b/src/test/java/com/android/volley/RequestTest.java
@@ -20,7 +20,10 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import com.android.volley.Request.Method;
import com.android.volley.Request.Priority;
+import java.util.Collections;
+import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
@@ -84,9 +87,30 @@ public class RequestTest {
assertFalse(0 == goodProtocol.getTrafficStatsTag());
}
+ @Test
+ public void getCacheKey() {
+ assertEquals(
+ "http://example.com",
+ new UrlParseRequest(Method.GET, "http://example.com").getCacheKey());
+ assertEquals(
+ "http://example.com",
+ new UrlParseRequest(Method.DEPRECATED_GET_OR_POST, "http://example.com")
+ .getCacheKey());
+ assertEquals(
+ "1-http://example.com",
+ new UrlParseRequest(Method.POST, "http://example.com").getCacheKey());
+ assertEquals(
+ "2-http://example.com",
+ new UrlParseRequest(Method.PUT, "http://example.com").getCacheKey());
+ }
+
private static class UrlParseRequest extends Request<Object> {
- public UrlParseRequest(String url) {
- super(Request.Method.GET, url, null);
+ UrlParseRequest(String url) {
+ this(Method.GET, url);
+ }
+
+ UrlParseRequest(int method, String url) {
+ super(method, url, null);
}
@Override
@@ -97,4 +121,72 @@ public class RequestTest {
return null;
}
}
+
+ @Test
+ public void nullKeyInPostParams() throws Exception {
+ Request<Object> request =
+ new Request<Object>(Method.POST, "url", null) {
+ @Override
+ protected void deliverResponse(Object response) {}
+
+ @Override
+ protected Response<Object> parseNetworkResponse(NetworkResponse response) {
+ return null;
+ }
+
+ @Override
+ protected Map<String, String> getParams() {
+ return Collections.singletonMap(null, "value");
+ }
+
+ @Override
+ protected Map<String, String> getPostParams() {
+ return Collections.singletonMap(null, "value");
+ }
+ };
+ try {
+ request.getBody();
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+ try {
+ request.getPostBody();
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void nullValueInPostParams() throws Exception {
+ Request<Object> request =
+ new Request<Object>(Method.POST, "url", null) {
+ @Override
+ protected void deliverResponse(Object response) {}
+
+ @Override
+ protected Response<Object> parseNetworkResponse(NetworkResponse response) {
+ return null;
+ }
+
+ @Override
+ protected Map<String, String> getParams() {
+ return Collections.singletonMap("key", null);
+ }
+
+ @Override
+ protected Map<String, String> getPostParams() {
+ return Collections.singletonMap("key", null);
+ }
+ };
+ try {
+ request.getBody();
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+ try {
+ request.getPostBody();
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+ }
}
diff --git a/src/test/java/com/android/volley/toolbox/HttpStackConformanceTest.java b/src/test/java/com/android/volley/toolbox/HttpStackConformanceTest.java
new file mode 100644
index 0000000..6794af8
--- /dev/null
+++ b/src/test/java/com/android/volley/toolbox/HttpStackConformanceTest.java
@@ -0,0 +1,192 @@
+package com.android.volley.toolbox;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import com.android.volley.Request;
+import com.android.volley.RetryPolicy;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.http.Header;
+import org.apache.http.HttpRequest;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.robolectric.RobolectricTestRunner;
+
+/** Tests to validate that HttpStack implementations conform with expected behavior. */
+@RunWith(RobolectricTestRunner.class)
+public class HttpStackConformanceTest {
+ @Mock private RetryPolicy mMockRetryPolicy;
+ @Mock private Request mMockRequest;
+
+ @Mock private HttpURLConnection mMockConnection;
+ @Mock private OutputStream mMockOutputStream;
+ @Spy private HurlStack mHurlStack = new HurlStack();
+
+ @Mock private HttpClient mMockHttpClient;
+ private HttpClientStack mHttpClientStack;
+
+ private final TestCase[] mTestCases =
+ new TestCase[] {
+ // TestCase for HurlStack.
+ new TestCase() {
+ @Override
+ public HttpStack getStack() {
+ return mHurlStack;
+ }
+
+ @Override
+ public void setOutputHeaderMap(final Map<String, String> outputHeaderMap) {
+ doAnswer(
+ new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ outputHeaderMap.put(
+ invocation.<String>getArgument(0),
+ invocation.<String>getArgument(1));
+ return null;
+ }
+ })
+ .when(mMockConnection)
+ .setRequestProperty(anyString(), anyString());
+ doAnswer(
+ new Answer<Map<String, List<String>>>() {
+ @Override
+ public Map<String, List<String>> answer(
+ InvocationOnMock invocation) {
+ Map<String, List<String>> result = new HashMap<>();
+ for (Map.Entry<String, String> entry :
+ outputHeaderMap.entrySet()) {
+ result.put(
+ entry.getKey(),
+ Collections.singletonList(
+ entry.getValue()));
+ }
+ return result;
+ }
+ })
+ .when(mMockConnection)
+ .getRequestProperties();
+ }
+ },
+
+ // TestCase for HttpClientStack.
+ new TestCase() {
+ @Override
+ public HttpStack getStack() {
+ return mHttpClientStack;
+ }
+
+ @Override
+ public void setOutputHeaderMap(final Map<String, String> outputHeaderMap) {
+ try {
+ doAnswer(
+ new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation)
+ throws Throwable {
+ HttpRequest request = invocation.getArgument(0);
+ for (Header header : request.getAllHeaders()) {
+ if (outputHeaderMap.containsKey(
+ header.getName())) {
+ fail(
+ "Multiple values for header "
+ + header.getName());
+ }
+ outputHeaderMap.put(
+ header.getName(),
+ header.getValue());
+ }
+ return null;
+ }
+ })
+ .when(mMockHttpClient)
+ .execute(any(HttpUriRequest.class));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ };
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mHttpClientStack = spy(new HttpClientStack(mMockHttpClient));
+
+ doReturn(mMockConnection).when(mHurlStack).createConnection(any(URL.class));
+ doReturn(mMockOutputStream).when(mMockConnection).getOutputStream();
+ when(mMockRequest.getUrl()).thenReturn("http://127.0.0.1");
+ when(mMockRequest.getRetryPolicy()).thenReturn(mMockRetryPolicy);
+ }
+
+ @Test
+ public void headerPrecedence() throws Exception {
+ Map<String, String> additionalHeaders = new HashMap<>();
+ additionalHeaders.put("A", "AddlA");
+ additionalHeaders.put("B", "AddlB");
+
+ Map<String, String> requestHeaders = new HashMap<>();
+ requestHeaders.put("A", "RequestA");
+ requestHeaders.put("C", "RequestC");
+ when(mMockRequest.getHeaders()).thenReturn(requestHeaders);
+
+ when(mMockRequest.getMethod()).thenReturn(Request.Method.POST);
+ when(mMockRequest.getBody()).thenReturn(new byte[0]);
+ when(mMockRequest.getBodyContentType()).thenReturn("BodyContentType");
+
+ for (TestCase testCase : mTestCases) {
+ // Test once without a Content-Type header in getHeaders().
+ Map<String, String> combinedHeaders = new HashMap<>();
+ testCase.setOutputHeaderMap(combinedHeaders);
+
+ testCase.getStack().performRequest(mMockRequest, additionalHeaders);
+
+ Map<String, String> expectedHeaders = new HashMap<>();
+ expectedHeaders.put("A", "RequestA");
+ expectedHeaders.put("B", "AddlB");
+ expectedHeaders.put("C", "RequestC");
+ expectedHeaders.put(HttpHeaderParser.HEADER_CONTENT_TYPE, "BodyContentType");
+
+ assertEquals(expectedHeaders, combinedHeaders);
+
+ // Reset and test again with a Content-Type header in getHeaders().
+ combinedHeaders.clear();
+
+ requestHeaders.put(HttpHeaderParser.HEADER_CONTENT_TYPE, "RequestContentType");
+ expectedHeaders.put(HttpHeaderParser.HEADER_CONTENT_TYPE, "RequestContentType");
+
+ testCase.getStack().performRequest(mMockRequest, additionalHeaders);
+ assertEquals(expectedHeaders, combinedHeaders);
+
+ // Clear the Content-Type header for the next TestCase.
+ requestHeaders.remove(HttpHeaderParser.HEADER_CONTENT_TYPE);
+ }
+ }
+
+ private interface TestCase {
+ HttpStack getStack();
+
+ void setOutputHeaderMap(Map<String, String> outputHeaderMap);
+ }
+}