001    /*
002     * Created by IntelliJ IDEA.
003     * User: d@hd.org
004     * Date: 25-Aug-02
005     * Time: 19:35:35
006    
007    Copyright (c) 1996-2012, Damon Hart-Davis
008    All rights reserved.
009    
010    Redistribution and use in source and binary forms, with or without
011    modification, are permitted provided that the following conditions are
012    met:
013    
014      * Redistributions of source code must retain the above copyright
015        notice, this list of conditions and the following disclaimer.
016    
017      * Redistributions in binary form must reproduce the above copyright
018        notice, this list of conditions and the following disclaimer in the
019        documentation and/or other materials provided with the
020        distribution.
021    
022    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
023    IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
024    TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
025    PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
026    OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
027    SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
028    LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
029    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
030    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
031    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
032    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
033    
034     */
035    package org.hd.d.pg2k.webSvr.util;
036    
037    import java.io.IOException;
038    
039    import javax.servlet.Filter;
040    import javax.servlet.FilterChain;
041    import javax.servlet.FilterConfig;
042    import javax.servlet.ServletException;
043    import javax.servlet.ServletRequest;
044    import javax.servlet.ServletResponse;
045    import javax.servlet.http.HttpServletRequest;
046    import javax.servlet.http.HttpServletResponse;
047    import javax.servlet.http.HttpServletResponseWrapper;
048    
049    import ORG.hd.d.IsDebug;
050    
051    /**Explicitly encourages browser to cache filtered items.
052     * This sets headers that explicitly encourage
053     * the client to cache the filtered item for a long-ish time
054     * (of the order of hours to weeks) on the assumption that
055     * the filtered item changes slowly if at all.
056     * <p>
057     * This should work even when the items are delivered over HTTPS,
058     * which would normally prevent them being cached.
059     * <p>
060     * This should work for HTTP/1.0 clients (we set Expires), and
061     * HTTP/1.1 client (we set Cache-Control).
062     * <p>
063     * This filter is stateless and as lightweight and simple as possible.
064     */
065    public final class ExplicitCacheTimeFilter implements Filter
066        {
067        /**Logger; never null. */
068        private final WebUtils.ServletLoggerWithFallback logger = new WebUtils.ServletLoggerWithFallback();
069    
070        /**Bring filter into service. */
071        public void init(final FilterConfig filterConfig)
072            {
073            logger.setContext(filterConfig.getServletContext());
074            }
075    
076        /**Take this filter out of service. */
077        public void destroy()
078            {
079            logger.setContext(null);
080            }
081    
082        /**Headers that we set to make cacheing work.
083         * We reject attempts to override any of these (and warn on System.err).
084         * <p>
085         * These should be matched case-insensitively.
086         */
087        private static final String DANGER_HEADERS[] =
088            { "Expires", "Pragma", "Cache-Control" };
089    
090        /**Name of attribute that we use to avoid duplicate application of this filter on one filter chain. */
091        private static final String ANTI_DUP_ATTR_NAME = "org.hd.d.pg2k.webSvr.util.ExplicitCacheTimeFilter.DUPFLAG";
092    
093        /**Filter one item.
094         * If none of the cache-controlling headers for HTTP/1.0 or 1.1 are
095         * already set, set them ourselves to a reasonably lengthy time.
096         */
097        public void doFilter(final ServletRequest request,
098                             final ServletResponse response,
099                             final FilterChain chain)
100            throws IOException, ServletException
101            {
102            // Avoid duplicate application of this filter,
103            // eg during servlet redirection.
104            if(null != request.getAttribute(ANTI_DUP_ATTR_NAME))
105                {
106                // Avoid counting the same traffic more than once.
107    if(IsDebug.isDebug) { logger.log("WARNING: ExplicitCacheTimeFilter: avoiding duplicate application to " + request); }
108                chain.doFilter(request, response);
109                return;
110                }
111            request.setAttribute(ANTI_DUP_ATTR_NAME, Boolean.TRUE);
112    
113            // If this is not an HTTP response, do not fiddle with it.
114            if(!(response instanceof HttpServletResponse) ||
115               !(request instanceof HttpServletRequest))
116                {
117                chain.doFilter(request, response);
118                return;
119                }
120    
121            final HttpServletResponse hsr = (HttpServletResponse) response;
122    
123            // If any of the `danger' headers is already set,
124            // indicating that the cache behaviour is already being
125            // explicitly handled/manipulated,
126            // then back out immediately!
127            for(int i = DANGER_HEADERS.length; --i >= 0; )
128                {
129                if(hsr.containsHeader(DANGER_HEADERS[i]))
130                    {
131    logger.log("WARNING: ExplicitCacheTimeFilter: backing off setting cache headers (having found header "+DANGER_HEADERS[i]+") for: " + ((HttpServletRequest) request).getRequestURI());
132                    chain.doFilter(request, response);
133                    return;
134                    }
135                }
136    
137            // Make wrapper to force headers to end up set the way we want...
138            final class Wrapper extends HttpServletResponseWrapper
139                {
140                /**Fixed cache time (ms); strictly positive. */
141                private static final long CACHE_MS = WebConsts.DEFAULT_STATIC_WEBITEMS_CACHE_MS;
142                /**Fixed cache-control header value including 'public' attribute; non-null. */
143                private static final String CACHE_CONTROL_VALUE = "public,max-age="+(CACHE_MS/1000);
144    
145                /**Pass the original response to the base class.
146                 * We also immediately set the values we want set.
147                 * We complain later if there is any attempt to set any of them.
148                 */
149                Wrapper(final HttpServletResponse hsResponse)
150                    {
151                    // Pass in original response...
152                    super(hsResponse);
153    
154    //logger.log("Setting cache headers (for "+CACHE_MS+"ms) for: " + ((HttpServletRequest) request).getRequestURI());
155    
156                    // Add Expires for HTTP/1.0 clients and
157                    // add Cache-Control for HTTP/1.1 client.
158                    // Maybe we should wrap the response with a modified one,
159                    // rather than modifying the existing one.
160                    hsResponse.setHeader("Cache-Control", CACHE_CONTROL_VALUE);
161                    hsResponse.setDateHeader("Expires", System.currentTimeMillis() + CACHE_MS);
162                    }
163    
164                @Override
165                public void setHeader(final String s, final String s1)
166                    {
167                    for(int i = DANGER_HEADERS.length; --i >= 0; )
168                        {
169                        if(DANGER_HEADERS[i].equalsIgnoreCase(s))
170                            {
171    logger.log("WARNING: ExplicitCacheTimeFilter: vetoing attempt to set header "+s+" for: " + ((HttpServletRequest) request).getRequestURI());
172                            return;
173                            }
174                        }
175                    super.setHeader(s, s1);
176                    }
177    
178                @Override
179                public void addHeader(final String s, final String s1)
180                    {
181                    for(int i = DANGER_HEADERS.length; --i >= 0; )
182                        {
183                        if(DANGER_HEADERS[i].equalsIgnoreCase(s))
184                            {
185    logger.log("ExplicitCacheTimeFilter: WARNING: vetoing attempt to add header "+s+" for: " + ((HttpServletRequest) request).getRequestURI());
186                            return;
187                            }
188                        }
189                     super.addHeader(s, s1);
190                    }
191                }
192    
193            // Continue, with new explicit cacheing headers in place.
194            chain.doFilter(request, new Wrapper(hsr));
195            }
196        }
197