View Javadoc
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 org.apache.hc.client5.http.impl.cache;
28  
29  import java.util.Date;
30  import java.util.Iterator;
31  
32  import org.apache.hc.client5.http.cache.HeaderConstants;
33  import org.apache.hc.client5.http.cache.HttpCacheEntry;
34  import org.apache.hc.client5.http.cache.Resource;
35  import org.apache.hc.client5.http.utils.DateUtils;
36  import org.apache.hc.core5.http.Header;
37  import org.apache.hc.core5.http.HeaderElement;
38  import org.apache.hc.core5.http.HttpHeaders;
39  import org.apache.hc.core5.http.HttpRequest;
40  import org.apache.hc.core5.http.MessageHeaders;
41  import org.apache.hc.core5.http.message.MessageSupport;
42  import org.apache.hc.core5.util.TimeValue;
43  
44  class CacheValidityPolicy {
45  
46      public static final TimeValue MAX_AGE = TimeValue.ofSeconds(Integer.MAX_VALUE + 1L);
47  
48      CacheValidityPolicy() {
49          super();
50      }
51  
52      public TimeValue getCurrentAge(final HttpCacheEntry entry, final Date now) {
53          return TimeValue.ofSeconds(getCorrectedInitialAge(entry).toSeconds() + getResidentTime(entry, now).toSeconds());
54      }
55  
56      public TimeValue getFreshnessLifetime(final HttpCacheEntry entry) {
57          final long maxAge = getMaxAge(entry);
58          if (maxAge > -1) {
59              return TimeValue.ofSeconds(maxAge);
60          }
61  
62          final Date dateValue = entry.getDate();
63          if (dateValue == null) {
64              return TimeValue.ZERO_MILLISECONDS;
65          }
66  
67          final Date expiry = DateUtils.parseDate(entry, HeaderConstants.EXPIRES);
68          if (expiry == null) {
69              return TimeValue.ZERO_MILLISECONDS;
70          }
71          final long diff = expiry.getTime() - dateValue.getTime();
72          return TimeValue.ofSeconds(diff / 1000);
73      }
74  
75      public boolean isResponseFresh(final HttpCacheEntry entry, final Date now) {
76          return getCurrentAge(entry, now).compareTo(getFreshnessLifetime(entry)) == -1;
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.
82       *
83       * The algorithm is as follows:
84       * if last-modified and date are defined, freshness lifetime is coefficient*(date-lastModified),
85       * else freshness lifetime is defaultLifetime
86       *
87       * @param entry the cache entry
88       * @param now what time is it currently (When is right NOW)
89       * @param coefficient Part of the heuristic for cache entry freshness
90       * @param defaultLifetime How long can I assume a cache entry is default TTL
91       * @return {@code true} if the response is fresh
92       */
93      public boolean isResponseHeuristicallyFresh(final HttpCacheEntry entry,
94              final Date now, final float coefficient, final TimeValue defaultLifetime) {
95          return getCurrentAge(entry, now).compareTo(getHeuristicFreshnessLifetime(entry, coefficient, defaultLifetime)) == -1;
96      }
97  
98      public TimeValue getHeuristicFreshnessLifetime(final HttpCacheEntry entry,
99              final float coefficient, final TimeValue defaultLifetime) {
100         final Date dateValue = entry.getDate();
101         final Date lastModifiedValue = DateUtils.parseDate(entry, HeaderConstants.LAST_MODIFIED);
102 
103         if (dateValue != null && lastModifiedValue != null) {
104             final long diff = dateValue.getTime() - lastModifiedValue.getTime();
105             if (diff < 0) {
106                 return TimeValue.ZERO_MILLISECONDS;
107             }
108             return TimeValue.ofSeconds((long) (coefficient * diff / 1000));
109         }
110 
111         return defaultLifetime;
112     }
113 
114     public boolean isRevalidatable(final HttpCacheEntry entry) {
115         return entry.getFirstHeader(HeaderConstants.ETAG) != null
116                 || entry.getFirstHeader(HeaderConstants.LAST_MODIFIED) != null;
117     }
118 
119     public boolean mustRevalidate(final HttpCacheEntry entry) {
120         return hasCacheControlDirective(entry, HeaderConstants.CACHE_CONTROL_MUST_REVALIDATE);
121     }
122 
123     public boolean proxyRevalidate(final HttpCacheEntry entry) {
124         return hasCacheControlDirective(entry, HeaderConstants.CACHE_CONTROL_PROXY_REVALIDATE);
125     }
126 
127     public boolean mayReturnStaleWhileRevalidating(final HttpCacheEntry entry, final Date now) {
128         final Iterator<HeaderElement> it = MessageSupport.iterate(entry, HeaderConstants.CACHE_CONTROL);
129         while (it.hasNext()) {
130             final HeaderElement elt = it.next();
131             if (HeaderConstants.STALE_WHILE_REVALIDATE.equalsIgnoreCase(elt.getName())) {
132                 try {
133                     // in seconds
134                     final int allowedStalenessLifetime = Integer.parseInt(elt.getValue());
135                     if (getStaleness(entry, now).compareTo(TimeValue.ofSeconds(allowedStalenessLifetime)) <= 0) {
136                         return true;
137                     }
138                 } catch (final NumberFormatException nfe) {
139                     // skip malformed directive
140                 }
141             }
142         }
143 
144         return false;
145     }
146 
147     public boolean mayReturnStaleIfError(final HttpRequest request, final HttpCacheEntry entry, final Date now) {
148         final TimeValue staleness = getStaleness(entry, now);
149         return mayReturnStaleIfError(request, HeaderConstants.CACHE_CONTROL, staleness)
150                 || mayReturnStaleIfError(entry, HeaderConstants.CACHE_CONTROL, staleness);
151     }
152 
153     private boolean mayReturnStaleIfError(final MessageHeaders headers, final String name, final TimeValue staleness) {
154         boolean result = false;
155         final Iterator<HeaderElement> it = MessageSupport.iterate(headers, name);
156         while (it.hasNext()) {
157             final HeaderElement elt = it.next();
158             if (HeaderConstants.STALE_IF_ERROR.equals(elt.getName())) {
159                 try {
160                     // in seconds
161                     final int staleIfError = Integer.parseInt(elt.getValue());
162                     if (staleness.compareTo(TimeValue.ofSeconds(staleIfError)) <= 0) {
163                         result = true;
164                         break;
165                     }
166                 } catch (final NumberFormatException nfe) {
167                     // skip malformed directive
168                 }
169             }
170         }
171         return result;
172     }
173 
174     /**
175      * This matters for deciding whether the cache entry is valid to serve as a
176      * response. If these values do not match, we might have a partial response
177      *
178      * @param entry The cache entry we are currently working with
179      * @return boolean indicating whether actual length matches Content-Length
180      */
181     protected boolean contentLengthHeaderMatchesActualLength(final HttpCacheEntry entry) {
182         final Header h = entry.getFirstHeader(HttpHeaders.CONTENT_LENGTH);
183         if (h != null) {
184             try {
185                 final long responseLen = Long.parseLong(h.getValue());
186                 final Resource resource = entry.getResource();
187                 if (resource == null) {
188                     return false;
189                 }
190                 final long resourceLen = resource.length();
191                 return responseLen == resourceLen;
192             } catch (final NumberFormatException ex) {
193                 return false;
194             }
195         }
196         return true;
197     }
198 
199     protected TimeValue getApparentAge(final HttpCacheEntry entry) {
200         final Date dateValue = entry.getDate();
201         if (dateValue == null) {
202             return MAX_AGE;
203         }
204         final long diff = entry.getResponseDate().getTime() - dateValue.getTime();
205         if (diff < 0L) {
206             return TimeValue.ZERO_MILLISECONDS;
207         }
208         return TimeValue.ofSeconds(diff / 1000);
209     }
210 
211     protected long getAgeValue(final HttpCacheEntry entry) {
212         // This is a header value, we leave as-is
213         long ageValue = 0;
214         for (final Header hdr : entry.getHeaders(HeaderConstants.AGE)) {
215             long hdrAge;
216             try {
217                 hdrAge = Long.parseLong(hdr.getValue());
218                 if (hdrAge < 0) {
219                     hdrAge = MAX_AGE.toSeconds();
220                 }
221             } catch (final NumberFormatException nfe) {
222                 hdrAge = MAX_AGE.toSeconds();
223             }
224             ageValue = (hdrAge > ageValue) ? hdrAge : ageValue;
225         }
226         return ageValue;
227     }
228 
229     protected TimeValue getCorrectedReceivedAge(final HttpCacheEntry entry) {
230         final TimeValue apparentAge = getApparentAge(entry);
231         final long ageValue = getAgeValue(entry);
232         return (apparentAge.toSeconds() > ageValue) ? apparentAge : TimeValue.ofSeconds(ageValue);
233     }
234 
235     protected TimeValue getResponseDelay(final HttpCacheEntry entry) {
236         final long diff = entry.getResponseDate().getTime() - entry.getRequestDate().getTime();
237         return TimeValue.ofSeconds(diff / 1000);
238     }
239 
240     protected TimeValue getCorrectedInitialAge(final HttpCacheEntry entry) {
241         return TimeValue.ofSeconds(getCorrectedReceivedAge(entry).toSeconds() + getResponseDelay(entry).toSeconds());
242     }
243 
244     protected TimeValue getResidentTime(final HttpCacheEntry entry, final Date now) {
245         final long diff = now.getTime() - entry.getResponseDate().getTime();
246         return TimeValue.ofSeconds(diff / 1000);
247     }
248 
249     protected long getMaxAge(final HttpCacheEntry entry) {
250         // This is a header value, we leave as-is
251         long maxAge = -1;
252         final Iterator<HeaderElement> it = MessageSupport.iterate(entry, HeaderConstants.CACHE_CONTROL);
253         while (it.hasNext()) {
254             final HeaderElement elt = it.next();
255             if (HeaderConstants.CACHE_CONTROL_MAX_AGE.equals(elt.getName()) || "s-maxage".equals(elt.getName())) {
256                 try {
257                     final long currMaxAge = Long.parseLong(elt.getValue());
258                     if (maxAge == -1 || currMaxAge < maxAge) {
259                         maxAge = currMaxAge;
260                     }
261                 } catch (final NumberFormatException nfe) {
262                     // be conservative if can't parse
263                     maxAge = 0;
264                 }
265             }
266         }
267         return maxAge;
268     }
269 
270     public boolean hasCacheControlDirective(final HttpCacheEntry entry, final String directive) {
271         final Iterator<HeaderElement> it = MessageSupport.iterate(entry, HeaderConstants.CACHE_CONTROL);
272         while (it.hasNext()) {
273             final HeaderElement elt = it.next();
274             if (directive.equalsIgnoreCase(elt.getName())) {
275                 return true;
276             }
277         }
278         return false;
279     }
280 
281     public TimeValue getStaleness(final HttpCacheEntry entry, final Date now) {
282         final TimeValue age = getCurrentAge(entry, now);
283         final TimeValue freshness = getFreshnessLifetime(entry);
284         if (age.compareTo(freshness) <= 0) {
285             return TimeValue.ZERO_MILLISECONDS;
286         }
287         return TimeValue.ofSeconds(age.toSeconds() - freshness.toSeconds());
288     }
289 
290 
291 }