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