1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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)
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
217
218
219
220 public long getCacheHits() {
221 return this.cacheHits.get();
222 }
223
224
225
226
227
228
229 public long getCacheMisses() {
230 return this.cacheMisses.get();
231 }
232
233
234
235
236
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
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
631
632
633
634
635 public boolean supportsRangeAndContentRangeHeaders() {
636 return SUPPORTS_RANGE_AND_CONTENT_RANGE_HEADERS;
637 }
638
639
640
641
642
643
644
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
970
971
972
973
974
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
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 }