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 17package com.android.volley.toolbox; 18 19import com.android.volley.Cache; 20import com.android.volley.NetworkResponse; 21 22import org.apache.http.impl.cookie.DateParseException; 23import org.apache.http.impl.cookie.DateUtils; 24import org.apache.http.protocol.HTTP; 25 26import java.util.Map; 27 28/** 29 * Utility methods for parsing HTTP headers. 30 */ 31public 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 */ 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 serverExpires = 0; 46 long softExpire = 0; 47 long maxAge = 0; 48 boolean hasCacheControl = false; 49 50 String serverEtag = null; 51 String headerValue; 52 53 headerValue = headers.get("Date"); 54 if (headerValue != null) { 55 serverDate = parseDateAsEpoch(headerValue); 56 } 57 58 headerValue = headers.get("Cache-Control"); 59 if (headerValue != null) { 60 hasCacheControl = true; 61 String[] tokens = headerValue.split(","); 62 for (int i = 0; i < tokens.length; i++) { 63 String token = tokens[i].trim(); 64 if (token.equals("no-cache") || token.equals("no-store")) { 65 return null; 66 } else if (token.startsWith("max-age=")) { 67 try { 68 maxAge = Long.parseLong(token.substring(8)); 69 } catch (Exception e) { 70 } 71 } else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) { 72 maxAge = 0; 73 } 74 } 75 } 76 77 headerValue = headers.get("Expires"); 78 if (headerValue != null) { 79 serverExpires = parseDateAsEpoch(headerValue); 80 } 81 82 serverEtag = headers.get("ETag"); 83 84 // Cache-Control takes precedence over an Expires header, even if both exist and Expires 85 // is more restrictive. 86 if (hasCacheControl) { 87 softExpire = now + maxAge * 1000; 88 } else if (serverDate > 0 && serverExpires >= serverDate) { 89 // Default semantic for Expire header in HTTP specification is softExpire. 90 softExpire = now + (serverExpires - serverDate); 91 } 92 93 Cache.Entry entry = new Cache.Entry(); 94 entry.data = response.data; 95 entry.etag = serverEtag; 96 entry.softTtl = softExpire; 97 entry.ttl = entry.softTtl; 98 entry.serverDate = serverDate; 99 100 return entry; 101 } 102 103 /** 104 * Parse date in RFC1123 format, and return its value as epoch 105 */ 106 public static long parseDateAsEpoch(String dateStr) { 107 try { 108 // Parse date in RFC1123 format if this header contains one 109 return DateUtils.parseDate(dateStr).getTime(); 110 } catch (DateParseException e) { 111 // Date in invalid format, fallback to 0 112 return 0; 113 } 114 } 115 116 /** 117 * Returns the charset specified in the Content-Type of this header, 118 * or the HTTP default (ISO-8859-1) if none can be found. 119 */ 120 public static String parseCharset(Map<String, String> headers) { 121 String contentType = headers.get(HTTP.CONTENT_TYPE); 122 if (contentType != null) { 123 String[] params = contentType.split(";"); 124 for (int i = 1; i < params.length; i++) { 125 String[] pair = params[i].trim().split("="); 126 if (pair.length == 2) { 127 if (pair[0].equals("charset")) { 128 return pair[1]; 129 } 130 } 131 } 132 } 133 134 return HTTP.DEFAULT_CONTENT_CHARSET; 135 } 136} 137