1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.volley.toolbox; 18 19 import com.android.volley.Cache; 20 import com.android.volley.Header; 21 import com.android.volley.NetworkResponse; 22 import com.android.volley.VolleyLog; 23 import java.text.ParseException; 24 import java.text.SimpleDateFormat; 25 import java.util.ArrayList; 26 import java.util.Date; 27 import java.util.List; 28 import java.util.Locale; 29 import java.util.Map; 30 import java.util.TimeZone; 31 import java.util.TreeMap; 32 33 /** Utility methods for parsing HTTP headers. */ 34 public class HttpHeaderParser { 35 36 static final String HEADER_CONTENT_TYPE = "Content-Type"; 37 38 private static final String DEFAULT_CONTENT_CHARSET = "ISO-8859-1"; 39 40 private static final String RFC1123_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz"; 41 42 /** 43 * Extracts a {@link com.android.volley.Cache.Entry} from a {@link NetworkResponse}. 44 * 45 * @param response The network response to parse headers from 46 * @return a cache entry for the given response, or null if the response is not cacheable. 47 */ parseCacheHeaders(NetworkResponse response)48 public static Cache.Entry parseCacheHeaders(NetworkResponse response) { 49 long now = System.currentTimeMillis(); 50 51 Map<String, String> headers = response.headers; 52 53 long serverDate = 0; 54 long lastModified = 0; 55 long serverExpires = 0; 56 long softExpire = 0; 57 long finalExpire = 0; 58 long maxAge = 0; 59 long staleWhileRevalidate = 0; 60 boolean hasCacheControl = false; 61 boolean mustRevalidate = false; 62 63 String serverEtag = null; 64 String headerValue; 65 66 headerValue = headers.get("Date"); 67 if (headerValue != null) { 68 serverDate = parseDateAsEpoch(headerValue); 69 } 70 71 headerValue = headers.get("Cache-Control"); 72 if (headerValue != null) { 73 hasCacheControl = true; 74 String[] tokens = headerValue.split(",", 0); 75 for (int i = 0; i < tokens.length; i++) { 76 String token = tokens[i].trim(); 77 if (token.equals("no-cache") || token.equals("no-store")) { 78 return null; 79 } else if (token.startsWith("max-age=")) { 80 try { 81 maxAge = Long.parseLong(token.substring(8)); 82 } catch (Exception e) { 83 } 84 } else if (token.startsWith("stale-while-revalidate=")) { 85 try { 86 staleWhileRevalidate = Long.parseLong(token.substring(23)); 87 } catch (Exception e) { 88 } 89 } else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) { 90 mustRevalidate = true; 91 } 92 } 93 } 94 95 headerValue = headers.get("Expires"); 96 if (headerValue != null) { 97 serverExpires = parseDateAsEpoch(headerValue); 98 } 99 100 headerValue = headers.get("Last-Modified"); 101 if (headerValue != null) { 102 lastModified = parseDateAsEpoch(headerValue); 103 } 104 105 serverEtag = headers.get("ETag"); 106 107 // Cache-Control takes precedence over an Expires header, even if both exist and Expires 108 // is more restrictive. 109 if (hasCacheControl) { 110 softExpire = now + maxAge * 1000; 111 finalExpire = mustRevalidate ? softExpire : softExpire + staleWhileRevalidate * 1000; 112 } else if (serverDate > 0 && serverExpires >= serverDate) { 113 // Default semantic for Expire header in HTTP specification is softExpire. 114 softExpire = now + (serverExpires - serverDate); 115 finalExpire = softExpire; 116 } 117 118 Cache.Entry entry = new Cache.Entry(); 119 entry.data = response.data; 120 entry.etag = serverEtag; 121 entry.softTtl = softExpire; 122 entry.ttl = finalExpire; 123 entry.serverDate = serverDate; 124 entry.lastModified = lastModified; 125 entry.responseHeaders = headers; 126 entry.allResponseHeaders = response.allHeaders; 127 128 return entry; 129 } 130 131 /** Parse date in RFC1123 format, and return its value as epoch */ parseDateAsEpoch(String dateStr)132 public static long parseDateAsEpoch(String dateStr) { 133 try { 134 // Parse date in RFC1123 format if this header contains one 135 return newRfc1123Formatter().parse(dateStr).getTime(); 136 } catch (ParseException e) { 137 // Date in invalid format, fallback to 0 138 VolleyLog.e(e, "Unable to parse dateStr: %s, falling back to 0", dateStr); 139 return 0; 140 } 141 } 142 143 /** Format an epoch date in RFC1123 format. */ formatEpochAsRfc1123(long epoch)144 static String formatEpochAsRfc1123(long epoch) { 145 return newRfc1123Formatter().format(new Date(epoch)); 146 } 147 newRfc1123Formatter()148 private static SimpleDateFormat newRfc1123Formatter() { 149 SimpleDateFormat formatter = new SimpleDateFormat(RFC1123_FORMAT, Locale.US); 150 formatter.setTimeZone(TimeZone.getTimeZone("GMT")); 151 return formatter; 152 } 153 154 /** 155 * Retrieve a charset from headers 156 * 157 * @param headers An {@link java.util.Map} of headers 158 * @param defaultCharset Charset to return if none can be found 159 * @return Returns the charset specified in the Content-Type of this header, or the 160 * defaultCharset if none can be found. 161 */ parseCharset(Map<String, String> headers, String defaultCharset)162 public static String parseCharset(Map<String, String> headers, String defaultCharset) { 163 String contentType = headers.get(HEADER_CONTENT_TYPE); 164 if (contentType != null) { 165 String[] params = contentType.split(";", 0); 166 for (int i = 1; i < params.length; i++) { 167 String[] pair = params[i].trim().split("=", 0); 168 if (pair.length == 2) { 169 if (pair[0].equals("charset")) { 170 return pair[1]; 171 } 172 } 173 } 174 } 175 176 return defaultCharset; 177 } 178 179 /** 180 * Returns the charset specified in the Content-Type of this header, or the HTTP default 181 * (ISO-8859-1) if none can be found. 182 */ parseCharset(Map<String, String> headers)183 public static String parseCharset(Map<String, String> headers) { 184 return parseCharset(headers, DEFAULT_CONTENT_CHARSET); 185 } 186 187 // Note - these are copied from NetworkResponse to avoid making them public (as needed to access 188 // them from the .toolbox package), which would mean they'd become part of the Volley API. 189 // TODO: Consider obfuscating official releases so we can share utility methods between Volley 190 // and Toolbox without making them public APIs. 191 toHeaderMap(List<Header> allHeaders)192 static Map<String, String> toHeaderMap(List<Header> allHeaders) { 193 Map<String, String> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); 194 // Later elements in the list take precedence. 195 for (Header header : allHeaders) { 196 headers.put(header.getName(), header.getValue()); 197 } 198 return headers; 199 } 200 toAllHeaderList(Map<String, String> headers)201 static List<Header> toAllHeaderList(Map<String, String> headers) { 202 List<Header> allHeaders = new ArrayList<>(headers.size()); 203 for (Map.Entry<String, String> header : headers.entrySet()) { 204 allHeaders.add(new Header(header.getKey(), header.getValue())); 205 } 206 return allHeaders; 207 } 208 } 209