/*
* 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.impl;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.Serializable;
import java.util.Iterator;
import java.util.LinkedList;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.binding.message.DefaultMessageContext;
import org.springframework.binding.message.MessageContext;
import org.springframework.binding.message.StateManageableMessageContext;
import org.springframework.context.MessageSource;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
import org.springframework.webflow.context.ExternalContext;
import org.springframework.webflow.core.collection.AttributeMap;
import org.springframework.webflow.core.collection.CollectionUtils;
import org.springframework.webflow.core.collection.LocalAttributeMap;
import org.springframework.webflow.core.collection.MutableAttributeMap;
import org.springframework.webflow.definition.FlowDefinition;
import org.springframework.webflow.definition.TransitionDefinition;
import org.springframework.webflow.engine.Flow;
import org.springframework.webflow.engine.RequestControlContext;
import org.springframework.webflow.engine.State;
import org.springframework.webflow.engine.Transition;
import org.springframework.webflow.engine.TransitionableState;
import org.springframework.webflow.execution.Event;
import org.springframework.webflow.execution.FlowExecution;
import org.springframework.webflow.execution.FlowExecutionException;
import org.springframework.webflow.execution.FlowExecutionKey;
import org.springframework.webflow.execution.FlowExecutionKeyFactory;
import org.springframework.webflow.execution.FlowExecutionListener;
import org.springframework.webflow.execution.FlowExecutionOutcome;
import org.springframework.webflow.execution.FlowSession;
import org.springframework.webflow.execution.RequestContext;
import org.springframework.webflow.execution.RequestContextHolder;
import org.springframework.webflow.execution.View;
/**
* Default implementation of FlowExecution that uses a stack-based data structure to manage spawned flow sessions. This
* class is closely coupled with package-private <code>FlowSessionImpl</code> and <code>RequestControlContextImpl</code>
* . The three classes work together to form a complete flow execution implementation based on a finite state machine.
* <p>
* This implementation of FlowExecution is serializable so it can be safely stored in an HTTP session or other
* persistent store such as a file, database, or client-side form field. Once deserialized, the
* {@link FlowExecutionImplFactory} is expected to be used to restore the execution to a usable state.
*
* @see FlowExecutionImplFactory
*
* @author Keith Donald
* @author Erwin Vervaet
* @author Jeremy Grelle
*/
public class FlowExecutionImpl implements FlowExecution, Externalizable {
private static final Log logger = LogFactory.getLog(FlowExecutionImpl.class);
private static final String FLASH_SCOPE_ATTRIBUTE = "flashScope";
/**
* The execution's root flow; the top level flow that acts as the starting point for this flow execution.
* <p>
* Transient to support restoration by the {@link FlowExecutionImplFactory}.
*/
private transient Flow flow;
/**
* A enum tracking the status of this flow execution.
*/
private FlowExecutionStatus status;
/**
* The stack of active, currently executing flow sessions. As subflows are spawned, they are pushed onto the stack.
* As they end, they are popped off the stack.
*/
private LinkedList<FlowSessionImpl> flowSessions;
/**
* A thread-safe listener list, holding listeners monitoring the lifecycle of this flow execution.
* <p>
* Transient to support restoration by the {@link FlowExecutionImplFactory}.
*/
private transient FlowExecutionListeners listeners;
/**
* The factory for getting the key to assign this flow execution when needed for persistence.
*/
private transient FlowExecutionKeyFactory keyFactory;
/**
* The key assigned to this flow execution. May be null if a key has not been assigned.
*/
private transient FlowExecutionKey key;
/**
* A data structure for attributes shared by all flow sessions.
* <p>
* Transient to support restoration by the {@link FlowExecutionImplFactory}.
*/
private transient MutableAttributeMap<Object> conversationScope;
/**
* A data structure for runtime system execution attributes.
* <p>
* Transient to support restoration by the {@link FlowExecutionImplFactory}.
*/
private transient AttributeMap<Object> attributes;
/**
* The outcome reached by this flow execution when it ends.
*/
private transient FlowExecutionOutcome outcome;
/**
* Default constructor required for externalizable serialization. Should NOT be called programmatically.
*/
public FlowExecutionImpl() {
}
/**
* Create a new flow execution executing the provided flow. Flow executions are normally created by a flow execution
* factory.
* @param flow the root flow of this flow execution
*/
public FlowExecutionImpl(Flow flow) {
Assert.notNull(flow, "The flow definition is required");
this.flow = flow;
status = FlowExecutionStatus.NOT_STARTED;
listeners = new FlowExecutionListeners();
attributes = CollectionUtils.EMPTY_ATTRIBUTE_MAP;
flowSessions = new LinkedList<FlowSessionImpl>();
conversationScope = new LocalAttributeMap<Object>();
conversationScope.put(FLASH_SCOPE_ATTRIBUTE, new LocalAttributeMap<Object>());
}
public String getCaption() {
return "execution of '" + flow.getId() + "'";
}
// implementing FlowExecutionContext
public FlowExecutionKey getKey() {
return key;
}
public FlowDefinition getDefinition() {
return flow;
}
public boolean hasStarted() {
return status == FlowExecutionStatus.ACTIVE || status == FlowExecutionStatus.ENDED;
}
public boolean isActive() {
return status == FlowExecutionStatus.ACTIVE;
}
public boolean hasEnded() {
return status == FlowExecutionStatus.ENDED;
}
public FlowExecutionOutcome getOutcome() {
return outcome;
}
public FlowSession getActiveSession() {
if (!isActive()) {
if (status == FlowExecutionStatus.NOT_STARTED) {
throw new IllegalStateException(
"No active FlowSession to access; this FlowExecution has not been started");
} else {
throw new IllegalStateException("No active FlowSession to access; this FlowExecution has ended");
}
}
return getActiveSessionInternal();
}
@SuppressWarnings("unchecked")
public MutableAttributeMap<Object> getFlashScope() {
return (MutableAttributeMap<Object>) conversationScope.get(FLASH_SCOPE_ATTRIBUTE);
}
public MutableAttributeMap<Object> getConversationScope() {
return conversationScope;
}
public AttributeMap<Object> getAttributes() {
return attributes;
}
// methods implementing FlowExecution
public void start(MutableAttributeMap<?> input, ExternalContext externalContext) throws FlowExecutionException,
IllegalStateException {
Assert.state(!hasStarted(), "This flow has already been started; you cannot call 'start()' more than once");
if (logger.isDebugEnabled()) {
logger.debug("Starting in " + externalContext + " with input " + input);
}
MessageContext messageContext = createMessageContext(null);
RequestControlContext requestContext = createRequestContext(externalContext, messageContext);
RequestContextHolder.setRequestContext(requestContext);
listeners.fireRequestSubmitted(requestContext);
try {
start(flow, input, requestContext);
} catch (FlowExecutionException e) {
handleException(e, requestContext);
} catch (Exception e) {
handleException(wrap(e), requestContext);
} finally {
saveFlashMessages(requestContext);
if (isActive()) {
try {
listeners.firePaused(requestContext);
} catch (Throwable e) {
logger.error("FlowExecutionListener threw exception", e);
}
}
try {
listeners.fireRequestProcessed(requestContext);
} catch (Throwable e) {
logger.error("FlowExecutionListener threw exception", e);
}
RequestContextHolder.setRequestContext(null);
}
}
public void resume(ExternalContext externalContext) throws FlowExecutionException, IllegalStateException {
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);
}
Flow activeFlow = getActiveSessionInternal().getFlow();
MessageContext messageContext = createMessageContext(activeFlow.getApplicationContext());
RequestControlContext requestContext = createRequestContext(externalContext, messageContext);
RequestContextHolder.setRequestContext(requestContext);
listeners.fireRequestSubmitted(requestContext);
try {
listeners.fireResuming(requestContext);
activeFlow.resume(requestContext);
} catch (FlowExecutionException e) {
handleException(e, requestContext);
} catch (Exception e) {
handleException(wrap(e), requestContext);
} finally {
saveFlashMessages(requestContext);
if (isActive()) {
try {
listeners.firePaused(requestContext);
} catch (Throwable e) {
logger.error("FlowExecutionListener threw exception", e);
}
}
try {
listeners.fireRequestProcessed(requestContext);
} catch (Throwable e) {
logger.error("FlowExecutionListener threw exception", e);
}
RequestContextHolder.setRequestContext(null);
}
}
/**
* Jump to a state of the currently active flow. If this execution has not been started, a new session will be
* activated and its current state will be set. This is a implementation-internal method that bypasses the
* {@link #start(MutableAttributeMap, ExternalContext)} operation and allows for jumping to an arbitrary flow state.
* Useful for testing.
* @param stateId the identifier of the state to jump to
*/
public void setCurrentState(String stateId) {
FlowSessionImpl session;
if (status == FlowExecutionStatus.NOT_STARTED) {
session = activateSession(flow);
status = FlowExecutionStatus.ACTIVE;
} else {
session = getActiveSessionInternal();
}
State state = session.getFlow().getStateInstance(stateId);
session.setCurrentState(state);
}
// custom serialization (implementation of Externalizable for optimized storage)
@SuppressWarnings("unchecked")
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
status = (FlowExecutionStatus) in.readObject();
flowSessions = (LinkedList<FlowSessionImpl>) 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() + "']";
}
}
}
// subclassing hooks
/**
* Create a flow execution control context.
* @param externalContext the external context triggering this request
*/
protected RequestControlContext createRequestContext(ExternalContext externalContext, MessageContext messageContext) {
return new RequestControlContextImpl(this, externalContext, messageContext);
}
/**
* Create a new flow session object. Subclasses can override this to return a special implementation if required.
* @param flow the flow that should be associated with the flow session
* @param parent the flow session that should be the parent of the newly created flow session (may be null)
* @return the newly created flow session
*/
protected FlowSessionImpl createFlowSession(Flow flow, FlowSessionImpl parent) {
return new FlowSessionImpl(flow, parent);
}
// package private request control context callbacks
void start(Flow flow, MutableAttributeMap<?> input, RequestControlContext context) {
listeners.fireSessionCreating(context, flow);
FlowSessionImpl session = activateSession(flow);
if (session.isRoot()) {
status = FlowExecutionStatus.ACTIVE;
}
if (input == null) {
input = new LocalAttributeMap<Object>();
}
if (hasEmbeddedModeAttribute(input)) {
session.setEmbeddedMode();
}
StateManageableMessageContext messageContext = (StateManageableMessageContext) context.getMessageContext();
messageContext.setMessageSource(flow.getApplicationContext());
listeners.fireSessionStarting(context, session, input);
flow.start(context, input);
listeners.fireSessionStarted(context, session);
}
void setCurrentState(State newState, RequestContext context) {
listeners.fireStateEntering(context, newState);
FlowSessionImpl session = getActiveSessionInternal();
State previousState = (State) session.getState();
session.setCurrentState(newState);
listeners.fireStateEntered(context, previousState);
}
public void viewRendering(View view, RequestContext context) {
listeners.fireViewRendering(context, view);
}
public void viewRendered(View view, RequestContext context) {
listeners.fireViewRendered(context, view);
}
boolean handleEvent(Event event, RequestControlContext context) {
listeners.fireEventSignaled(context, event);
return getActiveSessionInternal().getFlow().handleEvent(context);
}
boolean execute(Transition transition, RequestControlContext context) {
listeners.fireTransitionExecuting(context, transition);
return transition.execute((State) getActiveSession().getState(), context);
}
void endActiveFlowSession(String outcome, MutableAttributeMap<Object> output, RequestControlContext context) {
FlowSessionImpl session = getActiveSessionInternal();
listeners.fireSessionEnding(context, session, outcome, output);
session.getFlow().end(context, outcome, output);
flowSessions.removeLast();
boolean executionEnded = flowSessions.isEmpty();
if (executionEnded) {
// set the root flow execution outcome for external clients to use
this.outcome = new FlowExecutionOutcome(outcome, output);
status = FlowExecutionStatus.ENDED;
}
listeners.fireSessionEnded(context, session, outcome, output);
if (!executionEnded) {
// restore any variables that may have transient references
getActiveSessionInternal().getFlow().restoreVariables(context);
// treat the outcome as an event against the current state of the new active flow
context.handleEvent(new Event(session.getState(), outcome, output));
}
}
FlowExecutionKey assignKey() {
key = keyFactory.getKey(this);
if (logger.isDebugEnabled()) {
logger.debug("Assigned key " + key);
}
return key;
}
void updateCurrentFlowExecutionSnapshot() {
keyFactory.updateFlowExecutionSnapshot(this);
}
void removeCurrentFlowExecutionSnapshot() {
keyFactory.removeFlowExecutionSnapshot(this);
}
void removeAllFlowExecutionSnapshots() {
keyFactory.removeAllFlowExecutionSnapshots(this);
}
TransitionDefinition getMatchingTransition(String eventId) {
FlowSessionImpl session = getActiveSessionInternal();
if (session == null) {
return null;
}
TransitionableState currentState = (TransitionableState) session.getState();
TransitionDefinition transition = currentState.getTransition(eventId);
if (transition == null) {
transition = session.getFlow().getGlobalTransition(eventId);
}
return transition;
}
// package private setters for restoring transient state used by FlowExecutionImplServicesConfigurer
FlowExecutionListener[] getListeners() {
return listeners.getArray();
}
void setListeners(FlowExecutionListener[] listeners) {
this.listeners = new FlowExecutionListeners(listeners);
}
void setAttributes(AttributeMap<Object> attributes) {
this.attributes = attributes;
}
FlowExecutionKeyFactory getKeyFactory() {
return keyFactory;
}
void setKeyFactory(FlowExecutionKeyFactory keyFactory) {
this.keyFactory = keyFactory;
}
// Used by {@link FlowExecutionImplFactory}
/**
* Returns the list of flow session maintained by this flow execution.
*/
LinkedList<FlowSessionImpl> getFlowSessions() {
return flowSessions;
}
/**
* Are there any flow sessions in this flow execution?
*/
boolean hasSessions() {
return !flowSessions.isEmpty();
}
/**
* Are there any sessions for sub flows in this flow execution?
*/
boolean hasSubflowSessions() {
return flowSessions.size() > 1;
}
/**
* Returns the flow session for the root flow of this flow execution.
*/
FlowSessionImpl getRootSession() {
return flowSessions.getFirst();
}
/**
* Returns an iterator looping over the subflow sessions in this flow execution.
*/
Iterator<FlowSessionImpl> getSubflowSessionIterator() {
return flowSessions.listIterator(1);
}
/**
* Restore the flow definition of this flow execution.
*/
void setFlow(Flow flow) {
this.flow = flow;
}
/**
* Restore conversation scope for this flow execution.
*/
void setConversationScope(MutableAttributeMap<Object> conversationScope) {
this.conversationScope = conversationScope;
}
/**
* Restore the flow execution key.
*/
void setKey(FlowExecutionKey key) {
this.key = key;
}
// 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.
* @param flow the flow definition
* @return the new flow session
*/
private FlowSessionImpl activateSession(Flow flow) {
FlowSessionImpl parent = getActiveSessionInternal();
FlowSessionImpl session = createFlowSession(flow, parent);
flowSessions.add(session);
return session;
}
private FlowSessionImpl getActiveSessionInternal() {
if (flowSessions.isEmpty()) {
return null;
}
return flowSessions.getLast();
}
private void saveFlashMessages(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();
String flowId = session.getDefinition().getId();
String stateId = session.getState() != null ? session.getState().getId() : null;
return new FlowExecutionException(flowId, stateId, "Exception thrown in state '" + stateId + "' of flow '"
+ flowId + "'", e);
} else {
return new FlowExecutionException(flow.getId(), null, "Exception thrown within inactive flow '"
+ flow.getId() + "'", e);
}
}
/**
* Handles an exception that occurred performing an operation on this flow execution. First tries the set of
* exception handlers associated with the offending state, then the handlers at the flow level.
* @param exception the exception that occurred
* @param context the request control context the exception occurred in
* @throws FlowExecutionException re-throws the exception if it was not handled at the state or flow level
*/
private void handleException(FlowExecutionException exception, RequestControlContext context) {
listeners.fireExceptionThrown(context, exception);
if (logger.isDebugEnabled()) {
if (exception.getCause() != null) {
logger.debug("Attempting to handle [" + exception + "] with root cause [" + getRootCause(exception)
+ "]");
} else {
logger.debug("Attempting to handle [" + exception + "]");
}
}
if (!isActive()) {
throw exception;
}
boolean handled = false;
try {
if (tryStateHandlers(exception, context) || tryFlowHandlers(exception, context)) {
handled = true;
}
} catch (FlowExecutionException newException) {
// exception handling itself resulted in a new FlowExecutionException, try to handle it
handleException(newException, context);
handled = true;
}
if (!handled) {
if (logger.isDebugEnabled()) {
logger.debug("Rethrowing unhandled flow execution exception");
}
throw exception;
}
}
/**
* Get the root cause of the given throwable.
*/
private Throwable getRootCause(Throwable e) {
Throwable cause = e.getCause();
return cause == null ? e : getRootCause(cause);
}
/**
* Try to handle given exception using execution exception handlers registered at the state level. Returns null if
* no handler handled the exception.
* @return true if the exception was handled
*/
private boolean tryStateHandlers(FlowExecutionException exception, RequestControlContext context) {
if (exception.getStateId() != null) {
State state = getActiveSessionInternal().getFlow().getStateInstance(exception.getStateId());
return state.handleException(exception, context);
} else {
return false;
}
}
/**
* Try to handle given exception using execution exception handlers registered at the flow level. Returns null if no
* handler handled the exception.
* @return true if the exception was handled
*/
private boolean tryFlowHandlers(FlowExecutionException exception, RequestControlContext context) {
return getActiveSessionInternal().getFlow().handleException(exception, context);
}
private boolean hasEmbeddedModeAttribute(AttributeMap<?> input) {
if (input != null) {
String mode = (String) input.get("mode");
if (mode != null && mode.trim().toLowerCase().equals("embedded")) {
return true;
}
}
return false;
}
}