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.io.InputStream;
31  import java.util.Date;
32  import java.util.Iterator;
33  import java.util.Map;
34  import java.util.concurrent.ScheduledExecutorService;
35  
36  import org.apache.hc.client5.http.HttpRoute;
37  import org.apache.hc.client5.http.async.methods.SimpleBody;
38  import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
39  import org.apache.hc.client5.http.cache.CacheResponseStatus;
40  import org.apache.hc.client5.http.cache.HeaderConstants;
41  import org.apache.hc.client5.http.cache.HttpCacheEntry;
42  import org.apache.hc.client5.http.cache.HttpCacheStorage;
43  import org.apache.hc.client5.http.cache.ResourceFactory;
44  import org.apache.hc.client5.http.cache.ResourceIOException;
45  import org.apache.hc.client5.http.classic.ExecChain;
46  import org.apache.hc.client5.http.classic.ExecChainHandler;
47  import org.apache.hc.client5.http.impl.ExecSupport;
48  import org.apache.hc.client5.http.protocol.HttpClientContext;
49  import org.apache.hc.client5.http.schedule.SchedulingStrategy;
50  import org.apache.hc.client5.http.utils.DateUtils;
51  import org.apache.hc.core5.function.Factory;
52  import org.apache.hc.core5.http.ClassicHttpRequest;
53  import org.apache.hc.core5.http.ClassicHttpResponse;
54  import org.apache.hc.core5.http.ContentType;
55  import org.apache.hc.core5.http.Header;
56  import org.apache.hc.core5.http.HttpEntity;
57  import org.apache.hc.core5.http.HttpException;
58  import org.apache.hc.core5.http.HttpHeaders;
59  import org.apache.hc.core5.http.HttpHost;
60  import org.apache.hc.core5.http.HttpRequest;
61  import org.apache.hc.core5.http.HttpStatus;
62  import org.apache.hc.core5.http.HttpVersion;
63  import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
64  import org.apache.hc.core5.http.io.entity.EntityUtils;
65  import org.apache.hc.core5.http.io.entity.StringEntity;
66  import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
67  import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
68  import org.apache.hc.core5.http.protocol.HttpCoreContext;
69  import org.apache.hc.core5.net.URIAuthority;
70  import org.apache.hc.core5.util.Args;
71  import org.apache.hc.core5.util.ByteArrayBuffer;
72  import org.slf4j.Logger;
73  import org.slf4j.LoggerFactory;
74  
75  /**
76   * <p>
77   * Request executor in the request execution chain that is responsible for
78   * transparent client-side caching.
79   * </p>
80   * <p>
81   * The current implementation is conditionally
82   * compliant with HTTP/1.1 (meaning all the MUST and MUST NOTs are obeyed),
83   * although quite a lot, though not all, of the SHOULDs and SHOULD NOTs
84   * are obeyed too.
85   * </p>
86   * <p>
87   * Folks that would like to experiment with alternative storage backends
88   * should look at the {@link HttpCacheStorage} interface and the related
89   * package documentation there. You may also be interested in the provided
90   * {@link org.apache.hc.client5.http.impl.cache.ehcache.EhcacheHttpCacheStorage
91   * EhCache} and {@link
92   * org.apache.hc.client5.http.impl.cache.memcached.MemcachedHttpCacheStorage
93   * memcached} storage backends.
94   * </p>
95   * <p>
96   * Further responsibilities such as communication with the opposite
97   * endpoint is delegated to the next executor in the request execution
98   * chain.
99   * </p>
100  *
101  * @since 4.3
102  */
103 class CachingExec extends CachingExecBase implements ExecChainHandler {
104 
105     private final HttpCache responseCache;
106     private final DefaultCacheRevalidator cacheRevalidator;
107     private final ConditionalRequestBuilder<ClassicHttpRequest> conditionalRequestBuilder;
108 
109     private static final Logger LOG = LoggerFactory.getLogger(CachingExec.class);
110 
111     CachingExec(final HttpCache cache, final DefaultCacheRevalidator cacheRevalidator, final CacheConfig config) {
112         super(config);
113         this.responseCache = Args.notNull(cache, "Response cache");
114         this.cacheRevalidator = cacheRevalidator;
115         this.conditionalRequestBuilder = new ConditionalRequestBuilder<>(new Factory<ClassicHttpRequest, ClassicHttpRequest>() {
116 
117             @Override
118             public ClassicHttpRequest create(final ClassicHttpRequest classicHttpRequest) {
119                 return ClassicRequestBuilder.copy(classicHttpRequest).build();
120             }
121 
122         });
123     }
124 
125     CachingExec(
126             final HttpCache responseCache,
127             final CacheValidityPolicy validityPolicy,
128             final ResponseCachingPolicy responseCachingPolicy,
129             final CachedHttpResponseGenerator responseGenerator,
130             final CacheableRequestPolicy cacheableRequestPolicy,
131             final CachedResponseSuitabilityChecker suitabilityChecker,
132             final ResponseProtocolCompliance responseCompliance,
133             final RequestProtocolCompliance requestCompliance,
134             final DefaultCacheRevalidator cacheRevalidator,
135             final ConditionalRequestBuilder<ClassicHttpRequest> conditionalRequestBuilder,
136             final CacheConfig config) {
137         super(validityPolicy, responseCachingPolicy, responseGenerator, cacheableRequestPolicy,
138                 suitabilityChecker, responseCompliance, requestCompliance, config);
139         this.responseCache = responseCache;
140         this.cacheRevalidator = cacheRevalidator;
141         this.conditionalRequestBuilder = conditionalRequestBuilder;
142     }
143 
144     CachingExec(
145             final HttpCache cache,
146             final ScheduledExecutorService executorService,
147             final SchedulingStrategy schedulingStrategy,
148             final CacheConfig config) {
149         this(cache,
150                 executorService != null ? new DefaultCacheRevalidator(executorService, schedulingStrategy) : null,
151                 config);
152     }
153 
154     CachingExec(
155             final ResourceFactory resourceFactory,
156             final HttpCacheStorage storage,
157             final ScheduledExecutorService executorService,
158             final SchedulingStrategy schedulingStrategy,
159             final CacheConfig config) {
160         this(new BasicHttpCache(resourceFactory, storage), executorService, schedulingStrategy, config);
161     }
162 
163     @Override
164     public ClassicHttpResponse execute(
165             final ClassicHttpRequest request,
166             final ExecChain.Scope scope,
167             final ExecChain chain) throws IOException, HttpException {
168         Args.notNull(request, "HTTP request");
169         Args.notNull(scope, "Scope");
170 
171         final HttpRoute route = scope.route;
172         final HttpClientContext context = scope.clientContext;
173         context.setAttribute(HttpClientContext.HTTP_ROUTE, scope.route);
174         context.setAttribute(HttpClientContext.HTTP_REQUEST, request);
175 
176         final URIAuthority authority = request.getAuthority();
177         final String scheme = request.getScheme();
178         final HttpHost target = authority != null ? new HttpHost(scheme, authority) : route.getTargetHost();
179         final String via = generateViaHeader(request);
180 
181         // default response context
182         setResponseStatus(context, CacheResponseStatus.CACHE_MISS);
183 
184         if (clientRequestsOurOptions(request)) {
185             setResponseStatus(context, CacheResponseStatus.CACHE_MODULE_RESPONSE);
186             return new BasicClassicHttpResponse(HttpStatus.SC_NOT_IMPLEMENTED);
187         }
188 
189         final SimpleHttpResponse fatalErrorResponse = getFatallyNoncompliantResponse(request, context);
190         if (fatalErrorResponse != null) {
191             return convert(fatalErrorResponse, scope);
192         }
193 
194         requestCompliance.makeRequestCompliant(request);
195         request.addHeader("Via",via);
196 
197         if (!cacheableRequestPolicy.isServableFromCache(request)) {
198             LOG.debug("Request is not servable from cache");
199             responseCache.flushCacheEntriesInvalidatedByRequest(target, request);
200             return callBackend(target, request, scope, chain);
201         }
202 
203         final HttpCacheEntry entry = responseCache.getCacheEntry(target, request);
204         if (entry == null) {
205             LOG.debug("Cache miss");
206             return handleCacheMiss(target, request, scope, chain);
207         } else {
208             return handleCacheHit(target, request, scope, chain, entry);
209         }
210     }
211 
212     private static ClassicHttpResponse convert(final SimpleHttpResponse cacheResponse, final ExecChain.Scope scope) {
213         if (cacheResponse == null) {
214             return null;
215         }
216         final ClassicHttpResponse response = new BasicClassicHttpResponse(cacheResponse.getCode(), cacheResponse.getReasonPhrase());
217         for (final Iterator<Header> it = cacheResponse.headerIterator(); it.hasNext(); ) {
218             response.addHeader(it.next());
219         }
220         response.setVersion(cacheResponse.getVersion() != null ? cacheResponse.getVersion() : HttpVersion.DEFAULT);
221         final SimpleBody body = cacheResponse.getBody();
222         if (body != null) {
223             final ContentType contentType = body.getContentType();
224             final Header h = response.getFirstHeader(HttpHeaders.CONTENT_ENCODING);
225             final String contentEncoding = h != null ? h.getValue() : null;
226             if (body.isText()) {
227                 response.setEntity(new StringEntity(body.getBodyText(), contentType, contentEncoding, false));
228             } else {
229                 response.setEntity(new ByteArrayEntity(body.getBodyBytes(), contentType, contentEncoding, false));
230             }
231         }
232         scope.clientContext.setAttribute(HttpCoreContext.HTTP_RESPONSE, response);
233         return response;
234     }
235 
236     ClassicHttpResponse callBackend(
237             final HttpHost target,
238             final ClassicHttpRequest request,
239             final ExecChain.Scope scope,
240             final ExecChain chain) throws IOException, HttpException  {
241 
242         final Date requestDate = getCurrentDate();
243 
244         LOG.debug("Calling the backend");
245         final ClassicHttpResponse backendResponse = chain.proceed(request, scope);
246         try {
247             backendResponse.addHeader("Via", generateViaHeader(backendResponse));
248             return handleBackendResponse(target, request, scope, requestDate, getCurrentDate(), backendResponse);
249         } catch (final IOException | RuntimeException ex) {
250             backendResponse.close();
251             throw ex;
252         }
253     }
254 
255     private ClassicHttpResponse handleCacheHit(
256             final HttpHost target,
257             final ClassicHttpRequest request,
258             final ExecChain.Scope scope,
259             final ExecChain chain,
260             final HttpCacheEntry entry) throws IOException, HttpException {
261         final HttpClientContext context  = scope.clientContext;
262         context.setAttribute(HttpCoreContext.HTTP_REQUEST, request);
263         recordCacheHit(target, request);
264         final Date now = getCurrentDate();
265         if (suitabilityChecker.canCachedResponseBeUsed(target, request, entry, now)) {
266             LOG.debug("Cache hit");
267             try {
268                 return convert(generateCachedResponse(request, context, entry, now), scope);
269             } catch (final ResourceIOException ex) {
270                 recordCacheFailure(target, request);
271                 if (!mayCallBackend(request)) {
272                     return convert(generateGatewayTimeout(context), scope);
273                 }
274                 setResponseStatus(scope.clientContext, CacheResponseStatus.FAILURE);
275                 return chain.proceed(request, scope);
276             }
277         } else if (!mayCallBackend(request)) {
278             LOG.debug("Cache entry not suitable but only-if-cached requested");
279             return convert(generateGatewayTimeout(context), scope);
280         } else if (!(entry.getStatus() == HttpStatus.SC_NOT_MODIFIED && !suitabilityChecker.isConditional(request))) {
281             LOG.debug("Revalidating cache entry");
282             try {
283                 if (cacheRevalidator != null
284                         && !staleResponseNotAllowed(request, entry, now)
285                         && validityPolicy.mayReturnStaleWhileRevalidating(entry, now)) {
286                     LOG.debug("Serving stale with asynchronous revalidation");
287                     final String exchangeId = ExecSupport.getNextExchangeId();
288                     context.setExchangeId(exchangeId);
289                     final ExecChain.Scope fork = new ExecChain.Scope(
290                             exchangeId,
291                             scope.route,
292                             scope.originalRequest,
293                             scope.execRuntime.fork(null),
294                             HttpClientContext.create());
295                     final SimpleHttpResponse response = generateCachedResponse(request, context, entry, now);
296                     cacheRevalidator.revalidateCacheEntry(
297                             responseCache.generateKey(target, request, entry),
298                             new DefaultCacheRevalidator.RevalidationCall() {
299 
300                         @Override
301                         public ClassicHttpResponse execute() throws HttpException, IOException {
302                             return revalidateCacheEntry(target, request, fork, chain, entry);
303                         }
304 
305                     });
306                     return convert(response, scope);
307                 }
308                 return revalidateCacheEntry(target, request, scope, chain, entry);
309             } catch (final IOException ioex) {
310                 return convert(handleRevalidationFailure(request, context, entry, now), scope);
311             }
312         } else {
313             LOG.debug("Cache entry not usable; calling backend");
314             return callBackend(target, request, scope, chain);
315         }
316     }
317 
318     ClassicHttpResponse revalidateCacheEntry(
319             final HttpHost target,
320             final ClassicHttpRequest request,
321             final ExecChain.Scope scope,
322             final ExecChain chain,
323             final HttpCacheEntry cacheEntry) throws IOException, HttpException {
324         Date requestDate = getCurrentDate();
325         final ClassicHttpRequest conditionalRequest = conditionalRequestBuilder.buildConditionalRequest(
326                 scope.originalRequest, cacheEntry);
327 
328         ClassicHttpResponse backendResponse = chain.proceed(conditionalRequest, scope);
329         try {
330             Date responseDate = getCurrentDate();
331 
332             if (revalidationResponseIsTooOld(backendResponse, cacheEntry)) {
333                 backendResponse.close();
334                 final ClassicHttpRequest unconditional = conditionalRequestBuilder.buildUnconditionalRequest(
335                         scope.originalRequest);
336                 requestDate = getCurrentDate();
337                 backendResponse = chain.proceed(unconditional, scope);
338                 responseDate = getCurrentDate();
339             }
340 
341             backendResponse.addHeader(HeaderConstants.VIA, generateViaHeader(backendResponse));
342 
343             final int statusCode = backendResponse.getCode();
344             if (statusCode == HttpStatus.SC_NOT_MODIFIED || statusCode == HttpStatus.SC_OK) {
345                 recordCacheUpdate(scope.clientContext);
346             }
347 
348             if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
349                 final HttpCacheEntry updatedEntry = responseCache.updateCacheEntry(
350                         target, request, cacheEntry, backendResponse, requestDate, responseDate);
351                 if (suitabilityChecker.isConditional(request)
352                         && suitabilityChecker.allConditionalsMatch(request, updatedEntry, new Date())) {
353                     return convert(responseGenerator.generateNotModifiedResponse(updatedEntry), scope);
354                 }
355                 return convert(responseGenerator.generateResponse(request, updatedEntry), scope);
356             }
357 
358             if (staleIfErrorAppliesTo(statusCode)
359                     && !staleResponseNotAllowed(request, cacheEntry, getCurrentDate())
360                     && validityPolicy.mayReturnStaleIfError(request, cacheEntry, responseDate)) {
361                 try {
362                     final SimpleHttpResponse cachedResponse = responseGenerator.generateResponse(request, cacheEntry);
363                     cachedResponse.addHeader(HeaderConstants.WARNING, "110 localhost \"Response is stale\"");
364                     return convert(cachedResponse, scope);
365                 } finally {
366                     backendResponse.close();
367                 }
368             }
369             return handleBackendResponse(target, conditionalRequest, scope, requestDate, responseDate, backendResponse);
370         } catch (final IOException | RuntimeException ex) {
371             backendResponse.close();
372             throw ex;
373         }
374     }
375 
376     ClassicHttpResponse handleBackendResponse(
377             final HttpHost target,
378             final ClassicHttpRequest request,
379             final ExecChain.Scope scope,
380             final Date requestDate,
381             final Date responseDate,
382             final ClassicHttpResponse backendResponse) throws IOException {
383 
384         responseCompliance.ensureProtocolCompliance(scope.originalRequest, request, backendResponse);
385 
386         responseCache.flushCacheEntriesInvalidatedByExchange(target, request, backendResponse);
387         final boolean cacheable = responseCachingPolicy.isResponseCacheable(request, backendResponse);
388         if (cacheable) {
389             storeRequestIfModifiedSinceFor304Response(request, backendResponse);
390             return cacheAndReturnResponse(target, request, backendResponse, scope, requestDate, responseDate);
391         }
392         LOG.debug("Backend response is not cacheable");
393         responseCache.flushCacheEntriesFor(target, request);
394         return backendResponse;
395     }
396 
397     ClassicHttpResponse cacheAndReturnResponse(
398             final HttpHost target,
399             final HttpRequest request,
400             final ClassicHttpResponse backendResponse,
401             final ExecChain.Scope scope,
402             final Date requestSent,
403             final Date responseReceived) throws IOException {
404         LOG.debug("Caching backend response");
405         final ByteArrayBuffer buf;
406         final HttpEntity entity = backendResponse.getEntity();
407         if (entity != null) {
408             buf = new ByteArrayBuffer(1024);
409             final InputStream inStream = entity.getContent();
410             final byte[] tmp = new byte[2048];
411             long total = 0;
412             int l;
413             while ((l = inStream.read(tmp)) != -1) {
414                 buf.append(tmp, 0, l);
415                 total += l;
416                 if (total > cacheConfig.getMaxObjectSize()) {
417                     LOG.debug("Backend response content length exceeds maximum");
418                     backendResponse.setEntity(new CombinedEntity(entity, buf));
419                     return backendResponse;
420                 }
421             }
422         } else {
423             buf = null;
424         }
425         backendResponse.close();
426 
427         final HttpCacheEntry cacheEntry;
428         if (cacheConfig.isFreshnessCheckEnabled()) {
429             final HttpCacheEntry existingEntry = responseCache.getCacheEntry(target, request);
430             if (DateUtils.isAfter(existingEntry, backendResponse, HttpHeaders.DATE)) {
431                 LOG.debug("Backend already contains fresher cache entry");
432                 cacheEntry = existingEntry;
433             } else {
434                 cacheEntry = responseCache.createCacheEntry(target, request, backendResponse, buf, requestSent, responseReceived);
435                 LOG.debug("Backend response successfully cached");
436             }
437         } else {
438             cacheEntry = responseCache.createCacheEntry(target, request, backendResponse, buf, requestSent, responseReceived);
439             LOG.debug("Backend response successfully cached (freshness check skipped)");
440         }
441         return convert(responseGenerator.generateResponse(request, cacheEntry), scope);
442     }
443 
444     private ClassicHttpResponse handleCacheMiss(
445             final HttpHost target,
446             final ClassicHttpRequest request,
447             final ExecChain.Scope scope,
448             final ExecChain chain) throws IOException, HttpException {
449         recordCacheMiss(target, request);
450 
451         if (!mayCallBackend(request)) {
452             return new BasicClassicHttpResponse(HttpStatus.SC_GATEWAY_TIMEOUT, "Gateway Timeout");
453         }
454 
455         final Map<String, Variant> variants = responseCache.getVariantCacheEntriesWithEtags(target, request);
456         if (variants != null && !variants.isEmpty()) {
457             return negotiateResponseFromVariants(target, request, scope, chain, variants);
458         }
459 
460         return callBackend(target, request, scope, chain);
461     }
462 
463     ClassicHttpResponse negotiateResponseFromVariants(
464             final HttpHost target,
465             final ClassicHttpRequest request,
466             final ExecChain.Scope scope,
467             final ExecChain chain,
468             final Map<String, Variant> variants) throws IOException, HttpException {
469         final ClassicHttpRequest conditionalRequest = conditionalRequestBuilder.buildConditionalRequestFromVariants(request, variants);
470 
471         final Date requestDate = getCurrentDate();
472         final ClassicHttpResponse backendResponse = chain.proceed(conditionalRequest, scope);
473         try {
474             final Date responseDate = getCurrentDate();
475 
476             backendResponse.addHeader("Via", generateViaHeader(backendResponse));
477 
478             if (backendResponse.getCode() != HttpStatus.SC_NOT_MODIFIED) {
479                 return handleBackendResponse(target, request, scope, requestDate, responseDate, backendResponse);
480             }
481 
482             final Header resultEtagHeader = backendResponse.getFirstHeader(HeaderConstants.ETAG);
483             if (resultEtagHeader == null) {
484                 LOG.warn("304 response did not contain ETag");
485                 EntityUtils.consume(backendResponse.getEntity());
486                 backendResponse.close();
487                 return callBackend(target, request, scope, chain);
488             }
489 
490             final String resultEtag = resultEtagHeader.getValue();
491             final Variant matchingVariant = variants.get(resultEtag);
492             if (matchingVariant == null) {
493                 LOG.debug("304 response did not contain ETag matching one sent in If-None-Match");
494                 EntityUtils.consume(backendResponse.getEntity());
495                 backendResponse.close();
496                 return callBackend(target, request, scope, chain);
497             }
498 
499             if (revalidationResponseIsTooOld(backendResponse, matchingVariant.getEntry())
500                     && (request.getEntity() == null || request.getEntity().isRepeatable())) {
501                 EntityUtils.consume(backendResponse.getEntity());
502                 backendResponse.close();
503                 final ClassicHttpRequest unconditional = conditionalRequestBuilder.buildUnconditionalRequest(request);
504                 return callBackend(target, unconditional, scope, chain);
505             }
506 
507             recordCacheUpdate(scope.clientContext);
508 
509             final HttpCacheEntry responseEntry = responseCache.updateVariantCacheEntry(
510                     target, conditionalRequest, backendResponse, matchingVariant, requestDate, responseDate);
511             backendResponse.close();
512             if (shouldSendNotModifiedResponse(request, responseEntry)) {
513                 return convert(responseGenerator.generateNotModifiedResponse(responseEntry), scope);
514             }
515             final SimpleHttpResponse response = responseGenerator.generateResponse(request, responseEntry);
516             responseCache.reuseVariantEntryFor(target, request, matchingVariant);
517             return convert(response, scope);
518         } catch (final IOException | RuntimeException ex) {
519             backendResponse.close();
520             throw ex;
521         }
522     }
523 
524 }