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