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 com.atlassian.gzipfilter.selector.GzipCompatibilitySelector;
7   import com.atlassian.gzipfilter.util.HttpContentType;
8   import org.slf4j.Logger;
9   import org.slf4j.LoggerFactory;
10  
11  import java.io.IOException;
12  import java.io.PrintWriter;
13  import javax.servlet.ServletOutputStream;
14  import javax.servlet.ServletResponse;
15  import javax.servlet.http.HttpServletResponse;
16  import javax.servlet.http.HttpServletResponseWrapper;
17  
18  /**
19   * Implementation of HttpServletResponseWrapper that captures page data instead of
20   * sending to the writer.
21   * <p/>
22   * <p>Should be used in filter-chains or when forwarding/including pages
23   * using a RequestDispatcher.</p>
24   *
25   * @author <a href="mailto:joe@truemesh.com">Joe Walnes</a>
26   * @author <a href="mailto:scott@atlassian.com">Scott Farquhar</a>
27   */
28  public class SelectingResponseWrapper extends HttpServletResponseWrapper
29  {
30      private static final Logger log = LoggerFactory.getLogger(SelectingResponseWrapper.class);
31  
32      private final RoutablePrintWriter routablePrintWriter;
33      private final RoutableServletOutputStream routableServletOutputStream;
34      private final GzipCompatibilitySelector compatibilitySelector;
35  
36      private final GzipResponseWrapper wrappedResponse;
37  
38      private boolean gzippablePage = false;
39      private boolean headersCommitted = false;
40  
41      public SelectingResponseWrapper(final HttpServletResponse unWrappedResponse, GzipCompatibilitySelector compatibilitySelector, String defaultEncoding)
42      {
43          super(unWrappedResponse);
44          this.wrappedResponse = new GzipResponseWrapper(unWrappedResponse, defaultEncoding);
45          this.compatibilitySelector = compatibilitySelector;
46  
47          Runnable gzipHeadersCommitter = new Runnable()
48          {
49              /**
50               * This callback is our last chance to commit headers before response
51               * is committed by using output stream
52               */
53              public void run()
54              {
55                  commitGzipHeaders();
56              }
57          };
58          routablePrintWriter = new RoutablePrintWriter(
59                  new RoutablePrintWriterDestinationFactory(unWrappedResponse),
60                  gzipHeadersCommitter);
61          routableServletOutputStream = new RoutableServletOutputStream(
62                  new RoutableServletOutputStreamDestinationFactory(unWrappedResponse),
63                  gzipHeadersCommitter);
64      }
65  
66      public void setContentType(String type)
67      {
68          super.setContentType(type);
69  
70          if (type != null)
71          {
72              final HttpContentType httpContentType = new HttpContentType(type);
73              if (compatibilitySelector.shouldGzip(httpContentType.getType()))
74              {
75                  activateGzip(httpContentType.getEncoding());
76              }
77              else
78              {
79                  deactivateGzip();
80              }
81          }
82      }
83  
84      /**
85       * If a redirect is set, cancel the gzip header.  The JWebunit test http client fails when it tries to
86       * unzip an empty body from a redirect and other clients might also fail.  Since redirects are unlikely to have
87       * much of a body anyway, it makes sense not to gzip them.
88       */
89      public void sendRedirect(String location) throws IOException
90      {
91          if (!wrappedResponse.isCommitted() && gzippablePage)
92              deactivateGzip();
93          super.sendRedirect(location);
94      }
95  
96      /**
97       * <p>In addition to its normal behaviour this will also deactivate gzip
98       * encoding for those responses that MUST NOT have a body.</p>
99       * <p>This is done according to <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html">RFC 2616, section 10</a></p>
100      *
101      * @see HttpServletResponseWrapper#setStatus(int, String)
102      * @see #setStatus(int)
103      */
104     public void setStatus(int statusCode, String sm)
105     {
106         super.setStatus(statusCode, sm);
107         if (!shouldGzip(statusCode))
108         {
109             deactivateGzip();
110         }
111     }
112 
113     /**
114      * <p>In addition to its normal behaviour this will also deactivate gzip
115      * encoding for those responses that MUST NOT have a body.</p>
116      * <p>This is done according to <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html">RFC 2616, section 10</a></p>
117      *
118      * @see #setStatus(int, String)
119      * @see HttpServletResponseWrapper#setStatus(int)
120      */
121     public void setStatus(int statusCode)
122     {
123         super.setStatus(statusCode);
124         if (!shouldGzip(statusCode))
125         {
126             deactivateGzip();
127         }
128     }
129 
130     public void sendError(int sc, String msg) throws IOException
131     {
132         if (gzippablePage)
133         {
134             deactivateGzip();
135         }
136 
137         super.sendError(sc, msg);
138     }
139 
140     public void sendError(int sc) throws IOException
141     {
142         if (gzippablePage)
143         {
144             deactivateGzip();
145         }
146 
147         super.sendError(sc);
148     }
149 
150     /**
151      * Defines whether or not response with the given code should be gzipped.
152      * @param statusCode the response status code.
153      * @return <code>true</code> if the status code is good to be gzipped, <code>false</code> otherwise.
154      */
155     private boolean shouldGzip(int statusCode)
156     {
157         return statusCode != SC_NO_CONTENT && statusCode != SC_NOT_MODIFIED;
158     }
159 
160     /**
161      * Sets header for gzipped responses if it is possible
162      */
163     private void commitGzipHeaders()
164     {
165         if (headersCommitted) {
166             return;
167         }
168         if (!gzippablePage) {
169             log.trace("Not a gzippable page");
170             return;
171         }
172         if (wrappedResponse.isCommitted())
173         {
174             log.debug("Response is committed, can't set gzip headers");
175             return;
176         }
177 
178         log.debug("Setting gzip headers");
179         wrappedResponse.setHeader("Content-Encoding", "gzip");
180         // Technically, we should add a 'vary' header here, as we are sending a different response
181         // based on both the user-agent and the accept encoding.  This is so that proxies can
182         // cache multiple versions of a page, and send the right one back to clients.
183         // We should add 'Vary: user-agent, accept-encoding', but we can't.
184         //
185         // References:
186         //    http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6
187         //    http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.44
188         //    http://www.port80software.com/200ok/archive/2005/01/21/272.aspx
189         //
190         //
191         // However, there is a *huge* bug in all versions of Internet Explorer, such that if a vary header
192         // is set, it will refuse to cache the page (even on disk!).  The only Vary header that
193         // internet explorer accepts is 'Vary: user-agent'.
194         //
195         // Reference:
196         //    http://lists.over.net/pipermail/mod_gzip/2002-December/006826.html
197         //    http://mail-archives.apache.org/mod_mbox/httpd-dev/200511.mbox/%3C198.4b088455.30a1c19b@aol.com%3E
198         //    http://httpd.apache.org/docs/2.0/misc/known_client_problems.html
199         //    http://support.microsoft.com/kb/824847/en-us?spid=8722&sid=global
200 
201 
202         wrappedResponse.setHeader("Vary", "User-Agent");
203         //wrappedResponse.addHeader("Vary", "Accept-Encoding"); - Can't do due to internet explorer bugs
204 
205         // Another way to avoid this broken-ness is to add a cache-control header.
206         // set elsewhere, so we dno'
207         // So - instead, to avoid this brokenness, let's just ensure that this content isn't
208         // cached by any shared caches, and add a 'Cache-control: private' header
209         // Reference:
210         //    http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.1
211         //
212         // However - this may conflict with other cache-control headers we may have, so let's not do it
213         // See Scott and Anton for long discussion.
214 
215         //wrappedResponse.addHeader("Cache-control", "private");
216         headersCommitted = true;
217     }
218 
219     private void activateGzip(String encoding)
220     {
221         if (gzippablePage)
222         {
223             return; // already activated
224         }
225 
226         if (wrappedResponse.isCommitted()) {
227             log.debug("Response is committed, gzip can not be activated");
228             return;
229         }
230 
231         if (headersCommitted) {
232             log.debug("Headers are committed, gzip can not be activated");
233             return;
234         }
235 
236         //JRA-35223 - Cannot activate gzip for a response when someone already have set Content-Length header
237         //because this will produce a response with incorrect Content-Length (Content-Length set to size of
238         //uncompressed data, but compressed content which is smaller)
239         if(wrappedResponse.containsHeader("Content-Length"))
240         {
241             log.debug("Gzip compression can not be activated when the Content-Length header has already been set "
242                     + "on the response, and therefore uncompressed content will be sent instead");
243             return;
244         }
245 
246         if (encoding != null) {
247             wrappedResponse.setEncoding(encoding);
248         }
249 
250         routablePrintWriter.updateDestination(new RoutablePrintWriterDestinationFactory(wrappedResponse));
251         routableServletOutputStream.updateDestination(new RoutableServletOutputStreamDestinationFactory(wrappedResponse));
252         gzippablePage = true;
253         log.debug("gzip activated");
254     }
255 
256     private void deactivateGzip()
257     {
258         gzippablePage = false;
259 
260         routablePrintWriter.updateDestination(new RoutablePrintWriterDestinationFactory(getResponse()));
261         routableServletOutputStream.updateDestination(new RoutableServletOutputStreamDestinationFactory(getResponse()));
262         log.debug("gzip deactivated");
263     }
264 
265     /**
266      * Prevent content-length being set if page is parseable.
267      */
268     @Override
269     public void setContentLength(int contentLength)
270     {
271         if (!gzippablePage) { super.setContentLength(contentLength); }
272     }
273 
274     /**
275      * Prevent buffer from being flushed if this is a page being parsed.
276      */
277     @Override
278     public void flushBuffer() throws IOException
279     {
280         if (!gzippablePage)
281         {
282             log.debug("Flushing buffer");
283             super.flushBuffer();
284         }
285     }
286 
287     /**
288      * Prevent content-length being set if page is parseable.
289      */
290     @Override
291     public void setHeader(String name, String value)
292     {
293         if (name.toLowerCase().equals("content-type"))
294         { // ensure ContentType is always set through setContentType()
295             setContentType(value);
296         }
297         else if (!gzippablePage || !name.toLowerCase().equals("content-length"))
298         {
299             super.setHeader(name, value);
300         }
301     }
302 
303     /**
304      * Prevent content-length being set if page is parseable.
305      */
306     @Override
307     public void addHeader(String name, String value)
308     {
309         if (name.toLowerCase().equals("content-type"))
310         { // ensure ContentType is always set through setContentType()
311             setContentType(value);
312         }
313         else if (!gzippablePage || !name.toLowerCase().equals("content-length"))
314         {
315             super.addHeader(name, value);
316         }
317     }
318 
319     @Override
320     public ServletOutputStream getOutputStream()
321     {
322         return routableServletOutputStream;
323     }
324 
325     @Override
326     public PrintWriter getWriter()
327     {
328         return routablePrintWriter;
329     }
330 
331     public void finishResponse()
332     {
333         if (gzippablePage)
334         {
335             commitGzipHeaders();
336             wrappedResponse.finishResponse();
337         }
338     }
339 
340     private static class RoutablePrintWriterDestinationFactory implements RoutablePrintWriter.DestinationFactory
341     {
342         private final ServletResponse servletResponse;
343 
344         public RoutablePrintWriterDestinationFactory(final ServletResponse servletResponse)
345         {
346             this.servletResponse = servletResponse;
347         }
348 
349         public PrintWriter activateDestination() throws IOException
350         {
351             return servletResponse.getWriter();
352         }
353     }
354 
355     private static class RoutableServletOutputStreamDestinationFactory implements RoutableServletOutputStream.DestinationFactory
356     {
357         private final ServletResponse servletResponse;
358 
359         public RoutableServletOutputStreamDestinationFactory (final ServletResponse servletResponse)
360         {
361             this.servletResponse = servletResponse;
362         }
363 
364         public ServletOutputStream create() throws IOException
365         {
366             return servletResponse.getOutputStream();
367         }
368     }
369 }