/* ====================================================================
* The Apache Software License, Version 1.1
*
* Copyright (c) 2003 - 2004 Greg Luck. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The end-user documentation included with the redistribution, if
* any, must include the following acknowlegement:
* "This product includes software developed by Greg Luck
* (http://sourceforge.net/users/gregluck) and contributors.
* See http://sourceforge.net/project/memberlist.php?group_id=93232
* for a list of contributors"
* Alternately, this acknowledgement may appear in the software itself,
* if and wherever such third-party acknowlegements normally appear.
*
* 4. The names "EHCache" must not be used to endorse or promote products
* derived from this software without prior written permission. For written
* permission, please contact Greg Luck (gregluck at users.sourceforge.net).
*
* 5. Products derived from this software may not be called "EHCache"
* nor may "EHCache" appear in their names without prior written
* permission of Greg Luck.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL GREG LUCK OR OTHER
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
* USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
* ====================================================================
*
* This software consists of voluntary contributions made by contributors
* individuals on behalf of the EHCache project. For more
* information on EHCache, please see <http://ehcache.sourceforge.net/>.
*
*/
package com.idega.core.cache.filter;
import net.sf.ehcache.constructs.web.ResponseHeadersNotModifiableException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.FilterConfig;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import java.lang.reflect.Method;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
/**
* A generic {@link javax.servlet.Filter} with most of what we need done.
* <p/>
* Participates in the Template Method pattern with {@link javax.servlet.Filter}.
*
* @author <a href="mailto:gluck@thoughtworks.com">Greg Luck</a>
* @version $Id: Filter.java,v 1.3 2006/04/09 12:13:20 laddi Exp $
*/
public abstract class Filter implements javax.servlet.Filter {
/**
* If a request attribute NO_FILTER is set, then filtering will be skipped
*/
public static final String NO_FILTER = "NO_FILTER";
private static final Log LOG = LogFactory.getLog(Filter.class.getName());
/**
* The filter configuration.
*/
protected FilterConfig filterConfig;
/**
* The exceptions to log differently, as a comma separated list
*/
protected String exceptionsToLogDifferently;
/**
* A the level of the exceptions which will be logged differently
*/
protected String exceptionsToLogDifferentlyLevel;
/**
* Most {@link Throwable}s in Web applications propagate to the user. Usually they are logged where they first
* happened. Printing the stack trace once a {@link Throwable} as propagated to the servlet is sometimes
* just clutters the log.
* <p/>
* This field corresponds to an init-param of the same name. If set to true stack traces will be suppressed.
*/
protected boolean suppressStackTraces;
/**
* Performs the filtering. This method calls template method
* {@link #doFilter(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, javax.servlet.FilterChain) } which does the filtering.
* This method takes care of error reporting and handling.
* Errors are reported at {@link Log#warn(Object)} level because http tends to produce lots of errors.
*/
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
throws ServletException {
final HttpServletRequest httpRequest = (HttpServletRequest) request;
final HttpServletResponse httpResponse = (HttpServletResponse) response;
try {
//NO_FILTER set for RequestDispatcher forwards to avoid double gzipping
if (filterNotDisabled(httpRequest)) {
doFilter(httpRequest, httpResponse, chain);
} else {
chain.doFilter(request, response);
}
// Flush the response
httpResponse.getOutputStream().flush();
} catch (final Throwable throwable) {
logThrowable(throwable, httpRequest);
}
}
/**
* Filters can be disabled programmatically by adding a {@link #NO_FILTER} parameter to the request.
* This parameter is normally added to make RequestDispatcher include and forwards work.
* todo this should not be necessary
*
* @param httpRequest the request
* @return true if NO_FILTER is not set.
*/
private boolean filterNotDisabled(final HttpServletRequest httpRequest) {
return httpRequest.getAttribute(NO_FILTER) == null;
}
private void logThrowable(final Throwable throwable, final HttpServletRequest httpRequest) throws ServletException {
StringBuffer messageBuffer = new StringBuffer("Throwable thrown during doFilter on request with URI: ")
.append(httpRequest.getRequestURI())
.append(" and Query: ")
.append(httpRequest.getQueryString());
String message = messageBuffer.toString();
boolean matchFound = matches(throwable);
if (matchFound) {
try {
if (this.suppressStackTraces) {
Method method = Log.class.getMethod(this.exceptionsToLogDifferentlyLevel, new Class[]{Object.class});
method.invoke(LOG, new Object[]{throwable.getMessage()});
} else {
Method method = Log.class.getMethod(this.exceptionsToLogDifferentlyLevel,
new Class[]{Object.class, Throwable.class});
method.invoke(LOG, new Object[]{throwable.getMessage(), throwable});
}
} catch (Exception e) {
LOG.fatal("Could not invoke Log method for " + this.exceptionsToLogDifferentlyLevel, e);
}
throw new ServletException(message, throwable);
} else {
if (this.suppressStackTraces) {
LOG.warn(messageBuffer.append(throwable.getMessage()).append("\nTop StackTraceElement: ")
.append(throwable.getStackTrace()[0].toString()));
} else {
LOG.warn(messageBuffer.append(throwable.getMessage()), throwable);
}
throw new ServletException(throwable);
}
}
/**
* Checks whether a throwable, its root cause if it is a {@link ServletException}, or its cause, if it is a
* Chained Exception matches an entry in the exceptionsToLogDifferently list
*
* @param throwable
* @return true if the class name of any of the throwables is found in the exceptions to log differently
*/
private boolean matches(Throwable throwable) {
if (this.exceptionsToLogDifferently == null) {
return false;
}
if (this.exceptionsToLogDifferently.indexOf(throwable.getClass().getName()) != -1) {
return true;
}
if (throwable instanceof ServletException) {
Throwable rootCause = (((ServletException) throwable).getRootCause());
if (this.exceptionsToLogDifferently.indexOf(rootCause.getClass().getName()) != -1) {
return true;
}
}
if (throwable.getCause() != null) {
Throwable cause = throwable.getCause();
if (this.exceptionsToLogDifferently.indexOf(cause.getClass().getName()) != -1) {
return true;
}
}
return false;
}
/**
* Initialises the filter. Calls template method {@link #doInit()} to perform any filter specific initialisation.
*/
public final void init(final FilterConfig config) throws ServletException {
try {
this.filterConfig = config;
processInitParams(config);
// Attempt to initialise this filter
doInit();
} catch (final Exception e) {
LOG.fatal("Could not initialise servlet filter.", e);
throw new ServletException("Could not initialise servlet filter.", e);
}
}
private void processInitParams(final FilterConfig config) throws ServletException {
String exceptions = config.getInitParameter("exceptionsToLogDifferently");
String level = config.getInitParameter("exceptionsToLogDifferentlyLevel");
String suppressStackTracesString = config.getInitParameter("suppressStackTraces");
this.suppressStackTraces = Boolean.valueOf(suppressStackTracesString).booleanValue();
if (LOG.isDebugEnabled()) {
LOG.debug("Suppression of stack traces enabled for " + this.getClass().getName());
}
if (exceptions != null) {
validateMandatoryParameters(exceptions, level);
validateLevel(level);
this.exceptionsToLogDifferentlyLevel = level;
this.exceptionsToLogDifferently = exceptions;
if (LOG.isDebugEnabled()) {
LOG.debug("Different logging levels configured for " + this.getClass().getName());
}
}
}
private void validateMandatoryParameters(String exceptions, String level) throws ServletException {
if ((exceptions != null && level == null) || (level != null && exceptions == null)) {
throw new ServletException("Invalid init-params. Both exceptionsToLogDifferently"
+ " and exceptionsToLogDifferentlyLevelvalue should be specified if one is"
+ " specified.");
}
}
private void validateLevel(String level) throws ServletException {
//Check correct level set
if (!(level.equals("debug")
|| level.equals("info")
|| level.equals("warn")
|| level.equals("error")
|| level.equals("fatal"))) {
throw new ServletException("Invalid init-params value for \"exceptionsToLogDifferentlyLevel\"."
+ "Must be one of debug, info, warn, error or fatal.");
}
}
/**
* Destroys the filter. Calls template method {@link #doDestroy()} to perform any filter specific
* destruction tasks.
*/
public final void destroy() {
this.filterConfig = null;
doDestroy();
}
/**
* Checks if request accepts the named encoding.
*/
protected boolean acceptsEncoding(final HttpServletRequest request, final String name) {
final boolean accepts = headerContains(request, "Accept-Encoding", name);
return accepts;
}
/**
* Checks if request contains the header value.
*/
private boolean headerContains(final HttpServletRequest request, final String header, final String value) {
logRequestHeaders(request);
final Enumeration accepted = request.getHeaders(header);
while (accepted.hasMoreElements()) {
final String headerValue = (String) accepted.nextElement();
if (headerValue.indexOf(value) != -1) {
return true;
}
}
return false;
}
/**
* Logs the request headers, if debug is enabled.
*
* @param request
*/
protected void logRequestHeaders(final HttpServletRequest request) {
if (LOG.isDebugEnabled()) {
Map headers = new HashMap();
Enumeration enumeration = request.getHeaderNames();
StringBuffer logLine = new StringBuffer();
logLine.append("Request Headers");
while (enumeration.hasMoreElements()) {
String name = (String) enumeration.nextElement();
String headerValue = request.getHeader(name);
headers.put(name, headerValue);
logLine.append(": ").append(name).append(" -> ").append(headerValue);
}
LOG.debug(logLine);
}
}
/**
* Adds the gzip HTTP header to the response. This is need when a gzipped body
* is returned so that browsers can properly decompress it.
* @throws ResponseHeadersNotModifiableException Either the response is committed or we were called using the include method
* from a {@link javax.servlet.RequestDispatcher#include(javax.servlet.ServletRequest, javax.servlet.ServletResponse)}
* method and the set set header is ignored.
*/
protected void addGzipHeader(final HttpServletResponse response) throws ResponseHeadersNotModifiableException {
response.setHeader("Content-Encoding", "gzip");
boolean containsEncoding = response.containsHeader("Content-Encoding");
if (!containsEncoding) {
throw new ResponseHeadersNotModifiableException("Failure when attempting to set "
+ "Content-Encoding: gzip");
}
}
/**
* A template method that performs any Filter specific destruction tasks.
* Called from {@link #destroy()}
*/
protected abstract void doDestroy();
/**
* A template method that performs the filtering for a request.
* Called from {@link #doFilter(ServletRequest, ServletResponse, FilterChain)}.
*/
protected abstract void doFilter(final HttpServletRequest httpRequest, final HttpServletResponse httpResponse,
final FilterChain chain) throws Throwable;
/**
* A template method that performs any Filter specific initialisation tasks.
* Called from {@link #init(FilterConfig)}.
*/
protected abstract void doInit() throws Exception;
/**
* Returns the filter config.
*/
public FilterConfig getFilterConfig() {
return this.filterConfig;
}
/**
* Determine whether the user agent accepts GZIP encoding. This feature is part of HTTP1.1.
* If a browser accepts GZIP encoding it will advertise this by including in its HTTP header:
* <p/>
* <code>
* Accept-Encoding: gzip
* </code>
* <p/>
* Requests which do not accept GZIP encoding fall into the following categories:
* <ul>
* <li>Old browsers, notably IE 5 on Macintosh.
* <li>Search robots such as yahoo. While there are quite a few bots, they only hit individual
* pages once or twice a day. Note that Googlebot as of August 2004 now accepts GZIP.
* <li>Monitoring Scripts. The one by Steve Sulman was requesting once every few seconds and did
* not accept GZIP. This amounts to a Denial of Service.
* <li>Internet Explorer through a proxy. By default HTTP1.1 is enabled but disabled when going
* through a proxy. 90% of non gzip requests are caused by this.
* </ul>
* As of September 2004, about 34% of requests coming from the Internet did not accept GZIP encoding.
*
* @param request
* @return true, if the User Agent request accepts GZIP encoding
*/
protected boolean acceptsGzipEncoding(HttpServletRequest request) {
return acceptsEncoding(request, "gzip");
}
}