diff --git a/src/main/java/com/android/volley/NetworkResponse.java b/src/main/java/com/android/volley/NetworkResponse.java index a787fa74..fc4ada77 100644 --- a/src/main/java/com/android/volley/NetworkResponse.java +++ b/src/main/java/com/android/volley/NetworkResponse.java @@ -16,8 +16,7 @@ package com.android.volley; -import org.apache.http.HttpStatus; - +import java.net.HttpURLConnection; import java.util.Collections; import java.util.Map; @@ -48,11 +47,11 @@ public NetworkResponse(int statusCode, byte[] data, Map headers, } public NetworkResponse(byte[] data) { - this(HttpStatus.SC_OK, data, Collections.emptyMap(), false, 0); + this(HttpURLConnection.HTTP_OK, data, Collections.emptyMap(), false, 0); } public NetworkResponse(byte[] data, Map headers) { - this(HttpStatus.SC_OK, data, headers, false, 0); + this(HttpURLConnection.HTTP_OK, data, headers, false, 0); } /** The HTTP status code. */ 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 00000000..66b3ebdc --- /dev/null +++ b/src/main/java/com/android/volley/toolbox/AdaptedHttpStack.java @@ -0,0 +1,87 @@ +/* + * 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.Request; + +import org.apache.http.Header; +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; +import java.util.TreeMap; + +/** + * {@link BaseHttpStack} implementation wrapping a {@link HttpStack}. + * + *

{@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 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(); + + Header[] headers = apacheResp.getAllHeaders(); + Map> headerMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + for (Header header : headers) { + List valueList = headerMap.get(header.getName()); + if (valueList == null) { + valueList = new ArrayList<>(); + headerMap.put(header.getName(), valueList); + } + valueList.add(header.getValue()); + } + + if (apacheResp.getEntity() == null) { + return new HttpResponse(statusCode, headerMap); + } + + long contentLength = apacheResp.getEntity().getContentLength(); + if ((int) contentLength != contentLength) { + throw new IOException("Response too large: " + contentLength); + } + + return new HttpResponse( + statusCode, + headerMap, + (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 00000000..9b219573 --- /dev/null +++ b/src/main/java/com/android/volley/toolbox/BaseHttpStack.java @@ -0,0 +1,95 @@ +/* + * 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.Request; + +import org.apache.http.Header; +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. + * + *

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 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 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

headers = new ArrayList<>(); + for (Map.Entry> entry : response.getHeaders().entrySet()) { + for (String value : entry.getValue()) { + headers.add(new BasicHeader(entry.getKey(), value)); + } + } + apacheResponse.setHeaders(headers.toArray(new 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 96fb66e4..d52e9414 100644 --- a/src/main/java/com/android/volley/toolbox/BasicNetwork.java +++ b/src/main/java/com/android/volley/toolbox/BasicNetwork.java @@ -34,20 +34,15 @@ 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.Collections; -import java.util.Date; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -61,13 +56,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,8 +82,35 @@ public BasicNetwork(HttpStack httpStack) { /** * @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; } @@ -92,19 +124,18 @@ public NetworkResponse performRequest(Request request) throws VolleyError { Map responseHeaders = Collections.emptyMap(); try { // Gather headers. - Map headers = new HashMap(); + Map headers = new HashMap<>(); addCacheHeaders(headers, request.getCacheEntry()); - httpResponse = mHttpStack.performRequest(request, headers); - StatusLine statusLine = httpResponse.getStatusLine(); - int statusCode = statusLine.getStatusCode(); + httpResponse = mBaseHttpStack.executeRequest(request, headers); + int statusCode = httpResponse.getStatusCode(); - responseHeaders = convertHeaders(httpResponse.getAllHeaders()); + responseHeaders = convertHeaders(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, + return new NetworkResponse(HttpURLConnection.HTTP_NOT_MODIFIED, null, responseHeaders, true, SystemClock.elapsedRealtime() - requestStart); } @@ -114,14 +145,16 @@ public NetworkResponse performRequest(Request request) throws VolleyError { // 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, + return new NetworkResponse(HttpURLConnection.HTTP_NOT_MODIFIED, entry.data, entry.responseHeaders, true, SystemClock.elapsedRealtime() - requestStart); } // 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,7 +163,7 @@ public NetworkResponse performRequest(Request request) throws VolleyError { // 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(); @@ -139,14 +172,12 @@ public NetworkResponse performRequest(Request request) throws VolleyError { SystemClock.elapsedRealtime() - requestStart); } 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); } @@ -155,8 +186,8 @@ public NetworkResponse performRequest(Request request) throws VolleyError { if (responseContents != null) { networkResponse = new NetworkResponse(statusCode, responseContents, responseHeaders, false, SystemClock.elapsedRealtime() - requestStart); - if (statusCode == HttpStatus.SC_UNAUTHORIZED || - statusCode == HttpStatus.SC_FORBIDDEN) { + if (statusCode == HttpURLConnection.HTTP_UNAUTHORIZED || + statusCode == HttpURLConnection.HTTP_FORBIDDEN) { attemptRetryOnException("auth", request, new AuthFailureError(networkResponse)); } else if (statusCode >= 400 && statusCode <= 499) { @@ -184,12 +215,12 @@ public NetworkResponse performRequest(Request request) throws VolleyError { * 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()); } } @@ -224,8 +255,8 @@ private void addCacheHeaders(Map headers, Cache.Entry entry) { } 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)); } } @@ -234,13 +265,13 @@ protected void logError(String what, String url, long start) { 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 +284,13 @@ private byte[] entityToBytes(HttpEntity entity) throws IOException, ServerError } 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 +299,30 @@ private byte[] entityToBytes(HttpEntity entity) throws IOException, ServerError /** * Converts Headers[] to Map<String, String>. + * + * @deprecated Should never have been exposed in the API. This method may be removed in a future + * release of Volley. */ + // Visible for testing (see HttpHeaderParserTest). + @Deprecated protected static Map convertHeaders(Header[] headers) { - Map result = new TreeMap(String.CASE_INSENSITIVE_ORDER); + Map 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; } + + // TODO(#21): Pass all headers to NetworkResponse so clients can access all the headers with + // the same keys (e.g. for SetCookie headers). Just need to ensure case-insensitivity. + private static Map convertHeaders(Map> headers) { + Map result = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + for (Map.Entry> entry : headers.entrySet()) { + if (entry.getKey() != null) { + List values = entry.getValue(); + result.put(entry.getKey(), values.get(0)); + } + } + 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 377110ef..023ee212 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 @@ /** * 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 f53063cb..39cb7172 100644 --- a/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java +++ b/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java @@ -18,18 +18,26 @@ import com.android.volley.Cache; import com.android.volley.NetworkResponse; +import com.android.volley.VolleyLog; -import org.apache.http.impl.cookie.DateParseException; -import org.apache.http.impl.cookie.DateUtils; -import org.apache.http.protocol.HTTP; - +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; import java.util.Map; +import java.util.TimeZone; /** * 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}. * @@ -126,13 +134,26 @@ public static Cache.Entry parseCacheHeaders(NetworkResponse response) { 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 +163,7 @@ public static long parseDateAsEpoch(String dateStr) { * or the defaultCharset if none can be found. */ public static String parseCharset(Map 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 +184,6 @@ public static String parseCharset(Map headers, String defaultCha * or the HTTP default (ISO-8859-1) if none can be found. */ public static String parseCharset(Map headers) { - return parseCharset(headers, HTTP.DEFAULT_CONTENT_CHARSET); + return parseCharset(headers, DEFAULT_CONTENT_CHARSET); } } 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 00000000..c3af140a --- /dev/null +++ b/src/main/java/com/android/volley/toolbox/HttpResponse.java @@ -0,0 +1,78 @@ +/* + * 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 java.io.InputStream; +import java.util.List; +import java.util.Map; + +/** A response from an HTTP server. */ +public class HttpResponse { + private final int mStatusCode; + private final Map> 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, Map> 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, Map> 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. */ + public final Map> getHeaders() { + return 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 06f60179..5d34b445 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 @@ /** * 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 66f441d9..54f1e1de 100644 --- a/src/main/java/com/android/volley/toolbox/HurlStack.java +++ b/src/main/java/com/android/volley/toolbox/HurlStack.java @@ -20,26 +20,13 @@ 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.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 +34,9 @@ /** * 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 +73,10 @@ public HurlStack(UrlRewriter urlRewriter, SSLSocketFactory sslSocketFactory) { } @Override - public HttpResponse performRequest(Request request, Map additionalHeaders) + public HttpResponse executeRequest(Request request, Map additionalHeaders) throws IOException, AuthFailureError { String url = request.getUrl(); - HashMap map = new HashMap(); + HashMap map = new HashMap<>(); map.putAll(request.getHeaders()); map.putAll(additionalHeaders); if (mUrlRewriter != null) { @@ -106,26 +93,18 @@ public HttpResponse performRequest(Request request, Map addit } 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)); - } - for (Entry> header : connection.getHeaderFields().entrySet()) { - if (header.getKey() != null) { - Header h = new BasicHeader(header.getKey(), header.getValue().get(0)); - response.addHeader(h); - } + if (!hasResponseBody(request.getMethod(), responseCode)) { + return new HttpResponse(responseCode, connection.getHeaderFields()); } - return response; + + return new HttpResponse(responseCode, connection.getHeaderFields(), + connection.getContentLength(), inputStreamFromConnection(connection)); } /** @@ -137,29 +116,24 @@ public HttpResponse performRequest(Request request, Map addit */ 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 connection. */ - 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 +235,8 @@ private static void addBody(HttpURLConnection connection, Request request, by // 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 0e04e876..6ec08b15 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 static RequestQueue newRequestQueue(Context context, HttpStack stack) { * @return A started {@link RequestQueue} instance. */ public static RequestQueue newRequestQueue(Context context) { - return newRequestQueue(context, null); + return newRequestQueue(context, (BaseHttpStack) null); } } diff --git a/src/test/java/com/android/volley/mock/MockHttpClient.java b/src/test/java/com/android/volley/mock/MockHttpClient.java deleted file mode 100644 index c2a36bc6..00000000 --- a/src/test/java/com/android/volley/mock/MockHttpClient.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (C) 2011 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.mock; - -import org.apache.http.HttpEntity; -import org.apache.http.HttpHost; -import org.apache.http.HttpRequest; -import org.apache.http.HttpResponse; -import org.apache.http.HttpStatus; -import org.apache.http.ProtocolVersion; -import org.apache.http.StatusLine; -import org.apache.http.client.HttpClient; -import org.apache.http.client.ResponseHandler; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.conn.ClientConnectionManager; -import org.apache.http.message.BasicHttpResponse; -import org.apache.http.message.BasicStatusLine; -import org.apache.http.params.HttpParams; -import org.apache.http.protocol.HttpContext; - - -public class MockHttpClient implements HttpClient { - private int mStatusCode = HttpStatus.SC_OK; - private HttpEntity mResponseEntity = null; - - public void setResponseData(HttpEntity entity) { - mStatusCode = HttpStatus.SC_OK; - mResponseEntity = entity; - } - - public void setErrorCode(int statusCode) { - if (statusCode == HttpStatus.SC_OK) { - throw new IllegalArgumentException("statusCode cannot be 200 for an error"); - } - mStatusCode = statusCode; - } - - public HttpUriRequest requestExecuted = null; - - // This is the only one we actually use. - @Override - public HttpResponse execute(HttpUriRequest request, HttpContext context) { - requestExecuted = request; - StatusLine statusLine = new BasicStatusLine( - new ProtocolVersion("HTTP", 1, 1), mStatusCode, ""); - HttpResponse response = new BasicHttpResponse(statusLine); - response.setEntity(mResponseEntity); - - return response; - } - - - // Unimplemented methods ahoy - - @Override - public HttpResponse execute(HttpUriRequest request) { - throw new UnsupportedOperationException(); - } - - @Override - public HttpResponse execute(HttpHost target, HttpRequest request) { - throw new UnsupportedOperationException(); - } - - @Override - public T execute(HttpUriRequest arg0, ResponseHandler arg1) { - throw new UnsupportedOperationException(); - } - - @Override - public HttpResponse execute(HttpHost target, HttpRequest request, HttpContext context) { - throw new UnsupportedOperationException(); - } - - @Override - public T execute(HttpUriRequest arg0, ResponseHandler arg1, HttpContext arg2) { - throw new UnsupportedOperationException(); - } - - @Override - public T execute(HttpHost arg0, HttpRequest arg1, ResponseHandler arg2) { - throw new UnsupportedOperationException(); - } - - @Override - public T execute(HttpHost arg0, HttpRequest arg1, ResponseHandler arg2, - HttpContext arg3) { - throw new UnsupportedOperationException(); - } - - @Override - public ClientConnectionManager getConnectionManager() { - throw new UnsupportedOperationException(); - } - - @Override - public HttpParams getParams() { - throw new UnsupportedOperationException(); - } -} diff --git a/src/test/java/com/android/volley/mock/MockHttpStack.java b/src/test/java/com/android/volley/mock/MockHttpStack.java index 91872d39..56b29f18 100644 --- a/src/test/java/com/android/volley/mock/MockHttpStack.java +++ b/src/test/java/com/android/volley/mock/MockHttpStack.java @@ -18,15 +18,14 @@ import com.android.volley.AuthFailureError; import com.android.volley.Request; -import com.android.volley.toolbox.HttpStack; - -import org.apache.http.HttpResponse; +import com.android.volley.toolbox.BaseHttpStack; +import com.android.volley.toolbox.HttpResponse; import java.io.IOException; import java.util.HashMap; import java.util.Map; -public class MockHttpStack implements HttpStack { +public class MockHttpStack extends BaseHttpStack { private HttpResponse mResponseToReturn; @@ -59,13 +58,13 @@ public void setExceptionToThrow(IOException exception) { } @Override - public HttpResponse performRequest(Request request, Map additionalHeaders) + public HttpResponse executeRequest(Request request, Map additionalHeaders) throws IOException, AuthFailureError { if (mExceptionToThrow != null) { throw mExceptionToThrow; } mLastUrl = request.getUrl(); - mLastHeaders = new HashMap(); + mLastHeaders = new HashMap<>(); if (request.getHeaders() != null) { mLastHeaders.putAll(request.getHeaders()); } diff --git a/src/test/java/com/android/volley/toolbox/AdaptedHttpStackTest.java b/src/test/java/com/android/volley/toolbox/AdaptedHttpStackTest.java new file mode 100644 index 00000000..97f152be --- /dev/null +++ b/src/test/java/com/android/volley/toolbox/AdaptedHttpStackTest.java @@ -0,0 +1,142 @@ +package com.android.volley.toolbox; + +import android.util.Pair; + +import com.android.volley.Request; +import com.android.volley.mock.TestRequest; + +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.StatusLine; +import org.apache.http.conn.ConnectTimeoutException; +import org.apache.http.message.BasicHeader; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + +import java.io.IOException; +import java.io.InputStream; +import java.net.SocketTimeoutException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +public class AdaptedHttpStackTest { + private static final Request REQUEST = new TestRequest.Get(); + private static final Map ADDITIONAL_HEADERS = Collections.emptyMap(); + + @Mock + private HttpStack mHttpStack; + @Mock + private HttpResponse mHttpResponse; + @Mock + private StatusLine mStatusLine; + @Mock + private HttpEntity mHttpEntity; + @Mock + private InputStream mContent; + + private AdaptedHttpStack mAdaptedHttpStack; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mAdaptedHttpStack = new AdaptedHttpStack(mHttpStack); + when(mHttpResponse.getStatusLine()).thenReturn(mStatusLine); + } + + @Test(expected = SocketTimeoutException.class) + public void requestTimeout() throws Exception { + when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS)) + .thenThrow(new ConnectTimeoutException()); + + mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS); + } + + @Test + public void emptyResponse() throws Exception { + when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS)).thenReturn(mHttpResponse); + when(mStatusLine.getStatusCode()).thenReturn(12345); + when(mHttpResponse.getAllHeaders()).thenReturn(new Header[0]); + + com.android.volley.toolbox.HttpResponse response = + mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS); + + assertEquals(12345, response.getStatusCode()); + assertEquals(Collections.emptyMap(), response.getHeaders()); + assertNull(response.getContent()); + } + + @Test + public void nonEmptyResponse() throws Exception { + when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS)).thenReturn(mHttpResponse); + when(mStatusLine.getStatusCode()).thenReturn(12345); + when(mHttpResponse.getAllHeaders()).thenReturn(new Header[0]); + when(mHttpResponse.getEntity()).thenReturn(mHttpEntity); + when(mHttpEntity.getContentLength()).thenReturn((long) Integer.MAX_VALUE); + when(mHttpEntity.getContent()).thenReturn(mContent); + + com.android.volley.toolbox.HttpResponse response = + mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS); + + assertEquals(12345, response.getStatusCode()); + assertEquals(Collections.emptyMap(), response.getHeaders()); + assertEquals(Integer.MAX_VALUE, response.getContentLength()); + assertSame(mContent, response.getContent()); + } + + @Test(expected = IOException.class) + public void responseTooBig() throws Exception { + when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS)).thenReturn(mHttpResponse); + when(mStatusLine.getStatusCode()).thenReturn(12345); + when(mHttpResponse.getAllHeaders()).thenReturn(new Header[0]); + when(mHttpResponse.getEntity()).thenReturn(mHttpEntity); + when(mHttpEntity.getContentLength()).thenReturn(Integer.MAX_VALUE + 1L); + when(mHttpEntity.getContent()).thenReturn(mContent); + + mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS); + } + + @Test + public void responseWithHeaders() throws Exception { + when(mHttpStack.performRequest(REQUEST, ADDITIONAL_HEADERS)).thenReturn(mHttpResponse); + when(mStatusLine.getStatusCode()).thenReturn(12345); + when(mHttpResponse.getAllHeaders()).thenReturn(new Header[] { + new BasicHeader("header1", "value1_B"), + new BasicHeader("header3", "value3"), + new BasicHeader("HEADER2", "value2"), + new BasicHeader("header1", "value1_A") + }); + + com.android.volley.toolbox.HttpResponse response = + mAdaptedHttpStack.executeRequest(REQUEST, ADDITIONAL_HEADERS); + + assertEquals(12345, response.getStatusCode()); + assertNull(response.getContent()); + + List> orderedHeaders = new ArrayList<>(); + for (Map.Entry> entry : response.getHeaders().entrySet()) { + for (String value : entry.getValue()) { + orderedHeaders.add(Pair.create(entry.getKey(), value)); + } + } + + List> expectedHeaders = new ArrayList<>(); + expectedHeaders.add(Pair.create("header1", "value1_B")); + expectedHeaders.add(Pair.create("header1", "value1_A")); + expectedHeaders.add(Pair.create("HEADER2", "value2")); + expectedHeaders.add(Pair.create("header3", "value3")); + assertEquals(expectedHeaders, orderedHeaders); + } +} diff --git a/src/test/java/com/android/volley/toolbox/BaseHttpStackTest.java b/src/test/java/com/android/volley/toolbox/BaseHttpStackTest.java new file mode 100644 index 00000000..657b6167 --- /dev/null +++ b/src/test/java/com/android/volley/toolbox/BaseHttpStackTest.java @@ -0,0 +1,109 @@ +package com.android.volley.toolbox; + +import com.android.volley.AuthFailureError; +import com.android.volley.Request; +import com.android.volley.mock.TestRequest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +@RunWith(RobolectricTestRunner.class) +public class BaseHttpStackTest { + private static final Request REQUEST = new TestRequest.Get(); + private static final Map ADDITIONAL_HEADERS = Collections.emptyMap(); + + @Mock + private InputStream mContent; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void legacyRequestWithoutBody() throws Exception { + BaseHttpStack stack = new BaseHttpStack() { + @Override + public HttpResponse executeRequest( + Request request, Map additionalHeaders) + throws IOException, AuthFailureError { + assertSame(REQUEST, request); + assertSame(ADDITIONAL_HEADERS, additionalHeaders); + return new HttpResponse(12345, Collections.>emptyMap()); + } + }; + org.apache.http.HttpResponse resp = stack.performRequest(REQUEST, ADDITIONAL_HEADERS); + assertEquals(12345, resp.getStatusLine().getStatusCode()); + assertEquals(0, resp.getAllHeaders().length); + assertNull(resp.getEntity()); + } + + @Test + public void legacyResponseWithBody() throws Exception { + BaseHttpStack stack = new BaseHttpStack() { + @Override + public HttpResponse executeRequest( + Request request, Map additionalHeaders) + throws IOException, AuthFailureError { + assertSame(REQUEST, request); + assertSame(ADDITIONAL_HEADERS, additionalHeaders); + return new HttpResponse( + 12345, + Collections.>emptyMap(), + 555, + mContent); + } + }; + org.apache.http.HttpResponse resp = stack.performRequest(REQUEST, ADDITIONAL_HEADERS); + assertEquals(12345, resp.getStatusLine().getStatusCode()); + assertEquals(0, resp.getAllHeaders().length); + assertEquals(555L, resp.getEntity().getContentLength()); + assertSame(mContent, resp.getEntity().getContent()); + } + + @Test + public void legacyResponseHeaders() throws Exception { + BaseHttpStack stack = new BaseHttpStack() { + @Override + public HttpResponse executeRequest( + Request request, Map additionalHeaders) + throws IOException, AuthFailureError { + assertSame(REQUEST, request); + assertSame(ADDITIONAL_HEADERS, additionalHeaders); + Map> headers = new TreeMap<>(); + headers.put("HeaderA", Collections.singletonList("ValueA")); + List values = new ArrayList<>(); + values.add("ValueB_1"); + values.add("ValueB_2"); + headers.put("HeaderB", values); + return new HttpResponse(12345, headers); + } + }; + org.apache.http.HttpResponse resp = stack.performRequest(REQUEST, ADDITIONAL_HEADERS); + assertEquals(12345, resp.getStatusLine().getStatusCode()); + assertEquals(3, resp.getAllHeaders().length); + assertEquals("HeaderA", resp.getAllHeaders()[0].getName()); + assertEquals("ValueA", resp.getAllHeaders()[0].getValue()); + assertEquals("HeaderB", resp.getAllHeaders()[1].getName()); + assertEquals("ValueB_1", resp.getAllHeaders()[1].getValue()); + assertEquals("HeaderB", resp.getAllHeaders()[2].getName()); + assertEquals("ValueB_2", resp.getAllHeaders()[2].getValue()); + assertNull(resp.getEntity()); + } +} diff --git a/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java b/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java index c01d9b0d..486923ae 100644 --- a/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java +++ b/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java @@ -17,6 +17,7 @@ package com.android.volley.toolbox; import com.android.volley.AuthFailureError; +import com.android.volley.Cache.Entry; import com.android.volley.NetworkResponse; import com.android.volley.Request; import com.android.volley.Response; @@ -26,23 +27,27 @@ import com.android.volley.VolleyError; import com.android.volley.mock.MockHttpStack; -import org.apache.http.ProtocolVersion; -import org.apache.http.conn.ConnectTimeoutException; -import org.apache.http.entity.StringEntity; -import org.apache.http.message.BasicHttpResponse; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.robolectric.RobolectricTestRunner; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import java.net.SocketTimeoutException; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; import static org.mockito.MockitoAnnotations.initMocks; @RunWith(RobolectricTestRunner.class) @@ -50,7 +55,6 @@ public class BasicNetworkTest { @Mock private Request mMockRequest; @Mock private RetryPolicy mMockRetryPolicy; - private BasicNetwork mNetwork; @Before public void setUp() throws Exception { initMocks(this); @@ -58,14 +62,22 @@ public class BasicNetworkTest { @Test public void headersAndPostParams() throws Exception { MockHttpStack mockHttpStack = new MockHttpStack(); - BasicHttpResponse fakeResponse = new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), - 200, "OK"); - fakeResponse.setEntity(new StringEntity("foobar")); + InputStream responseStream = + new ByteArrayInputStream("foobar".getBytes()); + HttpResponse fakeResponse = new HttpResponse(200, + Collections.>emptyMap(), 6, responseStream); mockHttpStack.setResponseToReturn(fakeResponse); BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); Request request = buildRequest(); + Entry entry = new Entry(); + entry.etag = "foobar"; + entry.lastModified = 1503102002000L; + request.setCacheEntry(entry); httpNetwork.performRequest(request); assertEquals("foo", mockHttpStack.getLastHeaders().get("requestheader")); + assertEquals("foobar", mockHttpStack.getLastHeaders().get("If-None-Match")); + assertEquals("Sat, 19 Aug 2017 00:20:02 GMT", + mockHttpStack.getLastHeaders().get("If-Modified-Since")); assertEquals("requestpost=foo&", new String(mockHttpStack.getLastPostBody())); } @@ -85,22 +97,6 @@ public class BasicNetworkTest { verify(mMockRetryPolicy).retry(any(TimeoutError.class)); } - @Test public void connectTimeout() throws Exception { - MockHttpStack mockHttpStack = new MockHttpStack(); - mockHttpStack.setExceptionToThrow(new ConnectTimeoutException()); - BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); - Request request = buildRequest(); - request.setRetryPolicy(mMockRetryPolicy); - doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); - try { - httpNetwork.performRequest(request); - } catch (VolleyError e) { - // expected - } - // should retry connection timeouts - verify(mMockRetryPolicy).retry(any(TimeoutError.class)); - } - @Test public void noConnection() throws Exception { MockHttpStack mockHttpStack = new MockHttpStack(); mockHttpStack.setExceptionToThrow(new IOException()); @@ -119,8 +115,8 @@ public class BasicNetworkTest { @Test public void unauthorized() throws Exception { MockHttpStack mockHttpStack = new MockHttpStack(); - BasicHttpResponse fakeResponse = new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), - 401, "Unauthorized"); + HttpResponse fakeResponse = + new HttpResponse(401, Collections.>emptyMap()); mockHttpStack.setResponseToReturn(fakeResponse); BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); Request request = buildRequest(); @@ -137,8 +133,8 @@ public class BasicNetworkTest { @Test public void forbidden() throws Exception { MockHttpStack mockHttpStack = new MockHttpStack(); - BasicHttpResponse fakeResponse = new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), - 403, "Forbidden"); + HttpResponse fakeResponse = + new HttpResponse(403, Collections.>emptyMap()); mockHttpStack.setResponseToReturn(fakeResponse); BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); Request request = buildRequest(); @@ -156,8 +152,8 @@ public class BasicNetworkTest { @Test public void redirect() throws Exception { for (int i = 300; i <= 399; i++) { MockHttpStack mockHttpStack = new MockHttpStack(); - BasicHttpResponse fakeResponse = - new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), i, ""); + HttpResponse fakeResponse = + new HttpResponse(i, Collections.>emptyMap()); mockHttpStack.setResponseToReturn(fakeResponse); BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); Request request = buildRequest(); @@ -181,8 +177,8 @@ public class BasicNetworkTest { continue; } MockHttpStack mockHttpStack = new MockHttpStack(); - BasicHttpResponse fakeResponse = - new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), i, ""); + HttpResponse fakeResponse = + new HttpResponse(i, Collections.>emptyMap()); mockHttpStack.setResponseToReturn(fakeResponse); BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); Request request = buildRequest(); @@ -202,8 +198,8 @@ public class BasicNetworkTest { @Test public void serverError_enableRetries() throws Exception { for (int i = 500; i <= 599; i++) { MockHttpStack mockHttpStack = new MockHttpStack(); - BasicHttpResponse fakeResponse = - new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), i, ""); + HttpResponse fakeResponse = + new HttpResponse(i, Collections.>emptyMap()); mockHttpStack.setResponseToReturn(fakeResponse); BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack, new ByteArrayPool(4096)); @@ -225,8 +221,8 @@ public class BasicNetworkTest { @Test public void serverError_disableRetries() throws Exception { for (int i = 500; i <= 599; i++) { MockHttpStack mockHttpStack = new MockHttpStack(); - BasicHttpResponse fakeResponse = - new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), i, ""); + HttpResponse fakeResponse = + new HttpResponse(i, Collections.>emptyMap()); mockHttpStack.setResponseToReturn(fakeResponse); BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); Request request = buildRequest(); diff --git a/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java b/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java index fd8cf519..7d43d562 100644 --- a/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java +++ b/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java @@ -268,6 +268,8 @@ private static String rfc1123Date(long millis) { assertEquals("ISO-8859-1", HttpHeaderParser.parseCharset(headers)); } + // TODO(#21): Rewrite this test without using Apache HTTP after we rewrite the header logic + // to support multiple response headers with the same key. @Test public void parseCaseInsensitive() { long now = System.currentTimeMillis();