1 /* 2 * ==================================================================== 3 * Licensed to the Apache Software Foundation (ASF) under one 4 * or more contributor license agreements. See the NOTICE file 5 * distributed with this work for additional information 6 * regarding copyright ownership. The ASF licenses this file 7 * to you under the Apache License, Version 2.0 (the 8 * "License"); you may not use this file except in compliance 9 * with the License. You may obtain a copy of the License at 10 * 11 * http://www.apache.org/licenses/LICENSE-2.0 12 * 13 * Unless required by applicable law or agreed to in writing, 14 * software distributed under the License is distributed on an 15 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 * KIND, either express or implied. See the License for the 17 * specific language governing permissions and limitations 18 * under the License. 19 * ==================================================================== 20 * 21 * This software consists of voluntary contributions made by many 22 * individuals on behalf of the Apache Software Foundation. For more 23 * information on the Apache Software Foundation, please see 24 * <http://www.apache.org/>. 25 * 26 */ 27 package ch.boye.httpclientandroidlib.impl.client.cache; 28 29 import java.util.Date; 30 31 import ch.boye.httpclientandroidlib.Header; 32 import ch.boye.httpclientandroidlib.HeaderElement; 33 import ch.boye.httpclientandroidlib.HttpRequest; 34 import ch.boye.httpclientandroidlib.annotation.Immutable; 35 import ch.boye.httpclientandroidlib.client.cache.HeaderConstants; 36 import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry; 37 import ch.boye.httpclientandroidlib.client.utils.DateUtils; 38 import ch.boye.httpclientandroidlib.protocol.HTTP; 39 40 /** 41 * @since 4.1 42 */ 43 @Immutable 44 class CacheValidityPolicy { 45 46 public static final long MAX_AGE = 2147483648L; 47 CacheValidityPolicy()48 CacheValidityPolicy() { 49 super(); 50 } 51 getCurrentAgeSecs(final HttpCacheEntry entry, final Date now)52 public long getCurrentAgeSecs(final HttpCacheEntry entry, final Date now) { 53 return getCorrectedInitialAgeSecs(entry) + getResidentTimeSecs(entry, now); 54 } 55 getFreshnessLifetimeSecs(final HttpCacheEntry entry)56 public long getFreshnessLifetimeSecs(final HttpCacheEntry entry) { 57 final long maxage = getMaxAge(entry); 58 if (maxage > -1) { 59 return maxage; 60 } 61 62 final Date dateValue = entry.getDate(); 63 if (dateValue == null) { 64 return 0L; 65 } 66 67 final Date expiry = getExpirationDate(entry); 68 if (expiry == null) { 69 return 0; 70 } 71 final long diff = expiry.getTime() - dateValue.getTime(); 72 return (diff / 1000); 73 } 74 isResponseFresh(final HttpCacheEntry entry, final Date now)75 public boolean isResponseFresh(final HttpCacheEntry entry, final Date now) { 76 return (getCurrentAgeSecs(entry, now) < getFreshnessLifetimeSecs(entry)); 77 } 78 79 /** 80 * Decides if this response is fresh enough based Last-Modified and Date, if available. 81 * This entry is meant to be used when isResponseFresh returns false. The algorithm is as follows: 82 * 83 * if last-modified and date are defined, freshness lifetime is coefficient*(date-lastModified), 84 * else freshness lifetime is defaultLifetime 85 * 86 * @param entry the cache entry 87 * @param now what time is it currently (When is right NOW) 88 * @param coefficient Part of the heuristic for cache entry freshness 89 * @param defaultLifetime How long can I assume a cache entry is default TTL 90 * @return {@code true} if the response is fresh 91 */ isResponseHeuristicallyFresh(final HttpCacheEntry entry, final Date now, final float coefficient, final long defaultLifetime)92 public boolean isResponseHeuristicallyFresh(final HttpCacheEntry entry, 93 final Date now, final float coefficient, final long defaultLifetime) { 94 return (getCurrentAgeSecs(entry, now) < getHeuristicFreshnessLifetimeSecs(entry, coefficient, defaultLifetime)); 95 } 96 getHeuristicFreshnessLifetimeSecs(final HttpCacheEntry entry, final float coefficient, final long defaultLifetime)97 public long getHeuristicFreshnessLifetimeSecs(final HttpCacheEntry entry, 98 final float coefficient, final long defaultLifetime) { 99 final Date dateValue = entry.getDate(); 100 final Date lastModifiedValue = getLastModifiedValue(entry); 101 102 if (dateValue != null && lastModifiedValue != null) { 103 final long diff = dateValue.getTime() - lastModifiedValue.getTime(); 104 if (diff < 0) { 105 return 0; 106 } 107 return (long)(coefficient * (diff / 1000)); 108 } 109 110 return defaultLifetime; 111 } 112 isRevalidatable(final HttpCacheEntry entry)113 public boolean isRevalidatable(final HttpCacheEntry entry) { 114 return entry.getFirstHeader(HeaderConstants.ETAG) != null 115 || entry.getFirstHeader(HeaderConstants.LAST_MODIFIED) != null; 116 } 117 mustRevalidate(final HttpCacheEntry entry)118 public boolean mustRevalidate(final HttpCacheEntry entry) { 119 return hasCacheControlDirective(entry, HeaderConstants.CACHE_CONTROL_MUST_REVALIDATE); 120 } 121 proxyRevalidate(final HttpCacheEntry entry)122 public boolean proxyRevalidate(final HttpCacheEntry entry) { 123 return hasCacheControlDirective(entry, HeaderConstants.CACHE_CONTROL_PROXY_REVALIDATE); 124 } 125 mayReturnStaleWhileRevalidating(final HttpCacheEntry entry, final Date now)126 public boolean mayReturnStaleWhileRevalidating(final HttpCacheEntry entry, final Date now) { 127 for (final Header h : entry.getHeaders(HeaderConstants.CACHE_CONTROL)) { 128 for(final HeaderElement elt : h.getElements()) { 129 if (HeaderConstants.STALE_WHILE_REVALIDATE.equalsIgnoreCase(elt.getName())) { 130 try { 131 final int allowedStalenessLifetime = Integer.parseInt(elt.getValue()); 132 if (getStalenessSecs(entry, now) <= allowedStalenessLifetime) { 133 return true; 134 } 135 } catch (final NumberFormatException nfe) { 136 // skip malformed directive 137 } 138 } 139 } 140 } 141 142 return false; 143 } 144 mayReturnStaleIfError(final HttpRequest request, final HttpCacheEntry entry, final Date now)145 public boolean mayReturnStaleIfError(final HttpRequest request, 146 final HttpCacheEntry entry, final Date now) { 147 final long stalenessSecs = getStalenessSecs(entry, now); 148 return mayReturnStaleIfError(request.getHeaders(HeaderConstants.CACHE_CONTROL), 149 stalenessSecs) 150 || mayReturnStaleIfError(entry.getHeaders(HeaderConstants.CACHE_CONTROL), 151 stalenessSecs); 152 } 153 mayReturnStaleIfError(final Header[] headers, final long stalenessSecs)154 private boolean mayReturnStaleIfError(final Header[] headers, final long stalenessSecs) { 155 boolean result = false; 156 for(final Header h : headers) { 157 for(final HeaderElement elt : h.getElements()) { 158 if (HeaderConstants.STALE_IF_ERROR.equals(elt.getName())) { 159 try { 160 final int staleIfErrorSecs = Integer.parseInt(elt.getValue()); 161 if (stalenessSecs <= staleIfErrorSecs) { 162 result = true; 163 break; 164 } 165 } catch (final NumberFormatException nfe) { 166 // skip malformed directive 167 } 168 } 169 } 170 } 171 return result; 172 } 173 174 /** 175 * @deprecated (4.3) use {@link HttpCacheEntry#getDate()}. 176 * @param entry 177 * @return the Date of the entry 178 */ 179 @Deprecated getDateValue(final HttpCacheEntry entry)180 protected Date getDateValue(final HttpCacheEntry entry) { 181 return entry.getDate(); 182 } 183 getLastModifiedValue(final HttpCacheEntry entry)184 protected Date getLastModifiedValue(final HttpCacheEntry entry) { 185 final Header dateHdr = entry.getFirstHeader(HeaderConstants.LAST_MODIFIED); 186 if (dateHdr == null) { 187 return null; 188 } 189 return DateUtils.parseDate(dateHdr.getValue()); 190 } 191 getContentLengthValue(final HttpCacheEntry entry)192 protected long getContentLengthValue(final HttpCacheEntry entry) { 193 final Header cl = entry.getFirstHeader(HTTP.CONTENT_LEN); 194 if (cl == null) { 195 return -1; 196 } 197 198 try { 199 return Long.parseLong(cl.getValue()); 200 } catch (final NumberFormatException ex) { 201 return -1; 202 } 203 } 204 hasContentLengthHeader(final HttpCacheEntry entry)205 protected boolean hasContentLengthHeader(final HttpCacheEntry entry) { 206 return null != entry.getFirstHeader(HTTP.CONTENT_LEN); 207 } 208 209 /** 210 * This matters for deciding whether the cache entry is valid to serve as a 211 * response. If these values do not match, we might have a partial response 212 * 213 * @param entry The cache entry we are currently working with 214 * @return boolean indicating whether actual length matches Content-Length 215 */ contentLengthHeaderMatchesActualLength(final HttpCacheEntry entry)216 protected boolean contentLengthHeaderMatchesActualLength(final HttpCacheEntry entry) { 217 return !hasContentLengthHeader(entry) || getContentLengthValue(entry) == entry.getResource().length(); 218 } 219 getApparentAgeSecs(final HttpCacheEntry entry)220 protected long getApparentAgeSecs(final HttpCacheEntry entry) { 221 final Date dateValue = entry.getDate(); 222 if (dateValue == null) { 223 return MAX_AGE; 224 } 225 final long diff = entry.getResponseDate().getTime() - dateValue.getTime(); 226 if (diff < 0L) { 227 return 0; 228 } 229 return (diff / 1000); 230 } 231 getAgeValue(final HttpCacheEntry entry)232 protected long getAgeValue(final HttpCacheEntry entry) { 233 long ageValue = 0; 234 for (final Header hdr : entry.getHeaders(HeaderConstants.AGE)) { 235 long hdrAge; 236 try { 237 hdrAge = Long.parseLong(hdr.getValue()); 238 if (hdrAge < 0) { 239 hdrAge = MAX_AGE; 240 } 241 } catch (final NumberFormatException nfe) { 242 hdrAge = MAX_AGE; 243 } 244 ageValue = (hdrAge > ageValue) ? hdrAge : ageValue; 245 } 246 return ageValue; 247 } 248 getCorrectedReceivedAgeSecs(final HttpCacheEntry entry)249 protected long getCorrectedReceivedAgeSecs(final HttpCacheEntry entry) { 250 final long apparentAge = getApparentAgeSecs(entry); 251 final long ageValue = getAgeValue(entry); 252 return (apparentAge > ageValue) ? apparentAge : ageValue; 253 } 254 getResponseDelaySecs(final HttpCacheEntry entry)255 protected long getResponseDelaySecs(final HttpCacheEntry entry) { 256 final long diff = entry.getResponseDate().getTime() - entry.getRequestDate().getTime(); 257 return (diff / 1000L); 258 } 259 getCorrectedInitialAgeSecs(final HttpCacheEntry entry)260 protected long getCorrectedInitialAgeSecs(final HttpCacheEntry entry) { 261 return getCorrectedReceivedAgeSecs(entry) + getResponseDelaySecs(entry); 262 } 263 getResidentTimeSecs(final HttpCacheEntry entry, final Date now)264 protected long getResidentTimeSecs(final HttpCacheEntry entry, final Date now) { 265 final long diff = now.getTime() - entry.getResponseDate().getTime(); 266 return (diff / 1000L); 267 } 268 getMaxAge(final HttpCacheEntry entry)269 protected long getMaxAge(final HttpCacheEntry entry) { 270 long maxage = -1; 271 for (final Header hdr : entry.getHeaders(HeaderConstants.CACHE_CONTROL)) { 272 for (final HeaderElement elt : hdr.getElements()) { 273 if (HeaderConstants.CACHE_CONTROL_MAX_AGE.equals(elt.getName()) 274 || "s-maxage".equals(elt.getName())) { 275 try { 276 final long currMaxAge = Long.parseLong(elt.getValue()); 277 if (maxage == -1 || currMaxAge < maxage) { 278 maxage = currMaxAge; 279 } 280 } catch (final NumberFormatException nfe) { 281 // be conservative if can't parse 282 maxage = 0; 283 } 284 } 285 } 286 } 287 return maxage; 288 } 289 getExpirationDate(final HttpCacheEntry entry)290 protected Date getExpirationDate(final HttpCacheEntry entry) { 291 final Header expiresHeader = entry.getFirstHeader(HeaderConstants.EXPIRES); 292 if (expiresHeader == null) { 293 return null; 294 } 295 return DateUtils.parseDate(expiresHeader.getValue()); 296 } 297 hasCacheControlDirective(final HttpCacheEntry entry, final String directive)298 public boolean hasCacheControlDirective(final HttpCacheEntry entry, 299 final String directive) { 300 for (final Header h : entry.getHeaders(HeaderConstants.CACHE_CONTROL)) { 301 for(final HeaderElement elt : h.getElements()) { 302 if (directive.equalsIgnoreCase(elt.getName())) { 303 return true; 304 } 305 } 306 } 307 return false; 308 } 309 getStalenessSecs(final HttpCacheEntry entry, final Date now)310 public long getStalenessSecs(final HttpCacheEntry entry, final Date now) { 311 final long age = getCurrentAgeSecs(entry, now); 312 final long freshness = getFreshnessLifetimeSecs(entry); 313 if (age <= freshness) { 314 return 0L; 315 } 316 return (age - freshness); 317 } 318 319 320 } 321