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