View Javadoc

1   /**
2    *  Copyright 2003-2007 Greg Luck
3    *
4    *  Licensed under the Apache License, Version 2.0 (the "License");
5    *  you may not use this file except in compliance with the License.
6    *  You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *  Unless required by applicable law or agreed to in writing, software
11   *  distributed under the License is distributed on an "AS IS" BASIS,
12   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *  See the License for the specific language governing permissions and
14   *  limitations under the License.
15   */
16  
17  package net.sf.ehcache.constructs.web.filter;
18  
19  import java.io.IOException;
20  
21  import org.apache.commons.logging.LogFactory;
22  import org.apache.commons.logging.Log;
23  
24  import javax.servlet.FilterChain;
25  import javax.servlet.FilterConfig;
26  import javax.servlet.ServletException;
27  import javax.servlet.ServletRequest;
28  import javax.servlet.ServletResponse;
29  import javax.servlet.http.HttpServletRequest;
30  import javax.servlet.http.HttpServletResponse;
31  import java.lang.reflect.Method;
32  import java.util.Enumeration;
33  import java.util.HashMap;
34  import java.util.Map;
35  
36  /**
37   * A generic {@link javax.servlet.Filter} with most of what we need done.
38   * <p/>
39   * Participates in the Template Method pattern with {@link javax.servlet.Filter}.
40   *
41   * @author <a href="mailto:gluck@thoughtworks.com">Greg Luck</a>
42   * @version $Id: Filter.java 512 2007-07-10 09:18:45Z gregluck $
43   */
44  public abstract class Filter implements javax.servlet.Filter {
45      /**
46       * If a request attribute NO_FILTER is set, then filtering will be skipped
47       */
48      public static final String NO_FILTER = "NO_FILTER";
49      private static final Log LOG = LogFactory.getLog(Filter.class.getName());
50  
51      /**
52       * The filter configuration.
53       */
54      protected FilterConfig filterConfig;
55  
56      /**
57       * The exceptions to log differently, as a comma separated list
58       */
59      protected String exceptionsToLogDifferently;
60  
61  
62      /**
63       * A the level of the exceptions which will be logged differently
64       */
65      protected String exceptionsToLogDifferentlyLevel;
66  
67  
68      /**
69       * Most {@link Throwable}s in Web applications propagate to the user. Usually they are logged where they first
70       * happened. Printing the stack trace once a {@link Throwable} as propagated to the servlet is sometimes
71       * just clutters the log.
72       * <p/>
73       * This field corresponds to an init-param of the same name. If set to true stack traces will be suppressed.
74       */
75      protected boolean suppressStackTraces;
76  
77  
78      /**
79       * Performs the filtering.  This method calls template method
80       * {@link #doFilter(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse,javax.servlet.FilterChain) } which does the filtering.
81       * This method takes care of error reporting and handling.
82       * Errors are reported at {@link Log#warn(Object)} level because http tends to produce lots of errors.
83       * @throws IOException if an IOException occurs during this method it will be rethrown and will not be wrapped
84       */
85      public final void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
86              throws ServletException, IOException {
87          final HttpServletRequest httpRequest = (HttpServletRequest) request;
88          final HttpServletResponse httpResponse = (HttpServletResponse) response;
89          try {
90              //NO_FILTER set for RequestDispatcher forwards to avoid double gzipping
91              if (filterNotDisabled(httpRequest)) {
92                  doFilter(httpRequest, httpResponse, chain);
93              } else {
94                  chain.doFilter(request, response);
95              }
96          } catch (final Throwable throwable) {
97              logThrowable(throwable, httpRequest);
98          }
99      }
100 
101     /**
102      * Filters can be disabled programmatically by adding a {@link #NO_FILTER} parameter to the request.
103      * This parameter is normally added to make RequestDispatcher include and forwards work.
104      *
105      * @param httpRequest the request
106      * @return true if NO_FILTER is not set.
107      */
108     protected boolean filterNotDisabled(final HttpServletRequest httpRequest) {
109         return httpRequest.getAttribute(NO_FILTER) == null;
110     }
111 
112     /**
113      * This method should throw IOExceptions, not wrap them.
114      */
115     private void logThrowable(final Throwable throwable, final HttpServletRequest httpRequest)
116             throws ServletException, IOException {
117         StringBuffer messageBuffer = new StringBuffer("Throwable thrown during doFilter on request with URI: ")
118                 .append(httpRequest.getRequestURI())
119                 .append(" and Query: ")
120                 .append(httpRequest.getQueryString());
121         String message = messageBuffer.toString();
122         boolean matchFound = matches(throwable);
123         if (matchFound) {
124             try {
125                 if (suppressStackTraces) {
126                     Method method = Log.class.getMethod(exceptionsToLogDifferentlyLevel, new Class[]{Object.class});
127                     method.invoke(LOG, new Object[]{throwable.getMessage()});
128                 } else {
129                     Method method = Log.class.getMethod(exceptionsToLogDifferentlyLevel,
130                             new Class[]{Object.class, Throwable.class});
131                     method.invoke(LOG, new Object[]{throwable.getMessage(), throwable});
132                 }
133             } catch (Exception e) {
134                 LOG.fatal("Could not invoke Log method for " + exceptionsToLogDifferentlyLevel, e);
135             }
136             if (throwable instanceof IOException) {
137                 throw (IOException) throwable;
138             } else {
139                 throw new ServletException(message, throwable);
140             }
141         } else {
142 
143             if (suppressStackTraces) {
144                 LOG.warn(messageBuffer.append(throwable.getMessage()).append("\nTop StackTraceElement: ")
145                         .append(throwable.getStackTrace()[0].toString()));
146             } else {
147                 LOG.warn(messageBuffer.append(throwable.getMessage()), throwable);
148             }
149             if (throwable instanceof IOException) {
150                 throw (IOException) throwable;
151             } else {
152                 throw new ServletException(throwable);
153             }
154         }
155     }
156 
157     /**
158      * Checks whether a throwable, its root cause if it is a {@link ServletException}, or its cause, if it is a
159      * Chained Exception matches an entry in the exceptionsToLogDifferently list
160      *
161      * @param throwable
162      * @return true if the class name of any of the throwables is found in the exceptions to log differently
163      */
164     private boolean matches(Throwable throwable) {
165         if (exceptionsToLogDifferently == null) {
166             return false;
167         }
168         if (exceptionsToLogDifferently.indexOf(throwable.getClass().getName()) != -1) {
169             return true;
170         }
171         if (throwable instanceof ServletException) {
172             Throwable rootCause = (((ServletException) throwable).getRootCause());
173             if (exceptionsToLogDifferently.indexOf(rootCause.getClass().getName()) != -1) {
174                 return true;
175             }
176         }
177         if (throwable.getCause() != null) {
178             Throwable cause = throwable.getCause();
179             if (exceptionsToLogDifferently.indexOf(cause.getClass().getName()) != -1) {
180                 return true;
181             }
182         }
183         return false;
184     }
185 
186     /**
187      * Initialises the filter.  Calls template method {@link #doInit()} to perform any filter specific initialisation.
188      */
189     public final void init(final FilterConfig config) throws ServletException {
190         try {
191 
192             this.filterConfig = config;
193             processInitParams(config);
194 
195             // Attempt to initialise this filter
196             doInit();
197         } catch (final Exception e) {
198             LOG.fatal("Could not initialise servlet filter.", e);
199             throw new ServletException("Could not initialise servlet filter.", e);
200         }
201     }
202 
203     private void processInitParams(final FilterConfig config) throws ServletException {
204         String exceptions = config.getInitParameter("exceptionsToLogDifferently");
205         String level = config.getInitParameter("exceptionsToLogDifferentlyLevel");
206         String suppressStackTracesString = config.getInitParameter("suppressStackTraces");
207         suppressStackTraces = Boolean.valueOf(suppressStackTracesString).booleanValue();
208         if (LOG.isDebugEnabled()) {
209             LOG.debug("Suppression of stack traces enabled for " + this.getClass().getName());
210         }
211 
212         if (exceptions != null) {
213             validateMandatoryParameters(exceptions, level);
214             validateLevel(level);
215             exceptionsToLogDifferentlyLevel = level;
216             exceptionsToLogDifferently = exceptions;
217             if (LOG.isDebugEnabled()) {
218                 LOG.debug("Different logging levels configured for " + this.getClass().getName());
219             }
220         }
221     }
222 
223     private void validateMandatoryParameters(String exceptions, String level) throws ServletException {
224         if ((exceptions != null && level == null) || (level != null && exceptions == null)) {
225             throw new ServletException("Invalid init-params. Both exceptionsToLogDifferently"
226                     + " and exceptionsToLogDifferentlyLevelvalue should be specified if one is"
227                     + " specified.");
228         }
229     }
230 
231     private void validateLevel(String level) throws ServletException {
232         //Check correct level set
233         if (!(level.equals("debug")
234                 || level.equals("info")
235                 || level.equals("warn")
236                 || level.equals("error")
237                 || level.equals("fatal"))) {
238             throw new ServletException("Invalid init-params value for \"exceptionsToLogDifferentlyLevel\"."
239                     + "Must be one of debug, info, warn, error or fatal.");
240         }
241     }
242 
243     /**
244      * Destroys the filter. Calls template method {@link #doDestroy()}  to perform any filter specific
245      * destruction tasks.
246      */
247     public final void destroy() {
248         this.filterConfig = null;
249         doDestroy();
250     }
251 
252     /**
253      * Checks if request accepts the named encoding.
254      */
255     protected boolean acceptsEncoding(final HttpServletRequest request, final String name) {
256         final boolean accepts = headerContains(request, "Accept-Encoding", name);
257         return accepts;
258     }
259 
260     /**
261      * Checks if request contains the header value.
262      */
263     private boolean headerContains(final HttpServletRequest request, final String header, final String value) {
264 
265         logRequestHeaders(request);
266 
267         final Enumeration accepted = request.getHeaders(header);
268         while (accepted.hasMoreElements()) {
269             final String headerValue = (String) accepted.nextElement();
270             if (headerValue.indexOf(value) != -1) {
271                 return true;
272             }
273         }
274         return false;
275     }
276 
277     /**
278      * Logs the request headers, if debug is enabled.
279      *
280      * @param request
281      */
282     protected void logRequestHeaders(final HttpServletRequest request) {
283         if (LOG.isDebugEnabled()) {
284             Map headers = new HashMap();
285             Enumeration enumeration = request.getHeaderNames();
286             StringBuffer logLine = new StringBuffer();
287             logLine.append("Request Headers");
288             while (enumeration.hasMoreElements()) {
289                 String name = (String) enumeration.nextElement();
290                 String headerValue = request.getHeader(name);
291                 headers.put(name, headerValue);
292                 logLine.append(": ").append(name).append(" -> ").append(headerValue);
293             }
294             LOG.debug(logLine);
295         }
296     }
297 
298 
299     /**
300      * A template method that performs any Filter specific destruction tasks.
301      * Called from {@link #destroy()}
302      */
303     protected abstract void doDestroy();
304 
305 
306     /**
307      * A template method that performs the filtering for a request.
308      * Called from {@link #doFilter(ServletRequest,ServletResponse,FilterChain)}.
309      */
310     protected abstract void doFilter(final HttpServletRequest httpRequest, final HttpServletResponse httpResponse,
311                                      final FilterChain chain) throws Throwable;
312 
313     /**
314      * A template method that performs any Filter specific initialisation tasks.
315      * Called from {@link #init(FilterConfig)}.
316      */
317     protected abstract void doInit() throws Exception;
318 
319     /**
320      * Returns the filter config.
321      */
322     public FilterConfig getFilterConfig() {
323         return filterConfig;
324     }
325 
326     /**
327      * Determine whether the user agent accepts GZIP encoding. This feature is part of HTTP1.1.
328      * If a browser accepts GZIP encoding it will advertise this by including in its HTTP header:
329      * <p/>
330      * <code>
331      * Accept-Encoding: gzip
332      * </code>
333      * <p/>
334      * Requests which do not accept GZIP encoding fall into the following categories:
335      * <ul>
336      * <li>Old browsers, notably IE 5 on Macintosh.
337      * <li>Search robots such as yahoo. While there are quite a few bots, they only hit individual
338      * pages once or twice a day. Note that Googlebot as of August 2004 now accepts GZIP.
339      * <li>Internet Explorer through a proxy. By default HTTP1.1 is enabled but disabled when going
340      * through a proxy. 90% of non gzip requests are caused by this.
341      * <li>Site monitoring tools
342      * </ul>
343      * As of September 2004, about 34% of requests coming from the Internet did not accept GZIP encoding.
344      *
345      * @param request
346      * @return true, if the User Agent request accepts GZIP encoding
347      */
348     protected boolean acceptsGzipEncoding(HttpServletRequest request) {
349         return acceptsEncoding(request, "gzip");
350     }
351 
352 }
353