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.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      * Reports the number of times that the cache successfully responded
117      * to an {@link HttpRequest} without contacting the origin server.
118      * @return the number of cache hits
119      */
120     public long getCacheHits() {
121         return cacheHits.get();
122     }
123 
124     /**
125      * Reports the number of times that the cache contacted the origin
126      * server because it had no appropriate response cached.
127      * @return the number of cache misses
128      */
129     public long getCacheMisses() {
130         return cacheMisses.get();
131     }
132 
133     /**
134      * Reports the number of times that the cache was able to satisfy
135      * a response by revalidating an existing but stale cache entry.
136      * @return the number of cache revalidations
137      */
138     public long getCacheUpdates() {
139         return cacheUpdates.get();
140     }
141 
142     /**
143      * @since 5.2
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                     // in seconds
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      * Reports whether this {@code CachingHttpClient} implementation
308      * supports byte-range requests as specified by the {@code Range}
309      * and {@code Content-Range} headers.
310      * @return {@code true} if byte-range requests are supported
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         // either backend response or cached entry did not have a valid
335         // Date header, so we can't tell if they are out of order
336         // according to the origin clock; thus we can skip the
337         // unconditional retry recommended in 13.2.6 of RFC 2616.
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      * For 304 Not modified responses, adds a "Last-Modified" header with the
355      * value of the "If-Modified-Since" header passed in the request. This
356      * header is required to be able to reuse match the cache entry for
357      * subsequent requests but as defined in http specifications it is not
358      * included in 304 responses by backend servers. This header will not be
359      * included in the resulting response.
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 }