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.hc.client5.http.impl.cache;
28
29 import java.net.URI;
30 import java.time.Instant;
31 import java.util.Collection;
32 import java.util.Collections;
33 import java.util.HashSet;
34 import java.util.List;
35 import java.util.Map;
36 import java.util.Set;
37 import java.util.stream.Collectors;
38
39 import org.apache.hc.client5.http.cache.HttpAsyncCacheStorage;
40 import org.apache.hc.client5.http.cache.HttpCacheCASOperation;
41 import org.apache.hc.client5.http.cache.HttpCacheEntry;
42 import org.apache.hc.client5.http.cache.HttpCacheEntryFactory;
43 import org.apache.hc.client5.http.cache.HttpCacheUpdateException;
44 import org.apache.hc.client5.http.cache.Resource;
45 import org.apache.hc.client5.http.cache.ResourceFactory;
46 import org.apache.hc.client5.http.cache.ResourceIOException;
47 import org.apache.hc.client5.http.impl.Operations;
48 import org.apache.hc.client5.http.validator.ETag;
49 import org.apache.hc.client5.http.validator.ValidatorType;
50 import org.apache.hc.core5.concurrent.CallbackContribution;
51 import org.apache.hc.core5.concurrent.Cancellable;
52 import org.apache.hc.core5.concurrent.ComplexCancellable;
53 import org.apache.hc.core5.concurrent.FutureCallback;
54 import org.apache.hc.core5.http.HttpHeaders;
55 import org.apache.hc.core5.http.HttpHost;
56 import org.apache.hc.core5.http.HttpRequest;
57 import org.apache.hc.core5.http.HttpResponse;
58 import org.apache.hc.core5.http.HttpStatus;
59 import org.apache.hc.core5.http.Method;
60 import org.apache.hc.core5.util.ByteArrayBuffer;
61 import org.slf4j.Logger;
62 import org.slf4j.LoggerFactory;
63
64 class BasicHttpAsyncCache implements HttpAsyncCache {
65
66 private static final Logger LOG = LoggerFactory.getLogger(BasicHttpAsyncCache.class);
67
68 private final ResourceFactory resourceFactory;
69 private final HttpCacheEntryFactory cacheEntryFactory;
70 private final CacheKeyGenerator cacheKeyGenerator;
71 private final HttpAsyncCacheStorage storage;
72
73 public BasicHttpAsyncCache(
74 final ResourceFactory resourceFactory,
75 final HttpCacheEntryFactory cacheEntryFactory,
76 final HttpAsyncCacheStorage storage,
77 final CacheKeyGenerator cacheKeyGenerator) {
78 this.resourceFactory = resourceFactory;
79 this.cacheEntryFactory = cacheEntryFactory;
80 this.cacheKeyGenerator = cacheKeyGenerator;
81 this.storage = storage;
82 }
83
84 public BasicHttpAsyncCache(
85 final ResourceFactory resourceFactory,
86 final HttpAsyncCacheStorage storage,
87 final CacheKeyGenerator cacheKeyGenerator) {
88 this(resourceFactory, HttpCacheEntryFactory.INSTANCE, storage, cacheKeyGenerator);
89 }
90
91 public BasicHttpAsyncCache(final ResourceFactory resourceFactory, final HttpAsyncCacheStorage storage) {
92 this( resourceFactory, storage, CacheKeyGenerator.INSTANCE);
93 }
94
95 @Override
96 public Cancellable match(final HttpHost host, final HttpRequest request, final FutureCallback<CacheMatch> callback) {
97 final String rootKey = cacheKeyGenerator.generateKey(host, request);
98 if (LOG.isDebugEnabled()) {
99 LOG.debug("Get cache entry: {}", rootKey);
100 }
101 final ComplexCancellable complexCancellable = new ComplexCancellable();
102 complexCancellable.setDependency(storage.getEntry(rootKey, new FutureCallback<HttpCacheEntry>() {
103
104 @Override
105 public void completed(final HttpCacheEntry root) {
106 if (root != null) {
107 if (root.hasVariants()) {
108 final List<String> variantNames = CacheKeyGenerator.variantNames(root);
109 final String variantKey = cacheKeyGenerator.generateVariantKey(request, variantNames);
110 if (root.getVariants().contains(variantKey)) {
111 final String cacheKey = variantKey + rootKey;
112 if (LOG.isDebugEnabled()) {
113 LOG.debug("Get cache variant entry: {}", cacheKey);
114 }
115 complexCancellable.setDependency(storage.getEntry(
116 cacheKey,
117 new FutureCallback<HttpCacheEntry>() {
118
119 @Override
120 public void completed(final HttpCacheEntry entry) {
121 callback.completed(new CacheMatch(
122 entry != null ? new CacheHit(rootKey, cacheKey, entry) : null,
123 new CacheHit(rootKey, root)));
124 }
125
126 @Override
127 public void failed(final Exception ex) {
128 if (ex instanceof ResourceIOException) {
129 if (LOG.isWarnEnabled()) {
130 LOG.warn("I/O error retrieving cache entry with key {}", cacheKey);
131 }
132 callback.completed(null);
133 } else {
134 callback.failed(ex);
135 }
136 }
137
138 @Override
139 public void cancelled() {
140 callback.cancelled();
141 }
142
143 }));
144 return;
145 }
146 callback.completed(new CacheMatch(null, new CacheHit(rootKey, root)));
147 } else {
148 callback.completed(new CacheMatch(new CacheHit(rootKey, root), null));
149 }
150 } else {
151 callback.completed(null);
152 }
153 }
154
155 @Override
156 public void failed(final Exception ex) {
157 if (ex instanceof ResourceIOException) {
158 if (LOG.isWarnEnabled()) {
159 LOG.warn("I/O error retrieving cache entry with key {}", rootKey);
160 }
161 callback.completed(null);
162 } else {
163 callback.failed(ex);
164 }
165 }
166
167 @Override
168 public void cancelled() {
169 callback.cancelled();
170 }
171
172 }));
173 return complexCancellable;
174 }
175
176 @Override
177 public Cancellable getVariants(
178 final CacheHit hit, final FutureCallback<Collection<CacheHit>> callback) {
179 if (LOG.isDebugEnabled()) {
180 LOG.debug("Get variant cache entries: {}", hit.rootKey);
181 }
182 final ComplexCancellable complexCancellable = new ComplexCancellable();
183 final HttpCacheEntry root = hit.entry;
184 final String rootKey = hit.rootKey;
185 if (root != null && root.hasVariants()) {
186 final List<String> variantCacheKeys = root.getVariants().stream()
187 .map(e -> e + rootKey)
188 .collect(Collectors.toList());
189 complexCancellable.setDependency(storage.getEntries(
190 variantCacheKeys,
191 new FutureCallback<Map<String, HttpCacheEntry>>() {
192
193 @Override
194 public void completed(final Map<String, HttpCacheEntry> resultMap) {
195 final List<CacheHit> cacheHits = resultMap.entrySet().stream()
196 .map(e -> new CacheHit(hit.rootKey, e.getKey(), e.getValue()))
197 .collect(Collectors.toList());
198 callback.completed(cacheHits);
199 }
200
201 @Override
202 public void failed(final Exception ex) {
203 if (ex instanceof ResourceIOException) {
204 if (LOG.isWarnEnabled()) {
205 LOG.warn("I/O error retrieving cache entry with keys {}", variantCacheKeys);
206 }
207 callback.completed(Collections.emptyList());
208 } else {
209 callback.failed(ex);
210 }
211 }
212
213 @Override
214 public void cancelled() {
215 callback.cancelled();
216 }
217
218 }));
219 } else {
220 callback.completed(Collections.emptyList());
221 }
222 return complexCancellable;
223 }
224
225 Cancellable storeInternal(final String cacheKey, final HttpCacheEntry entry, final FutureCallback<Boolean> callback) {
226 if (LOG.isDebugEnabled()) {
227 LOG.debug("Store entry in cache: {}", cacheKey);
228 }
229
230 return storage.putEntry(cacheKey, entry, new FutureCallback<Boolean>() {
231
232 @Override
233 public void completed(final Boolean result) {
234 if (callback != null) {
235 callback.completed(result);
236 }
237 }
238
239 @Override
240 public void failed(final Exception ex) {
241 if (ex instanceof ResourceIOException) {
242 if (LOG.isWarnEnabled()) {
243 LOG.warn("I/O error storing cache entry with key {}", cacheKey);
244 }
245 if (callback != null) {
246 callback.completed(false);
247 }
248 } else {
249 if (callback != null) {
250 callback.failed(ex);
251 }
252 }
253 }
254
255 @Override
256 public void cancelled() {
257 if (callback != null) {
258 callback.cancelled();
259 }
260 }
261
262 });
263 }
264
265 Cancellable updateInternal(final String cacheKey, final HttpCacheCASOperation casOperation, final FutureCallback<Boolean> callback) {
266 return storage.updateEntry(cacheKey, casOperation, new FutureCallback<Boolean>() {
267
268 @Override
269 public void completed(final Boolean result) {
270 if (callback != null) {
271 callback.completed(result);
272 }
273 }
274
275 @Override
276 public void failed(final Exception ex) {
277 if (ex instanceof HttpCacheUpdateException) {
278 if (LOG.isWarnEnabled()) {
279 LOG.warn("Cannot update cache entry with key {}", cacheKey);
280 }
281 if (callback != null) {
282 callback.completed(false);
283 }
284 } else if (ex instanceof ResourceIOException) {
285 if (LOG.isWarnEnabled()) {
286 LOG.warn("I/O error updating cache entry with key {}", cacheKey);
287 }
288 if (callback != null) {
289 callback.completed(false);
290 }
291 } else {
292 if (callback != null) {
293 callback.failed(ex);
294 }
295 }
296 }
297
298 @Override
299 public void cancelled() {
300 if (callback != null) {
301 callback.cancelled();
302 }
303 }
304
305 });
306 }
307
308 private void removeInternal(final String cacheKey) {
309 storage.removeEntry(cacheKey, new FutureCallback<Boolean>() {
310
311 @Override
312 public void completed(final Boolean result) {
313 }
314
315 @Override
316 public void failed(final Exception ex) {
317 if (LOG.isWarnEnabled()) {
318 if (ex instanceof ResourceIOException) {
319 LOG.warn("I/O error removing cache entry with key {}", cacheKey);
320 } else {
321 LOG.warn("Unexpected error removing cache entry with key {}", cacheKey, ex);
322 }
323 }
324 }
325
326 @Override
327 public void cancelled() {
328 }
329
330 });
331 }
332
333 Cancellable store(
334 final String rootKey,
335 final String variantKey,
336 final HttpCacheEntry entry,
337 final FutureCallback<CacheHit> callback) {
338 if (variantKey == null) {
339 if (LOG.isDebugEnabled()) {
340 LOG.debug("Store entry in cache: {}", rootKey);
341 }
342 return storeInternal(rootKey, entry, new CallbackContribution<Boolean>(callback) {
343
344 @Override
345 public void completed(final Boolean result) {
346 callback.completed(new CacheHit(rootKey, entry));
347 }
348
349 });
350 }
351 final String variantCacheKey = variantKey + rootKey;
352
353 if (LOG.isDebugEnabled()) {
354 LOG.debug("Store variant entry in cache: {}", variantCacheKey);
355 }
356
357 return storeInternal(variantCacheKey, entry, new CallbackContribution<Boolean>(callback) {
358
359 @Override
360 public void completed(final Boolean result) {
361 if (LOG.isDebugEnabled()) {
362 LOG.debug("Update root entry: {}", rootKey);
363 }
364
365 updateInternal(rootKey,
366 existing -> {
367 final Set<String> variantMap = existing != null ? new HashSet<>(existing.getVariants()) : new HashSet<>();
368 variantMap.add(variantKey);
369 return cacheEntryFactory.createRoot(entry, variantMap);
370 },
371 new CallbackContribution<Boolean>(callback) {
372
373 @Override
374 public void completed(final Boolean result) {
375 callback.completed(new CacheHit(rootKey, variantCacheKey, entry));
376 }
377
378 });
379 }
380
381 });
382 }
383
384 @Override
385 public Cancellable store(
386 final HttpHost host,
387 final HttpRequest request,
388 final HttpResponse originResponse,
389 final ByteArrayBuffer content,
390 final Instant requestSent,
391 final Instant responseReceived,
392 final FutureCallback<CacheHit> callback) {
393 final String rootKey = cacheKeyGenerator.generateKey(host, request);
394 if (LOG.isDebugEnabled()) {
395 LOG.debug("Create cache entry: {}", rootKey);
396 }
397 final Resource resource;
398 try {
399 final ETag eTag = ETag.get(originResponse);
400 resource = content != null ? resourceFactory.generate(
401 rootKey,
402 eTag != null && eTag.getType() == ValidatorType.STRONG ? eTag.getValue() : null,
403 content.array(), 0, content.length()) : null;
404 } catch (final ResourceIOException ex) {
405 if (LOG.isWarnEnabled()) {
406 LOG.warn("I/O error creating cache entry with key {}", rootKey);
407 }
408 final HttpCacheEntry backup = cacheEntryFactory.create(
409 requestSent,
410 responseReceived,
411 host,
412 request,
413 originResponse,
414 content != null ? HeapResourceFactory.INSTANCE.generate(null, content.array(), 0, content.length()) : null);
415 callback.completed(new CacheHit(rootKey, backup));
416 return Operations.nonCancellable();
417 }
418 final HttpCacheEntry entry = cacheEntryFactory.create(requestSent, responseReceived, host, request, originResponse, resource);
419 final String variantKey = cacheKeyGenerator.generateVariantKey(request, entry);
420 return store(rootKey,variantKey, entry, callback);
421 }
422
423 @Override
424 public Cancellable update(
425 final CacheHit stale,
426 final HttpHost host,
427 final HttpRequest request,
428 final HttpResponse originResponse,
429 final Instant requestSent,
430 final Instant responseReceived,
431 final FutureCallback<CacheHit> callback) {
432 if (LOG.isDebugEnabled()) {
433 LOG.debug("Update cache entry: {}", stale.getEntryKey());
434 }
435 final HttpCacheEntry updatedEntry = cacheEntryFactory.createUpdated(
436 requestSent,
437 responseReceived,
438 host,
439 request,
440 originResponse,
441 stale.entry);
442 final String variantKey = cacheKeyGenerator.generateVariantKey(request, updatedEntry);
443 return store(stale.rootKey, variantKey, updatedEntry, callback);
444 }
445
446 @Override
447 public Cancellable storeFromNegotiated(
448 final CacheHit negotiated,
449 final HttpHost host,
450 final HttpRequest request,
451 final HttpResponse originResponse,
452 final Instant requestSent,
453 final Instant responseReceived,
454 final FutureCallback<CacheHit> callback) {
455 if (LOG.isDebugEnabled()) {
456 LOG.debug("Update negotiated cache entry: {}", negotiated.getEntryKey());
457 }
458 final HttpCacheEntry updatedEntry = cacheEntryFactory.createUpdated(
459 requestSent,
460 responseReceived,
461 host,
462 request,
463 originResponse,
464 negotiated.entry);
465
466 storeInternal(negotiated.getEntryKey(), updatedEntry, null);
467
468 final String rootKey = cacheKeyGenerator.generateKey(host, request);
469 final HttpCacheEntry copy = cacheEntryFactory.copy(updatedEntry);
470 final String variantKey = cacheKeyGenerator.generateVariantKey(request, copy);
471 return store(rootKey, variantKey, copy, callback);
472 }
473
474 private void evictAll(final HttpCacheEntry root, final String rootKey) {
475 if (LOG.isDebugEnabled()) {
476 LOG.debug("Evicting root cache entry {}", rootKey);
477 }
478 removeInternal(rootKey);
479 if (root.hasVariants()) {
480 for (final String variantKey : root.getVariants()) {
481 final String variantEntryKey = variantKey + rootKey;
482 if (LOG.isDebugEnabled()) {
483 LOG.debug("Evicting variant cache entry {}", variantEntryKey);
484 }
485 removeInternal(variantEntryKey);
486 }
487 }
488 }
489
490 private Cancellable evict(final String rootKey) {
491 return storage.getEntry(rootKey, new FutureCallback<HttpCacheEntry>() {
492
493 @Override
494 public void completed(final HttpCacheEntry root) {
495 if (root != null) {
496 if (LOG.isDebugEnabled()) {
497 LOG.debug("Evicting root cache entry {}", rootKey);
498 }
499 evictAll(root, rootKey);
500 }
501 }
502
503 @Override
504 public void failed(final Exception ex) {
505 }
506
507 @Override
508 public void cancelled() {
509 }
510
511 });
512 }
513
514 private Cancellable evict(final String rootKey, final HttpResponse response) {
515 return storage.getEntry(rootKey, new FutureCallback<HttpCacheEntry>() {
516
517 @Override
518 public void completed(final HttpCacheEntry root) {
519 if (root != null) {
520 if (LOG.isDebugEnabled()) {
521 LOG.debug("Evicting root cache entry {}", rootKey);
522 }
523 final ETag existingETag = root.getETag();
524 final ETag newETag = ETag.get(response);
525 if (existingETag != null && newETag != null &&
526 !ETag.strongCompare(existingETag, newETag) &&
527 !HttpCacheEntry.isNewer(root, response)) {
528 evictAll(root, rootKey);
529 }
530 }
531 }
532
533 @Override
534 public void failed(final Exception ex) {
535 }
536
537 @Override
538 public void cancelled() {
539 }
540
541 });
542 }
543
544 @Override
545 public Cancellable evictInvalidatedEntries(
546 final HttpHost host, final HttpRequest request, final HttpResponse response, final FutureCallback<Boolean> callback) {
547 if (LOG.isDebugEnabled()) {
548 LOG.debug("Flush cache entries invalidated by exchange: {}; {} {} -> {}",
549 host, request.getMethod(), request.getRequestUri(), response.getCode());
550 }
551 final int status = response.getCode();
552 if (status >= HttpStatus.SC_SUCCESS && status < HttpStatus.SC_CLIENT_ERROR &&
553 !Method.isSafe(request.getMethod())) {
554 final String rootKey = cacheKeyGenerator.generateKey(host, request);
555 evict(rootKey);
556 final URI requestUri = CacheKeyGenerator.normalize(CacheKeyGenerator.getRequestUri(host, request));
557 if (requestUri != null) {
558 final URI contentLocation = CacheSupport.getLocationURI(requestUri, response, HttpHeaders.CONTENT_LOCATION);
559 if (contentLocation != null && CacheSupport.isSameOrigin(requestUri, contentLocation)) {
560 final String cacheKey = cacheKeyGenerator.generateKey(contentLocation);
561 evict(cacheKey, response);
562 }
563 final URI location = CacheSupport.getLocationURI(requestUri, response, HttpHeaders.LOCATION);
564 if (location != null && CacheSupport.isSameOrigin(requestUri, location)) {
565 final String cacheKey = cacheKeyGenerator.generateKey(location);
566 evict(cacheKey, response);
567 }
568 }
569 }
570 callback.completed(Boolean.TRUE);
571 return Operations.nonCancellable();
572 }
573
574 }