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.Objects;
36  import java.util.Set;
37  
38  import org.apache.hc.client5.http.cache.HttpCacheCASOperation;
39  import org.apache.hc.client5.http.cache.HttpCacheEntry;
40  import org.apache.hc.client5.http.cache.HttpCacheEntryFactory;
41  import org.apache.hc.client5.http.cache.HttpCacheStorage;
42  import org.apache.hc.client5.http.cache.HttpCacheUpdateException;
43  import org.apache.hc.client5.http.cache.Resource;
44  import org.apache.hc.client5.http.cache.ResourceFactory;
45  import org.apache.hc.client5.http.cache.ResourceIOException;
46  import org.apache.hc.core5.http.Header;
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.http.message.RequestLine;
54  import org.apache.hc.core5.http.message.StatusLine;
55  import org.apache.hc.core5.util.ByteArrayBuffer;
56  import org.slf4j.Logger;
57  import org.slf4j.LoggerFactory;
58  
59  class BasicHttpCache implements HttpCache {
60  
61      private static final Logger LOG = LoggerFactory.getLogger(BasicHttpCache.class);
62  
63      private final ResourceFactory resourceFactory;
64      private final HttpCacheEntryFactory cacheEntryFactory;
65      private final CacheKeyGenerator cacheKeyGenerator;
66      private final HttpCacheStorage storage;
67  
68      public BasicHttpCache(
69              final ResourceFactory resourceFactory,
70              final HttpCacheEntryFactory cacheEntryFactory,
71              final HttpCacheStorage storage,
72              final CacheKeyGenerator cacheKeyGenerator) {
73          this.resourceFactory = resourceFactory;
74          this.cacheEntryFactory = cacheEntryFactory;
75          this.cacheKeyGenerator = cacheKeyGenerator;
76          this.storage = storage;
77      }
78  
79      public BasicHttpCache(
80              final ResourceFactory resourceFactory,
81              final HttpCacheStorage storage,
82              final CacheKeyGenerator cacheKeyGenerator) {
83          this(resourceFactory, HttpCacheEntryFactory.INSTANCE, storage, cacheKeyGenerator);
84      }
85  
86      public BasicHttpCache(final ResourceFactory resourceFactory, final HttpCacheStorage storage) {
87          this( resourceFactory, storage, new CacheKeyGenerator());
88      }
89  
90      public BasicHttpCache(final CacheConfig config) {
91          this(new HeapResourceFactory(), new BasicHttpCacheStorage(config));
92      }
93  
94      public BasicHttpCache() {
95          this(CacheConfig.DEFAULT);
96      }
97  
98      void storeInternal(final String cacheKey, final HttpCacheEntry entry) {
99          try {
100             storage.putEntry(cacheKey, entry);
101         } catch (final ResourceIOException ex) {
102             if (LOG.isWarnEnabled()) {
103                 LOG.warn("I/O error storing cache entry with key {}", cacheKey);
104             }
105         }
106     }
107 
108     void updateInternal(final String cacheKey, final HttpCacheCASOperation casOperation) {
109         try {
110             storage.updateEntry(cacheKey, casOperation);
111         } catch (final HttpCacheUpdateException ex) {
112             if (LOG.isWarnEnabled()) {
113                 LOG.warn("Cannot update cache entry with key {}", cacheKey);
114             }
115         } catch (final ResourceIOException ex) {
116             if (LOG.isWarnEnabled()) {
117                 LOG.warn("I/O error updating cache entry with key {}", cacheKey);
118             }
119         }
120     }
121 
122     HttpCacheEntry getInternal(final String cacheKey) {
123         try {
124             return storage.getEntry(cacheKey);
125         } catch (final ResourceIOException ex) {
126             if (LOG.isWarnEnabled()) {
127                 LOG.warn("I/O error retrieving cache entry with key {}", cacheKey);
128             }
129             return null;
130         }
131     }
132 
133     private void removeInternal(final String cacheKey) {
134         try {
135             storage.removeEntry(cacheKey);
136         } catch (final ResourceIOException ex) {
137             if (LOG.isWarnEnabled()) {
138                 LOG.warn("I/O error removing cache entry with key {}", cacheKey);
139             }
140         }
141     }
142 
143     @Override
144     public CacheMatch match(final HttpHost host, final HttpRequest request) {
145         final String rootKey = cacheKeyGenerator.generateKey(host, request);
146         if (LOG.isDebugEnabled()) {
147             LOG.debug("Get cache root entry: {}", rootKey);
148         }
149         final HttpCacheEntry root = getInternal(rootKey);
150         if (root == null) {
151             return null;
152         }
153         if (root.hasVariants()) {
154             final List<String> variantNames = CacheKeyGenerator.variantNames(root);
155             final String variantKey = cacheKeyGenerator.generateVariantKey(request, variantNames);
156             if (root.getVariants().contains(variantKey)) {
157                 final String cacheKey = variantKey + rootKey;
158                 if (LOG.isDebugEnabled()) {
159                     LOG.debug("Get cache variant entry: {}", cacheKey);
160                 }
161                 final HttpCacheEntry entry = getInternal(cacheKey);
162                 if (entry != null) {
163                     return new CacheMatch(new CacheHit(rootKey, cacheKey, entry), new CacheHit(rootKey, root));
164                 }
165             }
166             return new CacheMatch(null, new CacheHit(rootKey, root));
167         } else {
168             return new CacheMatch(new CacheHit(rootKey, root), null);
169         }
170     }
171 
172     @Override
173     public List<CacheHit> getVariants(final CacheHit hit) {
174         if (LOG.isDebugEnabled()) {
175             LOG.debug("Get variant cache entries: {}", hit.rootKey);
176         }
177         final HttpCacheEntry root = hit.entry;
178         final String rootKey = hit.rootKey;
179         if (root != null && root.hasVariants()) {
180             final List<CacheHit> variants = new ArrayList<>();
181             for (final String variantKey : root.getVariants()) {
182                 final String variantCacheKey = variantKey + rootKey;
183                 final HttpCacheEntry variant = getInternal(variantCacheKey);
184                 if (variant != null) {
185                     variants.add(new CacheHit(rootKey, variantCacheKey, variant));
186                 }
187             }
188             return variants;
189         }
190         return Collections.emptyList();
191     }
192 
193     CacheHit store(final String rootKey, final String variantKey, final HttpCacheEntry entry) {
194         if (variantKey == null) {
195             if (LOG.isDebugEnabled()) {
196                 LOG.debug("Store entry in cache: {}", rootKey);
197             }
198             storeInternal(rootKey, entry);
199             return new CacheHit(rootKey, entry);
200         } else {
201             final String variantCacheKey = variantKey + rootKey;
202 
203             if (LOG.isDebugEnabled()) {
204                 LOG.debug("Store variant entry in cache: {}", variantCacheKey);
205             }
206 
207             storeInternal(variantCacheKey, entry);
208 
209             if (LOG.isDebugEnabled()) {
210                 LOG.debug("Update root entry: {}", rootKey);
211             }
212 
213             updateInternal(rootKey, existing -> {
214                 final Set<String> variants = existing != null ? new HashSet<>(existing.getVariants()) : new HashSet<>();
215                 variants.add(variantKey);
216                 return cacheEntryFactory.createRoot(entry, variants);
217             });
218             return new CacheHit(rootKey, variantCacheKey, entry);
219         }
220     }
221 
222     @Override
223     public CacheHit store(
224             final HttpHost host,
225             final HttpRequest request,
226             final HttpResponse originResponse,
227             final ByteArrayBuffer content,
228             final Instant requestSent,
229             final Instant responseReceived) {
230         final String rootKey = cacheKeyGenerator.generateKey(host, request);
231         if (LOG.isDebugEnabled()) {
232             LOG.debug("Create cache entry: {}", rootKey);
233         }
234         final Resource resource;
235         try {
236             resource = content != null ? resourceFactory.generate(request.getRequestUri(), 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 Header existingETag = root.getFirstHeader(HttpHeaders.ETAG);
333         final Header newETag = response.getFirstHeader(HttpHeaders.ETAG);
334         if (existingETag != null && newETag != null &&
335                 !Objects.equals(existingETag.getValue(), newETag.getValue()) &&
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: {}; {} -> {}", host, new RequestLine(request), new StatusLine(response));
345         }
346         final int status = response.getCode();
347         if (status >= HttpStatus.SC_SUCCESS && status < HttpStatus.SC_CLIENT_ERROR &&
348                 !Method.isSafe(request.getMethod())) {
349             final String rootKey = cacheKeyGenerator.generateKey(host, request);
350             evict(rootKey);
351             final URI requestUri = CacheKeyGenerator.normalize(CacheKeyGenerator.getRequestUri(host, request));
352             if (requestUri != null) {
353                 final URI contentLocation = CacheSupport.getLocationURI(requestUri, response, HttpHeaders.CONTENT_LOCATION);
354                 if (contentLocation != null && CacheSupport.isSameOrigin(requestUri, contentLocation)) {
355                     final String cacheKey = cacheKeyGenerator.generateKey(contentLocation);
356                     evict(cacheKey, response);
357                 }
358                 final URI location = CacheSupport.getLocationURI(requestUri, response, HttpHeaders.LOCATION);
359                 if (location != null && CacheSupport.isSameOrigin(requestUri, location)) {
360                     final String cacheKey = cacheKeyGenerator.generateKey(location);
361                     evict(cacheKey, response);
362                 }
363             }
364         }
365     }
366 
367 }