View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
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 }