001: /*
002: * Copyright 2004-2007 the original author or authors.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016: package org.springframework.webflow.engine.support;
017:
018: import java.util.HashMap;
019: import java.util.Map;
020:
021: import org.apache.commons.logging.Log;
022: import org.apache.commons.logging.LogFactory;
023: import org.springframework.core.JdkVersion;
024: import org.springframework.core.NestedRuntimeException;
025: import org.springframework.core.style.ToStringCreator;
026: import org.springframework.util.Assert;
027: import org.springframework.webflow.engine.ActionList;
028: import org.springframework.webflow.engine.FlowExecutionExceptionHandler;
029: import org.springframework.webflow.engine.RequestControlContext;
030: import org.springframework.webflow.engine.TargetStateResolver;
031: import org.springframework.webflow.engine.Transition;
032: import org.springframework.webflow.execution.FlowExecutionException;
033: import org.springframework.webflow.execution.RequestContext;
034: import org.springframework.webflow.execution.ViewSelection;
035:
036: /**
037: * A flow execution exception handler that maps the occurence of a specific type of
038: * exception to a transition to a new {@link org.springframework.webflow.engine.State}.
039: * <p>
040: * The handled {@link FlowExecutionException} will be exposed in flash scope as
041: * {@link #STATE_EXCEPTION_ATTRIBUTE}. The underlying root cause of that exception
042: * will be exposed in flash scope as {@link #ROOT_CAUSE_EXCEPTION_ATTRIBUTE}.
043: *
044: * @author Keith Donald
045: */
046: public class TransitionExecutingStateExceptionHandler implements
047: FlowExecutionExceptionHandler {
048:
049: // this class should really have been called TransitionExecutingFlowExceptionHandler
050:
051: private static final Log logger = LogFactory
052: .getLog(TransitionExecutingStateExceptionHandler.class);
053:
054: /**
055: * The name of the attribute to expose a handled exception under in
056: * flash scope ("stateException").
057: */
058: public static final String STATE_EXCEPTION_ATTRIBUTE = "stateException";
059:
060: /**
061: * The name of the attribute to expose a root cause of a handled exception
062: * under in flash scope ("rootCauseException").
063: */
064: public static final String ROOT_CAUSE_EXCEPTION_ATTRIBUTE = "rootCauseException";
065:
066: /**
067: * The exceptionType->targetStateResolver map.
068: */
069: private Map exceptionTargetStateMappings = new HashMap();
070:
071: /**
072: * The list of actions to execute when this handler handles an exception.
073: */
074: private ActionList actionList = new ActionList();
075:
076: /**
077: * Adds an exception->state mapping to this handler.
078: * @param exceptionClass the type of exception to map
079: * @param targetStateId the id of the state to transition to if the
080: * specified type of exception is handled
081: * @return this handler, to allow for adding multiple mappings in a single
082: * statement
083: */
084: public TransitionExecutingStateExceptionHandler add(
085: Class exceptionClass, String targetStateId) {
086: return add(exceptionClass, new DefaultTargetStateResolver(
087: targetStateId));
088: }
089:
090: /**
091: * Adds a exception->state mapping to this handler.
092: * @param exceptionClass the type of exception to map
093: * @param targetStateResolver the resolver to calculate the state to
094: * transition to if the specified type of exception is handled
095: * @return this handler, to allow for adding multiple mappings in a single
096: * statement
097: */
098: public TransitionExecutingStateExceptionHandler add(
099: Class exceptionClass,
100: TargetStateResolver targetStateResolver) {
101: Assert.notNull(exceptionClass,
102: "The exception class is required");
103: Assert.notNull(targetStateResolver,
104: "The target state resolver is required");
105: exceptionTargetStateMappings.put(exceptionClass,
106: targetStateResolver);
107: return this ;
108: }
109:
110: /**
111: * Returns the list of actions to execute when this handler handles an exception.
112: * The returned list is mutable.
113: */
114: public ActionList getActionList() {
115: return actionList;
116: }
117:
118: public boolean handles(FlowExecutionException e) {
119: return getTargetStateResolver(e) != null;
120: }
121:
122: public ViewSelection handle(FlowExecutionException exception,
123: RequestControlContext context) {
124: if (logger.isDebugEnabled()) {
125: logger.debug("Handling state exception " + exception,
126: exception);
127: }
128: exposeException(context, exception, findRootCause(exception));
129: actionList.execute(context);
130: return context.execute(new Transition(
131: getTargetStateResolver(exception)));
132: }
133:
134: // helpers
135:
136: /**
137: * Exposes the given flow exception and root cause in flash scope to make
138: * them available for response rendering. Subclasses can override this
139: * if they want to expose the exceptions in a different way or do special
140: * processing before the exceptions are exposed.
141: * @param context the request control context
142: * @param exception the exception being handled
143: * @param rootCause root cause of the exception being handled (could be null)
144: * @since 1.0.2
145: */
146: protected void exposeException(RequestContext context,
147: FlowExecutionException exception, Throwable rootCause) {
148: // note that all Throwables are Serializable so putting them in flash
149: // scope should not be a problem
150: context.getFlashScope().put(STATE_EXCEPTION_ATTRIBUTE,
151: exception);
152: if (logger.isDebugEnabled()) {
153: logger.debug("Exposing state exception root cause "
154: + rootCause + " under attribute '"
155: + ROOT_CAUSE_EXCEPTION_ATTRIBUTE + "'");
156: }
157: context.getFlashScope().put(ROOT_CAUSE_EXCEPTION_ATTRIBUTE,
158: rootCause);
159: }
160:
161: /**
162: * Find the mapped target state resolver for given exception. Returns
163: * <code>null</code> if no mapping can be found for given exception. Will
164: * try all exceptions in the exception cause chain.
165: */
166: protected TargetStateResolver getTargetStateResolver(
167: FlowExecutionException e) {
168: if (JdkVersion.getMajorJavaVersion() == JdkVersion.JAVA_13) {
169: return getTargetStateResolver13(e);
170: } else {
171: return getTargetStateResolver14(e);
172: }
173: }
174:
175: /**
176: * Internal getTargetStateResolver implementation for use with JDK 1.3.
177: */
178: private TargetStateResolver getTargetStateResolver13(
179: NestedRuntimeException e) {
180: TargetStateResolver targetStateResolver;
181: if (isRootCause13(e)) {
182: return findTargetStateResolver(e.getClass());
183: } else {
184: targetStateResolver = (TargetStateResolver) exceptionTargetStateMappings
185: .get(e.getClass());
186: if (targetStateResolver != null) {
187: return targetStateResolver;
188: } else {
189: if (e.getCause() instanceof NestedRuntimeException) {
190: return getTargetStateResolver13((NestedRuntimeException) e
191: .getCause());
192: } else {
193: return null;
194: }
195: }
196: }
197: }
198:
199: /**
200: * Internal getTargetStateResolver implementation for use with JDK 1.4 or later.
201: */
202: private TargetStateResolver getTargetStateResolver14(Throwable t) {
203: TargetStateResolver targetStateResolver;
204: if (isRootCause14(t)) {
205: return findTargetStateResolver(t.getClass());
206: } else {
207: targetStateResolver = (TargetStateResolver) exceptionTargetStateMappings
208: .get(t.getClass());
209: if (targetStateResolver != null) {
210: return targetStateResolver;
211: } else {
212: return getTargetStateResolver14(t.getCause());
213: }
214: }
215: }
216:
217: /**
218: * Check if given exception is the root of the exception cause chain.
219: * For use with JDK 1.3.
220: */
221: private boolean isRootCause13(NestedRuntimeException e) {
222: return e.getCause() == null;
223: }
224:
225: /**
226: * Check if given exception is the root of the exception cause chain.
227: * For use with JDK 1.4 or later.
228: */
229: private boolean isRootCause14(Throwable t) {
230: return t.getCause() == null;
231: }
232:
233: /**
234: * Try to find a mapped target state resolver for given exception type. Will
235: * also try to lookup using the class hierarchy of given exception type.
236: * @param exceptionType the exception type to lookup
237: * @return the target state id or null if not found
238: */
239: private TargetStateResolver findTargetStateResolver(
240: Class exceptionType) {
241: while (exceptionType != null && exceptionType != Object.class) {
242: if (exceptionTargetStateMappings.containsKey(exceptionType)) {
243: return (TargetStateResolver) exceptionTargetStateMappings
244: .get(exceptionType);
245: } else {
246: exceptionType = exceptionType.getSuperclass();
247: }
248: }
249: return null;
250: }
251:
252: /**
253: * Find the root cause of given throwable.
254: */
255: protected Throwable findRootCause(Throwable t) {
256: if (JdkVersion.getMajorJavaVersion() == JdkVersion.JAVA_13) {
257: return findRootCause13(t);
258: } else {
259: return findRootCause14(t);
260: }
261: }
262:
263: /**
264: * Find the root cause of given throwable. For use on JDK 1.3.
265: */
266: private Throwable findRootCause13(Throwable t) {
267: if (t instanceof NestedRuntimeException) {
268: NestedRuntimeException nre = (NestedRuntimeException) t;
269: Throwable cause = nre.getCause();
270: if (cause == null) {
271: return nre;
272: } else {
273: return findRootCause13(cause);
274: }
275: } else {
276: return t;
277: }
278: }
279:
280: /**
281: * Find the root cause of given throwable. For use on JDK 1.4 or later.
282: */
283: private Throwable findRootCause14(Throwable e) {
284: Throwable cause = e.getCause();
285: if (cause == null) {
286: return e;
287: } else {
288: return findRootCause14(cause);
289: }
290: }
291:
292: public String toString() {
293: return new ToStringCreator(this ).append(
294: "exceptionTargetStateMappings",
295: exceptionTargetStateMappings).toString();
296: }
297: }
|