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.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      * Reports the number of times that the cache successfully responded
118      * to an {@link HttpRequest} without contacting the origin server.
119      * @return the number of cache hits
120      */
121     public long getCacheHits() {
122         return cacheHits.get();
123     }
124 
125     /**
126      * Reports the number of times that the cache contacted the origin
127      * server because it had no appropriate response cached.
128      * @return the number of cache misses
129      */
130     public long getCacheMisses() {
131         return cacheMisses.get();
132     }
133 
134     /**
135      * Reports the number of times that the cache was able to satisfy
136      * a response by revalidating an existing but stale cache entry.
137      * @return the number of cache revalidations
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                     // in seconds
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      * Reports whether this {@code CachingHttpClient} implementation
306      * supports byte-range requests as specified by the {@code Range}
307      * and {@code Content-Range} headers.
308      * @return {@code true} if byte-range requests are supported
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         return "0".equals(h != null ? h.getValue() : null);
329     }
330 
331     boolean revalidationResponseIsTooOld(final HttpResponse backendResponse, final HttpCacheEntry cacheEntry) {
332         // either backend response or cached entry did not have a valid
333         // Date header, so we can't tell if they are out of order
334         // according to the origin clock; thus we can skip the
335         // unconditional retry recommended in 13.2.6 of RFC 2616.
336         return DateUtils.isBefore(backendResponse, cacheEntry, HttpHeaders.DATE);
337     }
338 
339     boolean shouldSendNotModifiedResponse(final HttpRequest request, final HttpCacheEntry responseEntry) {
340         return (suitabilityChecker.isConditional(request)
341                 && suitabilityChecker.allConditionalsMatch(request, responseEntry, new Date()));
342     }
343 
344     boolean staleIfErrorAppliesTo(final int statusCode) {
345         return statusCode == HttpStatus.SC_INTERNAL_SERVER_ERROR
346                 || statusCode == HttpStatus.SC_BAD_GATEWAY
347                 || statusCode == HttpStatus.SC_SERVICE_UNAVAILABLE
348                 || statusCode == HttpStatus.SC_GATEWAY_TIMEOUT;
349     }
350 
351     /**
352      * For 304 Not modified responses, adds a "Last-Modified" header with the
353      * value of the "If-Modified-Since" header passed in the request. This
354      * header is required to be able to reuse match the cache entry for
355      * subsequent requests but as defined in http specifications it is not
356      * included in 304 responses by backend servers. This header will not be
357      * included in the resulting response.
358      */
359     void storeRequestIfModifiedSinceFor304Response(final HttpRequest request, final HttpResponse backendResponse) {
360         if (backendResponse.getCode() == HttpStatus.SC_NOT_MODIFIED) {
361             final Header h = request.getFirstHeader("If-Modified-Since");
362             if (h != null) {
363                 backendResponse.addHeader("Last-Modified", h.getValue());
364             }
365         }
366     }
367 
368 }