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