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