Flow.java

/*
 * 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
 *
 *      https://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;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.binding.mapping.Mapper;
import org.springframework.binding.mapping.MappingResults;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.style.StylerUtils;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.webflow.core.AnnotatedObject;
import org.springframework.webflow.core.collection.AttributeMap;
import org.springframework.webflow.core.collection.MutableAttributeMap;
import org.springframework.webflow.definition.FlowDefinition;
import org.springframework.webflow.definition.StateDefinition;
import org.springframework.webflow.definition.TransitionDefinition;
import org.springframework.webflow.execution.FlowExecutionException;
import org.springframework.webflow.execution.RequestContext;

/**
 * A single flow definition. A Flow definition is a reusable, self-contained controller module that provides the blue
 * print for a user dialog or conversation. Flows typically drive controlled navigations within web applications to
 * guide users through fulfillment of a business process/goal that takes place over a series of steps, modeled as
 * states.
 * <p>
 * A simple Flow definition could do nothing more than execute an action and display a view all in one request. A more
 * elaborate Flow definition may be long-lived and execute across a series of requests, invoking many possible paths,
 * actions, and subflows.
 * <p>
 * Especially in Intranet applications there are often "controlled navigations" where the user is not free to do what he
 * or she wants but must follow the guidelines provided by the system to complete a process that is transactional in
 * nature (the quintessential example would be a 'checkout' flow of a shopping cart application). This is a typical use
 * case appropriate to model as a flow.
 * <p>
 * Structurally a Flow is composed of a set of states. A {@link State} is a point in a flow where a behavior is
 * executed; for example, showing a view, executing an action, spawning a subflow, or terminating the flow. Different
 * types of states execute different behaviors in a polymorphic fashion.
 * <p>
 * Each {@link TransitionableState} type has one or more transitions that when executed move a flow to another state.
 * These transitions define the supported paths through the flow.
 * <p>
 * A state transition is triggered by the occurrence of an event. An event is something that happens the flow should
 * respond to, for example a user input event like ("submit") or an action execution result event like ("success"). When
 * an event occurs in a state of a Flow that event drives a state transition that decides what to do next.
 * <p>
 * Each Flow has exactly one start state. A start state is simply a marker noting the state executions of this Flow
 * definition should start in. The first state added to the flow will become the start state by default.
 * <p>
 * Flow definitions may have one or more flow exception handlers. A {@link FlowExecutionExceptionHandler} can execute
 * custom behavior in response to a specific exception (or set of exceptions) that occur in a state of one of this
 * flow's executions.
 * <p>
 * Instances of this class are typically built by {@link org.springframework.webflow.engine.builder.FlowBuilder}
 * implementations but may also be directly instantiated.
 * <p>
 * This class and the rest of the Spring Web Flow (SWF) engine have been designed with minimal dependencies on other
 * libraries. Spring Web Flow is usable in a standalone fashion. The engine system is fully usable outside an HTTP
 * servlet environment, for example in tests, or standalone applications. One of the major architectural
 * benefits of Spring Web Flow is the ability to design reusable, high-level controller modules that may be executed in
 * <i>any</i> environment.
 * <p>
 * Note: flows are singleton definition objects so they should be thread-safe. You can think a flow definition as
 * analogous to a Java class, defining all the behavior of an application module. The core behaviors
 * {@link #start(RequestControlContext, MutableAttributeMap) start}, {@link #resume(RequestControlContext)},
 * {@link #handleEvent(RequestControlContext) on event},
 * {@link #end(RequestControlContext, String, MutableAttributeMap) end}, and
 * {@link #handleException(FlowExecutionException, RequestControlContext)}. Each method accepts a {@link RequestContext
 * request context} that allows for this flow to access execution state in a thread safe manner. A flow execution is
 * what models a running instance of this flow definition, somewhat analogous to a java object that is an instance of a
 * class.
 * 
 * @see org.springframework.webflow.engine.State
 * @see org.springframework.webflow.engine.ActionState
 * @see org.springframework.webflow.engine.ViewState
 * @see org.springframework.webflow.engine.SubflowState
 * @see org.springframework.webflow.engine.EndState
 * @see org.springframework.webflow.engine.DecisionState
 * @see org.springframework.webflow.engine.Transition
 * @see org.springframework.webflow.engine.FlowExecutionExceptionHandler
 * 
 * @author Keith Donald
 * @author Erwin Vervaet
 * @author Colin Sampaleanu
 * @author Jeremy Grelle
 */
public class Flow extends AnnotatedObject implements FlowDefinition {

	/**
	 * Logger, can be used in subclasses.
	 */
	protected final Log logger = LogFactory.getLog(getClass());

	/**
	 * An assigned flow identifier uniquely identifying this flow among all other flows.
	 */
	private String id;

	/**
	 * The set of state definitions for this flow.
	 */
	private Set<State> states = new LinkedHashSet<>(9);

	/**
	 * The default start state for this flow.
	 */
	private State startState;

	/**
	 * The set of flow variables created by this flow.
	 */
	private Map<String, FlowVariable> variables = new LinkedHashMap<>();

	/**
	 * The mapper to map flow input attributes.
	 */
	private Mapper inputMapper;

	/**
	 * The list of actions to execute when this flow starts.
	 * <p>
	 * Start actions should execute with care as during startup a flow session has not yet fully initialized and some
	 * properties like its "currentState" have not yet been set.
	 */
	private ActionList startActionList = new ActionList();

	/**
	 * The set of global transitions that are shared by all states of this flow.
	 */
	private TransitionSet globalTransitionSet = new TransitionSet();

	/**
	 * The list of actions to execute when this flow ends.
	 */
	private ActionList endActionList = new ActionList();

	/**
	 * The mapper to map flow output attributes.
	 */
	private Mapper outputMapper;

	/**
	 * The set of exception handlers for this flow.
	 */
	private FlowExecutionExceptionHandlerSet exceptionHandlerSet = new FlowExecutionExceptionHandlerSet();

	/**
	 * An optional application context hosting services needed by this flow.
	 */
	private ApplicationContext applicationContext;

	/**
	 * Construct a new flow definition with the given id. The id should be unique among all flows.
	 * @param id the flow identifier
	 */
	public Flow(String id) {
		Assert.hasText(id, "This flow must be uniquely identified");
		this.id = id;
	}

	// convenient static factory methods

	/**
	 * Create a new flow with the given id and attributes.
	 * @param id the flow id
	 * @param attributes the attributes
	 * @return the flow
	 */
	public static Flow create(String id, AttributeMap<?> attributes) {
		Flow flow = new Flow(id);
		flow.getAttributes().putAll(attributes);
		return flow;
	}

	// implementing FlowDefinition

	public String getId() {
		return id;
	}

	public StateDefinition getStartState() {
		if (startState == null) {
			throw new IllegalStateException("No start state has been set for this flow ('" + getId()
					+ "') -- flow builder configuration error?");
		}
		return startState;
	}

	public StateDefinition getState(String stateId) {
		return getStateInstance(stateId);
	}

	public String[] getPossibleOutcomes() {
		List<String> possibleOutcomes = new ArrayList<>();
		for (State state : states) {
			if (state instanceof EndState) {
				possibleOutcomes.add(state.getId());
			}
		}
		return possibleOutcomes.toArray(new String[possibleOutcomes.size()]);
	}

	public ClassLoader getClassLoader() {
		if (applicationContext != null) {
			return applicationContext.getClassLoader();
		} else {
			return ClassUtils.getDefaultClassLoader();
		}
	}

	public ApplicationContext getApplicationContext() {
		return applicationContext;
	}

	public boolean inDevelopment() {
		return getAttributes().getBoolean("development", false);
	}

	/**
	 * Add given state definition to this flow definition. Marked protected, as this method is to be called by the
	 * (privileged) state definition classes themselves during state construction as part of a FlowBuilder invocation.
	 * @param state the state to add
	 * @throws IllegalArgumentException when the state cannot be added to the flow; for instance if another state shares
	 * the same id as the one provided or if given state already belongs to another flow
	 */
	protected void add(State state) throws IllegalArgumentException {
		if (this != state.getFlow() && state.getFlow() != null) {
			throw new IllegalArgumentException("State " + state + " cannot be added to this flow '" + getId()
					+ "' -- it already belongs to a different flow: '" + state.getFlow().getId() + "'");
		}
		if (this.states.contains(state) || this.containsState(state.getId())) {
			throw new IllegalArgumentException("This flow '" + getId() + "' already contains a state with id '"
					+ state.getId() + "' -- state ids must be locally unique to the flow definition; "
					+ "existing state-ids of this flow include: " + StylerUtils.style(getStateIds()));
		}
		boolean firstAdd = states.isEmpty();
		states.add(state);
		if (firstAdd) {
			setStartState(state);
		}
	}

	/**
	 * Returns the number of states defined in this flow.
	 * @return the state count
	 */
	public int getStateCount() {
		return states.size();
	}

	/**
	 * Is a state with the provided id present in this flow?
	 * @param stateId the state id
	 * @return true if yes, false otherwise
	 */
	public boolean containsState(String stateId) {
		for (State state : states) {
			if (state.getId().equals(stateId)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Set the start state for this flow to the state with the provided <code>stateId</code>; a state must exist by the
	 * provided <code>stateId</code>.
	 * @param stateId the id of the new start state
	 * @throws IllegalArgumentException when no state exists with the id you provided
	 */
	public void setStartState(String stateId) throws IllegalArgumentException {
		setStartState(getStateInstance(stateId));
	}

	/**
	 * Set the start state for this flow to the state provided; any state may be the start state.
	 * @param state the new start state
	 * @throws IllegalArgumentException given state has not been added to this flow
	 */
	public void setStartState(State state) throws IllegalArgumentException {
		if (!states.contains(state)) {
			throw new IllegalArgumentException("State '" + state + "' is not a state of flow '" + getId() + "'");
		}
		startState = state;
	}

	/**
	 * Return the <code>TransitionableState</code> with given <code>stateId</code>.
	 * @param stateId id of the state to look up
	 * @return the transitionable state
	 * @throws IllegalArgumentException if the identified state cannot be found
	 * @throws ClassCastException when the identified state is not transitionable
	 */
	public TransitionableState getTransitionableState(String stateId) throws IllegalArgumentException,
			ClassCastException {
		State state = getStateInstance(stateId);
		if (state != null && !(state instanceof TransitionableState)) {
			throw new ClassCastException("The state '" + stateId + "' of flow '" + getId() + "' must be transitionable");
		}
		return (TransitionableState) state;
	}

	/**
	 * Lookup the identified state instance of this flow.
	 * @param stateId the state id
	 * @return the state
	 * @throws IllegalArgumentException if the identified state cannot be found
	 */
	public State getStateInstance(String stateId) throws IllegalArgumentException {
		if (!StringUtils.hasText(stateId)) {
			throw new IllegalArgumentException("The specified stateId is invalid: state identifiers must be non-blank");
		}
		for (State state : states) {
			if (state.getId().equals(stateId)) {
				return state;
			}
		}
		throw new IllegalArgumentException("Cannot find state with id '" + stateId + "' in flow '" + getId() + "' -- "
				+ "Known state ids are '" + StylerUtils.style(getStateIds()) + "'");
	}

	/**
	 * Convenience accessor that returns an ordered array of the String <code>ids</code> for the state definitions
	 * associated with this flow definition.
	 * @return the state ids
	 */
	public String[] getStateIds() {
		String[] stateIds = new String[getStateCount()];
		int i = 0;
		for (State state : states) {
			stateIds[i++] = state.getId();
		}
		return stateIds;
	}

	/**
	 * Adds a flow variable.
	 * @param variable the variable
	 */
	public void addVariable(FlowVariable variable) {
		variables.put(variable.getName(), variable);
	}

	/**
	 * Adds flow variables.
	 * @param variables the variables
	 */
	public void addVariables(FlowVariable... variables) {
		if (variables == null) {
			return;
		}
		for (FlowVariable variable : variables) {
			addVariable(variable);
		}
	}

	/**
	 * Returns the flow variable with the given name.
	 * @param name the name of the variable
	 */
	public FlowVariable getVariable(String name) {
		return variables.get(name);
	}

	/**
	 * Returns the flow variables.
	 */
	public FlowVariable[] getVariables() {
		return variables.values().toArray(new FlowVariable[variables.size()]);
	}

	/**
	 * Returns the configured flow input mapper, or null if none.
	 * @return the input mapper
	 */
	public Mapper getInputMapper() {
		return inputMapper;
	}

	/**
	 * Sets the mapper to map flow input attributes.
	 * @param inputMapper the input mapper
	 */
	public void setInputMapper(Mapper inputMapper) {
		this.inputMapper = inputMapper;
	}

	/**
	 * Returns the list of actions executed by this flow when an execution of the flow <i>starts</i>. The returned list
	 * is mutable.
	 * @return the start action list
	 */
	public ActionList getStartActionList() {
		return startActionList;
	}

	/**
	 * Returns the list of actions executed by this flow when an execution of the flow <i>ends</i>. The returned list is
	 * mutable.
	 * @return the end action list
	 */
	public ActionList getEndActionList() {
		return endActionList;
	}

	/**
	 * Returns the configured flow output mapper, or null if none.
	 * @return the output mapper
	 */
	public Mapper getOutputMapper() {
		return outputMapper;
	}

	/**
	 * Sets the mapper to map flow output attributes.
	 * @param outputMapper the output mapper
	 */
	public void setOutputMapper(Mapper outputMapper) {
		this.outputMapper = outputMapper;
	}

	/**
	 * Returns the set of exception handlers, allowing manipulation of how exceptions are handled when thrown during
	 * flow execution. Exception handlers are invoked when an exception occurs at execution time and can execute custom
	 * exception handling logic as well as select an error view to display. Exception handlers attached at the flow
	 * level have an opportunity to handle exceptions that aren't handled at the state level.
	 * @return the exception handler set
	 */
	public FlowExecutionExceptionHandlerSet getExceptionHandlerSet() {
		return exceptionHandlerSet;
	}

	/**
	 * Returns the set of transitions eligible for execution by this flow if no state-level transition is matched. The
	 * returned set is mutable.
	 * @return the global transition set
	 */
	public TransitionSet getGlobalTransitionSet() {
		return globalTransitionSet;
	}

	/**
	 * Returns the transition that matches the event with the provided id.
	 * @param eventId the event id
	 * @return the transition that matches, or null if no match is found.
	 */
	public TransitionDefinition getGlobalTransition(String eventId) {
		for (Transition transition : globalTransitionSet) {
			if (transition.getId().equals(eventId)) {
				return transition;
			}
		}
		return null;
	}

	/**
	 * Sets a reference to the application context hosting application objects needed by this flow.
	 * @param applicationContext the application context
	 */
	public void setApplicationContext(ApplicationContext applicationContext) {
		this.applicationContext = applicationContext;
	}

	// id based equality

	public boolean equals(Object o) {
		if (!(o instanceof Flow)) {
			return false;
		}
		Flow other = (Flow) o;
		return id.equals(other.id);
	}

	public int hashCode() {
		return id.hashCode();
	}

	// behavioral code, could be overridden in subclasses

	/**
	 * Start a new session for this flow in its start state. This boils down to the following:
	 * <ol>
	 * <li>Create (setup) all registered flow variables ({@link #addVariable(FlowVariable)}) in flow scope.</li>
	 * <li>Map provided input data into the flow. Typically data will be mapped into flow scope using the registered
	 * input mapper ({@link #setInputMapper(Mapper)}).</li>
	 * <li>Execute all registered start actions ( {@link #getStartActionList()}).</li>
	 * <li>Enter the configured start state ({@link #setStartState(State)})</li>
	 * </ol>
	 * @param context the flow execution control context
	 * @param input eligible input into the session
	 * @throws FlowExecutionException when an exception occurs starting the flow
	 */
	public void start(RequestControlContext context, MutableAttributeMap<?> input) throws FlowExecutionException {
		assertStartStateSet();
		createVariables(context);
		if (inputMapper != null) {
			MappingResults results = inputMapper.map(input, context);
			if (results != null && results.hasErrorResults()) {
				throw new FlowInputMappingException(getId(), results);
			}
		}
		startActionList.execute(context);
		startState.enter(context);
	}

	/**
	 * Resume a paused session for this flow in its current view state.
	 * @param context the flow execution control context
	 * @throws FlowExecutionException when an exception occurs during the resume operation
	 */
	public void resume(RequestControlContext context) throws FlowExecutionException {
		restoreVariables(context);
		getCurrentViewState(context).resume(context);
	}

	/**
	 * Handle the last event that occurred against an active session of this flow.
	 * @param context the flow execution control context
	 */
	public boolean handleEvent(RequestControlContext context) {
		TransitionableState currentState = getCurrentTransitionableState(context);
		try {
			return currentState.handleEvent(context);
		} catch (NoMatchingTransitionException e) {
			// try the flow level transition set for a match
			Transition transition = globalTransitionSet.getTransition(context);
			if (transition != null) {
				return context.execute(transition);
				// return transition.execute(currentState, context);
			} else {
				// no matching global transition => let the original exception
				// propagate
				throw e;
			}
		}
	}

	/**
	 * Inform this flow definition that an execution session of itself has ended. As a result, the flow will do the
	 * following:
	 * <ol>
	 * <li>Execute all registered end actions ({@link #getEndActionList()}).</li>
	 * <li>Map data available in the flow execution control context into provided output map using a registered output
	 * mapper ( {@link #setOutputMapper(Mapper)}).</li>
	 * </ol>
	 * @param context the flow execution control context
	 * @param outcome the logical flow outcome that will be returned by the session, generally the id of the terminating
	 * end state
	 * @param output initial output produced by the session that is eligible for modification by this method
	 * @throws FlowExecutionException when an exception occurs ending this flow
	 */
	public void end(RequestControlContext context, String outcome, MutableAttributeMap<?> output)
			throws FlowExecutionException {
		endActionList.execute(context);
		if (outputMapper != null) {
			MappingResults results = outputMapper.map(context, output);
			if (results != null && results.hasErrorResults()) {
				throw new FlowOutputMappingException(getId(), results);
			}
		}
	}

	public void destroy() {
		if (applicationContext != null && applicationContext instanceof ConfigurableApplicationContext) {
			((ConfigurableApplicationContext) applicationContext).close();
		}
	}

	/**
	 * Handle an exception that occurred during an execution of this flow.
	 * @param exception the exception that occurred
	 * @param context the flow execution control context
	 */
	public boolean handleException(FlowExecutionException exception, RequestControlContext context)
			throws FlowExecutionException {
		return getExceptionHandlerSet().handleException(exception, context);
	}

	// internal helpers

	private void assertStartStateSet() {
		if (startState == null) {
			throw new IllegalStateException("Unable to start flow '" + id
					+ "'; the start state is not set -- flow builder configuration error?");
		}
	}

	private void createVariables(RequestContext context) {
		for (FlowVariable variable : variables.values()) {
			if (logger.isDebugEnabled()) {
				logger.debug("Creating " + variable);
			}
			variable.create(context);
		}
	}

	public void restoreVariables(RequestContext context) {
		for (FlowVariable variable : variables.values()) {
			if (logger.isDebugEnabled()) {
				logger.debug("Restoring " + variable);
			}
			variable.restore(context);
		}
	}

	private ViewState getCurrentViewState(RequestControlContext context) {
		State currentState = (State) context.getCurrentState();
		if (!(currentState instanceof ViewState)) {
			throw new IllegalStateException("You can only resume paused view states, and state "
					+ context.getCurrentState() + " is not a view state - programmer error");
		}
		return (ViewState) currentState;
	}

	private TransitionableState getCurrentTransitionableState(RequestControlContext context) {
		State currentState = (State) context.getCurrentState();
		if (!(currentState instanceof TransitionableState)) {
			throw new IllegalStateException("You can only signal events in transitionable states, and state "
					+ context.getCurrentState() + " is not transitionable - programmer error");
		}
		return (TransitionableState) currentState;
	}

	public String toString() {
		return new ToStringCreator(this).append("id", id).append("states", states).append("startState", startState)
				.append("variables", variables).append("inputMapper", inputMapper)
				.append("startActionList", startActionList).append("exceptionHandlerSet", exceptionHandlerSet)
				.append("globalTransitionSet", globalTransitionSet).append("endActionList", endActionList)
				.append("outputMapper", outputMapper).toString();
	}

}