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