View Javadoc

1   /* This software is published under the terms of the OpenSymphony Software
2    * License version 1.1, of which a copy has been included with this
3    * distribution in the LICENSE.txt file. */
4   package com.atlassian.gzipfilter;
5   
6   import java.io.IOException;
7   import java.io.PrintWriter;
8   
9   import javax.servlet.ServletOutputStream;
10  import javax.servlet.ServletResponse;
11  import javax.servlet.http.HttpServletResponse;
12  import javax.servlet.http.HttpServletResponseWrapper;
13  
14  import com.atlassian.gzipfilter.selector.GzipCompatibilitySelector;
15  import com.atlassian.gzipfilter.util.HttpContentType;
16  
17  /**
18   * Implementation of HttpServletResponseWrapper that captures page data instead of
19   * sending to the writer.
20   * <p/>
21   * <p>Should be used in filter-chains or when forwarding/including pages
22   * using a RequestDispatcher.</p>
23   *
24   * @author <a href="mailto:joe@truemesh.com">Joe Walnes</a>
25   * @author <a href="mailto:scott@atlassian.com">Scott Farquhar</a>
26   */
27  public class SelectingResponseWrapper extends HttpServletResponseWrapper
28  {
29  
30      private final RoutablePrintWriter routablePrintWriter;
31      private final RoutableServletOutputStream routableServletOutputStream;
32      private final GzipCompatibilitySelector compatibilitySelector;
33  
34      private final GzipResponseWrapper wrappedResponse;
35  
36      private boolean gzippablePage = false;
37  
38      public SelectingResponseWrapper(final HttpServletResponse unWrappedResponse, GzipCompatibilitySelector compatibilitySelector, String defaultEncoding)
39      {
40          super(unWrappedResponse);
41          this.wrappedResponse = new GzipResponseWrapper(unWrappedResponse, defaultEncoding);
42          this.compatibilitySelector = compatibilitySelector;
43  
44          routablePrintWriter = new RoutablePrintWriter(new RoutablePrintWriterDestinationFactory(unWrappedResponse));
45          routableServletOutputStream = new RoutableServletOutputStream(new RoutableServletOutputStreamDestinationFactory(unWrappedResponse));
46      }
47  
48      public void setContentType(String type)
49      {
50          super.setContentType(type);
51  
52          if (type != null)
53          {
54              final HttpContentType httpContentType = new HttpContentType(type);
55              if (compatibilitySelector.shouldGzip(httpContentType.getType()))
56              {
57                  activateGzip(httpContentType.getEncoding());
58              }
59              else
60              {
61                  deactivateGzip();
62              }
63          }
64      }
65  
66      /**
67       * If a redirect is set, cancel the gzip header.  The JWebunit test http client fails when it tries to
68       * unzip an empty body from a redirect and other clients might also fail.  Since redirects are unlikely to have
69       * much of a body anyway, it makes sense not to gzip them.
70       */
71      public void sendRedirect(String location) throws IOException
72      {
73          if (!wrappedResponse.isCommitted() && gzippablePage)
74              deactivateGzip();
75          super.sendRedirect(location);
76      }
77  
78      /**
79       * <p>In addition to its normal behaviour this will also deactivate gzip
80       * encoding for those responses that MUST NOT have a body.</p>
81       * <p>This is done according to <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html">RFC 2616, section 10</a></p> 
82       * 
83       * @see HttpServletResponseWrapper#setStatus(int, String)
84       * @see #setStatus(int)
85       */
86      public void setStatus(int statusCode, String sm)
87      {
88          super.setStatus(statusCode, sm);
89          if (!shouldGzip(statusCode))
90          {
91              deactivateGzip();
92          }
93      }
94  
95      /**
96       * <p>In addition to its normal behaviour this will also deactivate gzip
97       * encoding for those responses that MUST NOT have a body.</p>
98       * <p>This is done according to <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html">RFC 2616, section 10</a></p> 
99       * 
100      * @see #setStatus(int, String)
101      * @see HttpServletResponseWrapper#setStatus(int)
102      */
103     public void setStatus(int statusCode)
104     {
105         super.setStatus(statusCode);
106         if (!shouldGzip(statusCode))
107         {
108             deactivateGzip();
109         }
110     }
111     
112     /**
113      * Defines whether or not response with the given code should be gzipped.
114      * @param statusCode the response status code.
115      * @return <code>true</code> if the status code is good to be gzipped, <code>false</code> otherwise.
116      */
117     private boolean shouldGzip(int statusCode)
118     {
119         return statusCode != SC_NO_CONTENT && statusCode != SC_NOT_MODIFIED;
120     }
121 
122     private void activateGzip(String encoding)
123     {
124         if (gzippablePage)
125         {
126             return; // already activated
127         }
128         if (encoding != null)
129             wrappedResponse.setEncoding(encoding);
130 
131         if (!wrappedResponse.isCommitted())
132         {
133                 wrappedResponse.setHeader("Content-Encoding", "gzip");
134                 // Technically, we should add a 'vary' header here, as we are sending a different response
135                 // based on both the user-agent and the accept encoding.  This is so that proxies can
136                 // cache multiple versions of a page, and send the right one back to clients.
137                 // We should add 'Vary: user-agent, accept-encoding', but we can't.
138                 //
139                 // References:
140                 //    http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
141                 //    http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.44
142                 //    http://www.port80software.com/200ok/archive/2005/01/21/272.aspx
143                 //
144                 //
145                 // However, there is a *huge* bug in all versions of Internet Explorer, such that if a vary header
146                 // is set, it will refuse to cache the page (even on disk!).  The only Vary header that
147                 // internet explorer accepts is 'Vary: user-agent'.
148                 //
149                 // Reference:
150                 //    http://lists.over.net/pipermail/mod_gzip/2002-December/006826.html
151                 //    http://mail-archives.apache.org/mod_mbox/httpd-dev/200511.mbox/%3C198.4b088455.30a1c19b@aol.com%3E
152                 //    http://httpd.apache.org/docs/2.0/misc/known_client_problems.html
153                 //    http://support.microsoft.com/kb/824847/en-us?spid=8722&sid=global
154                 
155 
156                 wrappedResponse.setHeader("Vary", "User-Agent");
157 //                wrappedResponse.addHeader("Vary", "Accept-Encoding"); - Can't do due to internet explorer bugs
158 
159                 // Another way to avoid this broken-ness is to add a cache-control header.
160                 // set elsewhere, so we dno'
161                 // So - instead, to avoid this brokenness, let's just ensure that this content isn't
162                 // cached by any shared caches, and add a 'Cache-control: private' header
163                 // Reference:
164                 //    http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.1
165                 //
166                 // However - this may conflict with other cache-control headers we may have, so let's not do it
167                 // See Scott and Anton for long discussion.
168 
169 //                wrappedResponse.addHeader("Cache-control", "private");
170         }
171 
172         routablePrintWriter.updateDestination(new RoutablePrintWriterDestinationFactory(wrappedResponse));
173         routableServletOutputStream.updateDestination(new RoutableServletOutputStreamDestinationFactory(wrappedResponse));
174         gzippablePage = true;
175     }
176 
177     private void deactivateGzip()
178     {
179         gzippablePage = false;
180         
181         if (!wrappedResponse.isCommitted())
182         {
183             //need to set to empty string as setting this to null doesn't work on Resin
184             wrappedResponse.setHeader("Content-Encoding", "");
185             wrappedResponse.setHeader("Vary", "");
186 
187         }
188 
189         routablePrintWriter.updateDestination(new RoutablePrintWriterDestinationFactory(getResponse()));
190         routableServletOutputStream.updateDestination(new RoutableServletOutputStreamDestinationFactory(getResponse()));
191     }
192 
193     /**
194      * Prevent content-length being set if page is parseable.
195      */
196     public void setContentLength(int contentLength)
197     {
198         if (!gzippablePage) super.setContentLength(contentLength);
199     }
200 
201     /**
202      * Prevent buffer from being flushed if this is a page being parsed.
203      */
204     public void flushBuffer() throws IOException
205     {
206         if (!gzippablePage) super.flushBuffer();
207     }
208 
209     /**
210      * Prevent content-length being set if page is parseable.
211      */
212     public void setHeader(String name, String value)
213     {
214         if (name.toLowerCase().equals("content-type"))
215         { // ensure ContentType is always set through setContentType()
216             setContentType(value);
217         }
218         else if (!gzippablePage || !name.toLowerCase().equals("content-length"))
219         {
220             super.setHeader(name, value);
221         }
222     }
223 
224     /**
225      * Prevent content-length being set if page is parseable.
226      */
227     public void addHeader(String name, String value)
228     {
229         if (name.toLowerCase().equals("content-type"))
230         { // ensure ContentType is always set through setContentType()
231             setContentType(value);
232         }
233         else if (!gzippablePage || !name.toLowerCase().equals("content-length"))
234         {
235             super.addHeader(name, value);
236         }
237     }
238 
239     public ServletOutputStream getOutputStream()
240     {
241         return routableServletOutputStream;
242     }
243 
244     public PrintWriter getWriter()
245     {
246         return routablePrintWriter;
247     }
248 
249     public void finishResponse()
250     {
251         if (gzippablePage)
252             wrappedResponse.finishResponse();
253     }
254     
255     private static class RoutablePrintWriterDestinationFactory implements RoutablePrintWriter.DestinationFactory
256     {
257         private final ServletResponse servletResponse;
258         
259         public RoutablePrintWriterDestinationFactory(final ServletResponse servletResponse)
260         {
261             this.servletResponse = servletResponse;
262         }
263         
264         public PrintWriter activateDestination() throws IOException
265         {
266             return servletResponse.getWriter();
267         }
268     }
269     
270     private static class RoutableServletOutputStreamDestinationFactory implements RoutableServletOutputStream.DestinationFactory
271     {
272         private final ServletResponse servletResponse;
273         
274         public RoutableServletOutputStreamDestinationFactory (final ServletResponse servletResponse)
275         {
276             this.servletResponse = servletResponse;
277         }
278         
279         public ServletOutputStream create() throws IOException
280         {
281             return servletResponse.getOutputStream();
282         }
283     }
284 }