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.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 }