View Javadoc
1   /*
2    * Copyright (c) 2002-2025 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit;
16  
17  import static org.apache.http.client.utils.DateUtils.formatDate;
18  import static org.htmlunit.HttpHeader.CACHE_CONTROL;
19  import static org.htmlunit.HttpHeader.ETAG;
20  import static org.htmlunit.HttpHeader.EXPIRES;
21  import static org.htmlunit.HttpHeader.IF_MODIFIED_SINCE;
22  import static org.htmlunit.HttpHeader.IF_NONE_MATCH;
23  import static org.htmlunit.HttpHeader.LAST_MODIFIED;
24  
25  import java.net.URL;
26  import java.text.SimpleDateFormat;
27  import java.util.ArrayList;
28  import java.util.Collections;
29  import java.util.Date;
30  import java.util.HashMap;
31  import java.util.List;
32  import java.util.Map;
33  
34  import org.apache.commons.lang3.time.DateUtils;
35  import org.htmlunit.html.HtmlPage;
36  import org.htmlunit.http.HttpStatus;
37  import org.htmlunit.junit.BrowserRunner;
38  import org.htmlunit.junit.annotation.Alerts;
39  import org.htmlunit.util.MimeType;
40  import org.htmlunit.util.NameValuePair;
41  import org.htmlunit.util.mocks.WebResponseMock;
42  import org.junit.Test;
43  import org.junit.runner.RunWith;
44  
45  /**
46   * Tests for {@link Cache}.
47   *
48   * @author Marc Guillemot
49   * @author Ahmed Ashour
50   * @author Frank Danek
51   * @author Anton Demydenko
52   * @author Ronald Brill
53   * @author Ashley Frieze
54   * @author Lai Quang Duong
55   */
56  @RunWith(BrowserRunner.class)
57  public class CacheTest extends SimpleWebTestCase {
58  
59      private static final long ONE_MINUTE = 60_000L;
60      private static final long ONE_HOUR = ONE_MINUTE * 60;
61  
62      private final long now_ = new Date().getTime();
63      private final String tomorrow_ = formatDate(DateUtils.addDays(new Date(), 1));
64  
65      /**
66       * Composite test of {@link Cache#isCacheableContent(WebResponse)}.
67       */
68      @Test
69      public void isCacheableContent() {
70          final Cache cache = new Cache();
71          final Map<String, String> headers = new HashMap<>();
72          final WebResponse response = new WebResponseMock(null, headers);
73  
74          assertFalse(cache.isCacheableContent(response));
75  
76          headers.put(LAST_MODIFIED, "Sun, 15 Jul 2007 20:46:27 GMT");
77          assertTrue(cache.isCacheableContent(response));
78  
79          headers.put(LAST_MODIFIED, formatDate(DateUtils.addMinutes(new Date(), -5)));
80          assertTrue(cache.isCacheableContent(response));
81  
82          headers.put(LAST_MODIFIED, formatDate(new Date()));
83          assertFalse(cache.isCacheableContent(response));
84  
85          headers.put(LAST_MODIFIED, formatDate(DateUtils.addMinutes(new Date(), 10)));
86          assertFalse(cache.isCacheableContent(response));
87  
88          headers.put(EXPIRES, formatDate(DateUtils.addMinutes(new Date(), 5)));
89          assertFalse(cache.isCacheableContent(response));
90  
91          headers.put(EXPIRES, formatDate(DateUtils.addHours(new Date(), 1)));
92          assertTrue(cache.isCacheableContent(response));
93  
94          headers.remove(LAST_MODIFIED);
95          assertTrue(cache.isCacheableContent(response));
96  
97          headers.put(EXPIRES, "0");
98          assertFalse(cache.isCacheableContent(response));
99  
100         headers.put(EXPIRES, "-1");
101         assertFalse(cache.isCacheableContent(response));
102 
103         headers.put(CACHE_CONTROL, "no-store");
104         assertFalse(cache.isCacheableContent(response));
105     }
106 
107     /**
108      * @throws Exception if the test fails
109      */
110     @Test
111     public void contentWithNoHeadersIsNotCached() {
112         assertFalse(Cache.isWithinCacheWindow(new WebResponseMock(null, null), now_, now_));
113     }
114 
115     /**
116      * @throws Exception if the test fails
117      */
118     @Test
119     public void contentWithExpiryDateIsCached() {
120         final Map<String, String> headers = new HashMap<>();
121         headers.put(EXPIRES, tomorrow_);
122 
123         assertTrue(Cache.isWithinCacheWindow(new WebResponseMock(null, headers), now_, now_));
124     }
125 
126     /**
127      * @throws Exception if the test fails
128      */
129     @Test
130     public void contentWithExpiryDateInFutureButShortMaxAgeIsNotInCacheWindow() {
131         final Map<String, String> headers = new HashMap<>();
132         headers.put(EXPIRES, tomorrow_);
133         // max age is 1 second, so will have expired after a minute
134         headers.put(CACHE_CONTROL, "some-other-value, max-age=1");
135 
136         assertFalse(Cache.isWithinCacheWindow(new WebResponseMock(null, headers), now_ + ONE_MINUTE, now_));
137     }
138 
139     /**
140      * @throws Exception if the test fails
141      */
142     @Test
143     public void contentWithExpiryDateInFutureButShortSMaxAgeIsNotInCacheWindow() {
144         final Map<String, String> headers = new HashMap<>();
145         headers.put(EXPIRES, tomorrow_);
146         // s max age is 1 second, so will have expired after a minute
147         headers.put(CACHE_CONTROL, "some-other-value, s-maxage=1");
148 
149         assertFalse(Cache.isWithinCacheWindow(new WebResponseMock(null, headers), now_ + ONE_MINUTE, now_));
150     }
151 
152     /**
153      * @throws Exception if the test fails
154      */
155     @Test
156     public void contentWithBothMaxAgeAndSMaxUsesSMaxAsPriority() {
157         final Map<String, String> headers = new HashMap<>();
158         headers.put(CACHE_CONTROL, "some-other-value, max-age=1200, s-maxage=1");
159 
160         assertFalse(Cache.isWithinCacheWindow(new WebResponseMock(null, headers), now_ + ONE_MINUTE, now_));
161     }
162 
163     /**
164      * @throws Exception if the test fails
165      */
166     @Test
167     public void contentWithMaxAgeInFutureWillBeCached() {
168         final Map<String, String> headers = new HashMap<>();
169         headers.put(CACHE_CONTROL, "some-other-value, max-age=1200");
170 
171         assertTrue(Cache.isWithinCacheWindow(new WebResponseMock(null, headers), now_, now_));
172 
173         headers.clear();
174         headers.put(CACHE_CONTROL, "some-other-value, max-age=1200");
175 
176         assertTrue(Cache.isWithinCacheWindow(new WebResponseMock(null, headers), now_ + ONE_MINUTE, now_));
177     }
178 
179     /**
180      * @throws Exception if the test fails
181      */
182     @Test
183     public void contentWithLongLastModifiedTimeComparedToNowIsCachedOnDownload() {
184         final Map<String, String> headers = new HashMap<>();
185         headers.put(LAST_MODIFIED, formatDate(DateUtils.addDays(new Date(), -1)));
186 
187         assertTrue(Cache.isWithinCacheWindow(new WebResponseMock(null, headers), now_, now_));
188     }
189 
190     /**
191      * @throws Exception if the test fails
192      */
193     @Test
194     public void contentWithLastModifiedTimeIsCachedAfterAFewPercentOfCreationAge() {
195         final Map<String, String> headers = new HashMap<>();
196         headers.put(LAST_MODIFIED, formatDate(DateUtils.addDays(new Date(), -1)));
197 
198         assertTrue(Cache.isWithinCacheWindow(new WebResponseMock(null, headers), now_ + ONE_HOUR, now_));
199     }
200 
201     /**
202      * @throws Exception if the test fails
203      */
204     @Test
205     public void contentWithLastModifiedTimeIsNotCachedAfterALongerPeriod() {
206         final Map<String, String> headers = new HashMap<>();
207         headers.put(LAST_MODIFIED, formatDate(DateUtils.addDays(new Date(), -1)));
208 
209         assertFalse(Cache.isWithinCacheWindow(new WebResponseMock(null, headers), now_ + (ONE_HOUR * 5), now_));
210     }
211 
212     /**
213      * @throws Exception if the test fails
214      */
215     @Test
216     public void usage() throws Exception {
217         final String content = DOCTYPE_HTML
218             + "<html><head><title>page 1</title>\n"
219             + "<script src='foo1.js'></script>\n"
220             + "<script src='foo2.js'></script>\n"
221             + "</head><body>\n"
222             + "<a href='page2.html'>to page 2</a>\n"
223             + "</body></html>";
224 
225         final String content2 = DOCTYPE_HTML
226             + "<html><head><title>page 2</title>\n"
227             + "<script src='foo2.js'></script>\n"
228             + "</head><body>\n"
229             + "<a href='page1.html'>to page 1</a>\n"
230             + "</body></html>";
231 
232         final String script1 = "alert('in foo1');";
233         final String script2 = "alert('in foo2');";
234 
235         final WebClient webClient = getWebClient();
236         final MockWebConnection connection = new MockWebConnection();
237         webClient.setWebConnection(connection);
238 
239         final URL urlPage1 = new URL(URL_FIRST, "page1.html");
240         connection.setResponse(urlPage1, content);
241         final URL urlPage2 = new URL(URL_FIRST, "page2.html");
242         connection.setResponse(urlPage2, content2);
243 
244         final List<NameValuePair> headers = new ArrayList<>();
245         headers.add(new NameValuePair(LAST_MODIFIED, "Sun, 15 Jul 2007 20:46:27 GMT"));
246         connection.setResponse(new URL(URL_FIRST, "foo1.js"), script1, 200, "ok",
247                 MimeType.TEXT_JAVASCRIPT, headers);
248         connection.setResponse(new URL(URL_FIRST, "foo2.js"), script2, 200, "ok",
249                 MimeType.TEXT_JAVASCRIPT, headers);
250 
251         final List<String> collectedAlerts = new ArrayList<>();
252         webClient.setAlertHandler(new CollectingAlertHandler(collectedAlerts));
253 
254         final HtmlPage page1 = webClient.getPage(urlPage1);
255         final String[] expectedAlerts = {"in foo1", "in foo2"};
256         assertEquals(expectedAlerts, collectedAlerts);
257 
258         collectedAlerts.clear();
259         page1.getAnchors().get(0).click();
260 
261         assertEquals(new String[] {"in foo2"}, collectedAlerts);
262         assertEquals("no request for scripts should have been performed",
263                 urlPage2, connection.getLastWebRequest().getUrl());
264     }
265 
266     /**
267      * @throws Exception if the test fails
268      */
269     @Test
270     public void jsUrlEncoded() throws Exception {
271         final String content = DOCTYPE_HTML
272             + "<html>\n"
273             + "<head>\n"
274             + "  <title>page 1</title>\n"
275             + "  <script src='foo1.js'></script>\n"
276             + "  <script src='foo2.js?foo[1]=bar/baz'></script>\n"
277             + "</head>\n"
278             + "<body>\n"
279             + "  <a href='page2.html'>to page 2</a>\n"
280             + "</body>\n"
281             + "</html>";
282 
283         final String content2 = DOCTYPE_HTML
284             + "<html>\n"
285             + "<head>\n"
286             + "  <title>page 2</title>\n"
287             + "  <script src='foo2.js?foo[1]=bar/baz'></script>\n"
288             + "</head>\n"
289             + "<body>\n"
290             + "  <a href='page1.html'>to page 1</a>\n"
291             + "</body>\n"
292             + "</html>";
293 
294         final String script1 = "alert('in foo1');";
295         final String script2 = "alert('in foo2');";
296 
297         final URL urlPage1 = new URL(URL_FIRST, "page1.html");
298         getMockWebConnection().setResponse(urlPage1, content);
299         final URL urlPage2 = new URL(URL_FIRST, "page2.html");
300         getMockWebConnection().setResponse(urlPage2, content2);
301 
302         final List<NameValuePair> headers = new ArrayList<>();
303         headers.add(new NameValuePair(LAST_MODIFIED, "Sun, 15 Jul 2007 20:46:27 GMT"));
304         getMockWebConnection().setResponse(new URL(URL_FIRST, "foo1.js"), script1,
305                 200, "ok", MimeType.TEXT_JAVASCRIPT, headers);
306         getMockWebConnection().setDefaultResponse(script2, 200, "ok", MimeType.TEXT_JAVASCRIPT, headers);
307 
308         final WebClient webClient = getWebClientWithMockWebConnection();
309 
310         final List<String> collectedAlerts = new ArrayList<>();
311         webClient.setAlertHandler(new CollectingAlertHandler(collectedAlerts));
312 
313         final HtmlPage page1 = webClient.getPage(urlPage1);
314         final String[] expectedAlerts = {"in foo1", "in foo2"};
315         assertEquals(expectedAlerts, collectedAlerts);
316 
317         collectedAlerts.clear();
318         page1.getAnchors().get(0).click();
319 
320         assertEquals(new String[] {"in foo2"}, collectedAlerts);
321         assertEquals("no request for scripts should have been performed",
322                 urlPage2, getMockWebConnection().getLastWebRequest().getUrl());
323     }
324 
325     /**
326      * @throws Exception if the test fails
327      */
328     @Test
329     public void cssUrlEncoded() throws Exception {
330         final String content = DOCTYPE_HTML
331             + "<html>\n"
332             + "<head>\n"
333             + "  <title>page 1</title>\n"
334             + "  <link href='foo1.css' type='text/css' rel='stylesheet'>\n"
335             + "  <link href='foo2.js?foo[1]=bar/baz' type='text/css' rel='stylesheet'>\n"
336             + "</head>\n"
337             + "<body>\n"
338             + "  <a href='page2.html'>to page 2</a>\n"
339             + "  <script>\n"
340             + "    var sheets = document.styleSheets;\n"
341             + "    alert(sheets.length);\n"
342             + "    var rules = sheets[0].cssRules || sheets[0].rules;\n"
343             + "    alert(rules.length);\n"
344             + "    rules = sheets[1].cssRules || sheets[1].rules;\n"
345             + "    alert(rules.length);\n"
346             + "  </script>\n"
347             + "</body>\n"
348             + "</html>";
349 
350         final String content2 = DOCTYPE_HTML
351             + "<html>\n"
352             + "<head>\n"
353             + "  <title>page 2</title>\n"
354             + "  <link href='foo2.js?foo[1]=bar/baz' type='text/css' rel='stylesheet'>\n"
355             + "</head>\n"
356             + "<body>\n"
357             + "  <a href='page1.html'>to page 1</a>\n"
358             + "  <script>\n"
359             + "    var sheets = document.styleSheets;\n"
360             + "    alert(sheets.length);\n"
361             + "    var rules = sheets[0].cssRules || sheets[0].rules;\n"
362             + "    alert(rules.length);\n"
363             + "  </script>\n"
364             + "</body>\n"
365             + "</html>";
366 
367         final URL urlPage1 = new URL(URL_FIRST, "page1.html");
368         getMockWebConnection().setResponse(urlPage1, content);
369         final URL urlPage2 = new URL(URL_FIRST, "page2.html");
370         getMockWebConnection().setResponse(urlPage2, content2);
371 
372         final List<NameValuePair> headers = new ArrayList<>();
373         headers.add(new NameValuePair(LAST_MODIFIED, "Sun, 15 Jul 2007 20:46:27 GMT"));
374         getMockWebConnection().setResponse(new URL(URL_FIRST, "foo1.js"), "",
375                 200, "ok", MimeType.TEXT_CSS, headers);
376         getMockWebConnection().setDefaultResponse("", 200, "ok", MimeType.TEXT_CSS, headers);
377 
378         final WebClient webClient = getWebClientWithMockWebConnection();
379 
380         final List<String> collectedAlerts = new ArrayList<>();
381         webClient.setAlertHandler(new CollectingAlertHandler(collectedAlerts));
382 
383         final HtmlPage page1 = webClient.getPage(urlPage1);
384         final String[] expectedAlerts = {"2", "0", "0"};
385         assertEquals(expectedAlerts, collectedAlerts);
386         assertEquals(3, getMockWebConnection().getRequestCount());
387 
388         collectedAlerts.clear();
389         page1.getAnchors().get(0).click();
390 
391         assertEquals(new String[] {"1", "0"}, collectedAlerts);
392         assertEquals(4, getMockWebConnection().getRequestCount());
393         assertEquals("no request for scripts should have been performed",
394                 urlPage2, getMockWebConnection().getLastWebRequest().getUrl());
395     }
396 
397     /**
398      * @throws Exception if the test fails
399      */
400     @Test
401     public void maxSizeMaintained() throws Exception {
402         final String html = DOCTYPE_HTML
403             + "<html><head><title>page 1</title>\n"
404             + "<script src='foo1.js' type='text/javascript'/>\n"
405             + "<script src='foo2.js' type='text/javascript'/>\n"
406             + "</head><body>abc</body></html>";
407 
408         final WebClient client = getWebClient();
409         client.getCache().setMaxSize(1);
410 
411         final MockWebConnection connection = new MockWebConnection();
412         client.setWebConnection(connection);
413 
414         final URL pageUrl = new URL(URL_FIRST, "page1.html");
415         connection.setResponse(pageUrl, html);
416 
417         final List<NameValuePair> headers =
418             Collections.singletonList(new NameValuePair(LAST_MODIFIED, "Sun, 15 Jul 2007 20:46:27 GMT"));
419         connection.setResponse(new URL(URL_FIRST, "foo1.js"), ";", 200, "ok", MimeType.TEXT_JAVASCRIPT, headers);
420         connection.setResponse(new URL(URL_FIRST, "foo2.js"), ";", 200, "ok", MimeType.TEXT_JAVASCRIPT, headers);
421 
422         client.getPage(pageUrl);
423         assertEquals(1, client.getCache().getSize());
424 
425         client.getCache().clear();
426         assertEquals(0, client.getCache().getSize());
427     }
428 
429     /**
430      * TODO: improve CSS caching to cache a COPY of the object as stylesheet objects can be modified dynamically.
431      * @throws Exception if the test fails
432      */
433     @Test
434     public void cssIsCached() throws Exception {
435         final String html = DOCTYPE_HTML
436             + "<html><head><title>page 1</title>\n"
437             + "<style>.x { color: red; }</style>\n"
438             + "<link rel='stylesheet' type='text/css' href='foo.css' />\n"
439             + "</head>\n"
440             + "<body onload='document.styleSheets.item(0); document.styleSheets.item(1);'>x</body>\n"
441             + "</html>";
442 
443         final WebClient client = getWebClient();
444 
445         final MockWebConnection connection = new MockWebConnection();
446         client.setWebConnection(connection);
447 
448         final URL pageUrl = new URL(URL_FIRST, "page1.html");
449         connection.setResponse(pageUrl, html);
450 
451         final List<NameValuePair> headers =
452             Collections.singletonList(new NameValuePair(LAST_MODIFIED, "Sun, 15 Jul 2007 20:46:27 GMT"));
453         connection.setResponse(new URL(URL_FIRST, "foo.css"), "", 200, "OK", MimeType.TEXT_CSS, headers);
454 
455         client.getPage(pageUrl);
456         assertEquals(2, client.getCache().getSize());
457     }
458 
459     /**
460      * Check for correct caching if the css request gets redirected.
461      *
462      * @throws Exception if the test fails
463      */
464     @Test
465     public void cssIsCachedIfUrlWasRedirected() throws Exception {
466         final String html = DOCTYPE_HTML
467             + "<html><head><title>page 1</title>\n"
468             + "<link rel='stylesheet' type='text/css' href='foo.css' />\n"
469             + "</head>\n"
470             + "<body onload='document.styleSheets.item(0); document.styleSheets.item(1);'>x</body>\n"
471             + "</html>";
472 
473         final String css = ".x { color: red; }";
474 
475         final WebClient client = getWebClient();
476 
477         final MockWebConnection connection = new MockWebConnection();
478         client.setWebConnection(connection);
479 
480         final URL pageUrl = new URL(URL_FIRST, "page1.html");
481         connection.setResponse(pageUrl, html);
482 
483         final URL cssUrl = new URL(URL_FIRST, "foo.css");
484         final URL redirectUrl = new URL(URL_FIRST, "fooContent.css");
485 
486         List<NameValuePair> headers = new ArrayList<>();
487         headers.add(new NameValuePair("Location", redirectUrl.toExternalForm()));
488         connection.setResponse(cssUrl, "", 301, "Redirect", null, headers);
489 
490         headers = Collections.singletonList(new NameValuePair(LAST_MODIFIED, "Sun, 15 Jul 2007 20:46:27 GMT"));
491         connection.setResponse(redirectUrl, css, 200, "OK", MimeType.TEXT_CSS, headers);
492 
493         client.getPage(pageUrl);
494         client.getPage(pageUrl);
495 
496         // page1.html - foo.css - fooContent.css - page1.html
497         assertEquals(4, connection.getRequestCount());
498         // foo.css - fooContent.css
499         assertEquals(2, client.getCache().getSize());
500     }
501 
502     /**
503      * @throws Exception if the test fails
504      */
505     @Test
506     public void cssFromCacheIsUsed() throws Exception {
507         final String html = DOCTYPE_HTML
508             + "<html><head><title>page 1</title>\n"
509             + "<link rel='stylesheet' type='text/css' href='foo.css' />\n"
510             + "<link rel='stylesheet' type='text/css' href='foo.css' />\n"
511             + "</head>\n"
512             + "<body>x</body>\n"
513             + "</html>";
514 
515         final String css = ".x { color: red; }";
516 
517         final WebClient client = getWebClient();
518 
519         final MockWebConnection connection = new MockWebConnection();
520         client.setWebConnection(connection);
521 
522         final URL pageUrl = new URL(URL_FIRST, "page1.html");
523         connection.setResponse(pageUrl, html);
524 
525         final URL cssUrl = new URL(URL_FIRST, "foo.css");
526         final List<NameValuePair> headers = new ArrayList<>();
527         headers.add(new NameValuePair(LAST_MODIFIED, "Sun, 15 Jul 2007 20:46:27 GMT"));
528         connection.setResponse(cssUrl, css, 200, "OK", MimeType.TEXT_CSS, headers);
529 
530         client.getPage(pageUrl);
531 
532         assertEquals(2, connection.getRequestCount());
533         assertEquals(1, client.getCache().getSize());
534     }
535 
536     /**
537      * @throws Exception if the test fails
538      */
539     @Test
540     public void cssManuallyAddeToCache() throws Exception {
541         final String html = DOCTYPE_HTML
542             + "<html><head>\n"
543             + "<link rel='stylesheet' type='text/css' href='foo.css' />\n"
544             + "</head>\n"
545             + "<body>\n"
546             + "abc <div class='test'>def</div>\n"
547             + "</body>\n"
548             + "</html>";
549 
550         final String css = ".test { visibility: hidden; }";
551 
552         final WebClient client = getWebClient();
553 
554         final MockWebConnection connection = new MockWebConnection();
555         client.setWebConnection(connection);
556 
557         final URL pageUrl = new URL(URL_FIRST, "page1.html");
558         connection.setResponse(pageUrl, html);
559 
560         final URL cssUrl = new URL(URL_FIRST, "foo.css");
561         final List<NameValuePair> headers = new ArrayList<>();
562         headers.add(new NameValuePair(LAST_MODIFIED, "Sun, 15 Jul 2007 20:46:27 GMT"));
563         final WebRequest request = new WebRequest(cssUrl);
564         final WebResponseData data = new WebResponseData(css.getBytes("UTF-8"),
565                 HttpStatus.OK_200, HttpStatus.OK_200_MSG, headers);
566         final WebResponse response = new WebResponse(data, request, 100);
567         client.getCache().cacheIfPossible(new WebRequest(cssUrl), response, headers);
568 
569         final HtmlPage page = client.getPage(pageUrl);
570         assertEquals("abc", page.asNormalizedText());
571 
572         assertEquals(1, connection.getRequestCount());
573         assertEquals(1, client.getCache().getSize());
574     }
575 
576     /**
577      * Test that content retrieved with XHR is cached when right headers are here.
578      * @throws Exception if the test fails
579      */
580     @Test
581     @Alerts({"hello", "hello"})
582     public void xhrContentCached() throws Exception {
583         final String html = DOCTYPE_HTML
584             + "<html><head><title>page 1</title>\n"
585             + "<script>\n"
586             + "  function doTest() {\n"
587             + "    var xhr = new XMLHttpRequest();\n"
588             + "    xhr.open('GET', 'foo.txt', false);\n"
589             + "    xhr.send('');\n"
590             + "    alert(xhr.responseText);\n"
591             + "    xhr.send('');\n"
592             + "    alert(xhr.responseText);\n"
593             + "  }\n"
594             + "</script>\n"
595             + "</head>\n"
596             + "<body onload='doTest()'>x</body>\n"
597             + "</html>";
598 
599         final MockWebConnection connection = getMockWebConnection();
600 
601         final List<NameValuePair> headers =
602             Collections.singletonList(new NameValuePair(LAST_MODIFIED, "Sun, 15 Jul 2007 20:46:27 GMT"));
603         connection.setResponse(new URL(URL_FIRST, "foo.txt"), "hello", 200, "OK", MimeType.TEXT_PLAIN, headers);
604 
605         loadPageWithAlerts(html);
606 
607         assertEquals(2, connection.getRequestCount());
608     }
609 
610     /**
611      * @throws Exception if the test fails
612      */
613     @Test
614     public void testNoStoreCacheControl() throws Exception {
615         final String html = DOCTYPE_HTML
616             + "<html><head><title>page 1</title>\n"
617             + "<link rel='stylesheet' type='text/css' href='foo.css' />\n"
618             + "</head>\n"
619             + "<body>x</body>\n"
620             + "</html>";
621 
622         final WebClient client = getWebClient();
623 
624         final MockWebConnection connection = new MockWebConnection();
625         client.setWebConnection(connection);
626 
627         final List<NameValuePair> headers = new ArrayList<>();
628         headers.add(new NameValuePair(CACHE_CONTROL, "some-other-value, no-store"));
629 
630         final URL pageUrl = new URL(URL_FIRST, "page1.html");
631         connection.setResponse(pageUrl, html, 200, "OK", "text/html;charset=ISO-8859-1", headers);
632         connection.setResponse(new URL(URL_FIRST, "foo.css"), "", 200, "OK", MimeType.TEXT_JAVASCRIPT, headers);
633 
634         client.getPage(pageUrl);
635         assertEquals(0, client.getCache().getSize());
636         assertEquals(2, connection.getRequestCount());
637 
638         client.getPage(pageUrl);
639         assertEquals(0, client.getCache().getSize());
640         assertEquals(4, connection.getRequestCount());
641     }
642 
643     /**
644      * @throws Exception if an error occurs
645      */
646     @Test
647     public void testNoCacheCacheControl() throws Exception {
648         final String html = DOCTYPE_HTML
649                 + "<html><head><title>page 1</title>\n"
650                 + "</head>\n"
651                 + "<body>x</body>\n"
652                 + "</html>";
653 
654         final WebClient client = getWebClient();
655 
656         final MockWebConnection connection = new MockWebConnection();
657         client.setWebConnection(connection);
658 
659         final String date = "Thu, 02 Mar 2023 02:00:00 GMT";
660         final String etag = "foo";
661         final String lastModified = "Wed, 01 Mar 2023 01:00:00 GMT";
662 
663         final List<NameValuePair> headers = new ArrayList<>();
664         headers.add(new NameValuePair("Date", date));
665         headers.add(new NameValuePair(CACHE_CONTROL, "some-other-value, no-cache"));
666         headers.add(new NameValuePair(ETAG, etag));
667         headers.add(new NameValuePair(LAST_MODIFIED, lastModified));
668 
669         final URL pageUrl = new URL(URL_FIRST, "page1.html");
670         connection.setResponse(pageUrl, html, 200, "OK", "text/html;charset=ISO-8859-1", headers);
671 
672         client.getPage(pageUrl);
673         assertEquals(1, client.getCache().getSize());
674 
675         final String updatedDate = "Thu, 02 Mar 2023 02:00:10 GMT";
676         final List<NameValuePair> headers2 = new ArrayList<>();
677         headers2.add(new NameValuePair("Date", updatedDate));
678         headers2.add(new NameValuePair("Proxy-Authorization", "Basic YWxhZGRpbjpvcGVuc2VzYW1l"));
679         headers2.add(new NameValuePair("X-Content-Type-Options", "nosniff"));
680         connection.setResponse(pageUrl, html, 304, "Not Modified", "text/html;charset=ISO-8859-1", headers2);
681 
682         client.getPage(pageUrl);
683         assertEquals(2, connection.getRequestCount());
684 
685         final WebRequest lastRequest = connection.getLastWebRequest();
686         assertEquals(etag, lastRequest.getAdditionalHeader(IF_NONE_MATCH));
687         assertEquals(lastModified, lastRequest.getAdditionalHeader(IF_MODIFIED_SINCE));
688         assertEquals(1, client.getCache().getSize());
689 
690         WebResponse cached = client.getCache().getCachedResponse(connection.getLastWebRequest());
691         assertEquals(updatedDate, cached.getResponseHeaderValue("Date"));
692         assertEquals(null, cached.getResponseHeaderValue("Proxy-Authorization"));
693         assertEquals(null, cached.getResponseHeaderValue("X-Content-Type-Options"));
694 
695         final String updatedEtag = "bar";
696         final String updatedLastModified = "Wed, 01 Mar 2023 02:00:00 GMT";
697 
698         final List<NameValuePair> headers3 = new ArrayList<>();
699         headers3.add(new NameValuePair(CACHE_CONTROL, "some-other-value, no-cache"));
700         headers3.add(new NameValuePair(ETAG, updatedEtag));
701         headers3.add(new NameValuePair(LAST_MODIFIED, updatedLastModified));
702         connection.setResponse(pageUrl, html, 200, "OK", "text/html;charset=ISO-8859-1", headers3);
703 
704         client.getPage(pageUrl);
705         assertEquals(3, connection.getRequestCount());
706         assertEquals(1, client.getCache().getSize());
707 
708         cached = client.getCache().getCachedResponse(connection.getLastWebRequest());
709         assertEquals(null, cached.getResponseHeaderValue("Date"));
710         assertEquals(updatedEtag, cached.getResponseHeaderValue(ETAG));
711         assertEquals(updatedLastModified, cached.getResponseHeaderValue(LAST_MODIFIED));
712     }
713 
714     /**
715      * @throws Exception if the test fails
716      */
717     @Test
718     public void testMaxAgeCacheControl() throws Exception {
719         final String html = DOCTYPE_HTML
720             + "<html><head><title>page 1</title>\n"
721             + "<link rel='stylesheet' type='text/css' href='foo.css' />\n"
722             + "</head>\n"
723             + "<body>x</body>\n"
724             + "</html>";
725 
726         final WebClient client = getWebClient();
727 
728         final MockWebConnection connection = new MockWebConnection();
729         client.setWebConnection(connection);
730 
731         final List<NameValuePair> headers = new ArrayList<>();
732         headers.add(new NameValuePair(LAST_MODIFIED, "Tue, 20 Feb 2018 10:00:00 GMT"));
733         headers.add(new NameValuePair(CACHE_CONTROL, "some-other-value, max-age=1"));
734 
735         final URL pageUrl = new URL(URL_FIRST, "page1.html");
736         connection.setResponse(pageUrl, html, 200, "OK", "text/html;charset=ISO-8859-1", headers);
737         connection.setResponse(new URL(URL_FIRST, "foo.css"), "", 200, "OK", MimeType.TEXT_JAVASCRIPT, headers);
738 
739         client.getPage(pageUrl);
740         assertEquals(2, client.getCache().getSize());
741         assertEquals(2, connection.getRequestCount());
742         // resources should be still in cache
743         client.getPage(pageUrl);
744         assertEquals(2, client.getCache().getSize());
745         assertEquals(2, connection.getRequestCount());
746         // wait for max-age seconds + 1 for recache
747         Thread.sleep(2 * 1000);
748         client.getPage(pageUrl);
749         assertEquals(2, client.getCache().getSize());
750         assertEquals(4, connection.getRequestCount());
751 
752         // wait for max-age seconds + 1 for recache
753         Thread.sleep(2 * 1000);
754         client.getCache().clearOutdated();
755         assertEquals(0, client.getCache().getSize());
756         assertEquals(4, connection.getRequestCount());
757     }
758 
759     /**
760      * @throws Exception if the test fails
761      */
762     @Test
763     public void testSMaxageCacheControl() throws Exception {
764         final String html = DOCTYPE_HTML
765             + "<html><head><title>page 1</title>\n"
766             + "<link rel='stylesheet' type='text/css' href='foo.css' />\n"
767             + "</head>\n"
768             + "<body>x</body>\n"
769             + "</html>";
770 
771         final WebClient client = getWebClient();
772 
773         final MockWebConnection connection = new MockWebConnection();
774         client.setWebConnection(connection);
775 
776         final List<NameValuePair> headers = new ArrayList<>();
777         headers.add(new NameValuePair(LAST_MODIFIED, "Tue, 20 Feb 2018 10:00:00 GMT"));
778         headers.add(new NameValuePair(CACHE_CONTROL, "public, s-maxage=1, some-other-value, max-age=10"));
779 
780         final URL pageUrl = new URL(URL_FIRST, "page1.html");
781         connection.setResponse(pageUrl, html, 200, "OK", "text/html;charset=ISO-8859-1", headers);
782         connection.setResponse(new URL(URL_FIRST, "foo.css"), "", 200, "OK", MimeType.TEXT_JAVASCRIPT, headers);
783 
784         client.getPage(pageUrl);
785         assertEquals(2, client.getCache().getSize());
786         assertEquals(2, connection.getRequestCount());
787         // resources should be still in cache
788         client.getPage(pageUrl);
789         assertEquals(2, client.getCache().getSize());
790         assertEquals(2, connection.getRequestCount());
791         // wait for s-maxage seconds + 1 for recache
792         Thread.sleep(2 * 1000);
793         client.getPage(pageUrl);
794         assertEquals(2, client.getCache().getSize());
795         assertEquals(4, connection.getRequestCount());
796 
797         // wait for s-maxage seconds + 1 for recache
798         Thread.sleep(2 * 1000);
799         client.getCache().clearOutdated();
800         assertEquals(0, client.getCache().getSize());
801         assertEquals(4, connection.getRequestCount());
802     }
803 
804     /**
805      * @throws Exception if the test fails
806      */
807     @Test
808     public void testExpiresCacheControl() throws Exception {
809         final String html = DOCTYPE_HTML
810             + "<html><head><title>page 1</title>\n"
811             + "<link rel='stylesheet' type='text/css' href='foo.css' />\n"
812             + "</head>\n"
813             + "<body>x</body>\n"
814             + "</html>";
815 
816         final WebClient client = getWebClient();
817 
818         final MockWebConnection connection = new MockWebConnection();
819         client.setWebConnection(connection);
820 
821         final List<NameValuePair> headers = new ArrayList<>();
822         headers.add(new NameValuePair(LAST_MODIFIED, "Tue, 20 Feb 2018 10:00:00 GMT"));
823         final Date expi = new Date(System.currentTimeMillis() + 2 * 1000 + 10 * DateUtils.MILLIS_PER_MINUTE);
824         headers.add(new NameValuePair(EXPIRES, new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz").format(expi)));
825         headers.add(new NameValuePair(CACHE_CONTROL, "public, some-other-value"));
826 
827         final URL pageUrl = new URL(URL_FIRST, "page1.html");
828         connection.setResponse(pageUrl, html, 200, "OK", "text/html;charset=ISO-8859-1", headers);
829         connection.setResponse(new URL(URL_FIRST, "foo.css"), "", 200, "OK", MimeType.TEXT_JAVASCRIPT, headers);
830 
831         client.getPage(pageUrl);
832         assertEquals(2, client.getCache().getSize());
833         assertEquals(2, connection.getRequestCount());
834         // resources should be still in cache
835         client.getPage(pageUrl);
836         assertEquals(2, client.getCache().getSize());
837         assertEquals(2, connection.getRequestCount());
838         // wait for expires
839         Thread.sleep(2 * 1000);
840         client.getPage(pageUrl);
841         assertEquals(0, client.getCache().getSize());
842         assertEquals(4, connection.getRequestCount());
843     }
844 
845     /**
846      * @throws Exception if the test fails
847      */
848     @Test
849     public void testMaxAgeOverrulesExpiresCacheControl() throws Exception {
850         final String html = DOCTYPE_HTML
851             + "<html><head><title>page 1</title>\n"
852             + "<link rel='stylesheet' type='text/css' href='foo.css' />\n"
853             + "</head>\n"
854             + "<body>x</body>\n"
855             + "</html>";
856 
857         final WebClient client = getWebClient();
858 
859         final MockWebConnection connection = new MockWebConnection();
860         client.setWebConnection(connection);
861 
862         final List<NameValuePair> headers = new ArrayList<>();
863         headers.add(new NameValuePair(LAST_MODIFIED, "Tue, 20 Feb 2018 10:00:00 GMT"));
864         headers.add(new NameValuePair(EXPIRES, "0"));
865         headers.add(new NameValuePair(CACHE_CONTROL, "max-age=20"));
866 
867         final URL pageUrl = new URL(URL_FIRST, "page1.html");
868         connection.setResponse(pageUrl, html, 200, "OK", "text/html;charset=ISO-8859-1", headers);
869         connection.setResponse(new URL(URL_FIRST, "foo.css"), "", 200, "OK", MimeType.TEXT_JAVASCRIPT, headers);
870 
871         client.getPage(pageUrl);
872         assertEquals(2, client.getCache().getSize());
873         assertEquals(2, connection.getRequestCount());
874         // resources should be still in cache
875         client.getPage(pageUrl);
876         assertEquals(2, client.getCache().getSize());
877         assertEquals(2, connection.getRequestCount());
878     }
879 
880     /**
881      * Ensures {@link WebResponse#cleanUp()} is called for overflow deleted entries.
882      * @throws Exception if the test fails
883      */
884     @Test
885     public void cleanUpOverflow() throws Exception {
886         final WebRequest request1 = new WebRequest(URL_FIRST, HttpMethod.GET);
887 
888         final Map<String, String> headers = new HashMap<>();
889         headers.put(HttpHeader.EXPIRES, formatDate(DateUtils.addHours(new Date(), 1)));
890 
891         final WebResponseMock response1 = new WebResponseMock(request1, headers);
892 
893         final WebRequest request2 = new WebRequest(URL_SECOND, HttpMethod.GET);
894         final WebResponseMock response2 = new WebResponseMock(request2, headers);
895 
896         final Cache cache = new Cache();
897         cache.setMaxSize(1);
898         cache.cacheIfPossible(request1, response1, null);
899         assertEquals(0, response1.getCallCount("cleanUp"));
900         assertEquals(0, response2.getCallCount("cleanUp"));
901         assertEquals(6, response1.getCallCount("getResponseHeaderValue"));
902         assertEquals(0, response2.getCallCount("getResponseHeaderValue"));
903 
904         Thread.sleep(10);
905         cache.cacheIfPossible(request2, response2, null);
906         assertEquals(1, response1.getCallCount("cleanUp"));
907         assertEquals(0, response2.getCallCount("cleanUp"));
908         assertEquals(6, response1.getCallCount("getResponseHeaderValue"));
909         assertEquals(6, response2.getCallCount("getResponseHeaderValue"));
910     }
911 
912     /**
913      * Ensures {@link WebResponse#cleanUp()} is called on calling {@link Cache#clear()}.
914      */
915     @Test
916     public void cleanUpOnClear() {
917         final WebRequest request1 = new WebRequest(URL_FIRST, HttpMethod.GET);
918 
919         final Map<String, String> headers = new HashMap<>();
920         headers.put(HttpHeader.EXPIRES, formatDate(DateUtils.addHours(new Date(), 1)));
921 
922         final WebResponseMock response1 = new WebResponseMock(request1, headers);
923 
924         final Cache cache = new Cache();
925         cache.cacheIfPossible(request1, response1, null);
926         assertEquals(0, response1.getCallCount("cleanUp"));
927         assertEquals(6, response1.getCallCount("getResponseHeaderValue"));
928 
929         cache.clear();
930 
931         assertEquals(1, response1.getCallCount("cleanUp"));
932         assertEquals(6, response1.getCallCount("getResponseHeaderValue"));
933     }
934 }