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.concurrent.atomic.AtomicReference;
32  
33  import org.apache.hc.client5.http.cache.HttpCacheEntry;
34  import org.apache.hc.client5.http.cache.ResponseCacheControl;
35  import org.apache.hc.core5.http.Header;
36  import org.apache.hc.core5.http.HttpHeaders;
37  import org.apache.hc.core5.http.message.MessageSupport;
38  import org.apache.hc.core5.util.TimeValue;
39  import org.slf4j.Logger;
40  import org.slf4j.LoggerFactory;
41  
42  class CacheValidityPolicy {
43  
44      private static final Logger LOG = LoggerFactory.getLogger(CacheValidityPolicy.class);
45  
46      private final boolean shared;
47      private final boolean useHeuristicCaching;
48      private final float heuristicCoefficient;
49      private final TimeValue heuristicDefaultLifetime;
50  
51  
52      /**
53       * Constructs a CacheValidityPolicy with the provided CacheConfig. If the config is null, it will use
54       * default heuristic coefficient and default heuristic lifetime from CacheConfig.DEFAULT.
55       *
56       * @param config The CacheConfig to use for this CacheValidityPolicy. If null, default values are used.
57       */
58      CacheValidityPolicy(final CacheConfig config) {
59          super();
60          this.shared = config != null ? config.isSharedCache() : CacheConfig.DEFAULT.isSharedCache();
61          this.useHeuristicCaching = config != null ? config.isHeuristicCachingEnabled() : CacheConfig.DEFAULT.isHeuristicCachingEnabled();
62          this.heuristicCoefficient = config != null ? config.getHeuristicCoefficient() : CacheConfig.DEFAULT.getHeuristicCoefficient();
63          this.heuristicDefaultLifetime = config != null ? config.getHeuristicDefaultLifetime() : CacheConfig.DEFAULT.getHeuristicDefaultLifetime();
64      }
65  
66      /**
67       * Default constructor for CacheValidityPolicy. Initializes the policy with default values.
68       */
69      CacheValidityPolicy() {
70          this(null);
71      }
72  
73  
74      public TimeValue getCurrentAge(final HttpCacheEntry entry, final Instant now) {
75          return TimeValue.ofSeconds(getCorrectedInitialAge(entry).toSeconds() + getResidentTime(entry, now).toSeconds());
76      }
77  
78      /**
79       * Calculate the freshness lifetime of a response based on the provided cache control and cache entry.
80       * <ul>
81       * <li>If the cache is shared and the s-maxage response directive is present, use its value.</li>
82       * <li>If the max-age response directive is present, use its value.</li>
83       * <li>If the Expires response header field is present, use its value minus the value of the Date response header field.</li>
84       * <li>Otherwise, a heuristic freshness lifetime might be applicable.</li>
85       * </ul>
86       *
87       * @param responseCacheControl the cache control directives associated with the response.
88       * @param entry                the cache entry associated with the response.
89       * @return the calculated freshness lifetime as a {@link TimeValue}.
90       */
91      public TimeValue getFreshnessLifetime(final ResponseCacheControl responseCacheControl, final HttpCacheEntry entry) {
92          // If the cache is shared and the s-maxage response directive is present, use its value
93          if (shared) {
94              final long sharedMaxAge = responseCacheControl.getSharedMaxAge();
95              if (sharedMaxAge > -1) {
96                  if (LOG.isDebugEnabled()) {
97                      LOG.debug("Using s-maxage directive for freshness lifetime calculation: {} seconds", sharedMaxAge);
98                  }
99                  return TimeValue.ofSeconds(sharedMaxAge);
100             }
101         }
102 
103         // If the max-age response directive is present, use its value
104         final long maxAge = responseCacheControl.getMaxAge();
105         if (maxAge > -1) {
106             if (LOG.isDebugEnabled()) {
107                 LOG.debug("Using max-age directive for freshness lifetime calculation: {} seconds", maxAge);
108             }
109             return TimeValue.ofSeconds(maxAge);
110         }
111 
112         // If the Expires response header field is present, use its value minus the value of the Date response header field
113         final Instant dateValue = entry.getInstant();
114         if (dateValue != null) {
115             final Instant expiry = entry.getExpires();
116             if (expiry != null) {
117                 final Duration diff = Duration.between(dateValue, expiry);
118                 if (diff.isNegative()) {
119                     if (LOG.isDebugEnabled()) {
120                         LOG.debug("Negative freshness lifetime detected. Content is already expired. Returning zero freshness lifetime.");
121                     }
122                     return TimeValue.ZERO_MILLISECONDS;
123                 }
124                 return TimeValue.ofSeconds(diff.getSeconds());
125             }
126         }
127 
128         if (useHeuristicCaching) {
129             // No explicit expiration time is present in the response. A heuristic freshness lifetime might be applicable
130             if (LOG.isDebugEnabled()) {
131                 LOG.debug("No explicit expiration time present in the response. Using heuristic freshness lifetime calculation.");
132             }
133             return getHeuristicFreshnessLifetime(entry);
134         } else {
135             return TimeValue.ZERO_MILLISECONDS;
136         }
137     }
138 
139     TimeValue getHeuristicFreshnessLifetime(final HttpCacheEntry entry) {
140         final Instant dateValue = entry.getInstant();
141         final Instant lastModifiedValue = entry.getLastModified();
142 
143         if (dateValue != null && lastModifiedValue != null) {
144             final Duration diff = Duration.between(lastModifiedValue, dateValue);
145 
146             if (diff.isNegative()) {
147                 return TimeValue.ZERO_MILLISECONDS;
148             }
149             return TimeValue.ofSeconds((long) (heuristicCoefficient * diff.getSeconds()));
150         }
151 
152         return heuristicDefaultLifetime;
153     }
154 
155     TimeValue getApparentAge(final HttpCacheEntry entry) {
156         final Instant dateValue = entry.getInstant();
157         if (dateValue == null) {
158             return CacheSupport.MAX_AGE;
159         }
160         final Duration diff = Duration.between(dateValue, entry.getResponseInstant());
161         if (diff.isNegative()) {
162             return TimeValue.ZERO_MILLISECONDS;
163         }
164         return TimeValue.ofSeconds(diff.getSeconds());
165     }
166 
167     /**
168      * Extracts and processes the Age value from an HttpCacheEntry by tokenizing the Age header value.
169      * The Age header value is interpreted as a sequence of tokens, and the first token is parsed into a number
170      * representing the age in delta-seconds. If the first token cannot be parsed into a number, the Age value is
171      * considered as invalid and this method returns 0. If the first token represents a negative number or a number
172      * that exceeds Integer.MAX_VALUE, the Age value is set to MAX_AGE (in seconds).
173      * This method uses CacheSupport.parseTokens to robustly handle the Age header value.
174      * <p>
175      * Note: If the HttpCacheEntry contains multiple Age headers, only the first one is considered.
176      *
177      * @param entry The HttpCacheEntry from which to extract the Age value.
178      * @return The Age value in delta-seconds, or MAX_AGE in seconds if the Age value exceeds Integer.MAX_VALUE or
179      * is negative. If the Age value is invalid (cannot be parsed into a number or contains non-numeric characters),
180      * this method returns 0.
181      */
182     long getAgeValue(final HttpCacheEntry entry) {
183         final Header age = entry.getFirstHeader(HttpHeaders.AGE);
184         if (age != null) {
185             final AtomicReference<String> firstToken = new AtomicReference<>();
186             MessageSupport.parseTokens(age, token -> firstToken.compareAndSet(null, token));
187             final long delta = CacheSupport.deltaSeconds(firstToken.get());
188             if (delta == -1 && LOG.isDebugEnabled()) {
189                 LOG.debug("Malformed Age value: {}", age);
190             }
191             return delta > 0 ? delta : 0;
192         }
193         // If we've got here, there were no valid Age headers
194         return 0;
195     }
196 
197     TimeValue getCorrectedAgeValue(final HttpCacheEntry entry) {
198         final long ageValue = getAgeValue(entry);
199         final long responseDelay = getResponseDelay(entry).toSeconds();
200         return TimeValue.ofSeconds(ageValue + responseDelay);
201     }
202 
203     TimeValue getResponseDelay(final HttpCacheEntry entry) {
204         final Duration diff = Duration.between(entry.getRequestInstant(), entry.getResponseInstant());
205         return TimeValue.ofSeconds(diff.getSeconds());
206     }
207 
208     TimeValue getCorrectedInitialAge(final HttpCacheEntry entry) {
209         final long apparentAge = getApparentAge(entry).toSeconds();
210         final long correctedReceivedAge = getCorrectedAgeValue(entry).toSeconds();
211         return TimeValue.ofSeconds(Math.max(apparentAge, correctedReceivedAge));
212     }
213 
214     TimeValue getResidentTime(final HttpCacheEntry entry, final Instant now) {
215         final Duration diff = Duration.between(entry.getResponseInstant(), now);
216         return TimeValue.ofSeconds(diff.getSeconds());
217     }
218 
219 }