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