This commit is contained in:
Keith Donald
2009-03-13 18:56:24 +00:00
parent b3399fd44b
commit caa02fb9fb
7 changed files with 98 additions and 138 deletions

View File

@@ -19,16 +19,16 @@ import org.springframework.webflow.engine.support.TransitionExecutingFlowExecuti
import org.springframework.webflow.execution.FlowExecutionException; import org.springframework.webflow.execution.FlowExecutionException;
/** /**
* A strategy for handling an exception that occurs at runtime during the execution of a flow definition. * A strategy for handling an exception that occurs at runtime during an active flow execution.
* *
* Note: special care should be taken when implementing custom flow execution exception handlers. Exception handlers are * Note: special care should be taken when implementing custom flow execution exception handlers. Exception handlers are
* like Transitions in that they direct flow control when exceptions occur. They are more complex than Actions, which * like Transitions in that they direct flow control when exceptions occur. They are more complex than Actions, which
* can only execute behaviors and return results that drive flow control. For this reason, if implemented incorrectly, a * can only execute behaviors and return results that drive flow control. For this reason, if implemented incorrectly, a
* FlowExecutionHandler can leave a flow execution in an invalid state, which can render the flow execution unusable or * FlowExecutionHandler can leave a flow execution in an invalid state, which can render the flow execution unusable or
* its future use undefined. For example, if an exception thrown at flow startup gets routed to an exception handler, * its future use undefined. For example, if an exception thrown at flow session startup gets routed to an exception
* the handler must take responsibility for ensuring the flow execution returns control to the caller in a consistent * handler, the handler must take responsibility for ensuring the flow execution returns control to the caller in a
* state. Concretely, this means the exception handler must transition the flow to its start state. The handler should * consistent state. Concretely, this means the exception handler must transition the flow to its start state. The
* not simply return leaving the flow with no current state set. * handler should not simply return leaving the flow with no current state set.
* *
* Note: Because flow execution handlers are more difficult to implement correctly, consider catching exceptions in your * Note: Because flow execution handlers are more difficult to implement correctly, consider catching exceptions in your
* web flow action code and returning result events that drive standard transitions. Alternatively, consider use of the * web flow action code and returning result events that drive standard transitions. Alternatively, consider use of the

View File

@@ -167,11 +167,11 @@ public class FlowExecutionImpl implements FlowExecution, Externalizable {
} }
public boolean hasStarted() { public boolean hasStarted() {
return status != FlowExecutionStatus.NOT_STARTED; return status == FlowExecutionStatus.ACTIVE || status == FlowExecutionStatus.ENDED;
} }
public boolean isActive() { public boolean isActive() {
return !flowSessions.isEmpty(); return status == FlowExecutionStatus.ACTIVE;
} }
public boolean hasEnded() { public boolean hasEnded() {
@@ -183,11 +183,13 @@ public class FlowExecutionImpl implements FlowExecution, Externalizable {
} }
public FlowSession getActiveSession() { public FlowSession getActiveSession() {
if (status == FlowExecutionStatus.NOT_STARTED) { if (!isActive()) {
throw new IllegalStateException("No active session to access; this flow execution has not been started"); if (status == FlowExecutionStatus.NOT_STARTED) {
} throw new IllegalStateException(
if (status == FlowExecutionStatus.ENDED) { "No active FlowSession to access; this flow execution has not been started");
throw new IllegalStateException("No active session to access; this flow execution has ended"); } else {
throw new IllegalStateException("No active FlowSession to access; this flow execution has ended");
}
} }
return getActiveSessionInternal(); return getActiveSessionInternal();
} }
@@ -212,7 +214,6 @@ public class FlowExecutionImpl implements FlowExecution, Externalizable {
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("Starting in " + externalContext + " with input " + input); logger.debug("Starting in " + externalContext + " with input " + input);
} }
status = FlowExecutionStatus.STARTING;
MessageContext messageContext = createMessageContext(null); MessageContext messageContext = createMessageContext(null);
RequestControlContext requestContext = createRequestContext(externalContext, messageContext); RequestControlContext requestContext = createRequestContext(externalContext, messageContext);
RequestContextHolder.setRequestContext(requestContext); RequestContextHolder.setRequestContext(requestContext);
@@ -224,7 +225,7 @@ public class FlowExecutionImpl implements FlowExecution, Externalizable {
} catch (Exception e) { } catch (Exception e) {
handleException(wrap(e), requestContext); handleException(wrap(e), requestContext);
} finally { } finally {
if (!hasEnded()) { if (isActive()) {
saveMessages(requestContext); saveMessages(requestContext);
try { try {
listeners.firePaused(requestContext); listeners.firePaused(requestContext);
@@ -242,8 +243,9 @@ public class FlowExecutionImpl implements FlowExecution, Externalizable {
} }
public void resume(ExternalContext externalContext) throws FlowExecutionException, IllegalStateException { public void resume(ExternalContext externalContext) throws FlowExecutionException, IllegalStateException {
Assert.state(status == FlowExecutionStatus.STARTED, Assert
"This flow execution cannot be resumed; it is not started or has ended"); .state(status == FlowExecutionStatus.ACTIVE,
"This FlowExecution cannot be resumed because it is not active; it has either not been started or has ended");
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("Resuming in " + externalContext); logger.debug("Resuming in " + externalContext);
} }
@@ -260,7 +262,7 @@ public class FlowExecutionImpl implements FlowExecution, Externalizable {
} catch (Exception e) { } catch (Exception e) {
handleException(wrap(e), requestContext); handleException(wrap(e), requestContext);
} finally { } finally {
if (!hasEnded()) { if (isActive()) {
saveMessages(requestContext); saveMessages(requestContext);
try { try {
listeners.firePaused(requestContext); listeners.firePaused(requestContext);
@@ -288,7 +290,7 @@ public class FlowExecutionImpl implements FlowExecution, Externalizable {
FlowSessionImpl session; FlowSessionImpl session;
if (status == FlowExecutionStatus.NOT_STARTED) { if (status == FlowExecutionStatus.NOT_STARTED) {
session = activateSession(flow); session = activateSession(flow);
status = FlowExecutionStatus.STARTED; status = FlowExecutionStatus.ACTIVE;
} else { } else {
session = getActiveSessionInternal(); session = getActiveSessionInternal();
} }
@@ -296,19 +298,33 @@ public class FlowExecutionImpl implements FlowExecution, Externalizable {
session.setCurrentState(state); session.setCurrentState(state);
} }
private MessageContext createMessageContext(MessageSource messageSource) { // custom serialization (implementation of Externalizable for optimized storage)
StateManageableMessageContext messageContext = new DefaultMessageContext(messageSource);
Serializable messagesMemento = (Serializable) getFlashScope().extract("messagesMemento"); public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
if (messagesMemento != null) { status = (FlowExecutionStatus) in.readObject();
messageContext.restoreMessages(messagesMemento); flowSessions = (LinkedList) in.readObject();
}
return messageContext;
} }
private void saveMessages(RequestContext context) { public void writeExternal(ObjectOutput out) throws IOException {
StateManageableMessageContext messageContext = (StateManageableMessageContext) context.getMessageContext(); out.writeObject(status);
Serializable messagesMemento = messageContext.createMessagesMemento(); out.writeObject(flowSessions);
getFlashScope().put("messagesMemento", messagesMemento); }
public String toString() {
if (!isActive()) {
if (!hasStarted()) {
return "[Not yet started " + getCaption() + "]";
} else {
return "[Ended " + getCaption() + "]";
}
} else {
if (flow != null) {
return new ToStringCreator(this).append("flow", flow.getId()).append("flowSessions", flowSessions)
.toString();
} else {
return "[Unhydrated execution of '" + getRootSession().getFlowId() + "']";
}
}
} }
// subclassing hooks // subclassing hooks
@@ -337,7 +353,7 @@ public class FlowExecutionImpl implements FlowExecution, Externalizable {
listeners.fireSessionCreating(context, flow); listeners.fireSessionCreating(context, flow);
FlowSession session = activateSession(flow); FlowSession session = activateSession(flow);
if (session.isRoot()) { if (session.isRoot()) {
status = FlowExecutionStatus.STARTED; status = FlowExecutionStatus.ACTIVE;
} }
if (input == null) { if (input == null) {
input = new LocalAttributeMap(); input = new LocalAttributeMap();
@@ -372,17 +388,11 @@ public class FlowExecutionImpl implements FlowExecution, Externalizable {
boolean execute(Transition transition, RequestControlContext context) { boolean execute(Transition transition, RequestControlContext context) {
listeners.fireTransitionExecuting(context, transition); listeners.fireTransitionExecuting(context, transition);
return transition.execute(getCurrentState(), context); return transition.execute((State) getActiveSession().getState(), context);
} }
void endActiveFlowSession(String outcome, MutableAttributeMap output, RequestControlContext context) { void endActiveFlowSession(String outcome, MutableAttributeMap output, RequestControlContext context) {
FlowSessionImpl session = getActiveSessionInternal(); FlowSessionImpl session = getActiveSessionInternal();
if (session == null) {
throw new IllegalArgumentException("Cannot end the active FlowSession when one has not been activated");
}
if (session.isRoot()) {
status = FlowExecutionStatus.ENDING;
}
listeners.fireSessionEnding(context, session, outcome, output); listeners.fireSessionEnding(context, session, outcome, output);
session.getFlow().end(context, outcome, output); session.getFlow().end(context, outcome, output);
flowSessions.removeLast(); flowSessions.removeLast();
@@ -514,37 +524,17 @@ public class FlowExecutionImpl implements FlowExecution, Externalizable {
this.key = key; this.key = key;
} }
// custom serialization (implementation of Externalizable for optimized storage)
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
status = (FlowExecutionStatus) in.readObject();
flowSessions = (LinkedList) in.readObject();
}
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(status);
out.writeObject(flowSessions);
}
public String toString() {
if (!isActive()) {
if (!hasStarted()) {
return "[Not yet started " + getCaption() + "]";
} else {
return "[Ended " + getCaption() + "]";
}
} else {
if (flow != null) {
return new ToStringCreator(this).append("flow", flow.getId()).append("flowSessions", flowSessions)
.toString();
} else {
return "[Unhydrated execution of '" + getRootSession().getFlowId() + "']";
}
}
}
// internal helpers // internal helpers
private MessageContext createMessageContext(MessageSource messageSource) {
StateManageableMessageContext messageContext = new DefaultMessageContext(messageSource);
Serializable messagesMemento = (Serializable) getFlashScope().extract("messagesMemento");
if (messagesMemento != null) {
messageContext.restoreMessages(messagesMemento);
}
return messageContext;
}
/** /**
* Activate a new <code>FlowSession</code> for the flow definition. Creates the new flow session and pushes it onto * Activate a new <code>FlowSession</code> for the flow definition. Creates the new flow session and pushes it onto
* the stack. * the stack.
@@ -565,6 +555,12 @@ public class FlowExecutionImpl implements FlowExecution, Externalizable {
return (FlowSessionImpl) flowSessions.getLast(); return (FlowSessionImpl) flowSessions.getLast();
} }
private void saveMessages(RequestContext context) {
StateManageableMessageContext messageContext = (StateManageableMessageContext) context.getMessageContext();
Serializable messagesMemento = messageContext.createMessagesMemento();
getFlashScope().put("messagesMemento", messagesMemento);
}
private FlowExecutionException wrap(Exception e) { private FlowExecutionException wrap(Exception e) {
if (isActive()) { if (isActive()) {
FlowSession session = getActiveSession(); FlowSession session = getActiveSession();
@@ -596,6 +592,9 @@ public class FlowExecutionImpl implements FlowExecution, Externalizable {
logger.debug("Attempting to handle [" + exception + "]"); logger.debug("Attempting to handle [" + exception + "]");
} }
} }
if (!isActive()) {
throw exception;
}
boolean handled = false; boolean handled = false;
try { try {
if (tryStateHandlers(exception, context) || tryFlowHandlers(exception, context)) { if (tryStateHandlers(exception, context) || tryFlowHandlers(exception, context)) {
@@ -628,7 +627,8 @@ public class FlowExecutionImpl implements FlowExecution, Externalizable {
*/ */
private boolean tryStateHandlers(FlowExecutionException exception, RequestControlContext context) { private boolean tryStateHandlers(FlowExecutionException exception, RequestControlContext context) {
if (exception.getStateId() != null) { if (exception.getStateId() != null) {
return getCurrentFlow().getStateInstance(exception.getStateId()).handleException(exception, context); State state = getActiveSessionInternal().getFlow().getStateInstance(exception.getStateId());
return state.handleException(exception, context);
} else { } else {
return false; return false;
} }
@@ -640,28 +640,7 @@ public class FlowExecutionImpl implements FlowExecution, Externalizable {
* @return true if the exception was handled * @return true if the exception was handled
*/ */
private boolean tryFlowHandlers(FlowExecutionException exception, RequestControlContext context) { private boolean tryFlowHandlers(FlowExecutionException exception, RequestControlContext context) {
return getCurrentFlow().handleException(exception, context); return getActiveSessionInternal().getFlow().handleException(exception, context);
}
/**
* Returns the current flow which may or may not yet be active.
*/
private Flow getCurrentFlow() {
FlowSessionImpl session = getActiveSessionInternal();
if (session != null) {
return session.getFlow();
} else {
return flow;
}
}
private State getCurrentState() {
FlowSessionImpl session = getActiveSessionInternal();
if (session != null) {
return (State) session.getState();
} else {
return null;
}
} }
} }

View File

@@ -4,8 +4,7 @@ import org.springframework.core.enums.StaticLabeledEnum;
/** /**
* A enum used internally by {@link FlowExecutionImpl} to track its status. Flow Executions initially start out in * A enum used internally by {@link FlowExecutionImpl} to track its status. Flow Executions initially start out in
* NOT_STARTED. After start is called, they are STARTING. Once the root session is activated, they have STARTED. When * NOT_STARTED. After start is called, they are ACTIVE. After ending, their status is updated to ENDED.
* the execution begins to end, their status is ENDING. After ending, their status is updated to ENDED.
*/ */
public class FlowExecutionStatus extends StaticLabeledEnum { public class FlowExecutionStatus extends StaticLabeledEnum {
@@ -14,25 +13,15 @@ public class FlowExecutionStatus extends StaticLabeledEnum {
*/ */
public static final FlowExecutionStatus NOT_STARTED = new FlowExecutionStatus(0, "Not Started"); public static final FlowExecutionStatus NOT_STARTED = new FlowExecutionStatus(0, "Not Started");
/**
* The flow execution is in the process of starting; the root session has not yet been activated.
*/
public static final FlowExecutionStatus STARTING = new FlowExecutionStatus(1, "Starting");
/** /**
* The flow execution has started and a session is active. * The flow execution has started and a session is active.
*/ */
public static final FlowExecutionStatus STARTED = new FlowExecutionStatus(2, "Started"); public static final FlowExecutionStatus ACTIVE = new FlowExecutionStatus(1, "Active");
/**
* The flow execution is ending.
*/
public static final FlowExecutionStatus ENDING = new FlowExecutionStatus(3, "Ending");
/** /**
* The flow execution has ended. * The flow execution has ended.
*/ */
public static final FlowExecutionStatus ENDED = new FlowExecutionStatus(4, "Ended"); public static final FlowExecutionStatus ENDED = new FlowExecutionStatus(2, "Ended");
private FlowExecutionStatus(int code, String label) { private FlowExecutionStatus(int code, String label) {
super(code, label); super(code, label);

View File

@@ -32,7 +32,6 @@ import org.springframework.webflow.execution.Event;
import org.springframework.webflow.execution.FlowExecutionContext; import org.springframework.webflow.execution.FlowExecutionContext;
import org.springframework.webflow.execution.FlowExecutionException; import org.springframework.webflow.execution.FlowExecutionException;
import org.springframework.webflow.execution.FlowExecutionKey; import org.springframework.webflow.execution.FlowExecutionKey;
import org.springframework.webflow.execution.FlowSession;
import org.springframework.webflow.execution.View; import org.springframework.webflow.execution.View;
/** /**
@@ -99,13 +98,11 @@ class RequestControlContextImpl implements RequestControlContext {
// implementing RequestContext // implementing RequestContext
public FlowDefinition getActiveFlow() { public FlowDefinition getActiveFlow() {
FlowSession session = flowExecution.getActiveSession(); return flowExecution.getActiveSession().getDefinition();
return session != null ? session.getDefinition() : null;
} }
public StateDefinition getCurrentState() { public StateDefinition getCurrentState() {
FlowSession session = flowExecution.getActiveSession(); return flowExecution.getActiveSession().getState();
return session != null ? session.getState() : null;
} }
public TransitionDefinition getMatchingTransition(String eventId) throws IllegalStateException { public TransitionDefinition getMatchingTransition(String eventId) throws IllegalStateException {
@@ -125,13 +122,11 @@ class RequestControlContextImpl implements RequestControlContext {
} }
public MutableAttributeMap getViewScope() throws IllegalStateException { public MutableAttributeMap getViewScope() throws IllegalStateException {
FlowSession session = flowExecution.getActiveSession(); return flowExecution.getActiveSession().getViewScope();
return session != null ? session.getViewScope() : null;
} }
public MutableAttributeMap getFlowScope() { public MutableAttributeMap getFlowScope() {
FlowSession session = flowExecution.getActiveSession(); return flowExecution.getActiveSession().getScope();
return session != null ? session.getScope() : null;
} }
public MutableAttributeMap getConversationScope() { public MutableAttributeMap getConversationScope() {

View File

@@ -66,9 +66,9 @@ public interface FlowExecutionContext {
public boolean hasStarted(); public boolean hasStarted();
/** /**
* Is the flow execution active? A flow execution is active once it has started and remains active until it has * Is the flow execution active? A flow execution is active once it has an {@link #getActiveSession() active
* ended. * session} and remains active until it has ended.
* @return true if active, false if the flow execution has terminated or has not yet started * @return true if active, false if the flow execution has terminated or has not yet been started
*/ */
public boolean isActive(); public boolean isActive();
@@ -90,9 +90,10 @@ public interface FlowExecutionContext {
/** /**
* Returns the active flow session of this flow execution. The active flow session is the currently executing * Returns the active flow session of this flow execution. The active flow session is the currently executing
* session. It may be the "root flow" session, or it may be a subflow session if this flow execution has spawned a * session. It may be the "root flow" session, or it may be a subflow session if this flow execution has spawned a
* subflow. Returns null of this flow execution is in the process of starting. * subflow.
* @return the active flow session * @return the active flow session
* @throws IllegalStateException if this flow execution has not been started at all or has ended * @throws IllegalStateException if this flow execution is not active
* @see #isActive()
*/ */
public FlowSession getActiveSession() throws IllegalStateException; public FlowSession getActiveSession() throws IllegalStateException;

View File

@@ -58,18 +58,19 @@ import org.springframework.webflow.definition.TransitionDefinition;
public interface RequestContext { public interface RequestContext {
/** /**
* Returns the definition of the flow that is currently executing. Returns <code>null</code> when the flow execution * Returns the definition of the flow that is currently executing.
* is in the process of starting and has not yet activated its root flow session.
* @return the flow definition for the active session * @return the flow definition for the active session
* @throws IllegalStateException if the flow execution has not been started at all or has ended * @throws IllegalStateException if the flow execution is not active
* @see FlowExecutionContext#isActive()
*/ */
public FlowDefinition getActiveFlow() throws IllegalStateException; public FlowDefinition getActiveFlow() throws IllegalStateException;
/** /**
* Returns the current state of the executiong flow. Returns <code>null</code> if this flow execution is in the * Returns the current state of the executing flow. Returns <code>null</code> if the active flow's start state has
* process of starting and has not yet entered its start state. * not yet been entered.
* @return the current state, or <code>null</code> if in the process of starting * @return the current state, or <code>null</code> if in the process of starting
* @throws IllegalStateException if this flow execution has not been started at all or has ended * @throws IllegalStateException if this flow execution is not active
* @see FlowExecutionContext#isActive()
*/ */
public StateDefinition getCurrentState() throws IllegalStateException; public StateDefinition getCurrentState() throws IllegalStateException;
@@ -77,7 +78,8 @@ public interface RequestContext {
* Returns the transition that would execute on the occurrence of the given event. * Returns the transition that would execute on the occurrence of the given event.
* @param eventId the id of the user event * @param eventId the id of the user event
* @return the transition that would trigger, or <code>null</code> if no transition matches * @return the transition that would trigger, or <code>null</code> if no transition matches
* @throws IllegalStateException if this flow execution has not been started at all or has ended * @throws IllegalStateException if this flow execution is not active
* @see FlowExecutionContext#isActive()
*/ */
public TransitionDefinition getMatchingTransition(String eventId) throws IllegalStateException; public TransitionDefinition getMatchingTransition(String eventId) throws IllegalStateException;
@@ -106,23 +108,22 @@ public interface RequestContext {
/** /**
* Returns a mutable map for accessing and/or setting attributes in view scope. <b>View scoped attributes exist for * Returns a mutable map for accessing and/or setting attributes in view scope. <b>View scoped attributes exist for
* the life of the current view state.</b> * the life of the current view state.</b>
* @return the view scope, or null if the flow execution is in the process of starting but has not yet completed * @return the view scope
* startup
* @see #inViewState() * @see #inViewState()
* @throws IllegalStateException if this flow is not in a view-state, or the flow execution has not been started at * @throws IllegalStateException if this flow is not in a view-state or the flow execution is not active
* all or has ended * @see FlowExecutionContext#isActive()
*/ */
public MutableAttributeMap getViewScope() throws IllegalStateException; public MutableAttributeMap getViewScope() throws IllegalStateException;
/** /**
* Returns a mutable map for accessing and/or setting attributes in flow scope. <b>Flow scoped attributes exist for * Returns a mutable map for accessing and/or setting attributes in flow scope. <b>Flow scoped attributes exist for
* the life of the active flow session.</b> * the life of the active flow session.</b>
* @return the flow scope, or null if the the flow execution is in the process of starting but has not yet completed * @return the flow scope
* startup
* @see FlowSession * @see FlowSession
* @throws IllegalStateException if the flow execution has not been started at all or has ended * @throws IllegalStateException if the flow execution is not active
* @see FlowExecutionContext#isActive()
*/ */
public MutableAttributeMap getFlowScope(); public MutableAttributeMap getFlowScope() throws IllegalStateException;
/** /**
* Returns a mutable accessor for accessing and/or setting attributes in conversation scope. <b>Conversation scoped * Returns a mutable accessor for accessing and/or setting attributes in conversation scope. <b>Conversation scoped

View File

@@ -127,12 +127,6 @@ public class FlowExecutionImplTests extends TestCase {
new EndState(flow, "end"); new EndState(flow, "end");
FlowExecutionListener mockListener = new FlowExecutionListenerAdapter() { FlowExecutionListener mockListener = new FlowExecutionListenerAdapter() {
public void sessionCreating(RequestContext context, FlowDefinition definition) { public void sessionCreating(RequestContext context, FlowDefinition definition) {
assertNull(context.getCurrentState());
assertNull(context.getActiveFlow());
assertNull(context.getCurrentEvent());
assertNull(context.getCurrentTransition());
assertNull(context.getActiveFlow());
assertNull(context.getFlowExecutionContext().getActiveSession());
assertFalse(context.getFlowExecutionContext().isActive()); assertFalse(context.getFlowExecutionContext().isActive());
throw new IllegalStateException("Oops"); throw new IllegalStateException("Oops");
} }
@@ -149,6 +143,7 @@ public class FlowExecutionImplTests extends TestCase {
assertEquals(flow.getId(), e.getFlowId()); assertEquals(flow.getId(), e.getFlowId());
assertNull(e.getStateId()); assertNull(e.getStateId());
assertTrue(e.getCause() instanceof IllegalStateException); assertTrue(e.getCause() instanceof IllegalStateException);
e.printStackTrace();
assertTrue(e.getCause().getMessage().equals("Oops")); assertTrue(e.getCause().getMessage().equals("Oops"));
} }
} }