1   /**
2    *  Copyright 2003-2007 Greg Luck
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    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *  Unless required by applicable law or agreed to in writing, software
11   *  distributed under the License is distributed on an "AS IS" BASIS,
12   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *  See the License for the specific language governing permissions and
14   *  limitations under the License.
15   */
16  
17  package net.sf.ehcache.constructs.web.filter;
18  
19  import com.meterware.httpunit.HttpInternalErrorException;
20  import com.meterware.httpunit.WebResponse;
21  import junit.framework.AssertionFailedError;
22  import net.sf.ehcache.constructs.web.AbstractWebTest;
23  import net.sf.ehcache.constructs.web.PageInfo;
24  import org.apache.commons.httpclient.Header;
25  import org.apache.commons.httpclient.HttpClient;
26  import org.apache.commons.httpclient.HttpMethod;
27  import org.apache.commons.httpclient.methods.GetMethod;
28  import org.xml.sax.SAXException;
29  
30  import java.io.IOException;
31  import java.net.HttpURLConnection;
32  import java.util.ArrayList;
33  import java.util.List;
34  
35  /**
36   * @author <a href="mailto:gluck@thoughtworks.com">Greg Luck</a>
37   * @version $Id: CachingFilterTest.java 512 2007-07-10 09:18:45Z gregluck $
38   */
39  public class CachingFilterTest extends AbstractWebTest {
40  
41      private String cachedPageUrl = "/CachedPage.jsp";
42  
43      private void resetCachedPageUrl() {
44          cachedPageUrl = "/CachedPage.jsp";
45      }
46  
47  
48      /**
49       * Tests that a page which should be cached is cached.
50       * Also, check that the pages returned are good.
51       */
52      public void testCachedPageIsCached() throws Exception {
53          assertResponseGoodAndCached(cachedPageUrl, true);
54      }
55  
56      /**
57       * Tests whether the page is gzipped using the rawer HttpClient library.
58       * Lets us check that the responseBody is really gzipped.
59       */
60      public void testCachedPageIsGzippedWhenEncodingHeaderSet() throws IOException {
61          HttpClient httpClient = new HttpClient();
62          HttpMethod httpMethod = new GetMethod(buildUrl(cachedPageUrl));
63          httpMethod.setRequestHeader(new Header("Accept-encoding", "gzip"));
64          httpClient.executeMethod(httpMethod);
65          byte[] responseBody = httpMethod.getResponseBody();
66          assertTrue(PageInfo.isGzipped(responseBody));
67      }
68  
69      /**
70       * Tests whether the page is gzipped using the rawer HttpClient library.
71       * Lets us check that the responseBody is really not gzipped.
72       */
73      public void testCachedPageIsNotGzippedWhenEncodingHeaderNotSet() throws IOException {
74          HttpClient httpClient = new HttpClient();
75          HttpMethod httpMethod = new GetMethod(buildUrl(cachedPageUrl));
76          //httpMethod.setRequestHeader(new Header("Accept-encoding", "gzip"));
77          httpClient.executeMethod(httpMethod);
78          byte[] responseBody = httpMethod.getResponseBody();
79          assertFalse(PageInfo.isGzipped(responseBody));
80      }
81  
82      /**
83       * Checks sequence
84       *
85       * @throws IOException
86       */
87      public void testSequenceNonGzipThenGzipThenNonGzip() throws IOException {
88          testCachedPageIsNotGzippedWhenEncodingHeaderNotSet();
89          testCachedPageIsGzippedWhenEncodingHeaderSet();
90          testCachedPageIsNotGzippedWhenEncodingHeaderNotSet();
91      }
92  
93      /**
94       * Checks sequence
95       *
96       * @throws IOException
97       */
98      public void testSequenceGzipThenNonGzipThenGzip() throws IOException {
99          testCachedPageIsGzippedWhenEncodingHeaderSet();
100         testCachedPageIsNotGzippedWhenEncodingHeaderNotSet();
101         testCachedPageIsGzippedWhenEncodingHeaderSet();
102     }
103 
104 
105     /**
106      * The reentry check instruments the thread name to indicate it has entered a CachingFilter.
107      * The name is reset at the end. Check that this works for concurrent situations.
108      */
109     public void testCachedPageConcurrent() throws Exception {
110 
111         final List executables = new ArrayList();
112         for (int i = 0; i < 40; i++) {
113             final AbstractWebTest.Executable executable = new AbstractWebTest.Executable() {
114                 public void execute() throws Exception {
115                     testCachedPageIsCached();
116                 }
117             };
118             executables.add(executable);
119         }
120         runThreads(executables);
121     }
122 
123 
124     /**
125      * Tests that a cached full page can be included in another page which calls it with
126      * a jsp:include
127      * <p/>
128      * Fails when client request gzip
129      * wget -S  --header='Accept-encoding: gzip' http://localhost:9080/legaldispatchtocachedpage/Include.jsp
130      * --15:01:49--  http://localhost:9080/legaldispatchtocachedpage/Include.jsp
131      * => `Include.jsp.9'
132      * Resolving localhost... done.
133      * Connecting to localhost[127.0.0.1]:9080... connected.
134      * HTTP request sent, awaiting response...
135      * 1 HTTP/1.1 200 OK
136      * 2 Date: Mon, 22 Nov 2004 05:01:48 GMT
137      * 3 Server: Orion/2.0.3
138      * 4 Set-Cookie: JSESSIONID=EFEIDKLICCGB; Path=/
139      * 5 Cache-Control: private
140      * 6 Connection: Close
141      * 7 Content-Type: text/html
142      * <p/>
143      * [ <=>                                                                                                                                             ] 734          716.80K/s
144      * <p/>
145      * 15:01:49 (716.80 KB/s) - `Include.jsp.9' saved [734]
146      *
147      * @throws Exception
148      */
149     public void testFromJSPInclude() throws Exception {
150         try {
151             WebResponse response = getResponseFromAcceptGzipRequest("/legaldispatchtocachedpage/Include.jsp");
152             assertPropertlyFormed(response);
153             fail();
154         } catch (AssertionFailedError e) {
155             //noop Page is actually an error message in Orion
156         } catch (HttpInternalErrorException e) {
157             //noop 500 error in Tomcat. The log shows ResponseHeadersNotModifiableException when we try to set Gzip
158             //because it is already gzipped.
159         }
160 
161         //No point doing the rest of the tests because the page is always blank
162     }
163 
164     /**
165      * Tests that a cached full page can be included in another page which calls it with
166      * a jsp:include, where the client does not accept gzip encoding.
167      *
168      * @throws Exception
169      */
170     public void testFromJSPIncludeNoGzip() throws Exception {
171         try {
172             WebResponse response = getResponseFromNonAcceptGzipRequest("/legaldispatchtocachedpage/Include.jsp");
173             assertPropertlyFormed(response);
174             fail();
175         } catch (AssertionFailedError e) {
176             //noop Page is actually an error message
177         } catch (HttpInternalErrorException e) {
178             //noop 500 error in Tomcat. The log shows ResponseHeadersNotModifiableException when we try to set Gzip
179             //because it is already gzipped.
180         }
181 
182         //No point doing the rest of the tests because the page is always blank
183     }
184 
185     /**
186      * Tests that a cached full page included using the RequestDispatcher in a servlet
187      * throws a Server 500 error. In the server log a ResponseHeadersNotModifiableException
188      * with the message "Failure when attempting to set Content-Encoding: gzip" will be thrown.
189      * <p/>
190      * If we did not throw an error the client would get a strange result:
191      * <pre>
192      * wget -S  --header='Accept-encoding: gzip' http://localhost:9080/servletdispatchtocachedpage/IncludeCachedPageServlet
193      * --20:43:29--  http://localhost:9080/servletdispatchtocachedpage/IncludeCachedPageServlet
194      * => `IncludeCachedPageServlet.2'
195      * Resolving localhost... done.
196      * Connecting to localhost[127.0.0.1]:9080... connected.
197      * HTTP request sent, awaiting response...
198      * 1 HTTP/1.1 200 OK
199      * 2 Date: Mon, 22 Nov 2004 10:43:28 GMT
200      * 3 Server: Orion/2.0.3
201      * 4 Connection: Close
202      * 5 Content-Type: text/plain
203      * </pre>
204      * Though gzip, the header is not set. The content-type is text/plain, because in our case the
205      * test servlet did not set a type.
206      *
207      * @see {@link net.sf.ehcache.constructs.web.ResponseHeadersNotModifiableException}
208      */
209     public void testFromServletInclude() {
210         String url = "/servletdispatchtocachedpage/IncludeCachedPageServlet";
211         try {
212             getResponseFromAcceptGzipRequest(url);
213             fail();
214         } catch (HttpInternalErrorException e) {
215             //this is what should happen.
216         } catch (IOException e) {
217             fail();
218         } catch (SAXException e) {
219             fail();
220         }
221 
222     }
223 
224     /**
225      * Tests that a cached full page can be forwarded to using the RequestDispatcher in a servlet.
226      *
227      * @throws Exception
228      */
229     public void testFromServletForward() throws Exception {
230         String url = "/servletdispatchtocachedpage/ForwardToCachedPageServlet";
231         WebResponse response = getResponseFromAcceptGzipRequest(url);
232         assertResponseOk(response);
233         assertHeadersSane(response);
234         assertPageNotBlank(response);
235         assertPropertlyFormed(response);
236 
237         //runStandardTestsOnUrl(url);
238     }
239 
240     /**
241      * This sequence returns a valid page with 0 bytes!
242      * This will be seen as an empty page in the browser.
243      * wget response looks like:
244      * wget -S  --header='Accept-encoding: gzip' http://localhost:9080//blankpageproblem/Include.jsp
245      * --10:13:00--  http://localhost:9080/legaldispatchtocachedpage/Include.jsp
246      * => `Include.jsp.2'
247      * Resolving localhost... done.
248      * Connecting to localhost[127.0.0.1]:9080... connected.
249      * HTTP request sent, awaiting response...
250      * 1 HTTP/1.1 200 OK
251      * 2 Date: Mon, 22 Nov 2004 00:13:00 GMT
252      * 3 Server: Orion/2.0.3
253      * 4 Set-Cookie: JSESSIONID=JAPIBBCHOFKO; Path=/
254      * 5 Cache-Control: private
255      * 6 Connection: Close
256      * 7 Content-Type: text/html
257      * <p/>
258      * [ <=>                                                                                                                                             ] 0             --.--K/s
259      * <p/>
260      * 10:13:03 (0.00 B/s) - `Include.jsp.2' saved [0]
261      * <p/>
262      * The problem is a filter inplementation which does not write the response wrapper content
263      * when it detects a response already committed so nothing is writen by the filter.
264      *
265      * @throws Exception
266      */
267     public void testBlankPageProblemFilterFromJSPInclude() throws Exception {
268         WebResponse response = getResponseFromAcceptGzipRequest("/blankpageproblem/Include.jsp");
269         assertResponseOk(response);
270         assertHeadersSane(response);
271         try {
272             assertPageNotBlank(response);
273             fail();
274         } catch (AssertionFailedError e) {
275             //noop Page is blank. Should fail.
276         }
277 
278         //No point doing the rest of the tests because the page is always blank
279     }
280 
281 
282     /**
283      * Tests including a page cached with SimplePageCachingFilter, after setting Filter.noFilter(true)
284      * This works. It does not produce a blank page.
285      * wget response looks like:
286      * wget -S  --header='Accept-encoding: gzip' http://localhost:9080/legaldispatchtocachedpage/IncludeWithNoFilter.jsp
287      * --13:19:12--  http://localhost:9080/legaldispatchtocachedpage/IncludeWithNoFilter.jsp
288      * => `IncludeWithNoFilter.jsp'
289      * Resolving localhost... done.
290      * Connecting to localhost[127.0.0.1]:9080... connected.
291      * HTTP request sent, awaiting response...
292      * 1 HTTP/1.1 200 OK
293      * 2 Date: Mon, 22 Nov 2004 03:19:12 GMT
294      * 3 Server: Orion/2.0.3
295      * 4 Set-Cookie: JSESSIONID=LCIFMEDBEJKP; Path=/
296      * 5 Cache-Control: private
297      * 6 Connection: Close
298      * 7 Content-Type: text/html
299      * <p/>
300      * [ <=>                                                                                                                                             ] 1,846          1.76M/s
301      * <p/>
302      * 13:19:12 (1.76 MB/s) - `IncludeWithNoFilter.jsp' saved [1846]
303      * The problem is that the response is already committed so nothing is writen by the filter.
304      *
305      * @throws Exception
306      */
307     public void testFromJSPIncludeWithNoFilter() throws Exception {
308         WebResponse response = getResponseFromAcceptGzipRequest("/legaldispatchtocachedpage/IncludeWithNoFilter.jsp");
309         assertResponseOk(response);
310         assertHeadersSane(response);
311         assertPageNotBlank(response);
312         //Delay because of Windows timer inaccuracy
313         Thread.sleep(20);
314         assertResponseGoodAndNotCached("/legaldispatchtocachedpage/IncludeWithNoFilter.jsp", true);
315 
316         //No point doing the rest of the tests because the page is not cached
317     }
318 
319     /**
320      * This test calls a page which forwards to the cached page using jsp:forward.
321      * <p/>
322      * This works fine.
323      * <p/>
324      * wget response looks like:
325      * wget -S  --header='Accept-encoding: gzip' http://localhost:9080/legaldispatchtocachedpage/Forward.jsp
326      * --10:17:43--  http://localhost:9080/legaldispatchtocachedpage/Forward.jsp
327      * => `Forward.jsp'
328      * Resolving localhost... done.
329      * Connecting to localhost[127.0.0.1]:9080... connected.
330      * HTTP request sent, awaiting response...
331      * 1 HTTP/1.1 200 OK
332      * 2 Date: Mon, 22 Nov 2004 00:17:56 GMT
333      * 3 Server: Orion/2.0.3
334      * 4 Content-Location: http://localhost:9080/CachedPage.jsp
335      * 5 Content-Length: 735
336      * 6 Set-Cookie: JSESSIONID=ICDJCBCHOFKO; Path=/
337      * 7 Cache-Control: private
338      * 8 Connection: Close
339      * 9 Content-Type: text/html
340      * 10 Content-Encoding: gzip
341      * <p/>
342      * 10:17:56 (717.77 KB/s) - `Forward.jsp' saved [735/735]
343      *
344      * @throws Exception
345      */
346     public void testFromJSPForward() throws Exception {
347         WebResponse response = getResponseFromAcceptGzipRequest("/legaldispatchtocachedpage/Forward.jsp");
348         assertResponseGood(response, true);
349         runStandardTestsOnUrl("/legaldispatchtocachedpage/Forward.jsp");
350     }
351 
352     /**
353      * This test calls a page which forwards to the cached page using jsp:forward.
354      * <p/>
355      * This works fine.
356      * <p/>
357      * wget response looks like:
358      * wget -S  --header='Accept-encoding: gzip' http://localhost:9080/legaldispatchtocachedpage/Forward.jsp
359      * --10:17:43--  http://localhost:9080/legaldispatchtocachedpage/Forward.jsp
360      * => `Forward.jsp'
361      * Resolving localhost... done.
362      * Connecting to localhost[127.0.0.1]:9080... connected.
363      * HTTP request sent, awaiting response...
364      * 1 HTTP/1.1 200 OK
365      * 2 Date: Mon, 22 Nov 2004 00:17:56 GMT
366      * 3 Server: Orion/2.0.3
367      * 4 Content-Location: http://localhost:9080/CachedPage.jsp
368      * 5 Content-Length: 735
369      * 6 Set-Cookie: JSESSIONID=ICDJCBCHOFKO; Path=/
370      * 7 Cache-Control: private
371      * 8 Connection: Close
372      * 9 Content-Type: text/html
373      * 10 Content-Encoding: gzip
374      * <p/>
375      * 10:17:56 (717.77 KB/s) - `Forward.jsp' saved [735/735]
376      *
377      * @throws Exception
378      */
379     public void testCallCachedPageFromRedirect() throws Exception {
380         WebResponse response = getResponseFromAcceptGzipRequest("/legaldispatchtocachedpage/Redirect.jsp");
381         assertResponseGood(response, true);
382 
383         runStandardTestsOnUrl("/legaldispatchtocachedpage/Redirect.jsp");
384     }
385 
386 
387     /**
388      * @throws Exception Always reset the URL so we don't get dependencies
389      */
390     protected void tearDown() throws Exception {
391         resetCachedPageUrl();
392     }
393 
394     /**
395      * Some standard tests that should work.
396      *
397      * @param url
398      * @throws Exception
399      */
400     private void runStandardTestsOnUrl(String url) throws Exception {
401         cachedPageUrl = url;
402         testCachedPageIsCached();
403 
404         cachedPageUrl = url;
405         testCachedPageIsGzippedWhenEncodingHeaderSet();
406 
407         cachedPageUrl = url;
408         testCachedPageIsNotGzippedWhenEncodingHeaderNotSet();
409 
410         cachedPageUrl = url;
411         testSequenceGzipThenNonGzipThenGzip();
412 
413         cachedPageUrl = url;
414         testSequenceNonGzipThenGzipThenNonGzip();
415 
416         cachedPageUrl = url;
417         testCachedPageConcurrent();
418     }
419 
420 
421     /**
422      * A 0 length body should give a 0 length nongzipped body and content length
423      * Manual Test: wget -d --server-response --timestamping --header='If-modified-Since: Fri, 13 May 3006 23:54:18 GMT' --header='Accept-Encoding: gzip' http://localhost:9080/empty_caching_filter/empty.html
424      */
425     public void testIfModifiedZeroLengthHTML() throws Exception {
426 
427         String url = "http://localhost:9080/empty_caching_filter/empty.html";
428         HttpClient httpClient = new HttpClient();
429         HttpMethod httpMethod = new GetMethod(url);
430         httpMethod.addRequestHeader("If-modified-Since", "Fri, 13 May 3006 23:54:18 GMT");
431         httpMethod.addRequestHeader("Accept-Encoding", "gzip");
432         int responseCode = httpClient.executeMethod(httpMethod);
433         assertTrue(HttpURLConnection.HTTP_OK == responseCode || HttpURLConnection.HTTP_NOT_MODIFIED == responseCode);
434         String responseBody = httpMethod.getResponseBodyAsString();
435         assertTrue("".equals(responseBody) || null == responseBody);
436         checkNullOrZeroContentLength(httpMethod);
437     }
438 
439     /**
440      * Servlets and JSPs can send content even when the response is set to no content.
441      * In this case there should not be a body but there is. Orion seems to kill the body
442      * after is has left the Servlet filter chain. To avoid wget going into an inifinite
443      * retry loop, and presumably some other web clients, the content length should be 0
444      * and the body 0.
445      * <p/>
446      * wget -d --server-response --timestamping --header='If-modified-Since: Fri, 13 May 3006 23:54:18 GMT' --header='Accept-Encoding: gzip' http://localhost:9080/empty_caching_filter/SC_NOT_MODIFIED.jsp
447      */
448     public void testNotModifiedJSPGzipFilter() throws Exception {
449 
450         String url = "http://localhost:9080/empty_caching_filter/SC_NOT_MODIFIED.jsp";
451         HttpClient httpClient = new HttpClient();
452         HttpMethod httpMethod = new GetMethod(url);
453         httpMethod.addRequestHeader("If-modified-Since", "Fri, 13 May 3006 23:54:18 GMT");
454         httpMethod.addRequestHeader("Accept-Encoding", "gzip");
455         int responseCode = httpClient.executeMethod(httpMethod);
456         assertEquals(HttpURLConnection.HTTP_NOT_MODIFIED, responseCode);
457         String responseBody = httpMethod.getResponseBodyAsString();
458         assertEquals(null, responseBody);
459         assertNull(httpMethod.getResponseHeader("Content-Encoding"));
460         assertNotNull(httpMethod.getResponseHeader("Last-Modified").getValue());
461         checkNullOrZeroContentLength(httpMethod);
462 
463     }
464 
465     /**
466      * Servlets and JSPs can send content even when the response is set to no content.
467      * In this case there should not be a body but there is. Orion seems to kill the body
468      * after is has left the Servlet filter chain. To avoid wget going into an inifinite
469      * retry loop, and presumably some other web clients, the content length should be 0
470      * and the body 0.
471      * <p/>
472      * Manual Test: wget -d --server-response --timestamping --header='If-modified-Since: Fri, 13 May 3006 23:54:18 GMT' --header='Accept-Encoding: gzip' http://localhost:9080/empty_caching_filter/SC_NO_CONTENT.jsp
473      */
474     public void testNoContentJSPGzipFilter() throws Exception {
475 
476         String url = "http://localhost:9080/empty_caching_filter/SC_NO_CONTENT.jsp";
477         HttpClient httpClient = new HttpClient();
478         HttpMethod httpMethod = new GetMethod(url);
479         httpMethod.addRequestHeader("If-modified-Since", "Fri, 13 May 3006 23:54:18 GMT");
480         httpMethod.addRequestHeader("Accept-Encoding", "gzip");
481         int responseCode = httpClient.executeMethod(httpMethod);
482         assertEquals(HttpURLConnection.HTTP_NO_CONTENT, responseCode);
483         String responseBody = httpMethod.getResponseBodyAsString();
484         assertEquals(null, responseBody);
485         assertNull(httpMethod.getResponseHeader("Content-Encoding"));
486         assertNotNull(httpMethod.getResponseHeader("Last-Modified").getValue());
487         checkNullOrZeroContentLength(httpMethod);
488 
489     }
490 
491     /**
492      * When the servlet container generates a 404 page not found, we want to pass
493      * it through without caching and without adding anything to it.
494      * <p/>
495      * Manual Test: wget -d --server-response --header='Accept-Encoding: gzip'  http://localhost:9080/non_ok/PageNotFound.jsp
496      */
497     public void testNotFound() throws Exception {
498 
499         String url = "http://localhost:9080/non_ok/PageNotFound.jsp";
500         HttpClient httpClient = new HttpClient();
501         HttpMethod httpMethod = new GetMethod(url);
502         httpMethod.addRequestHeader("If-modified-Since", "Fri, 13 May 3006 23:54:18 GMT");
503         httpMethod.addRequestHeader("Accept-Encoding", "gzip");
504         int responseCode = httpClient.executeMethod(httpMethod);
505         assertEquals(HttpURLConnection.HTTP_NOT_FOUND, responseCode);
506         String responseBody = httpMethod.getResponseBodyAsString();
507         assertNotNull(responseBody);
508         assertNull(httpMethod.getResponseHeader("Content-Encoding"));
509     }
510 
511     /**
512      * When the servlet container generates a 404 page not found, we want to pass
513      * it through without caching and without adding anything to it.
514      * <p/>
515      * Manual Test: wget -d --server-response --header='Accept-Encoding: gzip'  http://localhost:9080/non_ok/SendRedirect.jsp
516      */
517     public void testRedirect() throws Exception {
518 
519         String url = "http://localhost:9080/non_ok/SendRedirect.jsp";
520         HttpClient httpClient = new HttpClient();
521         HttpMethod httpMethod = new GetMethod(url);
522         httpMethod.addRequestHeader("Accept-Encoding", "gzip");
523         int responseCode = httpClient.executeMethod(httpMethod);
524         //httpclient follows redirects, so gets the home page.
525         assertEquals(HttpURLConnection.HTTP_OK, responseCode);
526         String responseBody = httpMethod.getResponseBodyAsString();
527         assertNotNull(responseBody);
528         assertNull(httpMethod.getResponseHeader("Content-Encoding"));
529     }
530 
531 
532 }