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 }