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