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.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             final ETag eTag = ETag.get(originResponse);
237             resource = content != null ? resourceFactory.generate(
238                     rootKey,
239                     eTag != null && eTag.getType() == ValidatorType.STRONG ? eTag.getValue() : null,
240                     content.array(), 0, content.length()) : null;
241         } catch (final ResourceIOException ex) {
242             if (LOG.isWarnEnabled()) {
243                 LOG.warn("I/O error creating cache entry with key {}", rootKey);
244             }
245             final HttpCacheEntry backup = cacheEntryFactory.create(
246                     requestSent,
247                     responseReceived,
248                     host,
249                     request,
250                     originResponse,
251                     content != null ? HeapResourceFactory.INSTANCE.generate(null, content.array(), 0, content.length()) : null);
252             return new CacheHit(rootKey, backup);
253         }
254         final HttpCacheEntry entry = cacheEntryFactory.create(requestSent, responseReceived, host, request, originResponse, resource);
255         final String variantKey = cacheKeyGenerator.generateVariantKey(request, entry);
256         return store(rootKey,variantKey, entry);
257     }
258 
259     @Override
260     public CacheHit update(
261             final CacheHit stale,
262             final HttpHost host,
263             final HttpRequest request,
264             final HttpResponse originResponse,
265             final Instant requestSent,
266             final Instant responseReceived) {
267         if (LOG.isDebugEnabled()) {
268             LOG.debug("Update cache entry: {}", stale.getEntryKey());
269         }
270         final HttpCacheEntry updatedEntry = cacheEntryFactory.createUpdated(
271                 requestSent,
272                 responseReceived,
273                 host,
274                 request,
275                 originResponse,
276                 stale.entry);
277         final String variantKey = cacheKeyGenerator.generateVariantKey(request, updatedEntry);
278         return store(stale.rootKey, variantKey, updatedEntry);
279     }
280 
281     @Override
282     public CacheHit storeFromNegotiated(
283             final CacheHit negotiated,
284             final HttpHost host,
285             final HttpRequest request,
286             final HttpResponse originResponse,
287             final Instant requestSent,
288             final Instant responseReceived) {
289         if (LOG.isDebugEnabled()) {
290             LOG.debug("Update negotiated cache entry: {}", negotiated.getEntryKey());
291         }
292         final HttpCacheEntry updatedEntry = cacheEntryFactory.createUpdated(
293                 requestSent,
294                 responseReceived,
295                 host,
296                 request,
297                 originResponse,
298                negotiated.entry);
299         storeInternal(negotiated.getEntryKey(), updatedEntry);
300 
301         final String rootKey = cacheKeyGenerator.generateKey(host, request);
302         final HttpCacheEntry copy = cacheEntryFactory.copy(updatedEntry);
303         final String variantKey = cacheKeyGenerator.generateVariantKey(request, copy);
304         return store(rootKey, variantKey, copy);
305     }
306 
307     private void evictAll(final HttpCacheEntry root, final String rootKey) {
308         if (LOG.isDebugEnabled()) {
309             LOG.debug("Evicting root cache entry {}", rootKey);
310         }
311         removeInternal(rootKey);
312         if (root.hasVariants()) {
313             for (final String variantKey : root.getVariants()) {
314                 final String variantEntryKey = variantKey + rootKey;
315                 if (LOG.isDebugEnabled()) {
316                     LOG.debug("Evicting variant cache entry {}", variantEntryKey);
317                 }
318                 removeInternal(variantEntryKey);
319             }
320         }
321     }
322 
323     private void evict(final String rootKey) {
324         final HttpCacheEntry root = getInternal(rootKey);
325         if (root == null) {
326             return;
327         }
328         evictAll(root, rootKey);
329     }
330 
331     private void evict(final String rootKey, final HttpResponse response) {
332         final HttpCacheEntry root = getInternal(rootKey);
333         if (root == null) {
334             return;
335         }
336         final ETag existingETag = root.getETag();
337         final ETag newETag = ETag.get(response);
338         if (existingETag != null && newETag != null &&
339                 !ETag.strongCompare(existingETag, newETag) &&
340                 !HttpCacheEntry.isNewer(root, response)) {
341             evictAll(root, rootKey);
342         }
343     }
344 
345     @Override
346     public void evictInvalidatedEntries(final HttpHost host, final HttpRequest request, final HttpResponse response) {
347         if (LOG.isDebugEnabled()) {
348             LOG.debug("Evict cache entries invalidated by exchange: {}; {} -> {}", host, new RequestLine(request), new StatusLine(response));
349         }
350         final int status = response.getCode();
351         if (status >= HttpStatus.SC_SUCCESS && status < HttpStatus.SC_CLIENT_ERROR &&
352                 !Method.isSafe(request.getMethod())) {
353             final String rootKey = cacheKeyGenerator.generateKey(host, request);
354             evict(rootKey);
355             final URI requestUri = CacheKeyGenerator.normalize(CacheKeyGenerator.getRequestUri(host, request));
356             if (requestUri != null) {
357                 final URI contentLocation = CacheSupport.getLocationURI(requestUri, response, HttpHeaders.CONTENT_LOCATION);
358                 if (contentLocation != null && CacheSupport.isSameOrigin(requestUri, contentLocation)) {
359                     final String cacheKey = cacheKeyGenerator.generateKey(contentLocation);
360                     evict(cacheKey, response);
361                 }
362                 final URI location = CacheSupport.getLocationURI(requestUri, response, HttpHeaders.LOCATION);
363                 if (location != null && CacheSupport.isSameOrigin(requestUri, location)) {
364                     final String cacheKey = cacheKeyGenerator.generateKey(location);
365                     evict(cacheKey, response);
366                 }
367             }
368         }
369     }
370 
371 }