/* vim: set ts=2 et sw=2 cindent fo=qroca: */ package com.globant.katari.core.web; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.RequestDispatcher; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.Cookie; import org.apache.commons.lang.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** Filter to catch all the exceptions and show then in a user defined page. * * This filter catches downstream exceptions and forwards it to a configurable * freemarker template. It also logs the exception to this class logger, at the * ERROR level. * * In debug mode, this filter just propagates the exception upwards, to let the * container show the normal exception page. You can pass the parameter * previewErrorPage to see how an error page would be generated without debug * mode. Note that the presence of the previewErrorPage parameter does not * force the error page to show up, there must be an exception downstream for * that. * * If previewErrorPage is specified, subsequent errors will show the error * page, unless the user specifies previewErrorPage=false. This is implemented * as a cookie name 'previewErrorPage'. * * It is intended to be configured in two different places. First, just after * the sitemesh decorator filter, to catch the errors in the 'content' of the * page. This gives sitemesh the opportunity to decorate the error page, so the * user does not loose the navigation. * * Also, place it at the beginnig of the filter chain to catch all errors * generated in the decorator and intermediate filters. * * This filter generates the error page simply forwarding to templateName. The * most common option is to use a FreemarkerServlet, like the one used in * sitemesh. * * The error page template has access to the exception in a request parameter * called 'exception'. * * This filter makes the following variables available to the error page: * * - exception: contains the exception object. * * - type: the type that is expected for the response. It can be 'html' or * 'json'. This is used to support ajax error messages. The type is set to * json when the Accept header contains 'application/json'. It is 'html' * otherwise. */ public class ExceptionHandlerFilter implements Filter { /** The class logger.*/ private static Logger log = LoggerFactory.getLogger( ExceptionHandlerFilter.class); /** The Freemarker's template name, never null. */ private final String templateName; /** Checks if the application is running in debug mode.*/ private final boolean debugMode; /** The servlet context. * * This is used to generate the error page by forwarding the request to a * freemarker servlet. This is never null after init. */ private ServletContext servletContext = null; /** Builds a new instance of the filter. * * @param theTemplateName the default view for all errors. Cannot be null. * * @param isInDebugMode if the application is running in debug mode. */ public ExceptionHandlerFilter(final String theTemplateName, final boolean isInDebugMode) { Validate.notNull(theTemplateName, "The theTemplateName cannot be null"); templateName = theTemplateName; debugMode = isInDebugMode; } /** A response wrapper that provides access to the data submitted to the * client. */ private static class ResponseBufferer extends ServletOutputInterceptor { /** The output stream that holds the data that has been sent to the client. * * It is never null. */ private ByteArrayOutputStream output = new ByteArrayOutputStream(); /** Constructor. * * @param response the wrapped response. */ public ResponseBufferer(final HttpServletResponse response) { super(response, false); } /** Returns the generated response as a byte array. * * @return the byte array of the generated response, never null. */ public byte[] toByteArray() { return output.toByteArray(); } /** {@inheritDoc} */ protected OutputStream createOutputStream() { return output; } }; /** {@inheritDoc}. */ public void init(final FilterConfig filterConfig) throws ServletException { servletContext = filterConfig.getServletContext(); } /** Filters the request and generates an error page in case of exception. * * {@inheritDoc}. */ public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException { log.trace("Entering doFilter."); if (!(request instanceof HttpServletRequest)) { throw new RuntimeException("Not an http request"); } if (!(response instanceof HttpServletResponse)) { throw new RuntimeException("Not an http request"); } HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; ResponseBufferer wrapper = new ResponseBufferer(httpResponse); try { Cookie previewCookie = null; String previewParameterValue = request.getParameter("previewErrorPage"); if ("false".equals(previewParameterValue)) { previewCookie = new Cookie("previewErrorPage", "false"); } else if (previewParameterValue != null) { previewCookie = new Cookie("previewErrorPage", "true"); } if (previewCookie != null) { wrapper.addCookie(previewCookie); } chain.doFilter(request, wrapper); wrapper.flushBuffer(); response.getOutputStream().write(wrapper.toByteArray()); } catch (RuntimeException e) { if (!handleException(httpRequest, httpResponse, e)) { throw e; } } catch (ServletException e) { if (!handleException(httpRequest, httpResponse, e)) { throw e; } } catch (IOException e) { if (!handleException(httpRequest, httpResponse, e)) { throw e; } } log.trace("Leaving doFilter"); } /** Handles the exception caught from the filter chain. * * This operation uses the freemarker template to generate an error page with * the information obtained from the exception. * * @param request the servlet request. It cannot be null. * * @param response the servlet response. It cannot be null. * * @param e the exception caught. It cannot be null. * * @return true if the exception was handled here. If this operation returns * false, the caller is intended to rethrow the exception. * * @throws IOException in case of error generating the output. * * @throws ServletException in case of another unexpect error. */ private boolean handleException(final HttpServletRequest request, final HttpServletResponse response, final Exception e) throws IOException, ServletException { boolean preview = false; String previewParameterValue = request.getParameter("previewErrorPage"); if (previewParameterValue == null) { // previewErrorPage not in parameter, check for cookie. Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if (cookie.getName().equals("previewErrorPage")) { preview = "true".equals(cookie.getValue()); } } } } else if ("false".equals(previewParameterValue)) { preview = false; } else { // if previewErrorPage is present, we assume to want the preview no // matter the value of the parameter. preview = true; } // We don't generate the output in debug mode. if (debugMode && !preview) { return false; } else { log.error(e.getMessage(), e); request.setAttribute("exception", e); String acceptType = request.getHeader("Accept"); if (acceptType != null && acceptType.contains("application/json")) { response.setContentType("application/json; charset=utf-8"); request.setAttribute("type", "json"); } else { response.setContentType("text/html; charset=utf-8"); request.setAttribute("type", "html"); } response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); RequestDispatcher dispatcher; dispatcher = servletContext.getRequestDispatcher(templateName); dispatcher.include(request, response); } return true; } /** {@inheritDoc}. * * This implementation does nothing. */ public void destroy() { } }