1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
54
55
56
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
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
80
81
82
83
84
85
86
87
88
89
90
91 public TimeValue getFreshnessLifetime(final ResponseCacheControl responseCacheControl, final HttpCacheEntry entry) {
92
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
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
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
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
169
170
171
172
173
174
175
176
177
178
179
180
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
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 }