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.io.IOException;
30 import java.time.Instant;
31 import java.util.Iterator;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.concurrent.ConcurrentHashMap;
35 import java.util.concurrent.atomic.AtomicLong;
36
37 import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
38 import org.apache.hc.client5.http.cache.CacheResponseStatus;
39 import org.apache.hc.client5.http.cache.HeaderConstants;
40 import org.apache.hc.client5.http.cache.HttpCacheContext;
41 import org.apache.hc.client5.http.cache.HttpCacheEntry;
42 import org.apache.hc.client5.http.cache.ResourceIOException;
43 import org.apache.hc.core5.http.Header;
44 import org.apache.hc.core5.http.HeaderElement;
45 import org.apache.hc.core5.http.HttpHeaders;
46 import org.apache.hc.core5.http.HttpHost;
47 import org.apache.hc.core5.http.HttpMessage;
48 import org.apache.hc.core5.http.HttpRequest;
49 import org.apache.hc.core5.http.HttpResponse;
50 import org.apache.hc.core5.http.HttpStatus;
51 import org.apache.hc.core5.http.HttpVersion;
52 import org.apache.hc.core5.http.ProtocolVersion;
53 import org.apache.hc.core5.http.URIScheme;
54 import org.apache.hc.core5.http.message.MessageSupport;
55 import org.apache.hc.core5.http.protocol.HttpContext;
56 import org.apache.hc.core5.util.TimeValue;
57 import org.apache.hc.core5.util.VersionInfo;
58 import org.slf4j.Logger;
59 import org.slf4j.LoggerFactory;
60
61 public class CachingExecBase {
62
63 final static boolean SUPPORTS_RANGE_AND_CONTENT_RANGE_HEADERS = false;
64
65 final AtomicLong cacheHits = new AtomicLong();
66 final AtomicLong cacheMisses = new AtomicLong();
67 final AtomicLong cacheUpdates = new AtomicLong();
68
69 final Map<ProtocolVersion, String> viaHeaders = new ConcurrentHashMap<>(4);
70
71 final ResponseCachingPolicy responseCachingPolicy;
72 final CacheValidityPolicy validityPolicy;
73 final CachedHttpResponseGenerator responseGenerator;
74 final CacheableRequestPolicy cacheableRequestPolicy;
75 final CachedResponseSuitabilityChecker suitabilityChecker;
76 final ResponseProtocolCompliance responseCompliance;
77 final RequestProtocolCompliance requestCompliance;
78 final CacheConfig cacheConfig;
79
80 private static final Logger LOG = LoggerFactory.getLogger(CachingExecBase.class);
81
82 CachingExecBase(
83 final CacheValidityPolicy validityPolicy,
84 final ResponseCachingPolicy responseCachingPolicy,
85 final CachedHttpResponseGenerator responseGenerator,
86 final CacheableRequestPolicy cacheableRequestPolicy,
87 final CachedResponseSuitabilityChecker suitabilityChecker,
88 final ResponseProtocolCompliance responseCompliance,
89 final RequestProtocolCompliance requestCompliance,
90 final CacheConfig config) {
91 this.responseCachingPolicy = responseCachingPolicy;
92 this.validityPolicy = validityPolicy;
93 this.responseGenerator = responseGenerator;
94 this.cacheableRequestPolicy = cacheableRequestPolicy;
95 this.suitabilityChecker = suitabilityChecker;
96 this.requestCompliance = requestCompliance;
97 this.responseCompliance = responseCompliance;
98 this.cacheConfig = config != null ? config : CacheConfig.DEFAULT;
99 }
100
101 CachingExecBase(final CacheConfig config) {
102 super();
103 this.cacheConfig = config != null ? config : CacheConfig.DEFAULT;
104 this.validityPolicy = new CacheValidityPolicy();
105 this.responseGenerator = new CachedHttpResponseGenerator(this.validityPolicy);
106 this.cacheableRequestPolicy = new CacheableRequestPolicy();
107 this.suitabilityChecker = new CachedResponseSuitabilityChecker(this.validityPolicy, this.cacheConfig);
108 this.responseCompliance = new ResponseProtocolCompliance();
109 this.requestCompliance = new RequestProtocolCompliance(this.cacheConfig.isWeakETagOnPutDeleteAllowed());
110 this.responseCachingPolicy = new ResponseCachingPolicy(
111 this.cacheConfig.getMaxObjectSize(), this.cacheConfig.isSharedCache(),
112 this.cacheConfig.isNeverCacheHTTP10ResponsesWithQuery(), this.cacheConfig.is303CachingEnabled());
113 }
114
115
116
117
118
119
120 public long getCacheHits() {
121 return cacheHits.get();
122 }
123
124
125
126
127
128
129 public long getCacheMisses() {
130 return cacheMisses.get();
131 }
132
133
134
135
136
137
138 public long getCacheUpdates() {
139 return cacheUpdates.get();
140 }
141
142
143
144
145 SimpleHttpResponse getFatallyNonCompliantResponse(
146 final HttpRequest request,
147 final HttpContext context) {
148 final List<RequestProtocolError> fatalError = requestCompliance.requestIsFatallyNonCompliant(request);
149 if (fatalError != null && !fatalError.isEmpty()) {
150 setResponseStatus(context, CacheResponseStatus.CACHE_MODULE_RESPONSE);
151 return responseGenerator.getErrorForRequest(fatalError.get(0));
152 }
153 return null;
154 }
155
156 void recordCacheMiss(final HttpHost target, final HttpRequest request) {
157 cacheMisses.getAndIncrement();
158 if (LOG.isDebugEnabled()) {
159 LOG.debug("Cache miss [host: {}; uri: {}]", target, request.getRequestUri());
160 }
161 }
162
163 void recordCacheHit(final HttpHost target, final HttpRequest request) {
164 cacheHits.getAndIncrement();
165 if (LOG.isDebugEnabled()) {
166 LOG.debug("Cache hit [host: {}; uri: {}]", target, request.getRequestUri());
167 }
168 }
169
170 void recordCacheFailure(final HttpHost target, final HttpRequest request) {
171 cacheMisses.getAndIncrement();
172 if (LOG.isDebugEnabled()) {
173 LOG.debug("Cache failure [host: {}; uri: {}]", target, request.getRequestUri());
174 }
175 }
176
177 void recordCacheUpdate(final HttpContext context) {
178 cacheUpdates.getAndIncrement();
179 setResponseStatus(context, CacheResponseStatus.VALIDATED);
180 }
181
182 SimpleHttpResponse generateCachedResponse(
183 final HttpRequest request,
184 final HttpContext context,
185 final HttpCacheEntry entry,
186 final Instant now) throws ResourceIOException {
187 final SimpleHttpResponse cachedResponse;
188 if (request.containsHeader(HeaderConstants.IF_NONE_MATCH)
189 || request.containsHeader(HeaderConstants.IF_MODIFIED_SINCE)) {
190 cachedResponse = responseGenerator.generateNotModifiedResponse(entry);
191 } else {
192 cachedResponse = responseGenerator.generateResponse(request, entry);
193 }
194 setResponseStatus(context, CacheResponseStatus.CACHE_HIT);
195 if (TimeValue.isPositive(validityPolicy.getStaleness(entry, now))) {
196 cachedResponse.addHeader(HeaderConstants.WARNING,"110 localhost \"Response is stale\"");
197 }
198 return cachedResponse;
199 }
200
201 SimpleHttpResponse handleRevalidationFailure(
202 final HttpRequest request,
203 final HttpContext context,
204 final HttpCacheEntry entry,
205 final Instant now) throws IOException {
206 if (staleResponseNotAllowed(request, entry, now)) {
207 return generateGatewayTimeout(context);
208 } else {
209 return unvalidatedCacheHit(request, context, entry);
210 }
211 }
212
213 SimpleHttpResponse generateGatewayTimeout(
214 final HttpContext context) {
215 setResponseStatus(context, CacheResponseStatus.CACHE_MODULE_RESPONSE);
216 return SimpleHttpResponse.create(HttpStatus.SC_GATEWAY_TIMEOUT, "Gateway Timeout");
217 }
218
219 SimpleHttpResponse unvalidatedCacheHit(
220 final HttpRequest request,
221 final HttpContext context,
222 final HttpCacheEntry entry) throws IOException {
223 final SimpleHttpResponse cachedResponse = responseGenerator.generateResponse(request, entry);
224 setResponseStatus(context, CacheResponseStatus.CACHE_HIT);
225 cachedResponse.addHeader(HeaderConstants.WARNING, "111 localhost \"Revalidation failed\"");
226 return cachedResponse;
227 }
228
229 boolean staleResponseNotAllowed(final HttpRequest request, final HttpCacheEntry entry, final Instant now) {
230 return validityPolicy.mustRevalidate(entry)
231 || (cacheConfig.isSharedCache() && validityPolicy.proxyRevalidate(entry))
232 || explicitFreshnessRequest(request, entry, now);
233 }
234
235 boolean mayCallBackend(final HttpRequest request) {
236 final Iterator<HeaderElement> it = MessageSupport.iterate(request, HeaderConstants.CACHE_CONTROL);
237 while (it.hasNext()) {
238 final HeaderElement elt = it.next();
239 if ("only-if-cached".equals(elt.getName())) {
240 LOG.debug("Request marked only-if-cached");
241 return false;
242 }
243 }
244 return true;
245 }
246
247 boolean explicitFreshnessRequest(final HttpRequest request, final HttpCacheEntry entry, final Instant now) {
248 final Iterator<HeaderElement> it = MessageSupport.iterate(request, HeaderConstants.CACHE_CONTROL);
249 while (it.hasNext()) {
250 final HeaderElement elt = it.next();
251 if (HeaderConstants.CACHE_CONTROL_MAX_STALE.equals(elt.getName())) {
252 try {
253
254 final int maxStale = Integer.parseInt(elt.getValue());
255 final TimeValue age = validityPolicy.getCurrentAge(entry, now);
256 final TimeValue lifetime = validityPolicy.getFreshnessLifetime(entry);
257 if (age.toSeconds() - lifetime.toSeconds() > maxStale) {
258 return true;
259 }
260 } catch (final NumberFormatException nfe) {
261 return true;
262 }
263 } else if (HeaderConstants.CACHE_CONTROL_MIN_FRESH.equals(elt.getName())
264 || HeaderConstants.CACHE_CONTROL_MAX_AGE.equals(elt.getName())) {
265 return true;
266 }
267 }
268 return false;
269 }
270
271 String generateViaHeader(final HttpMessage msg) {
272
273 if (msg.getVersion() == null) {
274 msg.setVersion(HttpVersion.DEFAULT);
275 }
276 final ProtocolVersion pv = msg.getVersion();
277 final String existingEntry = viaHeaders.get(msg.getVersion());
278 if (existingEntry != null) {
279 return existingEntry;
280 }
281
282 final VersionInfo vi = VersionInfo.loadVersionInfo("org.apache.hc.client5", getClass().getClassLoader());
283 final String release = (vi != null) ? vi.getRelease() : VersionInfo.UNAVAILABLE;
284
285 final String value;
286 final int major = pv.getMajor();
287 final int minor = pv.getMinor();
288 if (URIScheme.HTTP.same(pv.getProtocol())) {
289 value = String.format("%d.%d localhost (Apache-HttpClient/%s (cache))", major, minor,
290 release);
291 } else {
292 value = String.format("%s/%d.%d localhost (Apache-HttpClient/%s (cache))", pv.getProtocol(), major,
293 minor, release);
294 }
295 viaHeaders.put(pv, value);
296
297 return value;
298 }
299
300 void setResponseStatus(final HttpContext context, final CacheResponseStatus value) {
301 if (context != null) {
302 context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, value);
303 }
304 }
305
306
307
308
309
310
311
312 boolean supportsRangeAndContentRangeHeaders() {
313 return SUPPORTS_RANGE_AND_CONTENT_RANGE_HEADERS;
314 }
315
316 Instant getCurrentDate() {
317 return Instant.now();
318 }
319
320 boolean clientRequestsOurOptions(final HttpRequest request) {
321 if (!HeaderConstants.OPTIONS_METHOD.equals(request.getMethod())) {
322 return false;
323 }
324
325 if (!"*".equals(request.getRequestUri())) {
326 return false;
327 }
328
329 final Header h = request.getFirstHeader(HeaderConstants.MAX_FORWARDS);
330 return "0".equals(h != null ? h.getValue() : null);
331 }
332
333 boolean revalidationResponseIsTooOld(final HttpResponse backendResponse, final HttpCacheEntry cacheEntry) {
334
335
336
337
338 return DateSupport.isBefore(backendResponse, cacheEntry, HttpHeaders.DATE);
339 }
340
341 boolean shouldSendNotModifiedResponse(final HttpRequest request, final HttpCacheEntry responseEntry) {
342 return (suitabilityChecker.isConditional(request)
343 && suitabilityChecker.allConditionalsMatch(request, responseEntry, Instant.now()));
344 }
345
346 boolean staleIfErrorAppliesTo(final int statusCode) {
347 return statusCode == HttpStatus.SC_INTERNAL_SERVER_ERROR
348 || statusCode == HttpStatus.SC_BAD_GATEWAY
349 || statusCode == HttpStatus.SC_SERVICE_UNAVAILABLE
350 || statusCode == HttpStatus.SC_GATEWAY_TIMEOUT;
351 }
352
353
354
355
356
357
358
359
360
361 void storeRequestIfModifiedSinceFor304Response(final HttpRequest request, final HttpResponse backendResponse) {
362 if (backendResponse.getCode() == HttpStatus.SC_NOT_MODIFIED) {
363 final Header h = request.getFirstHeader(HttpHeaders.IF_MODIFIED_SINCE);
364 if (h != null) {
365 backendResponse.addHeader(HttpHeaders.LAST_MODIFIED, h.getValue());
366 }
367 }
368 }
369
370 }