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;
/**
* 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
* 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
* 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,
* the handler must take responsibility for ensuring the flow execution returns control to the caller in a consistent
* state. Concretely, this means the exception handler must transition the flow to its start state. The handler should
* not simply return leaving the flow with no current state set.
* its future use undefined. For example, if an exception thrown at flow session startup gets routed to an exception
* handler, the handler must take responsibility for ensuring the flow execution returns control to the caller in a
* consistent state. Concretely, this means the exception handler must transition the flow to its start state. The
* 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
* 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() {
return status != FlowExecutionStatus.NOT_STARTED;
return status == FlowExecutionStatus.ACTIVE || status == FlowExecutionStatus.ENDED;
}
public boolean isActive() {
return !flowSessions.isEmpty();
return status == FlowExecutionStatus.ACTIVE;
}
public boolean hasEnded() {
@@ -183,11 +183,13 @@ public class FlowExecutionImpl implements FlowExecution, Externalizable {
}
public FlowSession getActiveSession() {
if (status == FlowExecutionStatus.NOT_STARTED) {
throw new IllegalStateException("No active session to access; this flow execution has not been started");
}
if (status == FlowExecutionStatus.ENDED) {
throw new IllegalStateException("No active session to access; this flow execution has ended");
if (!isActive()) {
if (status == FlowExecutionStatus.NOT_STARTED) {
throw new IllegalStateException(
"No active FlowSession to access; this flow execution has not been started");
} else {
throw new IllegalStateException("No active FlowSession to access; this flow execution has ended");
}
}
return getActiveSessionInternal();
}
@@ -212,7 +214,6 @@ public class FlowExecutionImpl implements FlowExecution, Externalizable {
if (logger.isDebugEnabled()) {
logger.debug("Starting in " + externalContext + " with input " + input);
}
status = FlowExecutionStatus.STARTING;
MessageContext messageContext = createMessageContext(null);
RequestControlContext requestContext = createRequestContext(externalContext, messageContext);
RequestContextHolder.setRequestContext(requestContext);
@@ -224,7 +225,7 @@ public class FlowExecutionImpl implements FlowExecution, Externalizable {
} catch (Exception e) {
handleException(wrap(e), requestContext);
} finally {
if (!hasEnded()) {
if (isActive()) {
saveMessages(requestContext);
try {
listeners.firePaused(requestContext);
@@ -242,8 +243,9 @@ public class FlowExecutionImpl implements FlowExecution, Externalizable {
}
public void resume(ExternalContext externalContext) throws FlowExecutionException, IllegalStateException {
Assert.state(status == FlowExecutionStatus.STARTED,
"This flow execution cannot be resumed; it is not started or has ended");
Assert
.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()) {
logger.debug("Resuming in " + externalContext);
}
@@ -260,7 +262,7 @@ public class FlowExecutionImpl implements FlowExecution, Externalizable {
} catch (Exception e) {
handleException(wrap(e), requestContext);
} finally {
if (!hasEnded()) {
if (isActive()) {
saveMessages(requestContext);
try {
listeners.firePaused(requestContext);
@@ -288,7 +290,7 @@ public class FlowExecutionImpl implements FlowExecution, Externalizable {
FlowSessionImpl session;
if (status == FlowExecutionStatus.NOT_STARTED) {
session = activateSession(flow);
status = FlowExecutionStatus.STARTED;
status = FlowExecutionStatus.ACTIVE;
} else {
session = getActiveSessionInternal();
}
@@ -296,19 +298,33 @@ public class FlowExecutionImpl implements FlowExecution, Externalizable {
session.setCurrentState(state);
}
private MessageContext createMessageContext(MessageSource messageSource) {
StateManageableMessageContext messageContext = new DefaultMessageContext(messageSource);
Serializable messagesMemento = (Serializable) getFlashScope().extract("messagesMemento");
if (messagesMemento != null) {
messageContext.restoreMessages(messagesMemento);
}
return messageContext;
// custom serialization (implementation of Externalizable for optimized storage)
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
status = (FlowExecutionStatus) in.readObject();
flowSessions = (LinkedList) in.readObject();
}
private void saveMessages(RequestContext context) {
StateManageableMessageContext messageContext = (StateManageableMessageContext) context.getMessageContext();
Serializable messagesMemento = messageContext.createMessagesMemento();
getFlashScope().put("messagesMemento", messagesMemento);
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() + "']";
}
}
}
// subclassing hooks
@@ -337,7 +353,7 @@ public class FlowExecutionImpl implements FlowExecution, Externalizable {
listeners.fireSessionCreating(context, flow);
FlowSession session = activateSession(flow);
if (session.isRoot()) {
status = FlowExecutionStatus.STARTED;
status = FlowExecutionStatus.ACTIVE;
}
if (input == null) {
input = new LocalAttributeMap();
@@ -372,17 +388,11 @@ public class FlowExecutionImpl implements FlowExecution, Externalizable {
boolean execute(Transition transition, RequestControlContext context) {
listeners.fireTransitionExecuting(context, transition);
return transition.execute(getCurrentState(), context);
return transition.execute((State) getActiveSession().getState(), context);
}
void endActiveFlowSession(String outcome, MutableAttributeMap output, RequestControlContext context) {
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);
session.getFlow().end(context, outcome, output);
flowSessions.removeLast();
@@ -514,37 +524,17 @@ public class FlowExecutionImpl implements FlowExecution, Externalizable {
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
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
* the stack.
@@ -565,6 +555,12 @@ public class FlowExecutionImpl implements FlowExecution, Externalizable {
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) {
if (isActive()) {
FlowSession session = getActiveSession();
@@ -596,6 +592,9 @@ public class FlowExecutionImpl implements FlowExecution, Externalizable {
logger.debug("Attempting to handle [" + exception + "]");
}
}
if (!isActive()) {
throw exception;
}
boolean handled = false;
try {
if (tryStateHandlers(exception, context) || tryFlowHandlers(exception, context)) {
@@ -628,7 +627,8 @@ public class FlowExecutionImpl implements FlowExecution, Externalizable {
*/
private boolean tryStateHandlers(FlowExecutionException exception, RequestControlContext context) {
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 {
return false;
}
@@ -640,28 +640,7 @@ public class FlowExecutionImpl implements FlowExecution, Externalizable {
* @return true if the exception was handled
*/
private boolean tryFlowHandlers(FlowExecutionException exception, RequestControlContext context) {
return getCurrentFlow().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;
}
return getActiveSessionInternal().getFlow().handleException(exception, context);
}
}

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
* NOT_STARTED. After start is called, they are STARTING. Once the root session is activated, they have STARTED. When
* the execution begins to end, their status is ENDING. After ending, their status is updated to ENDED.
* NOT_STARTED. After start is called, they are ACTIVE. After ending, their status is updated to ENDED.
*/
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");
/**
* 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.
*/
public static final FlowExecutionStatus STARTED = new FlowExecutionStatus(2, "Started");
/**
* The flow execution is ending.
*/
public static final FlowExecutionStatus ENDING = new FlowExecutionStatus(3, "Ending");
public static final FlowExecutionStatus ACTIVE = new FlowExecutionStatus(1, "Active");
/**
* 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) {
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.FlowExecutionException;
import org.springframework.webflow.execution.FlowExecutionKey;
import org.springframework.webflow.execution.FlowSession;
import org.springframework.webflow.execution.View;
/**
@@ -99,13 +98,11 @@ class RequestControlContextImpl implements RequestControlContext {
// implementing RequestContext
public FlowDefinition getActiveFlow() {
FlowSession session = flowExecution.getActiveSession();
return session != null ? session.getDefinition() : null;
return flowExecution.getActiveSession().getDefinition();
}
public StateDefinition getCurrentState() {
FlowSession session = flowExecution.getActiveSession();
return session != null ? session.getState() : null;
return flowExecution.getActiveSession().getState();
}
public TransitionDefinition getMatchingTransition(String eventId) throws IllegalStateException {
@@ -125,13 +122,11 @@ class RequestControlContextImpl implements RequestControlContext {
}
public MutableAttributeMap getViewScope() throws IllegalStateException {
FlowSession session = flowExecution.getActiveSession();
return session != null ? session.getViewScope() : null;
return flowExecution.getActiveSession().getViewScope();
}
public MutableAttributeMap getFlowScope() {
FlowSession session = flowExecution.getActiveSession();
return session != null ? session.getScope() : null;
return flowExecution.getActiveSession().getScope();
}
public MutableAttributeMap getConversationScope() {

View File

@@ -66,9 +66,9 @@ public interface FlowExecutionContext {
public boolean hasStarted();
/**
* Is the flow execution active? A flow execution is active once it has started and remains active until it has
* ended.
* @return true if active, false if the flow execution has terminated or has not yet started
* Is the flow execution active? A flow execution is active once it has an {@link #getActiveSession() active
* session} and remains active until it has ended.
* @return true if active, false if the flow execution has terminated or has not yet been started
*/
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
* 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
* @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;

View File

@@ -58,18 +58,19 @@ import org.springframework.webflow.definition.TransitionDefinition;
public interface RequestContext {
/**
* Returns the definition of the flow that is currently executing. Returns <code>null</code> when the flow execution
* is in the process of starting and has not yet activated its root flow session.
* Returns the definition of the flow that is currently executing.
* @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;
/**
* Returns the current state of the executiong flow. Returns <code>null</code> if this flow execution is in the
* process of starting and has not yet entered its start state.
* Returns the current state of the executing flow. Returns <code>null</code> if the active flow's start state has
* not yet been entered.
* @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;
@@ -77,7 +78,8 @@ public interface RequestContext {
* Returns the transition that would execute on the occurrence of the given event.
* @param eventId the id of the user event
* @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;
@@ -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
* 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
* startup
* @return the view scope
* @see #inViewState()
* @throws IllegalStateException if this flow is not in a view-state, or the flow execution has not been started at
* all or has ended
* @throws IllegalStateException if this flow is not in a view-state or the flow execution is not active
* @see FlowExecutionContext#isActive()
*/
public MutableAttributeMap getViewScope() throws IllegalStateException;
/**
* 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>
* @return the flow scope, or null if the the flow execution is in the process of starting but has not yet completed
* startup
* @return the flow scope
* @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

View File

@@ -127,12 +127,6 @@ public class FlowExecutionImplTests extends TestCase {
new EndState(flow, "end");
FlowExecutionListener mockListener = new FlowExecutionListenerAdapter() {
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());
throw new IllegalStateException("Oops");
}
@@ -149,6 +143,7 @@ public class FlowExecutionImplTests extends TestCase {
assertEquals(flow.getId(), e.getFlowId());
assertNull(e.getStateId());
assertTrue(e.getCause() instanceof IllegalStateException);
e.printStackTrace();
assertTrue(e.getCause().getMessage().equals("Oops"));
}
}