Package org.springframework.webflow.engine

Source Code of org.springframework.webflow.engine.Flow

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

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 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
* <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<State>(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<String, FlowVariable>();

  /**
   * 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<String>();
    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();
  }

}
TOP

Related Classes of org.springframework.webflow.engine.Flow

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.