net.sourceforge.stripes.exception.DefaultExceptionHandler Maven / Gradle / Ivy
Show all versions of stripes Show documentation
/* Copyright 2005-2006 Tim Fennell
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.sourceforge.stripes.exception;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.text.DecimalFormat;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.sourceforge.stripes.action.ActionBean;
import net.sourceforge.stripes.action.ActionBeanContext;
import net.sourceforge.stripes.action.FileBean;
import net.sourceforge.stripes.action.ForwardResolution;
import net.sourceforge.stripes.action.Resolution;
import net.sourceforge.stripes.action.ValidationErrorReportResolution;
import net.sourceforge.stripes.config.Configuration;
import net.sourceforge.stripes.controller.DispatcherHelper;
import net.sourceforge.stripes.controller.ExecutionContext;
import net.sourceforge.stripes.controller.FileUploadLimitExceededException;
import net.sourceforge.stripes.controller.StripesConstants;
import net.sourceforge.stripes.controller.StripesRequestWrapper;
import net.sourceforge.stripes.util.Log;
import net.sourceforge.stripes.util.ReflectUtil;
import net.sourceforge.stripes.validation.LocalizableError;
/**
* Default ExceptionHandler implementation that makes it easy for users to extend and
* add custom handling for different types of exception. When extending this class methods
* can be added that meet the following requirements:
*
*
* - Methods must be public
* - Methods must be non-abstract
* - Methods must have exactly three parameters
* - The first parameter type must be Throwable or a subclass thereof
* - The second and third arguments must be of type HttpServletRequest and
* HttpServletResponse respectively
* - Methods may optionally return a Resolution in which case the resolution
* will be executed
*
*
* When an exception is caught the exception handler attempts to find a method that
* can handle that type of exception. If none is found the exception's super-types are
* iterated through and methods looked for which match the super-types. If a matching
* method is found it will be invoked. Otherwise the exception will simply be rethrown
* by the exception handler - though first it will be wrapped in a StripesServletException
* if necessary in order to make it acceptable to the container.
*
* The following are examples of method signatures that might be added by subclasses:
*
*
* public Resolution handle(FileUploadLimitExceededException ex, HttpServletRequest req, HttpServletResponse resp) { ... }
* public void handle(MySecurityException ex, HttpServletRequest req, HttpServletResponse resp) { ... }
* public void catchAll(Throwable t, HttpServletRequest req, HttpServletResponse resp) { ... }
*
*
* @author Tim Fennell
* @since Stripes 1.3
*/
public class DefaultExceptionHandler implements ExceptionHandler {
private static final Log log = Log.getInstance(DefaultExceptionHandler.class);
private Configuration configuration;
/** A cache of exception types handled mapped to proxy objects that can do the handling. */
private Map, HandlerProxy> handlers =
new HashMap, HandlerProxy>();
/**
* Inner class that ties a class and method together an invokable object.
* @author Tim Fennell
* @since Stripes 1.3
*/
protected static class HandlerProxy {
private Object handler;
private Method handlerMethod;
/** Constructs a new HandlerProxy that will tie together the instance and method used. */
public HandlerProxy(Object handler, Method handlerMethod) {
this.handler = handler;
this.handlerMethod = handlerMethod;
}
/** Invokes the handler and executes the resolution if one is returned. */
public void handle(Throwable t, HttpServletRequest req, HttpServletResponse res) throws Exception {
Object resolution = handlerMethod.invoke(this.handler, t, req, res);
if (resolution != null && resolution instanceof Resolution) {
((Resolution) resolution).execute(req, res);
}
}
Method getHandlerMethod() {
return handlerMethod;
}
}
/**
* Implementation of the ExceptionHandler interface that attempts to find a method
* that is capable of handing the exception. If it finds one then it is delegated to, and if
* it returns a resolution it will be executed. Otherwise rethrows any unhandled exceptions,
* wrapped in a StripesServletException if necessary.
*
* @param throwable the exception being handled
* @param request the current request being processed
* @param response the response paired with the current request
*/
public void handle(Throwable throwable,
HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
try {
Throwable actual = unwrap(throwable);
Class> type = actual.getClass();
HandlerProxy proxy = null;
while (type != null && proxy == null) {
proxy = this.handlers.get(type);
type = type.getSuperclass();
}
if (proxy != null) {
proxy.handle(actual, request, response);
}
else if (throwable instanceof FileUploadLimitExceededException) {
Resolution resolution = handle((FileUploadLimitExceededException) throwable,
request, response);
if (resolution != null)
resolution.execute(request, response);
}
else if (throwable instanceof SourcePageNotFoundException) {
Resolution resolution = handle((SourcePageNotFoundException) throwable, request,
response);
if (resolution != null)
resolution.execute(request, response);
}
else {
// If there's no sensible proxy, rethrow the original throwable,
// NOT the unwrapped one since they may add extra information
log.warn(throwable, "Unhandled exception caught by the Stripes default exception handler.");
throw throwable;
}
}
catch (ServletException se) { throw se; }
catch (IOException ioe) { throw ioe; }
catch (Throwable t) {
String message = "Unhandled exception in exception handler.";
log.error(t, message);
throw new StripesServletException(message, t);
}
}
/**
*
* A default handler for {@link SourcePageNotFoundException}. That exception is thrown when
* validation errors occur on a request but the source page cannot be determined from the
* request parameters. Such a condition generally arises during application development when,
* for example, a parameter is accidentally omitted from a generated hyperlink or AJAX request.
*
*
* In the past, it was very difficult to determine what validation errors triggered the
* exception. This method returns a {@link ValidationErrorReportResolution}, which sends a
* simple HTML response to the client that very clearly details the validation errors.
*
*
* In production, most applications will provide their own handler for
* {@link SourcePageNotFoundException} by extending this class and overriding this method.
*
*
* @param exception The exception.
* @param request The servlet request.
* @param response The servlet response.
* @return A {@link ValidationErrorReportResolution}
*/
protected Resolution handle(SourcePageNotFoundException exception, HttpServletRequest request,
HttpServletResponse response) throws Exception {
return new ValidationErrorReportResolution(exception.getActionBeanContext());
}
/**
*
* {@link FileUploadLimitExceededException} is notoriously difficult to handle for several
* reasons:
*
* - The exception is thrown during construction of the {@link StripesRequestWrapper}. Many
* Stripes components rely on the presence of this wrapper, yet it cannot be created normally.
* - It happens before the request lifecycle has begun. There is no {@link ExecutionContext},
* {@link ActionBeanContext}, or {@link ActionBean} associated with the request yet.
* - None of the request parameters in the POST body can be read without risking denial of
* service. That includes the {@code _sourcePage} parameter that indicates the page from which
* the request was submitted.
*
*
*
* This exception handler makes an attempt to handle the exception as gracefully as possible. It
* relies on the HTTP Referer header to determine where the request was submitted from. It uses
* introspection to guess the field name of the {@link FileBean} field that exceeded the POST
* limit. It instantiates an {@link ActionBean} and {@link ActionBeanContext} and adds a
* validation error to report the field name, maximum POST size, and actual POST size. Finally,
* it forwards to the referer.
*
*
* While this is a best effort, it won't be ideal for all situations. If this method is unable
* to handle the exception properly for any reason, it rethrows the exception. Subclasses can
* call this method in a {@code try} block, providing additional processing in the {@code catch}
* block.
*
*
* A simple way to provide a single, global error page for this type of exception is to override
* {@link #getFileUploadExceededExceptionPath(HttpServletRequest)} to return the path to your
* global error page.
*
*
* @param exception The exception that needs to be handled
* @param request The servlet request
* @param response The servlet response
* @return A {@link Resolution} to forward to the path returned by
* {@link #getFileUploadExceededExceptionPath(HttpServletRequest)}
* @throws FileUploadLimitExceededException If
* {@link #getFileUploadExceededExceptionPath(HttpServletRequest)} returns null or
* this method is unable for any other reason to forward to the error page
*/
protected Resolution handle(FileUploadLimitExceededException exception,
HttpServletRequest request, HttpServletResponse response)
throws FileUploadLimitExceededException {
// Get the path to which we will forward to display the message
final String path = getFileUploadExceededExceptionPath(request);
if (path == null)
throw exception;
final StripesRequestWrapper wrapper;
final ActionBeanContext context;
final ActionBean actionBean;
try {
// Create a new request wrapper, avoiding the pitfalls of multipart
wrapper = new StripesRequestWrapper(request) {
@Override
protected void constructMultipartWrapper(HttpServletRequest request)
throws StripesServletException {
setLocale(configuration.getLocalePicker().pickLocale(request));
}
};
// Create the ActionBean and ActionBeanContext
context = configuration.getActionBeanContextFactory().getContextInstance(wrapper,
response);
actionBean = configuration.getActionResolver().getActionBean(context);
wrapper.setAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN, actionBean);
}
catch (ServletException e) {
log.error(e);
throw exception;
}
// Try to guess the field name by finding exactly one FileBean field
String fieldName = null;
try {
PropertyDescriptor[] pds = ReflectUtil.getPropertyDescriptors(actionBean.getClass());
for (PropertyDescriptor pd : pds) {
if (FileBean.class.isAssignableFrom(pd.getPropertyType())) {
if (fieldName == null) {
// First FileBean field found so set the field name
fieldName = pd.getName();
}
else {
// There's more than one FileBean field so don't use a field name
fieldName = null;
break;
}
}
}
}
catch (Exception e) {
// Not a big deal if we can't determine the field name
}
// Add validation error with parameters for max post size and actual posted size (KB)
DecimalFormat format = new DecimalFormat("0.00");
double max = (double) exception.getMaximum() / 1024;
double posted = (double) exception.getPosted() / 1024;
LocalizableError error = new LocalizableError("validation.file.postBodyTooBig", format
.format(max), format.format(posted));
if (fieldName == null)
context.getValidationErrors().addGlobalError(error);
else
context.getValidationErrors().add(fieldName, error);
// Create an ExecutionContext so that the validation errors can be filled in
ExecutionContext exectx = new ExecutionContext();
exectx.setActionBean(actionBean);
exectx.setActionBeanContext(context);
DispatcherHelper.fillInValidationErrors(exectx);
// Forward back to referer, using the wrapped request
return new ForwardResolution(path) {
@Override
public void execute(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
super.execute(wrapper, response);
}
};
}
/**
* Get the path to which the {@link Resolution} returned by
* {@link #handle(FileUploadLimitExceededException, HttpServletRequest, HttpServletResponse)}
* should forward to report the error. The default implementation attempts to determine this
* from the HTTP Referer header. If it is unable to do so, it returns null. Subclasses may
* override this method to return whatever they wish. The return value must be relative to the
* application context root.
*
* @param request The request that generated the exception
* @return The context-relative path from which the request was submitted
*/
protected String getFileUploadExceededExceptionPath(HttpServletRequest request) {
// Get the referer URL so we can bounce back to it
URL referer = null;
try {
referer = new URL(request.getHeader("referer"));
}
catch (Exception e) {
// Header not found? Invalid? Can't do anything with it :(
return null;
}
// Convert the referer path to a context-relative path
String path = referer.getFile();
String contextPath = request.getContextPath();
if (contextPath.length() > 1) {
// We can't handle it if the POST came from outside our app
if (!path.startsWith(contextPath + "/"))
return null;
path = path.replace(contextPath, "");
}
return path;
}
/** Stores the configuration and examines the handler for usable delegate methods. */
public void init(Configuration configuration) throws Exception {
this.configuration = configuration;
addHandler(this);
}
/**
* Adds a class to the set of configured delegate handlers. Examines all the methods on the
* class looking for public non-abstract methods with a signature matching that described in
* the class level javadoc. Each method is wrapped in a HandlerProxy and stored in a cache
* by the exception type it takes.
*
* @param handlerClass the class being configured
* @throws Exception if the handler class cannot be instantiated
*/
protected void addHandler(Class> handlerClass) throws Exception {
addHandler(handlerClass.newInstance());
}
/**
* Adds an object instance to the set of configured handles. Examines
* all the methods on the class looking for public non-abstract methods with a signature
* matching that described in the class level javadoc. Each method is wrapped in a
* HandlerProxy and stored in a cache by the exception type it takes.
*
* @param handler the handler instance being configured
*/
@SuppressWarnings("unchecked")
protected void addHandler(Object handler) throws Exception {
Method[] methods = handler.getClass().getMethods();
for (Method method : methods) {
// Check the method Signature
Class[] parameters = method.getParameterTypes();
int mods = method.getModifiers();
// Check all the reasons not to add it!
if (!Modifier.isPublic(mods)) continue;
if (Modifier.isAbstract(mods)) continue;
if (parameters.length != 3) continue;
if (!Throwable.class.isAssignableFrom(parameters[0])) continue;
if (!HttpServletRequest.class.equals(parameters[1])) continue;
if (!HttpServletResponse.class.equals(parameters[2])) continue;
if (handler == this && method.getName().equals("handle") &&
Throwable.class.equals(parameters[0])) continue;
// And if we made it this far, add it!
Class extends Throwable> type = parameters[0];
HandlerProxy proxy = new HandlerProxy(handler, method);
HandlerProxy previous = handlers.get(type);
if (previous != null) {
log.warn("More than one exception handler for exception type ", type, " in ",
handler.getClass().getSimpleName(), ". '", method.getName(),
"()' will be used instead of '", previous.getHandlerMethod().getName(), "()'.");
}
handlers.put(type, proxy);
log.debug("Added exception handler '", handler.getClass().getSimpleName(), ".",
method.getName(), "()' for exception type: ", type);
}
}
/** Provides subclasses with access to the configuration. */
protected Configuration getConfiguration() { return configuration; }
/**
* Unwraps the throwable passed in. If the throwable is a ServletException and has
* a root case, the root cause is returned, otherwise the throwable is returned as is.
*
* @param throwable a throwable
* @return another thowable, either the root cause of the one passed in
*/
protected Throwable unwrap(Throwable throwable) {
if (throwable instanceof ServletException) {
Throwable t = ((ServletException) throwable).getRootCause();
if (t != null) {
throwable = t;
}
}
return throwable;
}
}