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