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.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 }