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.time.Instant;
30  import java.util.HashMap;
31  import java.util.Map;
32  
33  import org.apache.hc.client5.http.cache.HeaderConstants;
34  import org.apache.hc.client5.http.cache.HttpCacheEntry;
35  import org.apache.hc.client5.http.cache.HttpCacheInvalidator;
36  import org.apache.hc.client5.http.cache.HttpCacheStorage;
37  import org.apache.hc.client5.http.cache.HttpCacheUpdateException;
38  import org.apache.hc.client5.http.cache.ResourceFactory;
39  import org.apache.hc.client5.http.cache.ResourceIOException;
40  import org.apache.hc.core5.http.Header;
41  import org.apache.hc.core5.http.HttpHost;
42  import org.apache.hc.core5.http.HttpRequest;
43  import org.apache.hc.core5.http.HttpResponse;
44  import org.apache.hc.core5.http.Method;
45  import org.apache.hc.core5.http.message.RequestLine;
46  import org.apache.hc.core5.http.message.StatusLine;
47  import org.apache.hc.core5.util.ByteArrayBuffer;
48  import org.slf4j.Logger;
49  import org.slf4j.LoggerFactory;
50  
51  class BasicHttpCache implements HttpCache {
52  
53      private static final Logger LOG = LoggerFactory.getLogger(BasicHttpCache.class);
54  
55      private final CacheUpdateHandler cacheUpdateHandler;
56      private final CacheKeyGenerator cacheKeyGenerator;
57      private final HttpCacheInvalidator cacheInvalidator;
58      private final HttpCacheStorage storage;
59  
60      public BasicHttpCache(
61              final ResourceFactory resourceFactory,
62              final HttpCacheStorage storage,
63              final CacheKeyGenerator cacheKeyGenerator,
64              final HttpCacheInvalidator cacheInvalidator) {
65          this.cacheUpdateHandler = new CacheUpdateHandler(resourceFactory);
66          this.cacheKeyGenerator = cacheKeyGenerator;
67          this.storage = storage;
68          this.cacheInvalidator = cacheInvalidator;
69      }
70  
71      public BasicHttpCache(
72              final ResourceFactory resourceFactory,
73              final HttpCacheStorage storage,
74              final CacheKeyGenerator cacheKeyGenerator) {
75          this(resourceFactory, storage, cacheKeyGenerator, new DefaultCacheInvalidator());
76      }
77  
78      public BasicHttpCache(final ResourceFactory resourceFactory, final HttpCacheStorage storage) {
79          this( resourceFactory, storage, new CacheKeyGenerator());
80      }
81  
82      public BasicHttpCache(final CacheConfig config) {
83          this(new HeapResourceFactory(), new BasicHttpCacheStorage(config));
84      }
85  
86      public BasicHttpCache() {
87          this(CacheConfig.DEFAULT);
88      }
89  
90      @Override
91      public String generateKey(final HttpHost host, final HttpRequest request, final HttpCacheEntry cacheEntry) {
92          if (cacheEntry == null) {
93              return cacheKeyGenerator.generateKey(host, request);
94          } else {
95              return cacheKeyGenerator.generateKey(host, request, cacheEntry);
96          }
97      }
98  
99      @Override
100     public void flushCacheEntriesFor(final HttpHost host, final HttpRequest request) {
101         if (LOG.isDebugEnabled()) {
102             LOG.debug("Flush cache entries: {}; {}", host, new RequestLine(request));
103         }
104         if (!Method.isSafe(request.getMethod())) {
105             final String cacheKey = cacheKeyGenerator.generateKey(host, request);
106             try {
107                 storage.removeEntry(cacheKey);
108             } catch (final ResourceIOException ex) {
109                 if (LOG.isWarnEnabled()) {
110                     LOG.warn("I/O error removing cache entry with key {}", cacheKey);
111                 }
112             }
113         }
114     }
115 
116     @Override
117     public void flushCacheEntriesInvalidatedByRequest(final HttpHost host, final HttpRequest request) {
118         if (LOG.isDebugEnabled()) {
119             LOG.debug("Flush cache entries invalidated by request: {}; {}", host, new RequestLine(request));
120         }
121         cacheInvalidator.flushCacheEntriesInvalidatedByRequest(host, request, cacheKeyGenerator, storage);
122     }
123 
124     @Override
125     public void flushCacheEntriesInvalidatedByExchange(final HttpHost host, final HttpRequest request, final HttpResponse response) {
126         if (LOG.isDebugEnabled()) {
127             LOG.debug("Flush cache entries invalidated by exchange: {}; {} -> {}", host, new RequestLine(request), new StatusLine(response));
128         }
129         if (!Method.isSafe(request.getMethod())) {
130             cacheInvalidator.flushCacheEntriesInvalidatedByExchange(host, request, response, cacheKeyGenerator, storage);
131         }
132     }
133 
134     void storeInCache(
135             final String cacheKey,
136             final HttpHost host,
137             final HttpRequest request,
138             final HttpCacheEntry entry) {
139         if (entry.hasVariants()) {
140             storeVariantEntry(cacheKey, host, request, entry);
141         } else {
142             storeEntry(cacheKey, entry);
143         }
144     }
145 
146     void storeEntry(final String cacheKey, final HttpCacheEntry entry) {
147         try {
148             storage.putEntry(cacheKey, entry);
149         } catch (final ResourceIOException ex) {
150             if (LOG.isWarnEnabled()) {
151                 LOG.warn("I/O error storing cache entry with key {}", cacheKey);
152             }
153         }
154     }
155 
156     void storeVariantEntry(
157             final String cacheKey,
158             final HttpHost host,
159             final HttpRequest req,
160             final HttpCacheEntry entry) {
161         final String variantKey = cacheKeyGenerator.generateVariantKey(req, entry);
162         final String variantCacheKey = cacheKeyGenerator.generateKey(host, req, entry);
163         storeEntry(variantCacheKey, entry);
164         try {
165             storage.updateEntry(cacheKey, existing -> cacheUpdateHandler.updateParentCacheEntry(req.getRequestUri(), existing, entry, variantKey, variantCacheKey));
166         } catch (final HttpCacheUpdateException ex) {
167             if (LOG.isWarnEnabled()) {
168                 LOG.warn("Cannot update cache entry with key {}", cacheKey);
169             }
170         } catch (final ResourceIOException ex) {
171             if (LOG.isWarnEnabled()) {
172                 LOG.warn("I/O error updating cache entry with key {}", cacheKey);
173             }
174         }
175     }
176 
177     @Override
178     public void reuseVariantEntryFor(
179             final HttpHost host, final HttpRequest request, final Variant variant) {
180         if (LOG.isDebugEnabled()) {
181             LOG.debug("Re-use variant entry: {}; {} / {}", host, new RequestLine(request), variant);
182         }
183         final String cacheKey = cacheKeyGenerator.generateKey(host, request);
184         final HttpCacheEntry entry = variant.getEntry();
185         final String variantKey = cacheKeyGenerator.generateVariantKey(request, entry);
186         final String variantCacheKey = variant.getCacheKey();
187 
188         try {
189             storage.updateEntry(cacheKey, existing -> cacheUpdateHandler.updateParentCacheEntry(request.getRequestUri(), existing, entry, variantKey, variantCacheKey));
190         } catch (final HttpCacheUpdateException ex) {
191             if (LOG.isWarnEnabled()) {
192                 LOG.warn("Cannot update cache entry with key {}", cacheKey);
193             }
194         } catch (final ResourceIOException ex) {
195             if (LOG.isWarnEnabled()) {
196                 LOG.warn("I/O error updating cache entry with key {}", cacheKey);
197             }
198         }
199     }
200 
201     @Override
202     public HttpCacheEntry updateCacheEntry(
203             final HttpHost host,
204             final HttpRequest request,
205             final HttpCacheEntry stale,
206             final HttpResponse originResponse,
207             final Instant requestSent,
208             final Instant responseReceived) {
209         if (LOG.isDebugEnabled()) {
210             LOG.debug("Update cache entry: {}; {}", host, new RequestLine(request));
211         }
212         final String cacheKey = cacheKeyGenerator.generateKey(host, request);
213         try {
214             final HttpCacheEntry updatedEntry = cacheUpdateHandler.updateCacheEntry(
215                     request.getRequestUri(),
216                     stale,
217                     requestSent,
218                     responseReceived,
219                     originResponse);
220             storeInCache(cacheKey, host, request, updatedEntry);
221             return updatedEntry;
222         } catch (final ResourceIOException ex) {
223             if (LOG.isWarnEnabled()) {
224                 LOG.warn("I/O error updating cache entry with key {}", cacheKey);
225             }
226             return stale;
227         }
228     }
229 
230     @Override
231     public HttpCacheEntry updateVariantCacheEntry(
232             final HttpHost host,
233             final HttpRequest request,
234             final HttpResponse originResponse,
235             final Variant variant,
236             final Instant requestSent,
237             final Instant responseReceived) {
238         if (LOG.isDebugEnabled()) {
239             LOG.debug("Update variant cache entry: {}; {} / {}", host, new RequestLine(request), variant);
240         }
241         final HttpCacheEntry entry = variant.getEntry();
242         final String cacheKey = variant.getCacheKey();
243         try {
244             final HttpCacheEntry updatedEntry = cacheUpdateHandler.updateCacheEntry(
245                     request.getRequestUri(),
246                     entry,
247                     requestSent,
248                     responseReceived,
249                     originResponse);
250             storeEntry(cacheKey, updatedEntry);
251             return updatedEntry;
252         } catch (final ResourceIOException ex) {
253             if (LOG.isWarnEnabled()) {
254                 LOG.warn("I/O error updating cache entry with key {}", cacheKey);
255             }
256             return entry;
257         }
258     }
259 
260     @Override
261     public HttpCacheEntry createCacheEntry(
262             final HttpHost host,
263             final HttpRequest request,
264             final HttpResponse originResponse,
265             final ByteArrayBuffer content,
266             final Instant requestSent,
267             final Instant responseReceived) {
268         if (LOG.isDebugEnabled()) {
269             LOG.debug("Create cache entry: {}; {}", host, new RequestLine(request));
270         }
271         final String cacheKey = cacheKeyGenerator.generateKey(host, request);
272         try {
273             final HttpCacheEntry entry = cacheUpdateHandler.createCacheEntry(request, originResponse, content, requestSent, responseReceived);
274             storeInCache(cacheKey, host, request, entry);
275             return entry;
276         } catch (final ResourceIOException ex) {
277             if (LOG.isWarnEnabled()) {
278                 LOG.warn("I/O error creating cache entry with key {}", cacheKey);
279             }
280             return new HttpCacheEntry(
281                     requestSent,
282                     responseReceived,
283                     originResponse.getCode(),
284                     originResponse.getHeaders(),
285                     content != null ? HeapResourceFactory.INSTANCE.generate(null, content.array(), 0, content.length()) : null);
286         }
287     }
288 
289     @Override
290     public HttpCacheEntry getCacheEntry(final HttpHost host, final HttpRequest request) {
291         if (LOG.isDebugEnabled()) {
292             LOG.debug("Get cache entry: {}; {}", host, new RequestLine(request));
293         }
294         final String cacheKey = cacheKeyGenerator.generateKey(host, request);
295         final HttpCacheEntry root;
296         try {
297             root = storage.getEntry(cacheKey);
298         } catch (final ResourceIOException ex) {
299             if (LOG.isWarnEnabled()) {
300                 LOG.warn("I/O error retrieving cache entry with key {}", cacheKey);
301             }
302             return null;
303         }
304         if (root == null) {
305             return null;
306         }
307         if (!root.hasVariants()) {
308             return root;
309         }
310         final String variantKey = cacheKeyGenerator.generateVariantKey(request, root);
311         final String variantCacheKey = root.getVariantMap().get(variantKey);
312         if (variantCacheKey == null) {
313             return null;
314         }
315         try {
316             return storage.getEntry(variantCacheKey);
317         } catch (final ResourceIOException ex) {
318             if (LOG.isWarnEnabled()) {
319                 LOG.warn("I/O error retrieving cache entry with key {}", variantCacheKey);
320             }
321             return null;
322         }
323     }
324 
325     @Override
326     public Map<String, Variant> getVariantCacheEntriesWithEtags(final HttpHost host, final HttpRequest request) {
327         if (LOG.isDebugEnabled()) {
328             LOG.debug("Get variant cache entries: {}; {}", host, new RequestLine(request));
329         }
330         final Map<String,Variant> variants = new HashMap<>();
331         final String cacheKey = cacheKeyGenerator.generateKey(host, request);
332         final HttpCacheEntry root;
333         try {
334             root = storage.getEntry(cacheKey);
335         } catch (final ResourceIOException ex) {
336             if (LOG.isWarnEnabled()) {
337                 LOG.warn("I/O error retrieving cache entry with key {}", cacheKey);
338             }
339             return variants;
340         }
341         if (root != null && root.hasVariants()) {
342             for(final Map.Entry<String, String> variant : root.getVariantMap().entrySet()) {
343                 final String variantCacheKey = variant.getValue();
344                 try {
345                     final HttpCacheEntry entry = storage.getEntry(variantCacheKey);
346                     if (entry != null) {
347                         final Header etagHeader = entry.getFirstHeader(HeaderConstants.ETAG);
348                         if (etagHeader != null) {
349                             variants.put(etagHeader.getValue(), new Variant(variantCacheKey, entry));
350                         }
351                     }
352                 } catch (final ResourceIOException ex) {
353                     if (LOG.isWarnEnabled()) {
354                         LOG.warn("I/O error retrieving cache entry with key {}", variantCacheKey);
355                     }
356                     return variants;
357                 }
358             }
359         }
360         return variants;
361     }
362 
363 }