aboutsummaryrefslogtreecommitdiff
path: root/core/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
diff options
context:
space:
mode:
Diffstat (limited to 'core/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java')
-rw-r--r--core/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java301
1 files changed, 301 insertions, 0 deletions
diff --git a/core/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java b/core/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
new file mode 100644
index 0000000..0b29e80
--- /dev/null
+++ b/core/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java
@@ -0,0 +1,301 @@
+/*
+ * 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.toolbox;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import com.android.volley.Cache;
+import com.android.volley.Header;
+import com.android.volley.NetworkResponse;
+import com.android.volley.VolleyLog;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.TimeZone;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+/** Utility methods for parsing HTTP headers. */
+public class HttpHeaderParser {
+
+ @RestrictTo({Scope.LIBRARY_GROUP})
+ public static final String HEADER_CONTENT_TYPE = "Content-Type";
+
+ private static final String DEFAULT_CONTENT_CHARSET = "ISO-8859-1";
+
+ private static final String RFC1123_PARSE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz";
+
+ // Hardcode 'GMT' rather than using 'zzz' since some platforms append an extraneous +00:00.
+ // See #287.
+ private static final String RFC1123_OUTPUT_FORMAT = "EEE, dd MMM yyyy HH:mm:ss 'GMT'";
+
+ /**
+ * Extracts a {@link com.android.volley.Cache.Entry} from a {@link NetworkResponse}.
+ *
+ * @param response The network response to parse headers from
+ * @return a cache entry for the given response, or null if the response is not cacheable.
+ */
+ @Nullable
+ public static Cache.Entry parseCacheHeaders(NetworkResponse response) {
+ long now = System.currentTimeMillis();
+
+ Map<String, String> headers = response.headers;
+ if (headers == null) {
+ return null;
+ }
+
+ long serverDate = 0;
+ long lastModified = 0;
+ long serverExpires = 0;
+ long softExpire = 0;
+ long finalExpire = 0;
+ long maxAge = 0;
+ long staleWhileRevalidate = 0;
+ boolean hasCacheControl = false;
+ boolean mustRevalidate = false;
+
+ String serverEtag = null;
+ String headerValue;
+
+ headerValue = headers.get("Date");
+ if (headerValue != null) {
+ serverDate = parseDateAsEpoch(headerValue);
+ }
+
+ headerValue = headers.get("Cache-Control");
+ if (headerValue != null) {
+ hasCacheControl = true;
+ String[] tokens = headerValue.split(",", 0);
+ for (int i = 0; i < tokens.length; i++) {
+ String token = tokens[i].trim();
+ if (token.equals("no-cache") || token.equals("no-store")) {
+ return null;
+ } else if (token.startsWith("max-age=")) {
+ try {
+ maxAge = Long.parseLong(token.substring(8));
+ } catch (Exception e) {
+ }
+ } else if (token.startsWith("stale-while-revalidate=")) {
+ try {
+ staleWhileRevalidate = Long.parseLong(token.substring(23));
+ } catch (Exception e) {
+ }
+ } else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) {
+ mustRevalidate = true;
+ }
+ }
+ }
+
+ headerValue = headers.get("Expires");
+ if (headerValue != null) {
+ serverExpires = parseDateAsEpoch(headerValue);
+ }
+
+ headerValue = headers.get("Last-Modified");
+ if (headerValue != null) {
+ lastModified = parseDateAsEpoch(headerValue);
+ }
+
+ serverEtag = headers.get("ETag");
+
+ // Cache-Control takes precedence over an Expires header, even if both exist and Expires
+ // is more restrictive.
+ if (hasCacheControl) {
+ softExpire = now + maxAge * 1000;
+ finalExpire = mustRevalidate ? softExpire : softExpire + staleWhileRevalidate * 1000;
+ } else if (serverDate > 0 && serverExpires >= serverDate) {
+ // Default semantic for Expire header in HTTP specification is softExpire.
+ softExpire = now + (serverExpires - serverDate);
+ finalExpire = softExpire;
+ }
+
+ Cache.Entry entry = new Cache.Entry();
+ entry.data = response.data;
+ entry.etag = serverEtag;
+ entry.softTtl = softExpire;
+ entry.ttl = finalExpire;
+ entry.serverDate = serverDate;
+ entry.lastModified = lastModified;
+ entry.responseHeaders = headers;
+ entry.allResponseHeaders = response.allHeaders;
+
+ return entry;
+ }
+
+ /** Parse date in RFC1123 format, and return its value as epoch */
+ public static long parseDateAsEpoch(String dateStr) {
+ try {
+ // Parse date in RFC1123 format if this header contains one
+ return newUsGmtFormatter(RFC1123_PARSE_FORMAT).parse(dateStr).getTime();
+ } catch (ParseException e) {
+ // Date in invalid format, fallback to 0
+ // If the value is either "0" or "-1" we only log to verbose,
+ // these values are pretty common and cause log spam.
+ String message = "Unable to parse dateStr: %s, falling back to 0";
+ if ("0".equals(dateStr) || "-1".equals(dateStr)) {
+ VolleyLog.v(message, dateStr);
+ } else {
+ VolleyLog.e(e, message, dateStr);
+ }
+
+ return 0;
+ }
+ }
+
+ /** Format an epoch date in RFC1123 format. */
+ static String formatEpochAsRfc1123(long epoch) {
+ return newUsGmtFormatter(RFC1123_OUTPUT_FORMAT).format(new Date(epoch));
+ }
+
+ private static SimpleDateFormat newUsGmtFormatter(String format) {
+ SimpleDateFormat formatter = new SimpleDateFormat(format, Locale.US);
+ formatter.setTimeZone(TimeZone.getTimeZone("GMT"));
+ return formatter;
+ }
+
+ /**
+ * Retrieve a charset from headers
+ *
+ * @param headers An {@link java.util.Map} of headers
+ * @param defaultCharset Charset to return if none can be found
+ * @return Returns the charset specified in the Content-Type of this header, or the
+ * defaultCharset if none can be found.
+ */
+ public static String parseCharset(
+ @Nullable Map<String, String> headers, String defaultCharset) {
+ if (headers == null) {
+ return defaultCharset;
+ }
+ String contentType = headers.get(HEADER_CONTENT_TYPE);
+ if (contentType != null) {
+ String[] params = contentType.split(";", 0);
+ for (int i = 1; i < params.length; i++) {
+ String[] pair = params[i].trim().split("=", 0);
+ if (pair.length == 2) {
+ if (pair[0].equals("charset")) {
+ return pair[1];
+ }
+ }
+ }
+ }
+
+ return defaultCharset;
+ }
+
+ /**
+ * Returns the charset specified in the Content-Type of this header, or the HTTP default
+ * (ISO-8859-1) if none can be found.
+ */
+ public static String parseCharset(@Nullable Map<String, String> headers) {
+ 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;
+ }
+
+ /**
+ * 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.
+ */
+ static List<Header> combineHeaders(List<Header> responseHeaders, Cache.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;
+ }
+
+ static Map<String, String> getCacheHeaders(Cache.Entry entry) {
+ // If there's no cache entry, we're done.
+ if (entry == null) {
+ return Collections.emptyMap();
+ }
+
+ Map<String, String> headers = new HashMap<>();
+
+ if (entry.etag != null) {
+ headers.put("If-None-Match", entry.etag);
+ }
+
+ if (entry.lastModified > 0) {
+ headers.put(
+ "If-Modified-Since", HttpHeaderParser.formatEpochAsRfc1123(entry.lastModified));
+ }
+
+ return headers;
+ }
+}