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.cache;
28  
29  import java.time.Instant;
30  import java.util.HashSet;
31  import java.util.Set;
32  
33  import org.apache.hc.client5.http.HeadersMatcher;
34  import org.apache.hc.client5.http.impl.cache.ContainsHeaderMatcher;
35  import org.apache.hc.client5.http.impl.cache.HttpCacheEntryMatcher;
36  import org.apache.hc.client5.http.impl.cache.HttpTestUtils;
37  import org.apache.hc.client5.http.utils.DateUtils;
38  import org.apache.hc.core5.http.ContentType;
39  import org.apache.hc.core5.http.Header;
40  import org.apache.hc.core5.http.HttpHeaders;
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.HttpStatus;
45  import org.apache.hc.core5.http.Method;
46  import org.apache.hc.core5.http.message.BasicHeader;
47  import org.apache.hc.core5.http.message.BasicHttpRequest;
48  import org.apache.hc.core5.http.message.BasicHttpResponse;
49  import org.apache.hc.core5.http.message.HeaderGroup;
50  import org.hamcrest.MatcherAssert;
51  import org.junit.jupiter.api.Assertions;
52  import org.junit.jupiter.api.BeforeEach;
53  import org.junit.jupiter.api.Test;
54  
55  public class TestHttpCacheEntryFactory {
56  
57      private Instant requestDate;
58      private Instant responseDate;
59  
60      private HttpCacheEntry entry;
61      private Instant now;
62      private Instant oneSecondAgo;
63      private Instant twoSecondsAgo;
64      private Instant eightSecondsAgo;
65      private Instant tenSecondsAgo;
66      private HttpHost host;
67      private HttpRequest request;
68      private HttpResponse response;
69      private HttpCacheEntryFactory impl;
70  
71      @BeforeEach
72      public void setUp() throws Exception {
73          requestDate = Instant.now().minusSeconds(1);
74          responseDate = Instant.now();
75  
76          now = Instant.now();
77          oneSecondAgo = now.minusSeconds(1);
78          twoSecondsAgo = now.minusSeconds(2);
79          eightSecondsAgo = now.minusSeconds(8);
80          tenSecondsAgo = now.minusSeconds(10);
81  
82          host = new HttpHost("foo.example.com");
83          request = new BasicHttpRequest("GET", "/stuff");
84          response = new BasicHttpResponse(HttpStatus.SC_NOT_MODIFIED, "Not Modified");
85  
86          impl = new HttpCacheEntryFactory();
87      }
88  
89      @Test
90      public void testFilterHopByHopAndConnectionSpecificHeaders() {
91          response.setHeaders(
92                  new BasicHeader(HttpHeaders.CONNECTION, "blah, blah, this, that"),
93                  new BasicHeader("Blah", "huh?"),
94                  new BasicHeader("BLAH", "huh?"),
95                  new BasicHeader("this", "huh?"),
96                  new BasicHeader("That", "huh?"),
97                  new BasicHeader("Keep-Alive", "timeout, max=20"),
98                  new BasicHeader("X-custom", "my stuff"),
99                  new BasicHeader(HttpHeaders.CONTENT_TYPE, ContentType.TEXT_PLAIN.toString()),
100                 new BasicHeader(HttpHeaders.CONTENT_LENGTH, "111"));
101         final HeaderGroup filteredHeaders = HttpCacheEntryFactory.filterHopByHopHeaders(response);
102         MatcherAssert.assertThat(filteredHeaders.getHeaders(), HeadersMatcher.same(
103                 new BasicHeader("X-custom", "my stuff"),
104                 new BasicHeader(HttpHeaders.CONTENT_TYPE, ContentType.TEXT_PLAIN.toString())
105         ));
106     }
107 
108     @Test
109     public void testHeadersAreMergedCorrectly() {
110         entry = HttpTestUtils.makeCacheEntry(
111                 new BasicHeader("Date", DateUtils.formatStandardDate(responseDate)),
112                 new BasicHeader("ETag", "\"etag\""));
113 
114         final HeaderGroup mergedHeaders = impl.mergeHeaders(entry, response);
115 
116         MatcherAssert.assertThat(mergedHeaders, ContainsHeaderMatcher.contains("Date", DateUtils.formatStandardDate(responseDate)));
117         MatcherAssert.assertThat(mergedHeaders, ContainsHeaderMatcher.contains("ETag", "\"etag\""));
118     }
119 
120     @Test
121     public void testNewerHeadersReplaceExistingHeaders() {
122         entry = HttpTestUtils.makeCacheEntry(
123                 new BasicHeader("Date", DateUtils.formatStandardDate(requestDate)),
124                 new BasicHeader("Cache-Control", "private"),
125                 new BasicHeader("ETag", "\"etag\""),
126                 new BasicHeader("Last-Modified", DateUtils.formatStandardDate(requestDate)),
127                 new BasicHeader("Cache-Control", "max-age=0"));
128 
129         response.setHeaders(
130                 new BasicHeader("Last-Modified", DateUtils.formatStandardDate(responseDate)),
131                 new BasicHeader("Cache-Control", "public"));
132 
133         final HeaderGroup mergedHeaders = impl.mergeHeaders(entry, response);
134 
135         MatcherAssert.assertThat(mergedHeaders, ContainsHeaderMatcher.contains("Date", DateUtils.formatStandardDate(requestDate)));
136         MatcherAssert.assertThat(mergedHeaders, ContainsHeaderMatcher.contains("ETag", "\"etag\""));
137         MatcherAssert.assertThat(mergedHeaders, ContainsHeaderMatcher.contains("Last-Modified", DateUtils.formatStandardDate(responseDate)));
138         MatcherAssert.assertThat(mergedHeaders, ContainsHeaderMatcher.contains("Cache-Control", "public"));
139     }
140 
141     @Test
142     public void testNewHeadersAreAddedByMerge() {
143         entry = HttpTestUtils.makeCacheEntry(
144                 new BasicHeader("Date", DateUtils.formatStandardDate(requestDate)),
145                 new BasicHeader("ETag", "\"etag\""));
146         response.setHeaders(
147                 new BasicHeader("Last-Modified", DateUtils.formatStandardDate(responseDate)),
148                 new BasicHeader("Cache-Control", "public"));
149 
150         final HeaderGroup mergedHeaders = impl.mergeHeaders(entry, response);
151 
152         MatcherAssert.assertThat(mergedHeaders, ContainsHeaderMatcher.contains("Date", DateUtils.formatStandardDate(requestDate)));
153         MatcherAssert.assertThat(mergedHeaders, ContainsHeaderMatcher.contains("ETag", "\"etag\""));
154         MatcherAssert.assertThat(mergedHeaders, ContainsHeaderMatcher.contains("Last-Modified", DateUtils.formatStandardDate(responseDate)));
155         MatcherAssert.assertThat(mergedHeaders, ContainsHeaderMatcher.contains("Cache-Control", "public"));
156     }
157 
158     @Test
159     public void entryWithMalformedDateIsStillUpdated() throws Exception {
160         entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo,
161                 new BasicHeader("ETag", "\"old\""),
162                 new BasicHeader("Date", "bad-date"));
163         response.setHeader("ETag", "\"new\"");
164         response.setHeader("Date", DateUtils.formatStandardDate(twoSecondsAgo));
165 
166         final HeaderGroup mergedHeaders = impl.mergeHeaders(entry, response);
167 
168         MatcherAssert.assertThat(mergedHeaders, ContainsHeaderMatcher.contains("ETag", "\"new\""));
169     }
170 
171     @Test
172     public void entryIsStillUpdatedByResponseWithMalformedDate() throws Exception {
173         entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo,
174                 new BasicHeader("ETag", "\"old\""),
175                 new BasicHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo)));
176         response.setHeader("ETag", "\"new\"");
177         response.setHeader("Date", "bad-date");
178 
179         final HeaderGroup mergedHeaders = impl.mergeHeaders(entry, response);
180 
181         MatcherAssert.assertThat(mergedHeaders, ContainsHeaderMatcher.contains("ETag", "\"new\""));
182     }
183 
184     @Test
185     public void testUpdateCacheEntryReturnsDifferentEntryInstance() {
186         entry = HttpTestUtils.makeCacheEntry();
187         final HttpCacheEntry newEntry = impl.createUpdated(requestDate, responseDate, host, request, response, entry);
188         Assertions.assertNotSame(newEntry, entry);
189     }
190 
191     @Test
192     public void testCreateRootVariantEntry() {
193         request.setHeaders(
194                 new BasicHeader("Keep-Alive", "timeout, max=20"),
195                 new BasicHeader("X-custom", "my stuff"),
196                 new BasicHeader(HttpHeaders.ACCEPT, "stuff"),
197                 new BasicHeader(HttpHeaders.ACCEPT_LANGUAGE, "en, de")
198         );
199         response.setHeaders(
200                 new BasicHeader(HttpHeaders.TRANSFER_ENCODING, "identity"),
201                 new BasicHeader(HttpHeaders.CONNECTION, "Keep-Alive, Blah"),
202                 new BasicHeader("Blah", "huh?"),
203                 new BasicHeader(HttpHeaders.DATE, DateUtils.formatStandardDate(twoSecondsAgo)),
204                 new BasicHeader(HttpHeaders.VARY, "Stuff"),
205                 new BasicHeader(HttpHeaders.ETAG, "\"some-etag\""),
206                 new BasicHeader("X-custom", "my stuff")
207         );
208 
209         final HttpCacheEntry newEntry = impl.create(tenSecondsAgo, oneSecondAgo, host, request, response, HttpTestUtils.makeRandomResource(1024));
210 
211         final Set<String> variants = new HashSet<>();
212         variants.add("variant1");
213         variants.add("variant2");
214         variants.add("variant3");
215 
216         final HttpCacheEntry newRoot = impl.createRoot(newEntry, variants);
217 
218         MatcherAssert.assertThat(newRoot, HttpCacheEntryMatcher.equivalent(
219                 HttpTestUtils.makeCacheEntry(
220                         tenSecondsAgo,
221                         oneSecondAgo,
222                         Method.GET,
223                         "http://foo.example.com:80/stuff",
224                         new Header[]{
225                                 new BasicHeader("X-custom", "my stuff"),
226                                 new BasicHeader(HttpHeaders.ACCEPT, "stuff"),
227                                 new BasicHeader(HttpHeaders.ACCEPT_LANGUAGE, "en, de")
228                         },
229                         HttpStatus.SC_NOT_MODIFIED,
230                         new Header[]{
231                                 new BasicHeader(HttpHeaders.DATE, DateUtils.formatStandardDate(twoSecondsAgo)),
232                                 new BasicHeader(HttpHeaders.VARY, "Stuff"),
233                                 new BasicHeader(HttpHeaders.ETAG, "\"some-etag\""),
234                                 new BasicHeader("X-custom", "my stuff")
235                         },
236                         variants
237                         )));
238 
239         Assertions.assertTrue(newRoot.hasVariants());
240         Assertions.assertNull(newRoot.getResource());
241     }
242 
243     @Test
244     public void testCreateResourceEntry() {
245         request.setHeaders(
246                 new BasicHeader("Keep-Alive", "timeout, max=20"),
247                 new BasicHeader("X-custom", "my stuff"),
248                 new BasicHeader(HttpHeaders.ACCEPT, "stuff"),
249                 new BasicHeader(HttpHeaders.ACCEPT_LANGUAGE, "en, de"),
250                 new BasicHeader(HttpHeaders.AUTHORIZATION, "Super secret")
251         );
252         response.setHeaders(
253                 new BasicHeader(HttpHeaders.TRANSFER_ENCODING, "identity"),
254                 new BasicHeader(HttpHeaders.CONNECTION, "Keep-Alive, Blah"),
255                 new BasicHeader("Blah", "huh?"),
256                 new BasicHeader(HttpHeaders.DATE, DateUtils.formatStandardDate(twoSecondsAgo)),
257                 new BasicHeader(HttpHeaders.ETAG, "\"some-etag\""),
258                 new BasicHeader("X-custom", "my stuff")
259         );
260 
261         final Resource resource = HttpTestUtils.makeRandomResource(128);
262         final HttpCacheEntry newEntry = impl.create(tenSecondsAgo, oneSecondAgo, host, request, response, resource);
263 
264         MatcherAssert.assertThat(newEntry, HttpCacheEntryMatcher.equivalent(
265                 HttpTestUtils.makeCacheEntry(
266                         tenSecondsAgo,
267                         oneSecondAgo,
268                         Method.GET,
269                         "http://foo.example.com:80/stuff",
270                         new Header[]{
271                                 new BasicHeader("X-custom", "my stuff"),
272                                 new BasicHeader(HttpHeaders.ACCEPT, "stuff"),
273                                 new BasicHeader(HttpHeaders.ACCEPT_LANGUAGE, "en, de")
274                         },
275                         HttpStatus.SC_NOT_MODIFIED,
276                         new Header[]{
277                                 new BasicHeader(HttpHeaders.DATE, DateUtils.formatStandardDate(twoSecondsAgo)),
278                                 new BasicHeader(HttpHeaders.ETAG, "\"some-etag\""),
279                                 new BasicHeader("X-custom", "my stuff")
280                         },
281                         resource
282                 )));
283 
284         Assertions.assertFalse(newEntry.hasVariants());
285     }
286 
287     @Test
288     public void testCreateUpdatedResourceEntry() {
289         final Resource resource = HttpTestUtils.makeRandomResource(128);
290         final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry(
291                 tenSecondsAgo,
292                 twoSecondsAgo,
293                 Method.GET,
294                 "/stuff",
295                 new Header[]{
296                         new BasicHeader("X-custom", "my stuff"),
297                         new BasicHeader(HttpHeaders.ACCEPT, "stuff"),
298                         new BasicHeader(HttpHeaders.ACCEPT_LANGUAGE, "en, de")
299                 },
300                 HttpStatus.SC_NOT_MODIFIED,
301                 new Header[]{
302                         new BasicHeader(HttpHeaders.DATE, DateUtils.formatStandardDate(twoSecondsAgo)),
303                         new BasicHeader(HttpHeaders.ETAG, "\"some-etag\""),
304                         new BasicHeader("X-custom", "my stuff"),
305                         new BasicHeader("Cache-Control", "max-age=0")
306                 },
307                 resource
308         );
309 
310         response.setHeaders(
311                 new BasicHeader(HttpHeaders.ETAG, "\"some-new-etag\""),
312                 new BasicHeader("Last-Modified", DateUtils.formatStandardDate(oneSecondAgo)),
313                 new BasicHeader("Cache-Control", "public")
314         );
315 
316         request.setHeaders(
317                 new BasicHeader("X-custom", "my other stuff"),
318                 new BasicHeader(HttpHeaders.ACCEPT, "stuff, other-stuff"),
319                 new BasicHeader(HttpHeaders.ACCEPT_LANGUAGE, "en, de")
320         );
321 
322         final HttpCacheEntry updatedEntry = impl.createUpdated(tenSecondsAgo, oneSecondAgo, host, request, response, entry);
323 
324         MatcherAssert.assertThat(updatedEntry, HttpCacheEntryMatcher.equivalent(
325                 HttpTestUtils.makeCacheEntry(
326                         tenSecondsAgo,
327                         oneSecondAgo,
328                         Method.GET,
329                         "http://foo.example.com:80/stuff",
330                         new Header[]{
331                                 new BasicHeader("X-custom", "my other stuff"),
332                                 new BasicHeader(HttpHeaders.ACCEPT, "stuff, other-stuff"),
333                                 new BasicHeader(HttpHeaders.ACCEPT_LANGUAGE, "en, de")
334                         },
335                         HttpStatus.SC_NOT_MODIFIED,
336                         new Header[]{
337                                 new BasicHeader(HttpHeaders.DATE, DateUtils.formatStandardDate(twoSecondsAgo)),
338                                 new BasicHeader("X-custom", "my stuff"),
339                                 new BasicHeader(HttpHeaders.ETAG, "\"some-new-etag\""),
340                                 new BasicHeader("Last-Modified", DateUtils.formatStandardDate(oneSecondAgo)),
341                                 new BasicHeader("Cache-Control", "public")
342                         },
343                         resource
344                 )));
345 
346         Assertions.assertFalse(updatedEntry.hasVariants());
347     }
348 
349     @Test
350     public void testUpdateNotModifiedIfResponseOlder() {
351         entry = HttpTestUtils.makeCacheEntry(twoSecondsAgo, now,
352                 new BasicHeader("Date", DateUtils.formatStandardDate(oneSecondAgo)),
353                 new BasicHeader("ETag", "\"new-etag\""));
354         response.setHeader("Date", DateUtils.formatStandardDate(tenSecondsAgo));
355         response.setHeader("ETag", "\"old-etag\"");
356 
357         final HttpCacheEntry newEntry = impl.createUpdated(Instant.now(), Instant.now(), host, request, response, entry);
358 
359         Assertions.assertSame(newEntry, entry);
360     }
361 
362     @Test
363     public void testUpdateHasLatestRequestAndResponseDates() {
364         entry = HttpTestUtils.makeCacheEntry(tenSecondsAgo, eightSecondsAgo);
365         final HttpCacheEntry updated = impl.createUpdated(twoSecondsAgo, oneSecondAgo, host, request, response, entry);
366 
367         Assertions.assertEquals(twoSecondsAgo, updated.getRequestInstant());
368         Assertions.assertEquals(oneSecondAgo, updated.getResponseInstant());
369     }
370 
371     @Test
372     public void cannotUpdateFromANon304OriginResponse() throws Exception {
373         entry = HttpTestUtils.makeCacheEntry();
374         response = new BasicHttpResponse(HttpStatus.SC_OK, "OK");
375         Assertions.assertThrows(IllegalArgumentException.class, () ->
376                 impl.createUpdated(Instant.now(), Instant.now(), host, request, response, entry));
377     }
378 
379 }