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