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