All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.webflow.Flow Maven / Gradle / Ivy

There is a newer version: 1.0.6
Show newest version
/*
 * Copyright 2002-2006 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;

import java.util.Collections;
import java.util.Iterator;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.binding.mapping.AttributeMapper;
import org.springframework.core.CollectionFactory;
import org.springframework.core.style.StylerUtils;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * A single flow definition. A Flow definition represents a reusable,
 * self-contained controller module that provides the blue print for a logical
 * page flow of a web application. A "logical page flow" is defined as a
 * controlled navigation that guides a single user through fulfillment of a
 * business process/goal that takes place over a series of steps, modeled as
 * states.
 * 

* 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, executing accross a series of requests, invoking many possible * paths, actions, and subflows. *

* Note: A flow is not a welcome page or an index page: don't use flows for * those cases, use simple controllers/actions/portlets instead. Don't use flows * where your application demands a significant amount of "free browsing": flows * force strict navigation. 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 quinessential example would be a * 'checkout' flow of a shopping cart application). This is a typical use case * appropriate for a flow. *

* 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. *

* Each {@link TransitionableState} type has one or more transitions that when * executed, move a Flow to another state, defining the supported paths * through the flow. A state transition is triggered by the occurence of an * event. An event is something that happens externally 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. *

* 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. *

* Flow definitions may have one or more flow exception handlers. A * {@link StateExceptionHandler} 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. *

* Instances of this class are typically built by * {@link org.springframework.webflow.builder.FlowBuilder} implementations, but * may also be directly subclassed. *

* This class, and the rest of the Spring Web Flow (SWF) core, has been designed * with minimal dependencies on other libraries, and is usable in a standalone * fashion (as well as in the context of other frameworks like Spring MVC, * Struts, or JSF, for example). The core system is fully usable outside an HTTP * servlet environment, for example in Portlets, 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 any environment. *

* Note: flows are singleton definition objects so they should be thread-safe! * * @see org.springframework.webflow.State * @see org.springframework.webflow.TransitionableState * @see org.springframework.webflow.ActionState * @see org.springframework.webflow.ActionList * @see org.springframework.webflow.ViewState * @see org.springframework.webflow.SubflowState * @see org.springframework.webflow.EndState * @see org.springframework.webflow.DecisionState * @see org.springframework.webflow.Transition * @see org.springframework.webflow.StateExceptionHandler * @see org.springframework.webflow.StateExceptionHandlerSet * @see org.springframework.webflow.executor.mvc.FlowController * @see org.springframework.webflow.executor.mvc.PortletFlowController * * @author Keith Donald * @author Erwin Vervaet * @author Colin Sampaleanu */ public class Flow extends AnnotatedObject { private static final Log logger = LogFactory.getLog(Flow.class); /** * 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 states = CollectionFactory.createLinkedSetIfPossible(9); /** * The default start state for this flow. */ private State startState; /** * The set of flow variables created by this flow. */ private Set variables = CollectionFactory.createLinkedSetIfPossible(3); /** * The mapper to map flow input attributes. */ private AttributeMapper inputMapper; /** * The list of actions to execute when this flow starts. *

* 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 AttributeMapper outputMapper; /** * The set of exception handlers for this flow. */ private StateExceptionHandlerSet exceptionHandlerSet = new StateExceptionHandlerSet(); /** * The set of inline flows contained by this flow. */ private Set inlineFlows = CollectionFactory.createLinkedSetIfPossible(3); /** * 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) { setId(id); } /** * Returns the unique id of this flow. */ public String getId() { return id; } /** * Set the unique id of this flow. */ public void setId(String id) { Assert.hasText(id, "This flow must have a unique, non-blank identifier"); this.id = id; } /** * 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, if already added nothing happens, if another * instance is added with the same id, an exception is thrown * @throws IllegalArgumentException when the state cannot be added to the * flow; specifically, if another state shares the same id as the one * provided */ 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"); } if (states.contains(state)) { 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 managed by this flow. * @return the state count */ public int getStateCount() { return states.size(); } /** * Returns the list of states in this flow. */ public State[] getStates() { return (State[])states.toArray(new State[states.size()]); } /** * Return the start state, throwing an exception if it has not yet been * marked. * @return the start state * @throws IllegalStateException when no start state has been marked */ public State getStartState() throws IllegalStateException { if (startState == null) { throw new IllegalStateException("No start state has been set for this flow ('" + getId() + "') -- flow builder configuration error?"); } return startState; } /** * Set the start state for this flow to the state with the provided * stateId; a state must exist by the provided * stateId. * @param stateId the id of the new start state * @throws NoSuchStateException when no state exists with the id you * provided */ public void setStartState(String stateId) throws NoSuchStateException { setStartState(getRequiredState(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 NoSuchStateException given state has not been added to this flow */ public void setStartState(State state) throws NoSuchStateException { if (!states.contains(state)) { throw new NoSuchStateException(this, state.getId()); } startState = state; } /** * 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) { return getState(stateId) != null; } /** * Return the state with the provided id, returning null if * no state exists with that id. * @param stateId the state id * @return the state with that id, or null if none exists */ public State getState(String stateId) { if (!StringUtils.hasText(stateId)) { throw new IllegalArgumentException("The specified stateId is invalid: state identifiers must be non-blank"); } Iterator it = states.iterator(); while (it.hasNext()) { State state = (State)it.next(); if (state.getId().equals(stateId)) { return state; } } return null; } /** * Return the state with the provided id, throwing a exception if no state * exists with that id. * @param stateId the state id * @return the state with that id * @throws NoSuchStateException when no state exists with that id */ public State getRequiredState(String stateId) throws NoSuchStateException { State state = getState(stateId); if (state == null) { throw new NoSuchStateException(this, stateId); } return state; } /** * Return the TransitionableState with given * stateId, or null when not found. * @param stateId id of the state to look up * @return the transitionable state, or null when not found * @throws IllegalStateException when the identified state is not * transitionable */ public TransitionableState getTransitionableState(String stateId) throws IllegalStateException { State state = getState(stateId); if (state != null && !(state instanceof TransitionableState)) { throw new IllegalStateException("The state '" + stateId + "' of flow '" + getId() + "' must be transitionable"); } return (TransitionableState)state; } /** * Return the TransitionableState with given * stateId, throwing an exception if not found. * @param stateId id of the state to look up * @return the transitionable state * @throws IllegalStateException when the identified state is not * transitionable * @throws NoSuchStateException when no transitionable state exists by this * id */ public TransitionableState getRequiredTransitionableState(String stateId) throws IllegalStateException, NoSuchStateException { TransitionableState state = getTransitionableState(stateId); if (state == null) { throw new NoSuchStateException(this, stateId); } return state; } /** * Convenience accessor that returns an ordered array of the String * ids for the state definitions associated with this flow * definition. * @return the state ids */ public String[] getStateIds() { String[] stateIds = new String[getStateCount()]; int i = 0; Iterator it = states.iterator(); while (it.hasNext()) { stateIds[i++] = ((State)it.next()).getId(); } return stateIds; } /** * Adds a flow variable. * @param variable the var */ public void addVariable(FlowVariable variable) { variables.add(variable); } /** * Adds the flow variables. * @param variables the vars */ public void addVariables(FlowVariable[] variables) { if (variables == null) { return; } for (int i = 0; i < variables.length; i++) { addVariable(variables[i]); } } /** * Returns the flow variables. */ public FlowVariable[] getVariables() { return (FlowVariable[])variables.toArray(new FlowVariable[variables.size()]); } /** * Returns the configured flow input mapper * @return the input mapper */ public AttributeMapper getInputMapper() { return inputMapper; } /** * Sets the mapper to map flow input attributes. * @param inputMapper the input mapper */ public void setInputMapper(AttributeMapper inputMapper) { this.inputMapper = inputMapper; } /** * Returns the list of actions executed by this flow when an execution of * the flow starts. * @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 ends. * @return the end action list */ public ActionList getEndActionList() { return endActionList; } /** * Returns the configured flow output mapper * @return the output mapper */ public AttributeMapper getOutputMapper() { return outputMapper; } /** * Sets the mapper to map flow output attributes. * @param outputMapper the output mapper */ public void setOutputMapper(AttributeMapper outputMapper) { this.outputMapper = outputMapper; } /** * Returns the set of exception handlers, allowing manipulation of how state * exceptions are handled when thrown during flow execution.

Exception * handlers are invoked when an exception occurs when this state is entered, * and can execute custom exception handling logic as well as select an * error view to display.

State exception handlers attached at the flow * level have a opportunity to handle exceptions that aren't handled at the * state level. * @return the state exception handler set */ public StateExceptionHandlerSet getExceptionHandlerSet() { return exceptionHandlerSet; } /** * Adds an inline flow to this flow. * @param flow the inline flow to add */ public void addInlineFlow(Flow flow) { inlineFlows.add(flow); } /** * Returns the list of inline flow ids. * @return a string array of inline flow identifiers */ public String[] getInlineFlowIds() { String[] flowIds = new String[getInlineFlowCount()]; int i = 0; Iterator it = inlineFlows.iterator(); while (it.hasNext()) { flowIds[i++] = ((Flow)it.next()).getId(); } return flowIds; } /** * Returns the list of inline flows. * @return the list of inline flows */ public Flow[] getInlineFlows() { return (Flow[])inlineFlows.toArray(new Flow[inlineFlows.size()]); } /** * Returns the count of registered inline flows. * @return the count */ public int getInlineFlowCount() { return inlineFlows.size(); } /** * Tests if this flow contains an in-line flow with the specified id. * @param id the inline flow id * @return true if this flow contains a inline flow with that id, false * otherwise */ public boolean containsInlineFlow(String id) { return getInlineFlow(id) != null; } /** * Returns the inline flow with the provided id, or null if * no such inline flow exists. * @param id the inline flow id * @return the inline flow */ public Flow getInlineFlow(String id) { if (!StringUtils.hasText(id)) { throw new IllegalArgumentException( "The specified inline flowId is invalid: flow identifiers must be non-blank"); } Iterator it = inlineFlows.iterator(); while (it.hasNext()) { Flow flow = (Flow)it.next(); if (flow.getId().equals(id)) { return flow; } } return null; } /** * Returns the set of transitions eligible for execution by this flow if no * state-level transition is matched. * @return the global transition set */ public TransitionSet getGlobalTransitionSet() { return globalTransitionSet; } 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(); } /** * Start a new session for this flow in its stat state. * @param context the flow execution control context * @param input eligible input into the session * @throws StateException when an exception occurs entering the start state */ public ViewSelection start(FlowExecutionControlContext context, AttributeMap input) throws StateException { createVariables(context); if (inputMapper != null) { inputMapper.map(input, context, Collections.EMPTY_MAP); } startActionList.execute(context); return startState.enter(context); } /** * Inform this flow definition that an event was signaled in the current * state of an active flow execution. * @param context the flow execution control context * @return the selected view * @throws StateException when an exception occurs processing the event */ public ViewSelection onEvent(Event event, FlowExecutionControlContext context) throws StateException { TransitionableState currentState = getCurrentTransitionableState(context); try { return currentState.onEvent(event, context); } catch (NoMatchingTransitionException e) { // try the flow level transition set for a match Transition transition = globalTransitionSet.getTransition(context); if (transition != null) { return transition.execute(currentState, context); } else { throw e; } } } /** * Inform this flow definition that a execution session of itself has ended. * @param context the flow execution control context * @param output initial output produced by the session that is eligible for * modification by this method. * @throws StateException when an exception occurs ending this flow */ public void end(FlowExecutionControlContext context, AttributeMap output) throws StateException { endActionList.execute(context); if (outputMapper != null) { outputMapper.map(context, output, Collections.EMPTY_MAP); } } /** * Handle an exception that occured during an execution of this flow. * @param exception the exception that occured * @param context the flow execution control context * @return the selected error view, or null if no handler * matched or returned a non-null view selection * @throws StateException passed in, if it was not handled */ public ViewSelection handleException(StateException exception, FlowExecutionControlContext context) throws StateException { return getExceptionHandlerSet().handleException(exception, context); } private void createVariables(RequestContext context) { Iterator it = variables.iterator(); while (it.hasNext()) { FlowVariable variable = (FlowVariable)it.next(); if (logger.isDebugEnabled()) { logger.debug("Creating " + variable); } variable.create(context); } } private TransitionableState getCurrentTransitionableState(FlowExecutionControlContext context) { State currentState = 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).append("inlineFlows", inlineFlows).toString(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy