/* * Copyright 2004-2012 the original author or authors. * * 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 org.springframework.webflow.engine.support; import java.util.HashMap; import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.core.style.ToStringCreator; import org.springframework.util.Assert; import org.springframework.webflow.engine.ActionList; import org.springframework.webflow.engine.FlowExecutionExceptionHandler; import org.springframework.webflow.engine.RequestControlContext; import org.springframework.webflow.engine.TargetStateResolver; import org.springframework.webflow.engine.Transition; import org.springframework.webflow.execution.FlowExecutionException; import org.springframework.webflow.execution.RequestContext; /** * A flow execution exception handler that maps the occurrence of a specific type of exception to a transition to a new * {@link org.springframework.webflow.engine.State}. * <p> * The handled {@link FlowExecutionException} will be exposed in flash scope as * {@link #FLOW_EXECUTION_EXCEPTION_ATTRIBUTE}. The underlying root cause of that exception will be exposed in flash * scope as {@link #ROOT_CAUSE_EXCEPTION_ATTRIBUTE}. * * @author Keith Donald */ public class TransitionExecutingFlowExecutionExceptionHandler implements FlowExecutionExceptionHandler { private static final Log logger = LogFactory.getLog(TransitionExecutingFlowExecutionExceptionHandler.class); /** * The name of the attribute to expose a handled exception under in flash scope ("flowExecutionException"). */ public static final String FLOW_EXECUTION_EXCEPTION_ATTRIBUTE = "flowExecutionException"; /** * The name of the attribute to expose a root cause of a handled exception under in flash scope * ("rootCauseException"). */ public static final String ROOT_CAUSE_EXCEPTION_ATTRIBUTE = "rootCauseException"; /** * The exceptionType to targetStateResolver map. */ private Map<Class<? extends Throwable>, TargetStateResolver> exceptionTargetStateMappings = new HashMap<Class<? extends Throwable>, TargetStateResolver>(); /** * The list of actions to execute when this handler handles an exception. */ private ActionList actionList = new ActionList(); /** * Adds an exception-to-target state mapping to this handler. * @param exceptionClass the type of exception to map * @param targetStateId the id of the state to transition to if the specified type of exception is handled * @return this handler, to allow for adding multiple mappings in a single statement */ public TransitionExecutingFlowExecutionExceptionHandler add(Class<? extends Throwable> exceptionClass, String targetStateId) { return add(exceptionClass, new DefaultTargetStateResolver(targetStateId)); } /** * Adds a exception-to-target state resolver mapping to this handler. * @param exceptionClass the type of exception to map * @param targetStateResolver the resolver to calculate the state to transition to if the specified type of * exception is handled * @return this handler, to allow for adding multiple mappings in a single statement */ public TransitionExecutingFlowExecutionExceptionHandler add(Class<? extends Throwable> exceptionClass, TargetStateResolver targetStateResolver) { Assert.notNull(exceptionClass, "The exception class is required"); Assert.notNull(targetStateResolver, "The target state resolver is required"); exceptionTargetStateMappings.put(exceptionClass, targetStateResolver); return this; } /** * Returns the list of actions to execute when this handler handles an exception. The returned list is mutable. */ public ActionList getActionList() { return actionList; } public boolean canHandle(FlowExecutionException e) { return getTargetStateResolver(e) != null; } public void handle(FlowExecutionException exception, RequestControlContext context) { if (logger.isDebugEnabled()) { logger.debug("Handling flow execution exception " + exception, exception); } exposeException(context, exception, findRootCause(exception)); actionList.execute(context); context.execute(new Transition(getTargetStateResolver(exception))); } // helpers /** * Exposes the given flow exception and root cause in flash scope to make them available for response rendering. * Subclasses can override this if they want to expose the exceptions in a different way or do special processing * before the exceptions are exposed. * @param context the request control context * @param exception the exception being handled * @param rootCause root cause of the exception being handled (could be null) */ protected void exposeException(RequestContext context, FlowExecutionException exception, Throwable rootCause) { // note that all Throwables are Serializable so putting them in flash // scope should not be a problem context.getFlashScope().put(FLOW_EXECUTION_EXCEPTION_ATTRIBUTE, exception); if (logger.isDebugEnabled()) { logger.debug("Exposing flow execution exception root cause " + rootCause + " under attribute '" + ROOT_CAUSE_EXCEPTION_ATTRIBUTE + "'"); } context.getFlashScope().put(ROOT_CAUSE_EXCEPTION_ATTRIBUTE, rootCause); } /** * Find the mapped target state resolver for given exception. Returns <code>null</code> if no mapping can be found * for given exception. Will try all exceptions in the exception cause chain. */ protected TargetStateResolver getTargetStateResolver(Throwable e) { TargetStateResolver targetStateResolver; if (isRootCause(e)) { return findTargetStateResolver(e.getClass()); } else { targetStateResolver = exceptionTargetStateMappings.get(e.getClass()); if (targetStateResolver != null) { return targetStateResolver; } else { return getTargetStateResolver(e.getCause()); } } } /** * Check if given exception is the root of the exception cause chain. For use with JDK 1.4 or later. */ private boolean isRootCause(Throwable t) { return t.getCause() == null; } /** * Try to find a mapped target state resolver for given exception type. Will also try to lookup using the class * hierarchy of given exception type. * @param exceptionType the exception type to lookup * @return the target state id or null if not found */ private TargetStateResolver findTargetStateResolver(Class<? extends Throwable> exceptionType) { Class<?> type = exceptionType; while (type != null && type != Object.class) { if (exceptionTargetStateMappings.containsKey(type)) { return exceptionTargetStateMappings.get(type); } else { type = type.getSuperclass(); } } return null; } /** * Find the root cause of given throwable. For use on JDK 1.4 or later. */ private Throwable findRootCause(Throwable e) { Throwable cause = e.getCause(); if (cause == null) { return e; } else { return findRootCause(cause); } } public String toString() { return new ToStringCreator(this).append("exceptionHandlingMappings", exceptionTargetStateMappings).toString(); } }