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.Objects;
37 import java.util.Set;
38 import java.util.stream.Collectors;
39
40 import org.apache.hc.client5.http.cache.HttpAsyncCacheStorage;
41 import org.apache.hc.client5.http.cache.HttpCacheCASOperation;
42 import org.apache.hc.client5.http.cache.HttpCacheEntry;
43 import org.apache.hc.client5.http.cache.HttpCacheEntryFactory;
44 import org.apache.hc.client5.http.cache.HttpCacheUpdateException;
45 import org.apache.hc.client5.http.cache.Resource;
46 import org.apache.hc.client5.http.cache.ResourceFactory;
47 import org.apache.hc.client5.http.cache.ResourceIOException;
48 import org.apache.hc.client5.http.impl.Operations;
49 import org.apache.hc.core5.concurrent.CallbackContribution;
50 import org.apache.hc.core5.concurrent.Cancellable;
51 import org.apache.hc.core5.concurrent.ComplexCancellable;
52 import org.apache.hc.core5.concurrent.FutureCallback;
53 import org.apache.hc.core5.http.Header;
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 resource = content != null ? resourceFactory.generate(request.getRequestUri(), 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 Header existingETag = root.getFirstHeader(HttpHeaders.ETAG);
524 final Header newETag = response.getFirstHeader(HttpHeaders.ETAG);
525 if (existingETag != null && newETag != null &&
526 !Objects.equals(existingETag.getValue(), newETag.getValue()) &&
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: {}; {} -> {}", host, new RequestLine(request), new StatusLine(response));
549 }
550 final int status = response.getCode();
551 if (status >= HttpStatus.SC_SUCCESS && status < HttpStatus.SC_CLIENT_ERROR &&
552 !Method.isSafe(request.getMethod())) {
553 final String rootKey = cacheKeyGenerator.generateKey(host, request);
554 evict(rootKey);
555 final URI requestUri = CacheKeyGenerator.normalize(CacheKeyGenerator.getRequestUri(host, request));
556 if (requestUri != null) {
557 final URI contentLocation = CacheSupport.getLocationURI(requestUri, response, HttpHeaders.CONTENT_LOCATION);
558 if (contentLocation != null && CacheSupport.isSameOrigin(requestUri, contentLocation)) {
559 final String cacheKey = cacheKeyGenerator.generateKey(contentLocation);
560 evict(cacheKey, response);
561 }
562 final URI location = CacheSupport.getLocationURI(requestUri, response, HttpHeaders.LOCATION);
563 if (location != null && CacheSupport.isSameOrigin(requestUri, location)) {
564 final String cacheKey = cacheKeyGenerator.generateKey(location);
565 evict(cacheKey, response);
566 }
567 }
568 }
569 callback.completed(Boolean.TRUE);
570 return Operations.nonCancellable();
571 }
572
573 }