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 static org.junit.jupiter.api.Assertions.assertEquals;
30  import static org.junit.jupiter.api.Assertions.assertFalse;
31  import static org.junit.jupiter.api.Assertions.assertNotNull;
32  import static org.junit.jupiter.api.Assertions.assertNull;
33  import static org.junit.jupiter.api.Assertions.assertSame;
34  import static org.mockito.Mockito.verify;
35  import static org.mockito.Mockito.verifyNoMoreInteractions;
36  
37  import java.net.URI;
38  import java.time.Instant;
39  import java.util.Collection;
40  import java.util.HashSet;
41  import java.util.Map;
42  import java.util.Set;
43  import java.util.concurrent.CountDownLatch;
44  import java.util.concurrent.atomic.AtomicReference;
45  import java.util.stream.Collectors;
46  
47  import org.apache.hc.client5.http.HeadersMatcher;
48  import org.apache.hc.client5.http.cache.HttpCacheEntry;
49  import org.apache.hc.client5.http.classic.methods.HttpGet;
50  import org.apache.hc.client5.http.utils.DateUtils;
51  import org.apache.hc.core5.http.HttpHeaders;
52  import org.apache.hc.core5.http.HttpHost;
53  import org.apache.hc.core5.http.HttpRequest;
54  import org.apache.hc.core5.http.HttpResponse;
55  import org.apache.hc.core5.http.HttpStatus;
56  import org.apache.hc.core5.http.message.BasicHeader;
57  import org.apache.hc.core5.http.message.BasicHttpRequest;
58  import org.apache.hc.core5.http.message.BasicHttpResponse;
59  import org.apache.hc.core5.net.URIBuilder;
60  import org.apache.hc.core5.util.ByteArrayBuffer;
61  import org.hamcrest.MatcherAssert;
62  import org.junit.jupiter.api.Assertions;
63  import org.junit.jupiter.api.BeforeEach;
64  import org.junit.jupiter.api.Test;
65  import org.mockito.Mockito;
66  
67  public class TestBasicHttpAsyncCache {
68  
69      private HttpHost host;
70      private Instant now;
71      private Instant tenSecondsAgo;
72      private SimpleHttpAsyncCacheStorage backing;
73      private BasicHttpAsyncCache impl;
74  
75      @BeforeEach
76      public void setUp() {
77          host = new HttpHost("foo.example.com");
78          now = Instant.now();
79          tenSecondsAgo = now.minusSeconds(10);
80          backing = Mockito.spy(new SimpleHttpAsyncCacheStorage());
81          impl = new BasicHttpAsyncCache(HeapResourceFactory.INSTANCE, backing);
82      }
83  
84      @Test
85      public void testGetCacheEntryReturnsNullOnCacheMiss() throws Exception {
86          final HttpHost host = new HttpHost("foo.example.com");
87          final HttpRequest request = new HttpGet("http://foo.example.com/bar");
88  
89          final CountDownLatch latch1 = new CountDownLatch(1);
90          final AtomicReference<CacheMatch> resultRef = new AtomicReference<>();
91  
92          impl.match(host, request, HttpTestUtils.countDown(latch1, resultRef::set));
93  
94          latch1.await();
95  
96          assertNull(resultRef.get());
97      }
98  
99      @Test
100     public void testGetCacheEntryFetchesFromCacheOnCacheHitIfNoVariants() throws Exception {
101         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry();
102         assertFalse(entry.hasVariants());
103         final HttpHost host = new HttpHost("foo.example.com");
104         final HttpRequest request = new HttpGet("http://foo.example.com/bar");
105 
106         final String key = CacheKeyGenerator.INSTANCE.generateKey(host, request);
107 
108         backing.map.put(key,entry);
109 
110         final CountDownLatch latch1 = new CountDownLatch(1);
111         final AtomicReference<CacheMatch> resultRef = new AtomicReference<>();
112 
113         impl.match(host, request, HttpTestUtils.countDown(latch1, resultRef::set));
114 
115         latch1.await();
116         final CacheMatch result = resultRef.get();
117 
118         assertNotNull(result);
119         assertNotNull(result.hit);
120         assertSame(entry, result.hit.entry);
121     }
122 
123     @Test
124     public void testGetCacheEntryReturnsNullIfNoVariantInCache() throws Exception {
125         final HttpRequest origRequest = new HttpGet("http://foo.example.com/bar");
126         origRequest.setHeader("Accept-Encoding","gzip");
127 
128         final ByteArrayBuffer buf = HttpTestUtils.makeRandomBuffer(128);
129         final HttpResponse origResponse = new BasicHttpResponse(HttpStatus.SC_OK, "OK");
130         origResponse.setHeader("Date", DateUtils.formatStandardDate(now));
131         origResponse.setHeader("Cache-Control", "max-age=3600, public");
132         origResponse.setHeader("ETag", "\"etag\"");
133         origResponse.setHeader("Vary", "Accept-Encoding");
134         origResponse.setHeader("Content-Encoding","gzip");
135 
136         final CountDownLatch latch1 = new CountDownLatch(1);
137 
138         impl.store(host, origRequest, origResponse, buf, now, now, HttpTestUtils.countDown(latch1));
139 
140         latch1.await();
141 
142         final HttpRequest request = new HttpGet("http://foo.example.com/bar");
143 
144         final CountDownLatch latch2 = new CountDownLatch(1);
145         final AtomicReference<CacheMatch> resultRef = new AtomicReference<>();
146         impl.match(host, request, HttpTestUtils.countDown(latch2, resultRef::set));
147 
148         latch2.await();
149         final CacheMatch result = resultRef.get();
150 
151         assertNotNull(result);
152         assertNull(result.hit);
153     }
154 
155     @Test
156     public void testGetCacheEntryReturnsVariantIfPresentInCache() throws Exception {
157         final HttpRequest origRequest = new HttpGet("http://foo.example.com/bar");
158         origRequest.setHeader("Accept-Encoding","gzip");
159 
160         final ByteArrayBuffer buf = HttpTestUtils.makeRandomBuffer(128);
161         final HttpResponse origResponse = new BasicHttpResponse(HttpStatus.SC_OK, "OK");
162         origResponse.setHeader("Date", DateUtils.formatStandardDate(now));
163         origResponse.setHeader("Cache-Control", "max-age=3600, public");
164         origResponse.setHeader("ETag", "\"etag\"");
165         origResponse.setHeader("Vary", "Accept-Encoding");
166         origResponse.setHeader("Content-Encoding","gzip");
167 
168         final CountDownLatch latch1 = new CountDownLatch(1);
169 
170         impl.store(host, origRequest, origResponse, buf, now, now, HttpTestUtils.countDown(latch1));
171 
172         latch1.await();
173 
174         final HttpRequest request = new HttpGet("http://foo.example.com/bar");
175         request.setHeader("Accept-Encoding","gzip");
176 
177         final CountDownLatch latch2 = new CountDownLatch(1);
178         final AtomicReference<CacheMatch> resultRef = new AtomicReference<>();
179         impl.match(host, request, HttpTestUtils.countDown(latch2, resultRef::set));
180 
181         latch2.await();
182         final CacheMatch result = resultRef.get();
183 
184         assertNotNull(result);
185         assertNotNull(result.hit);
186     }
187 
188     @Test
189     public void testGetCacheEntryReturnsVariantWithMostRecentDateHeader() throws Exception {
190         final HttpRequest origRequest = new HttpGet("http://foo.example.com/bar");
191         origRequest.setHeader("Accept-Encoding", "gzip");
192 
193         final ByteArrayBuffer buf = HttpTestUtils.makeRandomBuffer(128);
194 
195         // Create two response variants with different Date headers
196         final HttpResponse origResponse1 = new BasicHttpResponse(HttpStatus.SC_OK, "OK");
197         origResponse1.setHeader(HttpHeaders.DATE, DateUtils.formatStandardDate(now.minusSeconds(3600)));
198         origResponse1.setHeader(HttpHeaders.CACHE_CONTROL, "max-age=3600, public");
199         origResponse1.setHeader(HttpHeaders.ETAG, "\"etag1\"");
200         origResponse1.setHeader(HttpHeaders.VARY, "Accept-Encoding");
201 
202         final HttpResponse origResponse2 = new BasicHttpResponse(HttpStatus.SC_OK, "OK");
203         origResponse2.setHeader(HttpHeaders.DATE, DateUtils.formatStandardDate(now));
204         origResponse2.setHeader(HttpHeaders.CACHE_CONTROL, "max-age=3600, public");
205         origResponse2.setHeader(HttpHeaders.ETAG, "\"etag2\"");
206         origResponse2.setHeader(HttpHeaders.VARY, "Accept-Encoding");
207 
208         // Store the two variants in cache
209         final CountDownLatch latch1 = new CountDownLatch(2);
210 
211         impl.store(host, origRequest, origResponse1, buf, now, now, HttpTestUtils.countDown(latch1));
212         impl.store(host, origRequest, origResponse2, buf, now, now, HttpTestUtils.countDown(latch1));
213 
214         latch1.await();
215 
216         final HttpRequest request = new HttpGet("http://foo.example.com/bar");
217         request.setHeader("Accept-Encoding", "gzip");
218 
219         final CountDownLatch latch2 = new CountDownLatch(1);
220         final AtomicReference<CacheMatch> resultRef = new AtomicReference<>();
221         impl.match(host, request, HttpTestUtils.countDown(latch2, resultRef::set));
222 
223         latch2.await();
224         final CacheMatch result = resultRef.get();
225 
226         assertNotNull(result);
227         assertNotNull(result.hit);
228         final HttpCacheEntry entry = result.hit.entry;
229         assertNotNull(entry);
230 
231         // Retrieve the ETag header value from the original response and assert that
232         // the returned cache entry has the same ETag value
233         final String expectedEtag = origResponse2.getFirstHeader(HttpHeaders.ETAG).getValue();
234         final String actualEtag = entry.getFirstHeader(HttpHeaders.ETAG).getValue();
235 
236         assertEquals(expectedEtag, actualEtag);
237     }
238 
239     @Test
240     public void testGetVariantsRootNoVariants() throws Exception {
241         final HttpCacheEntry root = HttpTestUtils.makeCacheEntry();
242 
243         final CountDownLatch latch1 = new CountDownLatch(1);
244         final AtomicReference<Collection<CacheHit>> resultRef = new AtomicReference<>();
245         impl.getVariants(new CacheHit("root-key", root), HttpTestUtils.countDown(latch1, resultRef::set));
246 
247         latch1.await();
248         final Collection<CacheHit> variants = resultRef.get();
249 
250         assertNotNull(variants);
251         assertEquals(0, variants.size());
252     }
253 
254     @Test
255     public void testGetVariantsRootNonExistentVariants() throws Exception {
256         final Set<String> varinats = new HashSet<>();
257         varinats.add("variant1");
258         varinats.add("variant2");
259         final HttpCacheEntry root = HttpTestUtils.makeCacheEntry(varinats);
260 
261         final CountDownLatch latch1 = new CountDownLatch(1);
262         final AtomicReference<Collection<CacheHit>> resultRef = new AtomicReference<>();
263         impl.getVariants(new CacheHit("root-key", root), HttpTestUtils.countDown(latch1, resultRef::set));
264 
265         latch1.await();
266         final Collection<CacheHit> variants = resultRef.get();
267 
268         assertNotNull(variants);
269         assertEquals(0, variants.size());
270     }
271 
272     @Test
273     public void testGetVariantCacheEntriesReturnsAllVariants() throws Exception {
274         final HttpHost host = new HttpHost("foo.example.com");
275         final URI uri = new URI("http://foo.example.com/bar");
276         final HttpRequest req1 = new HttpGet(uri);
277         req1.setHeader("Accept-Encoding", "gzip");
278 
279         final String rootKey = CacheKeyGenerator.INSTANCE.generateKey(uri);
280 
281         final HttpResponse resp1 = HttpTestUtils.make200Response();
282         resp1.setHeader("Date", DateUtils.formatStandardDate(now));
283         resp1.setHeader("Cache-Control", "max-age=3600, public");
284         resp1.setHeader("ETag", "\"etag1\"");
285         resp1.setHeader("Vary", "Accept-Encoding");
286         resp1.setHeader("Content-Encoding","gzip");
287 
288         final HttpRequest req2 = new HttpGet(uri);
289         req2.setHeader("Accept-Encoding", "identity");
290 
291         final HttpResponse resp2 = HttpTestUtils.make200Response();
292         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
293         resp2.setHeader("Cache-Control", "max-age=3600, public");
294         resp2.setHeader("ETag", "\"etag2\"");
295         resp2.setHeader("Vary", "Accept-Encoding");
296         resp2.setHeader("Content-Encoding","gzip");
297 
298         final CountDownLatch latch1 = new CountDownLatch(2);
299 
300         final AtomicReference<CacheHit> resultRef1 = new AtomicReference<>();
301         final AtomicReference<CacheHit> resultRef2 = new AtomicReference<>();
302 
303         impl.store(host, req1, resp1, null, now, now, HttpTestUtils.countDown(latch1, resultRef1::set));
304         impl.store(host, req2, resp2, null, now, now, HttpTestUtils.countDown(latch1, resultRef2::set));
305 
306         latch1.await();
307 
308         final CacheHit hit1 = resultRef1.get();
309         final CacheHit hit2 = resultRef2.get();
310 
311         final Set<String> variants = new HashSet<>();
312         variants.add("{accept-encoding=gzip}");
313         variants.add("{accept-encoding=identity}");
314 
315         final CountDownLatch latch2 = new CountDownLatch(1);
316         final AtomicReference<Collection<CacheHit>> resultRef3 = new AtomicReference<>();
317 
318         impl.getVariants(new CacheHit(hit1.rootKey, HttpTestUtils.makeCacheEntry(variants)),
319                 HttpTestUtils.countDown(latch2, resultRef3::set));
320 
321         latch2.await();
322 
323         final Map<String, HttpCacheEntry> variantMap = resultRef3.get().stream()
324                 .collect(Collectors.toMap(CacheHit::getEntryKey, e -> e.entry));
325 
326                 assertNotNull(variantMap);
327         assertEquals(2, variantMap.size());
328         MatcherAssert.assertThat(variantMap.get("{accept-encoding=gzip}" + rootKey),
329                 HttpCacheEntryMatcher.equivalent(hit1.entry));
330         MatcherAssert.assertThat(variantMap.get("{accept-encoding=identity}" + rootKey),
331                 HttpCacheEntryMatcher.equivalent(hit2.entry));
332     }
333 
334     @Test
335     public void testUpdateCacheEntry() throws Exception {
336         final HttpHost host = new HttpHost("foo.example.com");
337         final URI uri = new URI("http://foo.example.com/bar");
338         final HttpRequest req1 = new HttpGet(uri);
339 
340         final HttpResponse resp1 = HttpTestUtils.make200Response();
341         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
342         resp1.setHeader("Cache-Control", "max-age=3600, public");
343         resp1.setHeader("ETag", "\"etag1\"");
344         resp1.setHeader("Content-Encoding","gzip");
345 
346         final HttpRequest revalidate = new HttpGet(uri);
347         revalidate.setHeader("If-None-Match","\"etag1\"");
348 
349         final HttpResponse resp2 = HttpTestUtils.make304Response();
350         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
351         resp2.setHeader("Cache-Control", "max-age=3600, public");
352 
353         final CountDownLatch latch1 = new CountDownLatch(1);
354 
355         final AtomicReference<CacheHit> resultRef1 = new AtomicReference<>();
356         impl.store(host, req1, resp1, null, now, now, HttpTestUtils.countDown(latch1, resultRef1::set));
357 
358         latch1.await();
359         final CacheHit hit1 = resultRef1.get();
360 
361         Assertions.assertNotNull(hit1);
362         Assertions.assertEquals(1, backing.map.size());
363         Assertions.assertSame(hit1.entry, backing.map.get(hit1.getEntryKey()));
364 
365         final CountDownLatch latch2 = new CountDownLatch(1);
366 
367         final AtomicReference<CacheHit> resultRef2 = new AtomicReference<>();
368         impl.update(hit1, host, req1, resp2, now, now, HttpTestUtils.countDown(latch2, resultRef2::set));
369 
370         latch2.await();
371         final CacheHit updated = resultRef2.get();
372 
373         Assertions.assertNotNull(updated);
374         Assertions.assertEquals(1, backing.map.size());
375         Assertions.assertSame(updated.entry, backing.map.get(hit1.getEntryKey()));
376 
377         MatcherAssert.assertThat(
378                 updated.entry.getHeaders(),
379                 HeadersMatcher.same(
380                         new BasicHeader("Server", "MockOrigin/1.0"),
381                         new BasicHeader("ETag", "\"etag1\""),
382                         new BasicHeader("Content-Encoding","gzip"),
383                         new BasicHeader("Date", DateUtils.formatStandardDate(now)),
384                         new BasicHeader("Cache-Control", "max-age=3600, public")
385                 ));
386     }
387 
388     @Test
389     public void testUpdateVariantCacheEntry() throws Exception {
390         final HttpHost host = new HttpHost("foo.example.com");
391         final URI uri = new URI("http://foo.example.com/bar");
392         final HttpRequest req1 = new HttpGet(uri);
393         req1.setHeader("User-Agent", "agent1");
394 
395         final HttpResponse resp1 = HttpTestUtils.make200Response();
396         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
397         resp1.setHeader("Cache-Control", "max-age=3600, public");
398         resp1.setHeader("ETag", "\"etag1\"");
399         resp1.setHeader("Content-Encoding","gzip");
400         resp1.setHeader("Vary", "User-Agent");
401 
402         final HttpRequest revalidate = new HttpGet(uri);
403         revalidate.setHeader("If-None-Match","\"etag1\"");
404 
405         final HttpResponse resp2 = HttpTestUtils.make304Response();
406         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
407         resp2.setHeader("Cache-Control", "max-age=3600, public");
408 
409         final CountDownLatch latch1 = new CountDownLatch(1);
410 
411         final AtomicReference<CacheHit> resultRef1 = new AtomicReference<>();
412         impl.store(host, req1, resp1, null, now, now, HttpTestUtils.countDown(latch1, resultRef1::set));
413 
414         latch1.await();
415         final CacheHit hit1 = resultRef1.get();
416 
417         Assertions.assertNotNull(hit1);
418         Assertions.assertEquals(2, backing.map.size());
419         Assertions.assertSame(hit1.entry, backing.map.get(hit1.getEntryKey()));
420 
421         final CountDownLatch latch2 = new CountDownLatch(1);
422 
423         final AtomicReference<CacheHit> resultRef2 = new AtomicReference<>();
424         impl.update(hit1, host, req1, resp2, now, now, HttpTestUtils.countDown(latch2, resultRef2::set));
425 
426         latch2.await();
427         final CacheHit updated = resultRef2.get();
428 
429         Assertions.assertNotNull(updated);
430         Assertions.assertEquals(2, backing.map.size());
431         Assertions.assertSame(updated.entry, backing.map.get(hit1.getEntryKey()));
432 
433         MatcherAssert.assertThat(
434                 updated.entry.getHeaders(),
435                 HeadersMatcher.same(
436                         new BasicHeader("Server", "MockOrigin/1.0"),
437                         new BasicHeader("ETag", "\"etag1\""),
438                         new BasicHeader("Content-Encoding","gzip"),
439                         new BasicHeader("Vary","User-Agent"),
440                         new BasicHeader("Date", DateUtils.formatStandardDate(now)),
441                         new BasicHeader("Cache-Control", "max-age=3600, public")
442                 ));
443     }
444 
445     @Test
446     public void testUpdateCacheEntryTurnsVariant() throws Exception {
447         final HttpHost host = new HttpHost("foo.example.com");
448         final URI uri = new URI("http://foo.example.com/bar");
449         final HttpRequest req1 = new HttpGet(uri);
450         req1.setHeader("User-Agent", "agent1");
451 
452         final HttpResponse resp1 = HttpTestUtils.make200Response();
453         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
454         resp1.setHeader("Cache-Control", "max-age=3600, public");
455         resp1.setHeader("ETag", "\"etag1\"");
456         resp1.setHeader("Content-Encoding","gzip");
457 
458         final HttpRequest revalidate = new HttpGet(uri);
459         revalidate.setHeader("If-None-Match","\"etag1\"");
460 
461         final HttpResponse resp2 = HttpTestUtils.make304Response();
462         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
463         resp2.setHeader("Cache-Control", "max-age=3600, public");
464         resp2.setHeader("Vary", "User-Agent");
465 
466         final CountDownLatch latch1 = new CountDownLatch(1);
467 
468         final AtomicReference<CacheHit> resultRef1 = new AtomicReference<>();
469         impl.store(host, req1, resp1, null, now, now, HttpTestUtils.countDown(latch1, resultRef1::set));
470 
471         latch1.await();
472         final CacheHit hit1 = resultRef1.get();
473 
474         Assertions.assertNotNull(hit1);
475         Assertions.assertEquals(1, backing.map.size());
476         Assertions.assertSame(hit1.entry, backing.map.get(hit1.getEntryKey()));
477 
478         final CountDownLatch latch2 = new CountDownLatch(1);
479 
480         final AtomicReference<CacheHit> resultRef2 = new AtomicReference<>();
481         impl.update(hit1, host, req1, resp2, now, now, HttpTestUtils.countDown(latch2, resultRef2::set));
482 
483         latch2.await();
484         final CacheHit updated = resultRef2.get();
485 
486         Assertions.assertNotNull(updated);
487         Assertions.assertEquals(2, backing.map.size());
488 
489         MatcherAssert.assertThat(
490                 updated.entry.getHeaders(),
491                 HeadersMatcher.same(
492                         new BasicHeader("Server", "MockOrigin/1.0"),
493                         new BasicHeader("ETag", "\"etag1\""),
494                         new BasicHeader("Content-Encoding","gzip"),
495                         new BasicHeader("Date", DateUtils.formatStandardDate(now)),
496                         new BasicHeader("Cache-Control", "max-age=3600, public"),
497                         new BasicHeader("Vary","User-Agent")));
498     }
499 
500     @Test
501     public void testStoreFromNegotiatedVariant() throws Exception {
502         final HttpHost host = new HttpHost("foo.example.com");
503         final URI uri = new URI("http://foo.example.com/bar");
504         final HttpRequest req1 = new HttpGet(uri);
505         req1.setHeader("User-Agent", "agent1");
506 
507         final HttpResponse resp1 = HttpTestUtils.make200Response();
508         resp1.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
509         resp1.setHeader("Cache-Control", "max-age=3600, public");
510         resp1.setHeader("ETag", "\"etag1\"");
511         resp1.setHeader("Content-Encoding","gzip");
512         resp1.setHeader("Vary", "User-Agent");
513 
514         final CountDownLatch latch1 = new CountDownLatch(1);
515 
516         final AtomicReference<CacheHit> resultRef1 = new AtomicReference<>();
517         impl.store(host, req1, resp1, null, now, now, HttpTestUtils.countDown(latch1, resultRef1::set));
518 
519         latch1.await();
520         final CacheHit hit1 = resultRef1.get();
521 
522         Assertions.assertNotNull(hit1);
523         Assertions.assertEquals(2, backing.map.size());
524         Assertions.assertSame(hit1.entry, backing.map.get(hit1.getEntryKey()));
525 
526         final HttpRequest req2 = new HttpGet(uri);
527         req2.setHeader("User-Agent", "agent2");
528 
529         final HttpResponse resp2 = HttpTestUtils.make304Response();
530         resp2.setHeader("Date", DateUtils.formatStandardDate(now));
531         resp2.setHeader("Cache-Control", "max-age=3600, public");
532 
533         final CountDownLatch latch2 = new CountDownLatch(1);
534 
535         final AtomicReference<CacheHit> resultRef2 = new AtomicReference<>();
536         impl.storeFromNegotiated(hit1, host, req2, resp2, now, now, HttpTestUtils.countDown(latch2, resultRef2::set));
537 
538         final CacheHit hit2 = resultRef2.get();
539 
540         Assertions.assertNotNull(hit2);
541         Assertions.assertEquals(3, backing.map.size());
542 
543         MatcherAssert.assertThat(
544                 hit2.entry.getHeaders(),
545                 HeadersMatcher.same(
546                         new BasicHeader("Server", "MockOrigin/1.0"),
547                         new BasicHeader("ETag", "\"etag1\""),
548                         new BasicHeader("Content-Encoding","gzip"),
549                         new BasicHeader("Vary","User-Agent"),
550                         new BasicHeader("Date", DateUtils.formatStandardDate(now)),
551                         new BasicHeader("Cache-Control", "max-age=3600, public")));
552     }
553 
554     @Test
555     public void testInvalidatesUnsafeRequests() throws Exception {
556         final HttpRequest request = new BasicHttpRequest("POST", "/path");
557         final HttpResponse response = HttpTestUtils.make200Response();
558 
559         final String key = CacheKeyGenerator.INSTANCE.generateKey(host, request);
560 
561         backing.putEntry(key, HttpTestUtils.makeCacheEntry());
562 
563         final CountDownLatch latch = new CountDownLatch(1);
564         impl.evictInvalidatedEntries(host, request, response, HttpTestUtils.countDown(latch));
565 
566         latch.await();
567 
568         verify(backing).getEntry(Mockito.eq(key), Mockito.any());
569         verify(backing).removeEntry(Mockito.eq(key), Mockito.any());
570         Assertions.assertNull(backing.getEntry(key));
571     }
572 
573     @Test
574     public void testDoesNotInvalidateSafeRequests() throws Exception {
575         final HttpRequest request1 = new BasicHttpRequest("GET", "/");
576         final HttpResponse response1 = HttpTestUtils.make200Response();
577         final CountDownLatch latch1 = new CountDownLatch(1);
578 
579         impl.evictInvalidatedEntries(host, request1, response1, HttpTestUtils.countDown(latch1));
580 
581         latch1.await();
582 
583         verifyNoMoreInteractions(backing);
584 
585         final HttpRequest request2 = new BasicHttpRequest("HEAD", "/");
586         final HttpResponse response2 = HttpTestUtils.make200Response();
587         final CountDownLatch latch2 = new CountDownLatch(1);
588 
589         impl.evictInvalidatedEntries(host, request2, response2, HttpTestUtils.countDown(latch2));
590 
591         latch2.await();
592 
593         verifyNoMoreInteractions(backing);
594     }
595 
596     @Test
597     public void testInvalidatesUnsafeRequestsWithVariants() throws Exception {
598         final HttpRequest request = new BasicHttpRequest("POST", "/path");
599         final String rootKey = CacheKeyGenerator.INSTANCE.generateKey(host, request);
600         final Set<String> variants = new HashSet<>();
601         variants.add("{var1}");
602         variants.add("{var2}");
603         final String variantKey1 = "{var1}" + rootKey;
604         final String variantKey2 = "{var2}" + rootKey;
605 
606         final HttpResponse response = HttpTestUtils.make200Response();
607 
608         backing.putEntry(rootKey, HttpTestUtils.makeCacheEntry(variants));
609         backing.putEntry(variantKey1, HttpTestUtils.makeCacheEntry());
610         backing.putEntry(variantKey2, HttpTestUtils.makeCacheEntry());
611 
612         final CountDownLatch latch = new CountDownLatch(1);
613         impl.evictInvalidatedEntries(host, request, response, HttpTestUtils.countDown(latch));
614 
615         latch.await();
616 
617         verify(backing).getEntry(Mockito.eq(rootKey), Mockito.any());
618         verify(backing).removeEntry(Mockito.eq(rootKey), Mockito.any());
619         verify(backing).removeEntry(Mockito.eq(variantKey1), Mockito.any());
620         verify(backing).removeEntry(Mockito.eq(variantKey2), Mockito.any());
621 
622         Assertions.assertNull(backing.getEntry(rootKey));
623         Assertions.assertNull(backing.getEntry(variantKey1));
624         Assertions.assertNull(backing.getEntry(variantKey2));
625     }
626 
627     @Test
628     public void testInvalidateUriSpecifiedByContentLocationAndFresher() throws Exception {
629         final HttpRequest request = new BasicHttpRequest("PUT", "/foo");
630         final String rootKey = CacheKeyGenerator.INSTANCE.generateKey(host, request);
631         final URI contentUri = new URIBuilder()
632                 .setHttpHost(host)
633                 .setPath("/bar")
634                 .build();
635         final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri);
636 
637         final HttpResponse response = HttpTestUtils.make200Response();
638         response.setHeader("ETag","\"new-etag\"");
639         response.setHeader("Date", DateUtils.formatStandardDate(now));
640         response.setHeader("Content-Location", contentUri.toASCIIString());
641 
642         backing.putEntry(rootKey, HttpTestUtils.makeCacheEntry());
643         backing.putEntry(contentKey, HttpTestUtils.makeCacheEntry(
644                 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
645                 new BasicHeader("ETag", "\"old-etag\"")
646         ));
647 
648         final CountDownLatch latch = new CountDownLatch(1);
649         impl.evictInvalidatedEntries(host, request, response, HttpTestUtils.countDown(latch));
650 
651         latch.await();
652 
653         verify(backing).getEntry(Mockito.eq(rootKey), Mockito.any());
654         verify(backing).removeEntry(Mockito.eq(rootKey), Mockito.any());
655         verify(backing).getEntry(Mockito.eq(contentKey), Mockito.any());
656         verify(backing).removeEntry(Mockito.eq(contentKey), Mockito.any());
657     }
658 
659     @Test
660     public void testInvalidateUriSpecifiedByLocationAndFresher() throws Exception {
661         final HttpRequest request = new BasicHttpRequest("PUT", "/foo");
662         final String rootKey = CacheKeyGenerator.INSTANCE.generateKey(host, request);
663         final URI contentUri = new URIBuilder()
664                 .setHttpHost(host)
665                 .setPath("/bar")
666                 .build();
667         final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri);
668 
669         final HttpResponse response = HttpTestUtils.make200Response();
670         response.setHeader("ETag","\"new-etag\"");
671         response.setHeader("Date", DateUtils.formatStandardDate(now));
672         response.setHeader("Location", contentUri.toASCIIString());
673 
674         backing.putEntry(rootKey, HttpTestUtils.makeCacheEntry());
675         backing.putEntry(contentKey, HttpTestUtils.makeCacheEntry(
676                 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
677                 new BasicHeader("ETag", "\"old-etag\"")
678         ));
679 
680         final CountDownLatch latch = new CountDownLatch(1);
681         impl.evictInvalidatedEntries(host, request, response, HttpTestUtils.countDown(latch));
682 
683         latch.await();
684 
685         verify(backing).getEntry(Mockito.eq(rootKey), Mockito.any());
686         verify(backing).removeEntry(Mockito.eq(rootKey), Mockito.any());
687         verify(backing).getEntry(Mockito.eq(contentKey), Mockito.any());
688         verify(backing).removeEntry(Mockito.eq(contentKey), Mockito.any());
689     }
690 
691     @Test
692     public void testDoesNotInvalidateForUnsuccessfulResponse() throws Exception {
693         final HttpRequest request = new BasicHttpRequest("PUT", "/foo");
694         final URI contentUri = new URIBuilder()
695                 .setHttpHost(host)
696                 .setPath("/bar")
697                 .build();
698         final HttpResponse response = HttpTestUtils.make500Response();
699         response.setHeader("ETag","\"new-etag\"");
700         response.setHeader("Date", DateUtils.formatStandardDate(now));
701         response.setHeader("Content-Location", contentUri.toASCIIString());
702 
703         final CountDownLatch latch = new CountDownLatch(1);
704         impl.evictInvalidatedEntries(host, request, response, HttpTestUtils.countDown(latch));
705 
706         latch.await();
707 
708         verifyNoMoreInteractions(backing);
709     }
710 
711     @Test
712     public void testInvalidateUriSpecifiedByContentLocationNonCanonical() throws Exception {
713         final HttpRequest request = new BasicHttpRequest("PUT", "/foo");
714         final String rootKey = CacheKeyGenerator.INSTANCE.generateKey(host, request);
715         final URI contentUri = new URIBuilder()
716                 .setHttpHost(host)
717                 .setPath("/bar")
718                 .build();
719         final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri);
720 
721         final HttpResponse response = HttpTestUtils.make200Response();
722         response.setHeader("ETag","\"new-etag\"");
723         response.setHeader("Date", DateUtils.formatStandardDate(now));
724 
725         response.setHeader("Content-Location", contentUri.toASCIIString());
726 
727         backing.putEntry(rootKey, HttpTestUtils.makeCacheEntry());
728 
729         backing.putEntry(contentKey, HttpTestUtils.makeCacheEntry(
730                 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
731                 new BasicHeader("ETag", "\"old-etag\"")));
732 
733         final CountDownLatch latch = new CountDownLatch(1);
734         impl.evictInvalidatedEntries(host, request, response, HttpTestUtils.countDown(latch));
735 
736         latch.await();
737 
738         verify(backing).getEntry(Mockito.eq(rootKey), Mockito.any());
739         verify(backing).removeEntry(Mockito.eq(rootKey), Mockito.any());
740         verify(backing).getEntry(Mockito.eq(contentKey), Mockito.any());
741         verify(backing).removeEntry(Mockito.eq(contentKey), Mockito.any());
742         Assertions.assertNull(backing.getEntry(rootKey));
743         Assertions.assertNull(backing.getEntry(contentKey));
744     }
745 
746     @Test
747     public void testInvalidateUriSpecifiedByContentLocationRelative() throws Exception {
748         final HttpRequest request = new BasicHttpRequest("PUT", "/foo");
749         final String rootKey = CacheKeyGenerator.INSTANCE.generateKey(host, request);
750         final URI contentUri = new URIBuilder()
751                 .setHttpHost(host)
752                 .setPath("/bar")
753                 .build();
754         final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri);
755 
756         final HttpResponse response = HttpTestUtils.make200Response();
757         response.setHeader("ETag","\"new-etag\"");
758         response.setHeader("Date", DateUtils.formatStandardDate(now));
759 
760         response.setHeader("Content-Location", "/bar");
761 
762         backing.putEntry(rootKey, HttpTestUtils.makeCacheEntry());
763 
764         backing.putEntry(contentKey, HttpTestUtils.makeCacheEntry(
765                 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
766                 new BasicHeader("ETag", "\"old-etag\"")));
767 
768         final CountDownLatch latch = new CountDownLatch(1);
769         impl.evictInvalidatedEntries(host, request, response, HttpTestUtils.countDown(latch));
770 
771         latch.await();
772 
773         verify(backing).getEntry(Mockito.eq(rootKey), Mockito.any());
774         verify(backing).removeEntry(Mockito.eq(rootKey), Mockito.any());
775         verify(backing).getEntry(Mockito.eq(contentKey), Mockito.any());
776         verify(backing).removeEntry(Mockito.eq(contentKey), Mockito.any());
777         Assertions.assertNull(backing.getEntry(rootKey));
778         Assertions.assertNull(backing.getEntry(contentKey));
779     }
780 
781     @Test
782     public void testDoesNotInvalidateUriSpecifiedByContentLocationOtherOrigin() throws Exception {
783         final HttpRequest request = new BasicHttpRequest("PUT", "/");
784         final URI contentUri = new URIBuilder()
785                 .setHost("bar.example.com")
786                 .setPath("/")
787                 .build();
788         final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri);
789 
790         final HttpResponse response = HttpTestUtils.make200Response();
791         response.setHeader("ETag","\"new-etag\"");
792         response.setHeader("Date", DateUtils.formatStandardDate(now));
793         response.setHeader("Content-Location", contentUri.toASCIIString());
794 
795         backing.putEntry(contentKey, HttpTestUtils.makeCacheEntry());
796 
797         final CountDownLatch latch = new CountDownLatch(1);
798         impl.evictInvalidatedEntries(host, request, response, HttpTestUtils.countDown(latch));
799 
800         latch.await();
801 
802         verify(backing, Mockito.never()).getEntry(contentKey);
803         verify(backing, Mockito.never()).removeEntry(Mockito.eq(contentKey), Mockito.any());
804     }
805 
806     @Test
807     public void testDoesNotInvalidateUriSpecifiedByContentLocationIfEtagsMatch() throws Exception {
808         final HttpRequest request = new BasicHttpRequest("PUT", "/foo");
809         final URI contentUri = new URIBuilder()
810                 .setHttpHost(host)
811                 .setPath("/bar")
812                 .build();
813         final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri);
814 
815         final HttpResponse response = HttpTestUtils.make200Response();
816         response.setHeader("ETag","\"same-etag\"");
817         response.setHeader("Date", DateUtils.formatStandardDate(now));
818         response.setHeader("Content-Location", contentUri.toASCIIString());
819 
820         backing.putEntry(contentKey, HttpTestUtils.makeCacheEntry(
821                 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
822                 new BasicHeader("ETag", "\"same-etag\"")));
823 
824         final CountDownLatch latch = new CountDownLatch(1);
825         impl.evictInvalidatedEntries(host, request, response, HttpTestUtils.countDown(latch));
826 
827         latch.await();
828 
829         verify(backing).getEntry(Mockito.eq(contentKey), Mockito.any());
830         verify(backing, Mockito.never()).removeEntry(Mockito.eq(contentKey), Mockito.any());
831     }
832 
833     @Test
834     public void testDoesNotInvalidateUriSpecifiedByContentLocationIfOlder() throws Exception {
835         final HttpRequest request = new BasicHttpRequest("PUT", "/foo");
836         final URI contentUri = new URIBuilder()
837                 .setHttpHost(host)
838                 .setPath("/bar")
839                 .build();
840         final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri);
841 
842         final HttpResponse response = HttpTestUtils.make200Response();
843         response.setHeader("ETag","\"new-etag\"");
844         response.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
845         response.setHeader("Content-Location", contentUri.toASCIIString());
846 
847         backing.putEntry(contentKey, HttpTestUtils.makeCacheEntry(
848                 new BasicHeader("Date", DateUtils.formatStandardDate(now)),
849                 new BasicHeader("ETag", "\"old-etag\"")));
850 
851         final CountDownLatch latch = new CountDownLatch(1);
852         impl.evictInvalidatedEntries(host, request, response, HttpTestUtils.countDown(latch));
853 
854         latch.await();
855 
856         verify(backing).getEntry(Mockito.eq(contentKey), Mockito.any());
857         verify(backing, Mockito.never()).removeEntry(Mockito.eq(contentKey), Mockito.any());
858     }
859 
860     @Test
861     public void testDoesNotInvalidateUriSpecifiedByContentLocationIfResponseHasNoEtag() throws Exception {
862         final HttpRequest request = new BasicHttpRequest("PUT", "/foo");
863         final URI contentUri = new URIBuilder()
864                 .setHttpHost(host)
865                 .setPath("/bar")
866                 .build();
867         final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri);
868 
869         final HttpResponse response = HttpTestUtils.make200Response();
870         response.removeHeaders("ETag");
871         response.setHeader("Date", DateUtils.formatStandardDate(now));
872         response.setHeader("Content-Location", contentUri.toASCIIString());
873 
874         backing.putEntry(contentKey, HttpTestUtils.makeCacheEntry(
875                 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)),
876                 new BasicHeader("ETag", "\"old-etag\"")));
877 
878         final CountDownLatch latch = new CountDownLatch(1);
879         impl.evictInvalidatedEntries(host, request, response, HttpTestUtils.countDown(latch));
880 
881         latch.await();
882 
883         verify(backing).getEntry(Mockito.eq(contentKey), Mockito.any());
884         verify(backing, Mockito.never()).removeEntry(Mockito.eq(contentKey), Mockito.any());
885     }
886 
887     @Test
888     public void testDoesNotInvalidateUriSpecifiedByContentLocationIfEntryHasNoEtag() throws Exception {
889         final HttpRequest request = new BasicHttpRequest("PUT", "/foo");
890         final URI contentUri = new URIBuilder()
891                 .setHttpHost(host)
892                 .setPath("/bar")
893                 .build();
894         final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri);
895 
896         final HttpResponse response = HttpTestUtils.make200Response();
897         response.setHeader("ETag", "\"some-etag\"");
898         response.setHeader("Date", DateUtils.formatStandardDate(now));
899         response.setHeader("Content-Location", contentUri.toASCIIString());
900 
901         backing.putEntry(contentKey, HttpTestUtils.makeCacheEntry(
902                 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo))));
903 
904         final CountDownLatch latch = new CountDownLatch(1);
905         impl.evictInvalidatedEntries(host, request, response, HttpTestUtils.countDown(latch));
906 
907         latch.await();
908 
909         verify(backing).getEntry(Mockito.eq(contentKey), Mockito.any());
910         verify(backing, Mockito.never()).removeEntry(Mockito.eq(contentKey), Mockito.any());
911     }
912 
913     @Test
914     public void testInvalidatesUriSpecifiedByContentLocationIfResponseHasNoDate() throws Exception {
915         final HttpRequest request = new BasicHttpRequest("PUT", "/foo");
916         final URI contentUri = new URIBuilder()
917                 .setHttpHost(host)
918                 .setPath("/bar")
919                 .build();
920         final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri);
921 
922         final HttpResponse response = HttpTestUtils.make200Response();
923         response.setHeader("ETag", "\"new-etag\"");
924         response.removeHeaders("Date");
925         response.setHeader("Content-Location", contentUri.toASCIIString());
926 
927         backing.putEntry(contentKey, HttpTestUtils.makeCacheEntry(
928                 new BasicHeader("ETag", "\"old-etag\""),
929                 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo))));
930 
931         final CountDownLatch latch = new CountDownLatch(1);
932         impl.evictInvalidatedEntries(host, request, response, HttpTestUtils.countDown(latch));
933 
934         latch.await();
935 
936         verify(backing).getEntry(Mockito.eq(contentKey), Mockito.any());
937         verify(backing).removeEntry(Mockito.eq(contentKey), Mockito.any());
938     }
939 
940     @Test
941     public void testInvalidatesUriSpecifiedByContentLocationIfEntryHasNoDate() throws Exception {
942         final HttpRequest request = new BasicHttpRequest("PUT", "/foo");
943         final URI contentUri = new URIBuilder()
944                 .setHttpHost(host)
945                 .setPath("/bar")
946                 .build();
947         final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri);
948 
949         final HttpResponse response = HttpTestUtils.make200Response();
950         response.setHeader("ETag","\"new-etag\"");
951         response.setHeader("Date", DateUtils.formatStandardDate(now));
952         response.setHeader("Content-Location", contentUri.toASCIIString());
953 
954         backing.putEntry(contentKey, HttpTestUtils.makeCacheEntry(
955                 new BasicHeader("ETag", "\"old-etag\"")));
956 
957         final CountDownLatch latch = new CountDownLatch(1);
958         impl.evictInvalidatedEntries(host, request, response, HttpTestUtils.countDown(latch));
959 
960         latch.await();
961 
962         verify(backing).getEntry(Mockito.eq(contentKey), Mockito.any());
963         verify(backing).removeEntry(Mockito.eq(contentKey), Mockito.any());
964     }
965 
966     @Test
967     public void testInvalidatesUriSpecifiedByContentLocationIfResponseHasMalformedDate() throws Exception {
968         final HttpRequest request = new BasicHttpRequest("PUT", "/foo");
969         final URI contentUri = new URIBuilder()
970                 .setHttpHost(host)
971                 .setPath("/bar")
972                 .build();
973         final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri);
974 
975         final HttpResponse response = HttpTestUtils.make200Response();
976         response.setHeader("ETag","\"new-etag\"");
977         response.setHeader("Date", "huh?");
978         response.setHeader("Content-Location", contentUri.toASCIIString());
979 
980         backing.putEntry(contentKey, HttpTestUtils.makeCacheEntry(
981                 new BasicHeader("ETag", "\"old-etag\""),
982                 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo))));
983 
984         final CountDownLatch latch = new CountDownLatch(1);
985         impl.evictInvalidatedEntries(host, request, response, HttpTestUtils.countDown(latch));
986 
987         latch.await();
988 
989         verify(backing).getEntry(Mockito.eq(contentKey), Mockito.any());
990         verify(backing).removeEntry(Mockito.eq(contentKey), Mockito.any());
991     }
992 
993     @Test
994     public void testInvalidatesUriSpecifiedByContentLocationIfEntryHasMalformedDate() throws Exception {
995         final HttpRequest request = new BasicHttpRequest("PUT", "/foo");
996         final URI contentUri = new URIBuilder()
997                 .setHttpHost(host)
998                 .setPath("/bar")
999                 .build();
1000         final String contentKey = CacheKeyGenerator.INSTANCE.generateKey(contentUri);
1001 
1002         final HttpResponse response = HttpTestUtils.make200Response();
1003         response.setHeader("ETag","\"new-etag\"");
1004         response.setHeader("Date", DateUtils.formatStandardDate(now));
1005         response.setHeader("Content-Location", contentUri.toASCIIString());
1006 
1007         backing.putEntry(contentKey, HttpTestUtils.makeCacheEntry(
1008                 new BasicHeader("ETag", "\"old-etag\""),
1009                 new BasicHeader("Date", "huh?")));
1010 
1011         final CountDownLatch latch = new CountDownLatch(1);
1012         impl.evictInvalidatedEntries(host, request, response, HttpTestUtils.countDown(latch));
1013 
1014         latch.await();
1015 
1016         verify(backing).getEntry(Mockito.eq(contentKey), Mockito.any());
1017         verify(backing).removeEntry(Mockito.eq(contentKey), Mockito.any());
1018     }
1019 
1020 }