1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 }