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.http.impl.client.cache;
28  
29  import java.io.IOException;
30  import java.util.Date;
31  import java.util.HashMap;
32  import java.util.List;
33  import java.util.Map;
34  import java.util.concurrent.atomic.AtomicLong;
35  
36  import org.apache.commons.logging.Log;
37  import org.apache.commons.logging.LogFactory;
38  import org.apache.http.Header;
39  import org.apache.http.HeaderElement;
40  import org.apache.http.HttpException;
41  import org.apache.http.HttpHost;
42  import org.apache.http.HttpMessage;
43  import org.apache.http.HttpRequest;
44  import org.apache.http.HttpResponse;
45  import org.apache.http.HttpStatus;
46  import org.apache.http.HttpVersion;
47  import org.apache.http.ProtocolVersion;
48  import org.apache.http.RequestLine;
49  import org.apache.http.annotation.ThreadSafe;
50  import org.apache.http.client.cache.CacheResponseStatus;
51  import org.apache.http.client.cache.HeaderConstants;
52  import org.apache.http.client.cache.HttpCacheContext;
53  import org.apache.http.client.cache.HttpCacheEntry;
54  import org.apache.http.client.cache.HttpCacheStorage;
55  import org.apache.http.client.cache.ResourceFactory;
56  import org.apache.http.client.methods.CloseableHttpResponse;
57  import org.apache.http.client.methods.HttpExecutionAware;
58  import org.apache.http.client.methods.HttpRequestWrapper;
59  import org.apache.http.client.protocol.HttpClientContext;
60  import org.apache.http.client.utils.DateUtils;
61  import org.apache.http.conn.routing.HttpRoute;
62  import org.apache.http.impl.execchain.ClientExecChain;
63  import org.apache.http.message.BasicHttpResponse;
64  import org.apache.http.protocol.HTTP;
65  import org.apache.http.protocol.HttpContext;
66  import org.apache.http.util.Args;
67  import org.apache.http.util.EntityUtils;
68  import org.apache.http.util.VersionInfo;
69  
70  /**
71   * <p>CachingExec is intended to transparently add client-side caching
72   * to the HttpClient {@link ClientExecChain execution chain}.
73   * The current implementation is conditionally compliant with HTTP/1.1
74   * (meaning all the MUST and MUST NOTs are obeyed), although quite a lot,
75   * though not all, of the SHOULDs and SHOULD NOTs are obeyed too.</p>
76   *
77   * <p>Folks that would like to experiment with alternative storage backends
78   * should look at the {@link HttpCacheStorage} interface and the related
79   * package documentation there. You may also be interested in the provided
80   * {@link org.apache.http.impl.client.cache.ehcache.EhcacheHttpCacheStorage
81   * EhCache} and {@link
82   * org.apache.http.impl.client.cache.memcached.MemcachedHttpCacheStorage
83   * memcached} storage backends.</p>
84   * </p>
85   *
86   * @since 4.3
87   */
88  @ThreadSafe // So long as the responseCache implementation is threadsafe
89  public class CachingExec implements ClientExecChain {
90  
91      private final static boolean SUPPORTS_RANGE_AND_CONTENT_RANGE_HEADERS = false;
92  
93      private final AtomicLong cacheHits = new AtomicLong();
94      private final AtomicLong cacheMisses = new AtomicLong();
95      private final AtomicLong cacheUpdates = new AtomicLong();
96  
97      private final Map<ProtocolVersion, String> viaHeaders = new HashMap<ProtocolVersion, String>(4);
98  
99      private final CacheConfig cacheConfig;
100     private final ClientExecChain backend;
101     private final HttpCache responseCache;
102     private final CacheValidityPolicy validityPolicy;
103     private final CachedHttpResponseGenerator responseGenerator;
104     private final CacheableRequestPolicy cacheableRequestPolicy;
105     private final CachedResponseSuitabilityChecker suitabilityChecker;
106     private final ConditionalRequestBuilder conditionalRequestBuilder;
107     private final ResponseProtocolCompliance responseCompliance;
108     private final RequestProtocolCompliance requestCompliance;
109     private final ResponseCachingPolicy responseCachingPolicy;
110 
111     private final AsynchronousValidator asynchRevalidator;
112 
113     private final Log log = LogFactory.getLog(getClass());
114 
115     public CachingExec(
116             final ClientExecChain backend,
117             final HttpCache cache,
118             final CacheConfig config) {
119         this(backend, cache, config, null);
120     }
121 
122     public CachingExec(
123             final ClientExecChain backend,
124             final HttpCache cache,
125             final CacheConfig config,
126             final AsynchronousValidator asynchRevalidator) {
127         super();
128         Args.notNull(backend, "HTTP backend");
129         Args.notNull(cache, "HttpCache");
130         this.cacheConfig = config != null ? config : CacheConfig.DEFAULT;
131         this.backend = backend;
132         this.responseCache = cache;
133         this.validityPolicy = new CacheValidityPolicy();
134         this.responseGenerator = new CachedHttpResponseGenerator(this.validityPolicy);
135         this.cacheableRequestPolicy = new CacheableRequestPolicy();
136         this.suitabilityChecker = new CachedResponseSuitabilityChecker(this.validityPolicy, config);
137         this.conditionalRequestBuilder = new ConditionalRequestBuilder();
138         this.responseCompliance = new ResponseProtocolCompliance();
139         this.requestCompliance = new RequestProtocolCompliance();
140         this.responseCachingPolicy = new ResponseCachingPolicy(
141                 this.cacheConfig.getMaxObjectSize(), this.cacheConfig.isSharedCache(),
142                 this.cacheConfig.isNeverCacheHTTP10ResponsesWithQuery());
143         this.asynchRevalidator = asynchRevalidator;
144     }
145 
146     public CachingExec(
147             final ClientExecChain backend,
148             final ResourceFactory resourceFactory,
149             final HttpCacheStorage storage,
150             final CacheConfig config) {
151         this(backend, new BasicHttpCache(resourceFactory, storage, config), config);
152     }
153 
154     public CachingExec(final ClientExecChain backend) {
155         this(backend, new BasicHttpCache(), CacheConfig.DEFAULT);
156     }
157 
158     CachingExec(
159             final ClientExecChain backend,
160             final HttpCache responseCache,
161             final CacheValidityPolicy validityPolicy,
162             final ResponseCachingPolicy responseCachingPolicy,
163             final CachedHttpResponseGenerator responseGenerator,
164             final CacheableRequestPolicy cacheableRequestPolicy,
165             final CachedResponseSuitabilityChecker suitabilityChecker,
166             final ConditionalRequestBuilder conditionalRequestBuilder,
167             final ResponseProtocolCompliance responseCompliance,
168             final RequestProtocolCompliance requestCompliance,
169             final CacheConfig config,
170             final AsynchronousValidator asynchRevalidator) {
171         this.cacheConfig = config != null ? config : CacheConfig.DEFAULT;
172         this.backend = backend;
173         this.responseCache = responseCache;
174         this.validityPolicy = validityPolicy;
175         this.responseCachingPolicy = responseCachingPolicy;
176         this.responseGenerator = responseGenerator;
177         this.cacheableRequestPolicy = cacheableRequestPolicy;
178         this.suitabilityChecker = suitabilityChecker;
179         this.conditionalRequestBuilder = conditionalRequestBuilder;
180         this.responseCompliance = responseCompliance;
181         this.requestCompliance = requestCompliance;
182         this.asynchRevalidator = asynchRevalidator;
183     }
184 
185     /**
186      * Reports the number of times that the cache successfully responded
187      * to an {@link HttpRequest} without contacting the origin server.
188      * @return the number of cache hits
189      */
190     public long getCacheHits() {
191         return cacheHits.get();
192     }
193 
194     /**
195      * Reports the number of times that the cache contacted the origin
196      * server because it had no appropriate response cached.
197      * @return the number of cache misses
198      */
199     public long getCacheMisses() {
200         return cacheMisses.get();
201     }
202 
203     /**
204      * Reports the number of times that the cache was able to satisfy
205      * a response by revalidating an existing but stale cache entry.
206      * @return the number of cache revalidations
207      */
208     public long getCacheUpdates() {
209         return cacheUpdates.get();
210     }
211 
212     public CloseableHttpResponse execute(
213             final HttpRoute route,
214             final HttpRequestWrapper request) throws IOException, HttpException {
215         return execute(route, request, HttpClientContext.create(), null);
216     }
217 
218     public CloseableHttpResponse execute(
219             final HttpRoute route,
220             final HttpRequestWrapper request,
221             final HttpClientContext context) throws IOException, HttpException {
222         return execute(route, request, context, null);
223     }
224 
225     public CloseableHttpResponse execute(
226             final HttpRoute route,
227             final HttpRequestWrapper request,
228             final HttpClientContext context,
229             final HttpExecutionAware execAware) throws IOException, HttpException {
230 
231         final HttpHost target = route.getTargetHost();
232         final String via = generateViaHeader(request.getOriginal());
233 
234         // default response context
235         setResponseStatus(context, CacheResponseStatus.CACHE_MISS);
236 
237         if (clientRequestsOurOptions(request)) {
238             setResponseStatus(context, CacheResponseStatus.CACHE_MODULE_RESPONSE);
239             return Proxies.enhanceResponse(new OptionsHttp11Response());
240         }
241 
242         final HttpResponse fatalErrorResponse = getFatallyNoncompliantResponse(request, context);
243         if (fatalErrorResponse != null) {
244             return Proxies.enhanceResponse(fatalErrorResponse);
245         }
246 
247         requestCompliance.makeRequestCompliant(request);
248         request.addHeader("Via",via);
249 
250         flushEntriesInvalidatedByRequest(route.getTargetHost(), request);
251 
252         if (!cacheableRequestPolicy.isServableFromCache(request)) {
253             log.debug("Request is not servable from cache");
254             return callBackend(route, request, context, execAware);
255         }
256 
257         final HttpCacheEntry entry = satisfyFromCache(target, request);
258         if (entry == null) {
259             log.debug("Cache miss");
260             return handleCacheMiss(route, request, context, execAware);
261         } else {
262             return handleCacheHit(route, request, context, execAware, entry);
263         }
264     }
265 
266     private CloseableHttpResponse handleCacheHit(
267             final HttpRoute route,
268             final HttpRequestWrapper request,
269             final HttpClientContext context,
270             final HttpExecutionAware execAware,
271             final HttpCacheEntry entry) throws IOException, HttpException {
272         final HttpHost target = route.getTargetHost();
273         recordCacheHit(target, request);
274         CloseableHttpResponse out = null;
275         final Date now = getCurrentDate();
276         if (suitabilityChecker.canCachedResponseBeUsed(target, request, entry, now)) {
277             log.debug("Cache hit");
278             out = Proxies.enhanceResponse(generateCachedResponse(request, context, entry, now));
279         } else if (!mayCallBackend(request)) {
280             log.debug("Cache entry not suitable but only-if-cached requested");
281             out = Proxies.enhanceResponse(generateGatewayTimeout(context));
282         } else if (validityPolicy.isRevalidatable(entry)
283                 && !(entry.getStatusCode() == HttpStatus.SC_NOT_MODIFIED
284                 && !suitabilityChecker.isConditional(request))) {
285             log.debug("Revalidating cache entry");
286             return revalidateCacheEntry(route, request, context, execAware, entry, now);
287         } else {
288             log.debug("Cache entry not usable; calling backend");
289             return callBackend(route, request, context, execAware);
290         }
291         context.setAttribute(HttpClientContext.HTTP_ROUTE, route);
292         context.setAttribute(HttpClientContext.HTTP_TARGET_HOST, target);
293         context.setAttribute(HttpClientContext.HTTP_REQUEST, request);
294         context.setAttribute(HttpClientContext.HTTP_RESPONSE, out);
295         context.setAttribute(HttpClientContext.HTTP_REQ_SENT, Boolean.TRUE);
296         return out;
297     }
298 
299     private CloseableHttpResponse revalidateCacheEntry(
300             final HttpRoute route,
301             final HttpRequestWrapper request,
302             final HttpClientContext context,
303             final HttpExecutionAware execAware,
304             final HttpCacheEntry entry,
305             final Date now) throws IOException, HttpException {
306 
307         try {
308             if (asynchRevalidator != null
309                 && !staleResponseNotAllowed(request, entry, now)
310                 && validityPolicy.mayReturnStaleWhileRevalidating(entry, now)) {
311                 log.trace("Serving stale with asynchronous revalidation");
312                 final HttpResponse resp = generateCachedResponse(request, context, entry, now);
313                 asynchRevalidator.revalidateCacheEntry(this, route, request, context, execAware, entry);
314                 return Proxies.enhanceResponse(resp);
315             }
316             return revalidateCacheEntry(route, request, context, execAware, entry);
317         } catch (final IOException ioex) {
318             return Proxies.enhanceResponse(
319                     handleRevalidationFailure(request, context, entry, now));
320         }
321     }
322 
323     private CloseableHttpResponse handleCacheMiss(
324             final HttpRoute route,
325             final HttpRequestWrapper request,
326             final HttpClientContext context,
327             final HttpExecutionAware execAware) throws IOException, HttpException {
328         final HttpHost target = route.getTargetHost();
329         recordCacheMiss(target, request);
330 
331         if (!mayCallBackend(request)) {
332             return Proxies.enhanceResponse(
333                     new BasicHttpResponse(
334                             HttpVersion.HTTP_1_1, HttpStatus.SC_GATEWAY_TIMEOUT, "Gateway Timeout"));
335         }
336 
337         final Map<String, Variant> variants = getExistingCacheVariants(target, request);
338         if (variants != null && variants.size() > 0) {
339             return Proxies.enhanceResponse(
340                     negotiateResponseFromVariants(route, request, context, execAware, variants));
341         }
342 
343         return callBackend(route, request, context, execAware);
344     }
345 
346     private HttpCacheEntry satisfyFromCache(
347             final HttpHost target, final HttpRequestWrapper request) {
348         HttpCacheEntry entry = null;
349         try {
350             entry = responseCache.getCacheEntry(target, request);
351         } catch (final IOException ioe) {
352             log.warn("Unable to retrieve entries from cache", ioe);
353         }
354         return entry;
355     }
356 
357     private HttpResponse getFatallyNoncompliantResponse(
358             final HttpRequestWrapper request,
359             final HttpContext context) {
360         HttpResponse fatalErrorResponse = null;
361         final List<RequestProtocolError> fatalError = requestCompliance.requestIsFatallyNonCompliant(request);
362 
363         for (final RequestProtocolError error : fatalError) {
364             setResponseStatus(context, CacheResponseStatus.CACHE_MODULE_RESPONSE);
365             fatalErrorResponse = requestCompliance.getErrorForRequest(error);
366         }
367         return fatalErrorResponse;
368     }
369 
370     private Map<String, Variant> getExistingCacheVariants(
371             final HttpHost target,
372             final HttpRequestWrapper request) {
373         Map<String,Variant> variants = null;
374         try {
375             variants = responseCache.getVariantCacheEntriesWithEtags(target, request);
376         } catch (final IOException ioe) {
377             log.warn("Unable to retrieve variant entries from cache", ioe);
378         }
379         return variants;
380     }
381 
382     private void recordCacheMiss(final HttpHost target, final HttpRequestWrapper request) {
383         cacheMisses.getAndIncrement();
384         if (log.isTraceEnabled()) {
385             final RequestLine rl = request.getRequestLine();
386             log.trace("Cache miss [host: " + target + "; uri: " + rl.getUri() + "]");
387         }
388     }
389 
390     private void recordCacheHit(final HttpHost target, final HttpRequestWrapper request) {
391         cacheHits.getAndIncrement();
392         if (log.isTraceEnabled()) {
393             final RequestLine rl = request.getRequestLine();
394             log.trace("Cache hit [host: " + target + "; uri: " + rl.getUri() + "]");
395         }
396     }
397 
398     private void recordCacheUpdate(final HttpContext context) {
399         cacheUpdates.getAndIncrement();
400         setResponseStatus(context, CacheResponseStatus.VALIDATED);
401     }
402 
403     private void flushEntriesInvalidatedByRequest(
404             final HttpHost target,
405             final HttpRequestWrapper request) {
406         try {
407             responseCache.flushInvalidatedCacheEntriesFor(target, request);
408         } catch (final IOException ioe) {
409             log.warn("Unable to flush invalidated entries from cache", ioe);
410         }
411     }
412 
413     private HttpResponse generateCachedResponse(final HttpRequestWrapper request,
414             final HttpContext context, final HttpCacheEntry entry, final Date now) {
415         final HttpResponse cachedResponse;
416         if (request.containsHeader(HeaderConstants.IF_NONE_MATCH)
417                 || request.containsHeader(HeaderConstants.IF_MODIFIED_SINCE)) {
418             cachedResponse = responseGenerator.generateNotModifiedResponse(entry);
419         } else {
420             cachedResponse = responseGenerator.generateResponse(entry);
421         }
422         setResponseStatus(context, CacheResponseStatus.CACHE_HIT);
423         if (validityPolicy.getStalenessSecs(entry, now) > 0L) {
424             cachedResponse.addHeader(HeaderConstants.WARNING,"110 localhost \"Response is stale\"");
425         }
426         return cachedResponse;
427     }
428 
429     private HttpResponse handleRevalidationFailure(
430             final HttpRequestWrapper request,
431             final HttpContext context,
432             final HttpCacheEntry entry,
433             final Date now) {
434         if (staleResponseNotAllowed(request, entry, now)) {
435             return generateGatewayTimeout(context);
436         } else {
437             return unvalidatedCacheHit(context, entry);
438         }
439     }
440 
441     private HttpResponse generateGatewayTimeout(final HttpContext context) {
442         setResponseStatus(context, CacheResponseStatus.CACHE_MODULE_RESPONSE);
443         return new BasicHttpResponse(HttpVersion.HTTP_1_1,
444                 HttpStatus.SC_GATEWAY_TIMEOUT, "Gateway Timeout");
445     }
446 
447     private HttpResponse unvalidatedCacheHit(
448             final HttpContext context, final HttpCacheEntry entry) {
449         final HttpResponse cachedResponse = responseGenerator.generateResponse(entry);
450         setResponseStatus(context, CacheResponseStatus.CACHE_HIT);
451         cachedResponse.addHeader(HeaderConstants.WARNING, "111 localhost \"Revalidation failed\"");
452         return cachedResponse;
453     }
454 
455     private boolean staleResponseNotAllowed(
456             final HttpRequestWrapper request,
457             final HttpCacheEntry entry,
458             final Date now) {
459         return validityPolicy.mustRevalidate(entry)
460             || (cacheConfig.isSharedCache() && validityPolicy.proxyRevalidate(entry))
461             || explicitFreshnessRequest(request, entry, now);
462     }
463 
464     private boolean mayCallBackend(final HttpRequestWrapper request) {
465         for (final Header h: request.getHeaders(HeaderConstants.CACHE_CONTROL)) {
466             for (final HeaderElement elt : h.getElements()) {
467                 if ("only-if-cached".equals(elt.getName())) {
468                     log.trace("Request marked only-if-cached");
469                     return false;
470                 }
471             }
472         }
473         return true;
474     }
475 
476     private boolean explicitFreshnessRequest(
477             final HttpRequestWrapper request,
478             final HttpCacheEntry entry,
479             final Date now) {
480         for(final Header h : request.getHeaders(HeaderConstants.CACHE_CONTROL)) {
481             for(final HeaderElement elt : h.getElements()) {
482                 if (HeaderConstants.CACHE_CONTROL_MAX_STALE.equals(elt.getName())) {
483                     try {
484                         final int maxstale = Integer.parseInt(elt.getValue());
485                         final long age = validityPolicy.getCurrentAgeSecs(entry, now);
486                         final long lifetime = validityPolicy.getFreshnessLifetimeSecs(entry);
487                         if (age - lifetime > maxstale) {
488                             return true;
489                         }
490                     } catch (final NumberFormatException nfe) {
491                         return true;
492                     }
493                 } else if (HeaderConstants.CACHE_CONTROL_MIN_FRESH.equals(elt.getName())
494                             || HeaderConstants.CACHE_CONTROL_MAX_AGE.equals(elt.getName())) {
495                     return true;
496                 }
497             }
498         }
499         return false;
500     }
501 
502     private String generateViaHeader(final HttpMessage msg) {
503 
504         final ProtocolVersion pv = msg.getProtocolVersion();
505         final String existingEntry = viaHeaders.get(pv);
506         if (existingEntry != null) {
507             return existingEntry;
508         }
509 
510         final VersionInfo vi = VersionInfo.loadVersionInfo("org.apache.http.client", getClass().getClassLoader());
511         final String release = (vi != null) ? vi.getRelease() : VersionInfo.UNAVAILABLE;
512 
513         String value;
514         if ("http".equalsIgnoreCase(pv.getProtocol())) {
515             value = String.format("%d.%d localhost (Apache-HttpClient/%s (cache))", pv.getMajor(), pv.getMinor(),
516                     release);
517         } else {
518             value = String.format("%s/%d.%d localhost (Apache-HttpClient/%s (cache))", pv.getProtocol(), pv.getMajor(),
519                     pv.getMinor(), release);
520         }
521         viaHeaders.put(pv, value);
522 
523         return value;
524     }
525 
526     private void setResponseStatus(final HttpContext context, final CacheResponseStatus value) {
527         if (context != null) {
528             context.setAttribute(HttpCacheContext.CACHE_RESPONSE_STATUS, value);
529         }
530     }
531 
532     /**
533      * Reports whether this {@code CachingHttpClient} implementation
534      * supports byte-range requests as specified by the {@code Range}
535      * and {@code Content-Range} headers.
536      * @return {@code true} if byte-range requests are supported
537      */
538     public boolean supportsRangeAndContentRangeHeaders() {
539         return SUPPORTS_RANGE_AND_CONTENT_RANGE_HEADERS;
540     }
541 
542     Date getCurrentDate() {
543         return new Date();
544     }
545 
546     boolean clientRequestsOurOptions(final HttpRequest request) {
547         final RequestLine line = request.getRequestLine();
548 
549         if (!HeaderConstants.OPTIONS_METHOD.equals(line.getMethod())) {
550             return false;
551         }
552 
553         if (!"*".equals(line.getUri())) {
554             return false;
555         }
556 
557         if (!"0".equals(request.getFirstHeader(HeaderConstants.MAX_FORWARDS).getValue())) {
558             return false;
559         }
560 
561         return true;
562     }
563 
564     CloseableHttpResponse callBackend(
565             final HttpRoute route,
566             final HttpRequestWrapper request,
567             final HttpClientContext context,
568             final HttpExecutionAware execAware) throws IOException, HttpException  {
569 
570         final Date requestDate = getCurrentDate();
571 
572         log.trace("Calling the backend");
573         final CloseableHttpResponse backendResponse = backend.execute(route, request, context, execAware);
574         try {
575             backendResponse.addHeader("Via", generateViaHeader(backendResponse));
576             return handleBackendResponse(route, request, context, execAware,
577                     requestDate, getCurrentDate(), backendResponse);
578         } catch (final IOException ex) {
579             backendResponse.close();
580             throw ex;
581         } catch (final RuntimeException ex) {
582             backendResponse.close();
583             throw ex;
584         }
585     }
586 
587     private boolean revalidationResponseIsTooOld(final HttpResponse backendResponse,
588             final HttpCacheEntry cacheEntry) {
589         final Header entryDateHeader = cacheEntry.getFirstHeader(HTTP.DATE_HEADER);
590         final Header responseDateHeader = backendResponse.getFirstHeader(HTTP.DATE_HEADER);
591         if (entryDateHeader != null && responseDateHeader != null) {
592             final Date entryDate = DateUtils.parseDate(entryDateHeader.getValue());
593             final Date respDate = DateUtils.parseDate(responseDateHeader.getValue());
594             if (entryDate == null || respDate == null) {
595                 // either backend response or cached entry did not have a valid
596                 // Date header, so we can't tell if they are out of order
597                 // according to the origin clock; thus we can skip the
598                 // unconditional retry recommended in 13.2.6 of RFC 2616.
599                 return false;
600             }
601             if (respDate.before(entryDate)) {
602                 return true;
603             }
604         }
605         return false;
606     }
607 
608     HttpResponse negotiateResponseFromVariants(
609             final HttpRoute route,
610             final HttpRequestWrapper request,
611             final HttpClientContext context,
612             final HttpExecutionAware execAware,
613             final Map<String, Variant> variants) throws IOException, HttpException {
614         final HttpRequestWrapper conditionalRequest = conditionalRequestBuilder
615             .buildConditionalRequestFromVariants(request, variants);
616 
617         final Date requestDate = getCurrentDate();
618         final CloseableHttpResponse backendResponse = backend.execute(
619                 route, conditionalRequest, context, execAware);
620         try {
621             final Date responseDate = getCurrentDate();
622 
623             backendResponse.addHeader("Via", generateViaHeader(backendResponse));
624 
625             if (backendResponse.getStatusLine().getStatusCode() != HttpStatus.SC_NOT_MODIFIED) {
626                 return handleBackendResponse(
627                         route, request, context, execAware,
628                         requestDate, responseDate, backendResponse);
629             }
630 
631             final Header resultEtagHeader = backendResponse.getFirstHeader(HeaderConstants.ETAG);
632             if (resultEtagHeader == null) {
633                 log.warn("304 response did not contain ETag");
634                 EntityUtils.consume(backendResponse.getEntity());
635                 backendResponse.close();
636                 return callBackend(route, request, context, execAware);
637             }
638 
639             final String resultEtag = resultEtagHeader.getValue();
640             final Variant matchingVariant = variants.get(resultEtag);
641             if (matchingVariant == null) {
642                 log.debug("304 response did not contain ETag matching one sent in If-None-Match");
643                 EntityUtils.consume(backendResponse.getEntity());
644                 backendResponse.close();
645                 return callBackend(route, request, context, execAware);
646             }
647 
648             final HttpCacheEntry matchedEntry = matchingVariant.getEntry();
649 
650             if (revalidationResponseIsTooOld(backendResponse, matchedEntry)) {
651                 EntityUtils.consume(backendResponse.getEntity());
652                 backendResponse.close();
653                 return retryRequestUnconditionally(route, request, context, execAware, matchedEntry);
654             }
655 
656             recordCacheUpdate(context);
657 
658             final HttpCacheEntry responseEntry = getUpdatedVariantEntry(
659                     route.getTargetHost(), conditionalRequest, requestDate, responseDate,
660                     backendResponse, matchingVariant, matchedEntry);
661             backendResponse.close();
662 
663             final HttpResponse resp = responseGenerator.generateResponse(responseEntry);
664             tryToUpdateVariantMap(route.getTargetHost(), request, matchingVariant);
665 
666             if (shouldSendNotModifiedResponse(request, responseEntry)) {
667                 return responseGenerator.generateNotModifiedResponse(responseEntry);
668             }
669             return resp;
670         } catch (final IOException ex) {
671             backendResponse.close();
672             throw ex;
673         } catch (final RuntimeException ex) {
674             backendResponse.close();
675             throw ex;
676         }
677     }
678 
679     private CloseableHttpResponse retryRequestUnconditionally(
680             final HttpRoute route,
681             final HttpRequestWrapper request,
682             final HttpClientContext context,
683             final HttpExecutionAware execAware,
684             final HttpCacheEntry matchedEntry) throws IOException, HttpException {
685         final HttpRequestWrapper unconditional = conditionalRequestBuilder
686             .buildUnconditionalRequest(request, matchedEntry);
687         return callBackend(route, unconditional, context, execAware);
688     }
689 
690     private HttpCacheEntry getUpdatedVariantEntry(
691             final HttpHost target,
692             final HttpRequestWrapper conditionalRequest,
693             final Date requestDate,
694             final Date responseDate,
695             final CloseableHttpResponse backendResponse,
696             final Variant matchingVariant,
697             final HttpCacheEntry matchedEntry) throws IOException {
698         HttpCacheEntry responseEntry = matchedEntry;
699         try {
700             responseEntry = responseCache.updateVariantCacheEntry(target, conditionalRequest,
701                     matchedEntry, backendResponse, requestDate, responseDate, matchingVariant.getCacheKey());
702         } catch (final IOException ioe) {
703             log.warn("Could not update cache entry", ioe);
704         } finally {
705             backendResponse.close();
706         }
707         return responseEntry;
708     }
709 
710     private void tryToUpdateVariantMap(
711             final HttpHost target,
712             final HttpRequestWrapper request,
713             final Variant matchingVariant) {
714         try {
715             responseCache.reuseVariantEntryFor(target, request, matchingVariant);
716         } catch (final IOException ioe) {
717             log.warn("Could not update cache entry to reuse variant", ioe);
718         }
719     }
720 
721     private boolean shouldSendNotModifiedResponse(
722             final HttpRequestWrapper request,
723             final HttpCacheEntry responseEntry) {
724         return (suitabilityChecker.isConditional(request)
725                 && suitabilityChecker.allConditionalsMatch(request, responseEntry, new Date()));
726     }
727 
728     CloseableHttpResponse revalidateCacheEntry(
729             final HttpRoute route,
730             final HttpRequestWrapper request,
731             final HttpClientContext context,
732             final HttpExecutionAware execAware,
733             final HttpCacheEntry cacheEntry) throws IOException, HttpException {
734 
735         final HttpRequestWrapper conditionalRequest = conditionalRequestBuilder.buildConditionalRequest(request, cacheEntry);
736 
737         Date requestDate = getCurrentDate();
738         CloseableHttpResponse backendResponse = backend.execute(
739                 route, conditionalRequest, context, execAware);
740         Date responseDate = getCurrentDate();
741 
742         if (revalidationResponseIsTooOld(backendResponse, cacheEntry)) {
743             backendResponse.close();
744             final HttpRequestWrapper unconditional = conditionalRequestBuilder
745                 .buildUnconditionalRequest(request, cacheEntry);
746             requestDate = getCurrentDate();
747             backendResponse = backend.execute(route, unconditional, context, execAware);
748             responseDate = getCurrentDate();
749         }
750 
751         backendResponse.addHeader(HeaderConstants.VIA, generateViaHeader(backendResponse));
752 
753         final int statusCode = backendResponse.getStatusLine().getStatusCode();
754         if (statusCode == HttpStatus.SC_NOT_MODIFIED || statusCode == HttpStatus.SC_OK) {
755             recordCacheUpdate(context);
756         }
757 
758         if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
759             final HttpCacheEntry updatedEntry = responseCache.updateCacheEntry(
760                     route.getTargetHost(), request, cacheEntry,
761                     backendResponse, requestDate, responseDate);
762             if (suitabilityChecker.isConditional(request)
763                     && suitabilityChecker.allConditionalsMatch(request, updatedEntry, new Date())) {
764                 return Proxies.enhanceResponse(
765                         responseGenerator.generateNotModifiedResponse(updatedEntry));
766             }
767             return Proxies.enhanceResponse(responseGenerator.generateResponse(updatedEntry));
768         }
769 
770         if (staleIfErrorAppliesTo(statusCode)
771             && !staleResponseNotAllowed(request, cacheEntry, getCurrentDate())
772             && validityPolicy.mayReturnStaleIfError(request, cacheEntry, responseDate)) {
773             try {
774                 final HttpResponse cachedResponse = responseGenerator.generateResponse(cacheEntry);
775                 cachedResponse.addHeader(HeaderConstants.WARNING, "110 localhost \"Response is stale\"");
776                 return Proxies.enhanceResponse(cachedResponse);
777             } finally {
778                 backendResponse.close();
779             }
780         }
781         return handleBackendResponse(
782                 route, conditionalRequest, context, execAware,
783                 requestDate, responseDate, backendResponse);
784     }
785 
786     private boolean staleIfErrorAppliesTo(final int statusCode) {
787         return statusCode == HttpStatus.SC_INTERNAL_SERVER_ERROR
788                 || statusCode == HttpStatus.SC_BAD_GATEWAY
789                 || statusCode == HttpStatus.SC_SERVICE_UNAVAILABLE
790                 || statusCode == HttpStatus.SC_GATEWAY_TIMEOUT;
791     }
792 
793     CloseableHttpResponse handleBackendResponse(
794             final HttpRoute route,
795             final HttpRequestWrapper request,
796             final HttpClientContext context,
797             final HttpExecutionAware execAware,
798             final Date requestDate,
799             final Date responseDate,
800             final CloseableHttpResponse backendResponse) throws IOException {
801 
802         log.trace("Handling Backend response");
803         responseCompliance.ensureProtocolCompliance(request, backendResponse);
804 
805         final HttpHost target = route.getTargetHost();
806         final boolean cacheable = responseCachingPolicy.isResponseCacheable(request, backendResponse);
807         responseCache.flushInvalidatedCacheEntriesFor(target, request, backendResponse);
808         if (cacheable && !alreadyHaveNewerCacheEntry(target, request, backendResponse)) {
809             try {
810                 storeRequestIfModifiedSinceFor304Response(request, backendResponse);
811                 return Proxies.enhanceResponse(responseCache.cacheAndReturnResponse(
812                         target, request, backendResponse, requestDate, responseDate));
813             } finally {
814                 backendResponse.close();
815             }
816         }
817         if (!cacheable) {
818             try {
819                 responseCache.flushCacheEntriesFor(target, request);
820             } catch (final IOException ioe) {
821                 log.warn("Unable to flush invalid cache entries", ioe);
822             }
823         }
824         return backendResponse;
825     }
826 
827     /**
828      * For 304 Not modified responses, adds a "Last-Modified" header with the
829      * value of the "If-Modified-Since" header passed in the request. This
830      * header is required to be able to reuse match the cache entry for
831      * subsequent requests but as defined in http specifications it is not
832      * included in 304 responses by backend servers. This header will not be
833      * included in the resulting response.
834      */
835     private void storeRequestIfModifiedSinceFor304Response(
836             final HttpRequest request, final HttpResponse backendResponse) {
837         if (backendResponse.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_MODIFIED) {
838             final Header h = request.getFirstHeader("If-Modified-Since");
839             if (h != null) {
840                 backendResponse.addHeader("Last-Modified", h.getValue());
841             }
842         }
843     }
844 
845     private boolean alreadyHaveNewerCacheEntry(final HttpHost target, final HttpRequestWrapper request,
846             final HttpResponse backendResponse) {
847         HttpCacheEntry existing = null;
848         try {
849             existing = responseCache.getCacheEntry(target, request);
850         } catch (final IOException ioe) {
851             // nop
852         }
853         if (existing == null) {
854             return false;
855         }
856         final Header entryDateHeader = existing.getFirstHeader(HTTP.DATE_HEADER);
857         if (entryDateHeader == null) {
858             return false;
859         }
860         final Header responseDateHeader = backendResponse.getFirstHeader(HTTP.DATE_HEADER);
861         if (responseDateHeader == null) {
862             return false;
863         }
864         final Date entryDate = DateUtils.parseDate(entryDateHeader.getValue());
865         final Date responseDate = DateUtils.parseDate(responseDateHeader.getValue());
866         if (entryDate == null || responseDate == null) {
867             return false;
868         }
869         return responseDate.before(entryDate);
870     }
871 
872 }