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.util.Args;
77 import org.apache.http.util.EntityUtils;
78 import org.apache.http.util.VersionInfo;
79
80 @Contract(threading = ThreadingBehavior.SAFE)
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
216
217
218
219 public long getCacheHits() {
220 return this.cacheHits.get();
221 }
222
223
224
225
226
227
228 public long getCacheMisses() {
229 return this.cacheMisses.get();
230 }
231
232
233
234
235
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
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
630
631
632
633
634 public boolean supportsRangeAndContentRangeHeaders() {
635 return SUPPORTS_RANGE_AND_CONTENT_RANGE_HEADERS;
636 }
637
638
639
640
641
642
643
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
969
970
971
972
973
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
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 }