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.NetworkResponse; 21 22 import org.apache.http.impl.cookie.DateParseException; 23 import org.apache.http.impl.cookie.DateUtils; 24 import org.apache.http.protocol.HTTP; 25 26 import java.util.Map; 27 28 /** 29 * Utility methods for parsing HTTP headers. 30 */ 31 public class HttpHeaderParser { 32 33 /** 34 * Extracts a {@link Cache.Entry} from a {@link NetworkResponse}. 35 * 36 * @param response The network response to parse headers from 37 * @return a cache entry for the given response, or null if the response is not cacheable. 38 */ parseCacheHeaders(NetworkResponse response)39 public static Cache.Entry parseCacheHeaders(NetworkResponse response) { 40 long now = System.currentTimeMillis(); 41 42 Map<String, String> headers = response.headers; 43 44 long serverDate = 0; 45 long lastModified = 0; 46 long serverExpires = 0; 47 long softExpire = 0; 48 long finalExpire = 0; 49 long maxAge = 0; 50 long staleWhileRevalidate = 0; 51 boolean hasCacheControl = false; 52 boolean mustRevalidate = false; 53 54 String serverEtag = null; 55 String headerValue; 56 57 headerValue = headers.get("Date"); 58 if (headerValue != null) { 59 serverDate = parseDateAsEpoch(headerValue); 60 } 61 62 headerValue = headers.get("Cache-Control"); 63 if (headerValue != null) { 64 hasCacheControl = true; 65 String[] tokens = headerValue.split(","); 66 for (int i = 0; i < tokens.length; i++) { 67 String token = tokens[i].trim(); 68 if (token.equals("no-cache") || token.equals("no-store")) { 69 return null; 70 } else if (token.startsWith("max-age=")) { 71 try { 72 maxAge = Long.parseLong(token.substring(8)); 73 } catch (Exception e) { 74 } 75 } else if (token.startsWith("stale-while-revalidate=")) { 76 try { 77 staleWhileRevalidate = Long.parseLong(token.substring(23)); 78 } catch (Exception e) { 79 } 80 } else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) { 81 mustRevalidate = true; 82 } 83 } 84 } 85 86 headerValue = headers.get("Expires"); 87 if (headerValue != null) { 88 serverExpires = parseDateAsEpoch(headerValue); 89 } 90 91 headerValue = headers.get("Last-Modified"); 92 if (headerValue != null) { 93 lastModified = parseDateAsEpoch(headerValue); 94 } 95 96 serverEtag = headers.get("ETag"); 97 98 // Cache-Control takes precedence over an Expires header, even if both exist and Expires 99 // is more restrictive. 100 if (hasCacheControl) { 101 softExpire = now + maxAge * 1000; 102 finalExpire = mustRevalidate 103 ? softExpire 104 : softExpire + staleWhileRevalidate * 1000; 105 } else if (serverDate > 0 && serverExpires >= serverDate) { 106 // Default semantic for Expire header in HTTP specification is softExpire. 107 softExpire = now + (serverExpires - serverDate); 108 finalExpire = softExpire; 109 } 110 111 Cache.Entry entry = new Cache.Entry(); 112 entry.data = response.data; 113 entry.etag = serverEtag; 114 entry.softTtl = softExpire; 115 entry.ttl = finalExpire; 116 entry.serverDate = serverDate; 117 entry.lastModified = lastModified; 118 entry.responseHeaders = headers; 119 120 return entry; 121 } 122 123 /** 124 * Parse date in RFC1123 format, and return its value as epoch 125 */ parseDateAsEpoch(String dateStr)126 public static long parseDateAsEpoch(String dateStr) { 127 try { 128 // Parse date in RFC1123 format if this header contains one 129 return DateUtils.parseDate(dateStr).getTime(); 130 } catch (DateParseException e) { 131 // Date in invalid format, fallback to 0 132 return 0; 133 } 134 } 135 136 /** 137 * Retrieve a charset from headers 138 * 139 * @param headers An {@link java.util.Map} of headers 140 * @param defaultCharset Charset to return if none can be found 141 * @return Returns the charset specified in the Content-Type of this header, 142 * or the defaultCharset if none can be found. 143 */ parseCharset(Map<String, String> headers, String defaultCharset)144 public static String parseCharset(Map<String, String> headers, String defaultCharset) { 145 String contentType = headers.get(HTTP.CONTENT_TYPE); 146 if (contentType != null) { 147 String[] params = contentType.split(";"); 148 for (int i = 1; i < params.length; i++) { 149 String[] pair = params[i].trim().split("="); 150 if (pair.length == 2) { 151 if (pair[0].equals("charset")) { 152 return pair[1]; 153 } 154 } 155 } 156 } 157 158 return defaultCharset; 159 } 160 161 /** 162 * Returns the charset specified in the Content-Type of this header, 163 * or the HTTP default (ISO-8859-1) if none can be found. 164 */ parseCharset(Map<String, String> headers)165 public static String parseCharset(Map<String, String> headers) { 166 return parseCharset(headers, HTTP.DEFAULT_CONTENT_CHARSET); 167 } 168 } 169