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