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.ArrayList;
32  import java.util.Collections;
33  import java.util.HashSet;
34  import java.util.List;
35  import java.util.Set;
36  
37  import org.apache.hc.client5.http.cache.HttpCacheCASOperation;
38  import org.apache.hc.client5.http.cache.HttpCacheEntry;
39  import org.apache.hc.client5.http.cache.HttpCacheEntryFactory;
40  import org.apache.hc.client5.http.cache.HttpCacheStorage;
41  import org.apache.hc.client5.http.cache.HttpCacheUpdateException;
42  import org.apache.hc.client5.http.cache.Resource;
43  import org.apache.hc.client5.http.cache.ResourceFactory;
44  import org.apache.hc.client5.http.cache.ResourceIOException;
45  import org.apache.hc.client5.http.validator.ETag;
46  import org.apache.hc.client5.http.validator.ValidatorType;
47  import org.apache.hc.core5.http.HttpHeaders;
48  import org.apache.hc.core5.http.HttpHost;
49  import org.apache.hc.core5.http.HttpRequest;
50  import org.apache.hc.core5.http.HttpResponse;
51  import org.apache.hc.core5.http.HttpStatus;
52  import org.apache.hc.core5.http.Method;
53  import org.apache.hc.core5.util.ByteArrayBuffer;
54  import org.slf4j.Logger;
55  import org.slf4j.LoggerFactory;
56  
57  class BasicHttpCache implements HttpCache {
58  
59      private static final Logger LOG = LoggerFactory.getLogger(BasicHttpCache.class);
60  
61      private final ResourceFactory resourceFactory;
62      private final HttpCacheEntryFactory cacheEntryFactory;
63      private final CacheKeyGenerator cacheKeyGenerator;
64      private final HttpCacheStorage storage;
65  
66      public BasicHttpCache(
67              final ResourceFactory resourceFactory,
68              final HttpCacheEntryFactory cacheEntryFactory,
69              final HttpCacheStorage storage,
70              final CacheKeyGenerator cacheKeyGenerator) {
71          this.resourceFactory = resourceFactory;
72          this.cacheEntryFactory = cacheEntryFactory;
73          this.cacheKeyGenerator = cacheKeyGenerator;
74          this.storage = storage;
75      }
76  
77      public BasicHttpCache(
78              final ResourceFactory resourceFactory,
79              final HttpCacheStorage storage,
80              final CacheKeyGenerator cacheKeyGenerator) {
81          this(resourceFactory, HttpCacheEntryFactory.INSTANCE, storage, cacheKeyGenerator);
82      }
83  
84      public BasicHttpCache(final ResourceFactory resourceFactory, final HttpCacheStorage storage) {
85          this( resourceFactory, storage, new CacheKeyGenerator());
86      }
87  
88      public BasicHttpCache(final CacheConfig config) {
89          this(new HeapResourceFactory(), new BasicHttpCacheStorage(config));
90      }
91  
92      public BasicHttpCache() {
93          this(CacheConfig.DEFAULT);
94      }
95  
96      void storeInternal(final String cacheKey, final HttpCacheEntry entry) {
97          try {
98              storage.putEntry(cacheKey, entry);
99          } catch (final ResourceIOException ex) {
100             if (LOG.isWarnEnabled()) {
101                 LOG.warn("I/O error storing cache entry with key {}", cacheKey);
102             }
103         }
104     }
105 
106     void updateInternal(final String cacheKey, final HttpCacheCASOperation casOperation) {
107         try {
108             storage.updateEntry(cacheKey, casOperation);
109         } catch (final HttpCacheUpdateException ex) {
110             if (LOG.isWarnEnabled()) {
111                 LOG.warn("Cannot update cache entry with key {}", cacheKey);
112             }
113         } catch (final ResourceIOException ex) {
114             if (LOG.isWarnEnabled()) {
115                 LOG.warn("I/O error updating cache entry with key {}", cacheKey);
116             }
117         }
118     }
119 
120     HttpCacheEntry getInternal(final String cacheKey) {
121         try {
122             return storage.getEntry(cacheKey);
123         } catch (final ResourceIOException ex) {
124             if (LOG.isWarnEnabled()) {
125                 LOG.warn("I/O error retrieving cache entry with key {}", cacheKey);
126             }
127             return null;
128         }
129     }
130 
131     private void removeInternal(final String cacheKey) {
132         try {
133             storage.removeEntry(cacheKey);
134         } catch (final ResourceIOException ex) {
135             if (LOG.isWarnEnabled()) {
136                 LOG.warn("I/O error removing cache entry with key {}", cacheKey);
137             }
138         }
139     }
140 
141     @Override
142     public CacheMatch match(final HttpHost host, final HttpRequest request) {
143         final String rootKey = cacheKeyGenerator.generateKey(host, request);
144         if (LOG.isDebugEnabled()) {
145             LOG.debug("Get cache root entry: {}", rootKey);
146         }
147         final HttpCacheEntry root = getInternal(rootKey);
148         if (root == null) {
149             return null;
150         }
151         if (root.hasVariants()) {
152             final List<String> variantNames = CacheKeyGenerator.variantNames(root);
153             final String variantKey = cacheKeyGenerator.generateVariantKey(request, variantNames);
154             if (root.getVariants().contains(variantKey)) {
155                 final String cacheKey = variantKey + rootKey;
156                 if (LOG.isDebugEnabled()) {
157                     LOG.debug("Get cache variant entry: {}", cacheKey);
158                 }
159                 final HttpCacheEntry entry = getInternal(cacheKey);
160                 if (entry != null) {
161                     return new CacheMatch(new CacheHit(rootKey, cacheKey, entry), new CacheHit(rootKey, root));
162                 }
163             }
164             return new CacheMatch(null, new CacheHit(rootKey, root));
165         }
166         return new CacheMatch(new CacheHit(rootKey, root), null);
167     }
168 
169     @Override
170     public List<CacheHit> getVariants(final CacheHit hit) {
171         if (LOG.isDebugEnabled()) {
172             LOG.debug("Get variant cache entries: {}", hit.rootKey);
173         }
174         final HttpCacheEntry root = hit.entry;
175         final String rootKey = hit.rootKey;
176         if (root != null && root.hasVariants()) {
177             final List<CacheHit> variants = new ArrayList<>();
178             for (final String variantKey : root.getVariants()) {
179                 final String variantCacheKey = variantKey + rootKey;
180                 final HttpCacheEntry variant = getInternal(variantCacheKey);
181                 if (variant != null) {
182                     variants.add(new CacheHit(rootKey, variantCacheKey, variant));
183                 }
184             }
185             return variants;
186         }
187         return Collections.emptyList();
188     }
189 
190     CacheHit store(final String rootKey, final String variantKey, final HttpCacheEntry entry) {
191         if (variantKey == null) {
192             if (LOG.isDebugEnabled()) {
193                 LOG.debug("Store entry in cache: {}", rootKey);
194             }
195             storeInternal(rootKey, entry);
196             return new CacheHit(rootKey, entry);
197         }
198         final String variantCacheKey = variantKey + rootKey;
199 
200         if (LOG.isDebugEnabled()) {
201             LOG.debug("Store variant entry in cache: {}", variantCacheKey);
202         }
203 
204         storeInternal(variantCacheKey, entry);
205 
206         if (LOG.isDebugEnabled()) {
207             LOG.debug("Update root entry: {}", rootKey);
208         }
209 
210         updateInternal(rootKey, existing -> {
211             final Set<String> variants = existing != null ? new HashSet<>(existing.getVariants()) : new HashSet<>();
212             variants.add(variantKey);
213             return cacheEntryFactory.createRoot(entry, variants);
214         });
215         return new CacheHit(rootKey, variantCacheKey, entry);
216     }
217 
218     @Override
219     public CacheHit store(
220             final HttpHost host,
221             final HttpRequest request,
222             final HttpResponse originResponse,
223             final ByteArrayBuffer content,
224             final Instant requestSent,
225             final Instant responseReceived) {
226         final String rootKey = cacheKeyGenerator.generateKey(host, request);
227         if (LOG.isDebugEnabled()) {
228             LOG.debug("Create cache entry: {}", rootKey);
229         }
230         final Resource resource;
231         try {
232             final ETag eTag = ETag.get(originResponse);
233             resource = content != null ? resourceFactory.generate(
234                     rootKey,
235                     eTag != null && eTag.getType() == ValidatorType.STRONG ? eTag.getValue() : null,
236                     content.array(), 0, content.length()) : null;
237         } catch (final ResourceIOException ex) {
238             if (LOG.isWarnEnabled()) {
239                 LOG.warn("I/O error creating cache entry with key {}", rootKey);
240             }
241             final HttpCacheEntry backup = cacheEntryFactory.create(
242                     requestSent,
243                     responseReceived,
244                     host,
245                     request,
246                     originResponse,
247                     content != null ? HeapResourceFactory.INSTANCE.generate(null, content.array(), 0, content.length()) : null);
248             return new CacheHit(rootKey, backup);
249         }
250         final HttpCacheEntry entry = cacheEntryFactory.create(requestSent, responseReceived, host, request, originResponse, resource);
251         final String variantKey = cacheKeyGenerator.generateVariantKey(request, entry);
252         return store(rootKey,variantKey, entry);
253     }
254 
255     @Override
256     public CacheHit update(
257             final CacheHit stale,
258             final HttpHost host,
259             final HttpRequest request,
260             final HttpResponse originResponse,
261             final Instant requestSent,
262             final Instant responseReceived) {
263         if (LOG.isDebugEnabled()) {
264             LOG.debug("Update cache entry: {}", stale.getEntryKey());
265         }
266         final HttpCacheEntry updatedEntry = cacheEntryFactory.createUpdated(
267                 requestSent,
268                 responseReceived,
269                 host,
270                 request,
271                 originResponse,
272                 stale.entry);
273         final String variantKey = cacheKeyGenerator.generateVariantKey(request, updatedEntry);
274         return store(stale.rootKey, variantKey, updatedEntry);
275     }
276 
277     @Override
278     public CacheHit storeFromNegotiated(
279             final CacheHit negotiated,
280             final HttpHost host,
281             final HttpRequest request,
282             final HttpResponse originResponse,
283             final Instant requestSent,
284             final Instant responseReceived) {
285         if (LOG.isDebugEnabled()) {
286             LOG.debug("Update negotiated cache entry: {}", negotiated.getEntryKey());
287         }
288         final HttpCacheEntry updatedEntry = cacheEntryFactory.createUpdated(
289                 requestSent,
290                 responseReceived,
291                 host,
292                 request,
293                 originResponse,
294                negotiated.entry);
295         storeInternal(negotiated.getEntryKey(), updatedEntry);
296 
297         final String rootKey = cacheKeyGenerator.generateKey(host, request);
298         final HttpCacheEntry copy = cacheEntryFactory.copy(updatedEntry);
299         final String variantKey = cacheKeyGenerator.generateVariantKey(request, copy);
300         return store(rootKey, variantKey, copy);
301     }
302 
303     private void evictAll(final HttpCacheEntry root, final String rootKey) {
304         if (LOG.isDebugEnabled()) {
305             LOG.debug("Evicting root cache entry {}", rootKey);
306         }
307         removeInternal(rootKey);
308         if (root.hasVariants()) {
309             for (final String variantKey : root.getVariants()) {
310                 final String variantEntryKey = variantKey + rootKey;
311                 if (LOG.isDebugEnabled()) {
312                     LOG.debug("Evicting variant cache entry {}", variantEntryKey);
313                 }
314                 removeInternal(variantEntryKey);
315             }
316         }
317     }
318 
319     private void evict(final String rootKey) {
320         final HttpCacheEntry root = getInternal(rootKey);
321         if (root == null) {
322             return;
323         }
324         evictAll(root, rootKey);
325     }
326 
327     private void evict(final String rootKey, final HttpResponse response) {
328         final HttpCacheEntry root = getInternal(rootKey);
329         if (root == null) {
330             return;
331         }
332         final ETag existingETag = root.getETag();
333         final ETag newETag = ETag.get(response);
334         if (existingETag != null && newETag != null &&
335                 !ETag.strongCompare(existingETag, newETag) &&
336                 !HttpCacheEntry.isNewer(root, response)) {
337             evictAll(root, rootKey);
338         }
339     }
340 
341     @Override
342     public void evictInvalidatedEntries(final HttpHost host, final HttpRequest request, final HttpResponse response) {
343         if (LOG.isDebugEnabled()) {
344             LOG.debug("Evict cache entries invalidated by exchange: {}; {} {} -> {}",
345                     host, request.getMethod(), request.getRequestUri(), response.getCode());
346         }
347         final int status = response.getCode();
348         if (status >= HttpStatus.SC_SUCCESS && status < HttpStatus.SC_CLIENT_ERROR &&
349                 !Method.isSafe(request.getMethod())) {
350             final String rootKey = cacheKeyGenerator.generateKey(host, request);
351             evict(rootKey);
352             final URI requestUri = CacheKeyGenerator.normalize(CacheKeyGenerator.getRequestUri(host, request));
353             if (requestUri != null) {
354                 final URI contentLocation = CacheSupport.getLocationURI(requestUri, response, HttpHeaders.CONTENT_LOCATION);
355                 if (contentLocation != null && CacheSupport.isSameOrigin(requestUri, contentLocation)) {
356                     final String cacheKey = cacheKeyGenerator.generateKey(contentLocation);
357                     evict(cacheKey, response);
358                 }
359                 final URI location = CacheSupport.getLocationURI(requestUri, response, HttpHeaders.LOCATION);
360                 if (location != null && CacheSupport.isSameOrigin(requestUri, location)) {
361                     final String cacheKey = cacheKeyGenerator.generateKey(location);
362                     evict(cacheKey, response);
363                 }
364             }
365         }
366     }
367 
368 }