001 /* DERIVED FROM APACHE CODE, modifications (c) Damon Hart-Davis 2001-2006, under same terms:
002 *
003 * CompressionFilter.java
004 * $Header: /rw/CVS-repository.s0.l/general/PG2Ksrc/javasrc/webSvr/org/hd/d/pg2k/webSvr/util/CompressionFilter.java,v 1.8 2005/08/18 09:30:46 dhd Exp $
005 * $Revision: 1.8 $
006 * $Date: 2005/08/18 09:30:46 $
007 *
008 * ====================================================================
009 *
010 * The Apache Software License, Version 1.1
011 *
012 * Copyright (c) 1999 The Apache Software Foundation. All rights
013 * reserved.
014 *
015 * Redistribution and use in source and binary forms, with or without
016 * modification, are permitted provided that the following conditions
017 * are met:
018 *
019 * 1. Redistributions of source code must retain the above copyright
020 * notice, this list of conditions and the following disclaimer.
021 *
022 * 2. Redistributions in binary form must reproduce the above copyright
023 * notice, this list of conditions and the following disclaimer in
024 * the documentation and/or other materials provided with the
025 * distribution.
026 *
027 * 3. The end-user documentation included with the redistribution, if
028 * any, must include the following acknowlegement:
029 * "This product includes software developed by the
030 * Apache Software Foundation (http://www.apache.org/)."
031 * Alternately, this acknowlegement may appear in the software itself,
032 * if and wherever such third-party acknowlegements normally appear.
033 *
034 * 4. The names "The Jakarta Project", "Tomcat", and "Apache Software
035 * Foundation" must not be used to endorse or promote products derived
036 * from this software without prior written permission. For written
037 * permission, please contact apache@apache.org.
038 *
039 * 5. Products derived from this software may not be called "Apache"
040 * nor may "Apache" appear in their names without prior written
041 * permission of the Apache Group.
042 *
043 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
044 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
045 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
046 * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
047 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
048 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
049 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
050 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
051 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
052 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
053 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
054 * SUCH DAMAGE.
055 * ====================================================================
056 *
057 * This software consists of voluntary contributions made by many
058 * individuals on behalf of the Apache Software Foundation. For more
059 * information on the Apache Software Foundation, please see
060 * <http://www.apache.org/>.
061 *
062 * [Additional notices, if required by prior licensing conditions]
063 *
064 */
065
066 package org.hd.d.pg2k.webSvr.util;
067
068 import java.io.IOException;
069 import java.util.Enumeration;
070 import java.util.StringTokenizer;
071
072 import javax.servlet.Filter;
073 import javax.servlet.FilterChain;
074 import javax.servlet.FilterConfig;
075 import javax.servlet.ServletContext;
076 import javax.servlet.ServletException;
077 import javax.servlet.ServletRequest;
078 import javax.servlet.ServletResponse;
079 import javax.servlet.http.HttpServletRequest;
080 import javax.servlet.http.HttpServletResponse;
081
082 import org.hd.d.pg2k.svrCore.stats.StatsLogger;
083
084 import ORG.hd.d.IsDebug;
085
086 /**
087 * Implementation of <code>javax.servlet.Filter</code> used to compress
088 * the ServletResponse if it is bigger than a threshold.
089 * <p>
090 * We might need to modify this to suppress caching for HTTP/1.0
091 * clients (eg by setting Pragma: no-cache and Expires: 0).
092 *
093 * @author Amy Roh (original)
094 * @author Damon Hart-Davis
095 * @version $Revision: 1.8 $, $Date: 2005/08/18 09:30:46 $
096 */
097
098 public final class CompressionFilter implements Filter
099 {
100 /**Encoding token indicating that GZIP compression is accepted.
101 * The test should be case-insensitive and work if the
102 * client-supplied token starts with "x-".
103 */
104 static final String GZIP_TOKEN = "gzip";
105
106
107 /**Our logger which falls back to System.out if servlet log not available; never null. */
108 private final WebUtils.ServletLoggerWithFallback logger = new WebUtils.ServletLoggerWithFallback();
109
110 /**The stats set to which we log HTTP gzip-(un)compressed-transport counts.
111 * This is one of three codes for:
112 * <ul>
113 * <li>The compression filter is invoked (may only be available in debug modes).
114 * <li>Not compressable (eg explicitly disabled).
115 * <li>Compressable (including client browser support,
116 * though possibly not compressed if not large enough, etc).
117 * </ul>
118 * <p>
119 * We may enable or disable this logging (etc) from system parameters,
120 * but this is nonetheless our unique identifier.
121 */
122 private final StatsLogger.StatsConfig statsIDCompression =
123 new StatsLogger.StatsConfig("HTTP-GZIP-COMPRESSION",
124 logger, // Dump in general log.
125 false, // Only dump summaries...
126 12 * 3600, // About every 24 hours.
127 true); // Adaptive.
128
129 /**The filter configuration object we are associated with.
130 * If this value is null, this filter instance is not currently configured.
131 */
132 private FilterConfig config;
133
134 public FilterConfig getFilterConfig()
135 {
136 return (config);
137 }
138
139 public void setFilterConfig(final FilterConfig c)
140 {
141 config = c;
142 logger.setContext((c == null) ? null : c.getServletContext());
143 }
144
145 /**The name of the application-level attribute to disable compression if Boolean.FALSE, or null. */
146 private String overrideAttribute;
147
148 /**The threshold result size below which not to try to compress. */
149 protected int compressionThreshold;
150
151 /**Debug level for this filter. */
152 private int debug = 0;
153
154 /**
155 * Place this filter into service.
156 *
157 * @param filterConfig The filter configuration object
158 */
159 public void init(final FilterConfig filterConfig)
160 {
161 config = filterConfig;
162 logger.setContext((filterConfig == null) ? null : filterConfig.getServletContext());
163 if(filterConfig != null)
164 {
165 overrideAttribute = filterConfig.getInitParameter("overrideAttribute");
166
167 final String str = filterConfig.getInitParameter("compressionThreshold");
168 if(str != null)
169 {
170 compressionThreshold = Integer.parseInt(str);
171 }
172 else
173 {
174 compressionThreshold = 0;
175 }
176 final String value = filterConfig.getInitParameter("debug");
177 if(value != null)
178 {
179 debug = Integer.parseInt(value);
180 }
181 else
182 {
183 debug = IsDebug.isDebug ? 1 : 0;
184 }
185 }
186 else
187 {
188 compressionThreshold = 0;
189 }
190
191 }
192
193 /**Take this filter out of service. */
194 public void destroy()
195 {
196 config = null;
197 logger.setContext(null);
198 }
199
200 /**Name of attribute that we use to avoid duplicate application of this filter on one filter chain. */
201 private static final String ANTI_DUP_ATTR_NAME = "org.hd.d.pg2k.webSvr.util.CompressionFilter.DUPFLAG";
202
203 /**Filters the server operation.
204 * The <code>doFilter</code> method of the Filter is called by the container
205 * each time a request/response pair is passed through the chain due
206 * to a client request for a resource at the end of the chain.
207 * The FilterChain passed into this method allows the Filter to pass on the
208 * request and response to the next entity in the chain.
209 * <p>
210 * This method first examines the request to check whether the client supports
211 * compression.
212 * <p>
213 * It passes the request and response unaltered if there is no support for
214 * compression.
215 * <p>
216 * If compression support is available, this filter creates a
217 * CompressionServletResponseWrapper object which compresses the content and
218 * modifies the header if the content length is big enough.
219 * It then invokes the next entity in the chain using the FilterChain object
220 * (<code>chain.doFilter()</code>).
221 **/
222 public void doFilter(final ServletRequest request, final ServletResponse response,
223 final FilterChain chain) throws IOException, ServletException
224 {
225 if(debug > 0)
226 {
227 System.out.println("@doFilter");
228 }
229
230 StatsLogger.captureDataPoint(statsIDCompression, "COMPFILT"); // Note comp filter invokation...
231
232 boolean supportCompression = false;
233
234 // Fake loop to break out of as soon as we determine
235 // that we are *not* going to do compression...
236 do
237 {
238 // Don't compress if not HTTP.
239 if(!(request instanceof HttpServletRequest) ||
240 !(response instanceof HttpServletResponse))
241 {
242 supportCompression = false; // No compression...
243 break;
244 }
245
246 final HttpServletRequest httpServletRequest = ((HttpServletRequest) request);
247 final HttpServletResponse httpServletResponse = ((HttpServletResponse) response);
248
249 // Avoid duplicate application of a compression filter,
250 // eg during servlet redirection.
251 if(null != httpServletRequest.getAttribute(ANTI_DUP_ATTR_NAME))
252 {
253 supportCompression = false; // Avoid duplicate compression...
254 StatsLogger.captureDataPoint(statsIDCompression, "NOCOMPDUP");
255 break;
256 }
257 httpServletRequest.setAttribute(ANTI_DUP_ATTR_NAME, Boolean.TRUE);
258
259 // Don't compress if application-level attribute forbids it.
260 final ServletContext ctxt = config.getServletContext();
261 if((overrideAttribute != null) &&
262 Boolean.FALSE.equals(ctxt.getAttribute(overrideAttribute)))
263 {
264 supportCompression = false; // Compression is explicitly disabled.
265 break;
266 }
267
268 // Don't compress if this output is already encoded in some way.
269 // Amongst other things, this prevents back-to-back applications
270 // of two compression streams which just results in garbage for the user.
271 if(httpServletResponse.containsHeader("Content-Encoding"))
272 {
273 StatsLogger.captureDataPoint(statsIDCompression, "CE-ALREADY-SET"); // No compression possible...
274 if(IsDebug.isDebug || (debug > 1)) { System.err.println("WARNING: CompressionFilter saw Content-Encoding: aborting..."); }
275 supportCompression = false;
276 break;
277 } // Already encoded.
278
279 // We cannot apply any encoding/compression if there is a Content-MD5 header,
280 // since it should be applied/recomputed after this compression,
281 // and the GZIP checksum will probably do to protect the content in transit.
282 if(httpServletResponse.containsHeader("Content-MD5"))
283 {
284 //StatsLogger.captureDataPoint(statsIDCompression, "CE-ALREADY-SET"); // No compression possible...
285 if(IsDebug.isDebug || (debug > 1)) { System.err.println("WARNING: CompressionFilter saw Content-MD5: aborting..."); }
286 supportCompression = false;
287 break;
288 } // Already encoded.
289
290 // Only compress if client can accept gzip encoding.
291 final Enumeration e = httpServletRequest.getHeaders("Accept-Encoding");
292 gzipSearch: while(e.hasMoreElements())
293 {
294 final String name = (String) e.nextElement();
295 if(name.indexOf(GZIP_TOKEN) != -1)
296 {
297 final StringTokenizer st = new StringTokenizer(name, ", ");
298 while(st.hasMoreTokens())
299 {
300 final String tok = st.nextToken();
301 if(tok.equalsIgnoreCase(GZIP_TOKEN) ||
302 tok.equalsIgnoreCase("x-" + GZIP_TOKEN))
303 {
304 if(debug > 0)
305 {
306 System.out.println("supports compression");
307 }
308 supportCompression = true;
309 break gzipSearch;
310 }
311 }
312 }
313 }
314 } while(false);
315
316
317 if(!supportCompression)
318 {
319 if(debug > 0)
320 { System.out.println("doFilter gets called WITHOUT compression"); }
321 chain.doFilter(request, response);
322 StatsLogger.captureDataPoint(statsIDCompression, "NOCOMP"); // No compression possible...
323 return;
324 }
325 else
326 {
327 if(response instanceof HttpServletResponse)
328 {
329 final CompressionServletResponseWrapper wrappedResponse =
330 new CompressionServletResponseWrapper((HttpServletResponse) response);
331 wrappedResponse.setCompressionThreshold(compressionThreshold);
332 if(debug > 0)
333 {
334 System.out.println("doFilter gets called WITH compression");
335 }
336
337 try
338 {
339 chain.doFilter(request, wrappedResponse);
340 }
341 finally
342 {
343 wrappedResponse.finishResponse();
344 }
345 StatsLogger.captureDataPoint(statsIDCompression, "CANCOMP"); // Compression possible...
346 return;
347 }
348 }
349 }
350 }
351