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