Chapter 3. Flow execution

3.1. Introduction

Once a flow has been defined any number of executions of it can be launched in parallel at runtime. Execution of a flow is carried out by a dedicated system that is based internally on a state machine that runs atop the Java VM. As the the life of an flow execution can span more than one request into the server, this system is also responsible for persisting conversational state across requests.

This chapter documents Spring Web Flow's flow execution system. You'll learn the core constructs of the system and how to execute flows out-of-container within a JUnit test environment.

3.2. FlowExecution

A flow execution is a single instance of a flow at a given point in time, realized by an instance of org.springframework.webflow.execution.FlowExecution. A flow execution represents the state of a conversation at a point in time. Given an instance of org.springframework.webflow.Flow, any number of flow executions can be created. A flow definition serves as the instructional blueprint for a flow execution.

It may be helpful to think of a flow as analagous to Class and a flow execution as analagous to an instance of that Class .

Once created, a new flow execution is initially inactive, waiting to be started. Once started, a flow execution enters its startState and continues executing until it enters a ViewState where user input is required to continue or it enters an EndState where it terminates.

When a flow execution reaches a ViewState it is said to have paused, where it waits in that state for user input to be provided so it can continue. After pausing the ViewSelection returned is typically used to issue a response to the user that provides a vehicle for collecting the required input.

User input is provided by signaling an event that resumes the flow execution by communicating what user action was taken. Parameters sent in the signal event request form the basis for user input. The flow execution responds to an event in standard fashion by executing a matching state transition in the resuming ViewState.

Once a flow execution has resumed after being paused by a view state, it continues executing until it again enters another view state or enters an end state where it terminates. Once a flow execution has terminated it cannot be resumed.

3.2.1. Flow execution lifecycle

As outlined, a flow execution can go through a number of phases throughout its lifecycle; for example, created, active, paused, ended.

Spring Web Flow gives you full control over the ability to observe the lifecycle of an executing flow by implementing a org.springframework.webflow.execution.FlowExecutionListener.

The different phases of a flow execution is shown graphically below:

Flow execution lifecycle

3.2.2. Flow execution properties

The configurable properties of a flow execution are summarized below:

Table 3.1. Flow Execution properties

Property nameDescriptionCardinalityDefault value
flowThe definition of the flow to be executed.1 
listenersThe set of observers observing the lifecycle of the flow execution.0..*Empty

The configurable constructs related to flow execution are shown graphically below:

Flow execution

3.3. Flow execution context

Once created, a flow execution, representing the state of a conversation at a point in time, maintains contextual state about itself that can be reasoned upon by clients. In addition, a flow execution exposes two data structures, called scopes, that allow clients to set arbitrary attributes that are managed by the conversation.

The contextual properties associated with a flow execution are summarized below:

Table 3.2. Flow Execution properties

Property nameDescriptionCardinalityDefault value
active A flag indicating if the flow execution is active. An inactive flow execution has either ended or has never been started. 1 
flow The definition of the flow execution. The flow serves as the blueprint for the conversation. It may be helpful to think of a flow as like a Class and a flow execution as like an instance of that Class. This method may always be safely called. 1 
activeSession The active flow session, tracking the flow that is currently executing and what state it is in. The active session can change over the life of the flow execution because a flow can spawn another flow as a subflow. This property can only be queried while the flow execution is active. 1 
scope A data map that forms the basis for conversational scope. Arbitrary attributes placed in this map will be retained for the scope of the conversation. This map is shared by all flow sessions. 1 

As a flow execution is manipulated by clients its contextual state changes. Consider how contextual state is effected when the following events occur:

Table 3.3. An ordered set of events and their effects on flow execution context

Flow Execution EventActive?Value of the activeSession property
createdfalseThrows an IllegalStateException
startedtrue A FlowSession whose flow is the top-level flow and whose state is the flow's start state.
state enteredtrue A FlowSession whose flow is the top-level flow and whose state is the newly entered state.
subflow spawnedtrue A FlowSession whose flow is the subflow and whose state is the subflow's start state.
subflow endedtrue A FlowSession whose flow is again the top-level flow and whose state is the resuming state.
endedfalseThrows an IllegalStateException

As you can see, the activeSession of a flow execution changes when a subflow is spawned. Each flow execution maintains a stack of flow sessions, where each flow session represents a spawned instance of a flow definition. When a flow execution starts, the session stack initially consists of one (1) entry, an instance dubbed the root session. When a subflow is spawned, the stack increases to two (2) entries. When the subflow ends, the stack decreases back to one (1) entry. The active session is always the session at the top of the stack.

The contextual properties associated with a org.springframework.webflow.FlowSession are summarized below:

Table 3.4. Flow Session properties

Property nameDescriptionCardinalityDefault value
flow The definition of the flow the session is an instance of. 1 
state The current state of the session. 1 
status A status indicator describing what the session is currently doing. Valid values are CREATED, ACTIVE, PAUSED, SUSPENDED, RESUMING, ENDING, and ENDED. 1 
scope A data map that forms the basis for flow scope. Arbitrary attributes placed in this map will be retained for the scope of the flow session. This map is local to the session. 1 

The following graphic illustrates an example flow execution context and flow session stack:

Flow execution context

In this illustration a flow execution has been created for the Book Flight flow. The execution is currently active and the activeSession indicates it is in the Display Seating Chart state of the Assign Seats flow, which was spawned as a subflow from the Enter Seat Assignments state.

Note how the active session status is paused, indicating the flow execution is currently waiting for user input to be provided to continue. In this case, it is expected the user will choose a seat for their flight.

3.4. Flow execution testing

Spring Web Flow provides support within the org.springframework.webflow.test package for testing flow executions with JUnit. This support is provided as convenience but is entirely optional, as a flow execution is instantiable in any environment with the standard new operator.

The general strategy for testing flows follows:

  1. Your own implementations of definitional artifacts used by a flow such as actions, attribute mappers, and exception handlers should be unit tested in isolation. Spring Web Flow ships convenient stubs to assist with this.

  2. The execution of a flow should be tested as part of a system integration test. Such a test should exercise all possible paths of the flow, asserting that the flow responds to events as expected.

Note: a flow execution integration test may select mock or stub implementations of application services called by the flow and/or may exercise production implementations. Both are useful system test configurations.

3.4.1. Flow execution test example

To help illustrate testing a flow execution, first consider the following flow to search a phonebook for contacts:

Phonebook Search Flow - UML Model

Phonebook Search Flow - XML Definition

Above you see a flow with four (4) states that execute these behaviors, respectively:

  1. The first state displays a search criteria form so the user can enter who they wish to search for.

  2. On form submit and successful data binding and validation the search is executed.

  3. After search execution, a results page is displayed.

  4. From the results page the user may select a result they wish to browse additional details on or they may request a new search. On select, the "detail" flow is spawned and when it finishes the search is re-executed.

From this behavior narrative the following assertable test scenarios can be extracted:

  1. That when a flow execution starts, it enters the enterCriteria state and makes a searchCriteria view selection containing a form object to be used as the basis for form field population.

  2. That on submit with valid input, the search is executed and a searchResults view selection is made.

  3. That on submit with invalid input, the searchCriteria view is selected.

  4. That on newSearch, the searchCriteria view is selected.

  5. That on select, the detail flow is spawned and passed the id of the selected result as expected.

To assist with writing these assertions Spring Web Flow ships with JUnit-based flow execution test support within the org.springframwork.webflow.test package. These base test classes are indicated below:

Table 3.5. Flow execution test support hierarchy

Class nameDescription
AbstractFlowExecutionTestsThe most generic base class for flow execution tests.
AbstractExternalizedFlowExecutionTestsThe base class for flow execution tests whose flow is defined within an externalized resource, such as a file.
AbstractXmlFlowExecutionTestsThe base class for flow execution tests whose flow is defined within an externalized XML resource.

The completed test for this example extending AbstractXmlFlowExecutionTests is shown below:

    public class SearchFlowExecutionTests extends AbstractXmlFlowExecutionTests {
	
        public void testStartFlow() {
            ViewSelection view = startFlow();
            assertCurrentStateEquals("enterCriteria");
            assertViewNameEquals("searchCriteria", view);
            assertModelAttributeNotNull("searchCriteria", view);
        }
        
        public void testCriteriaSubmitSuccess() {
            startFlow();

            Map input = new HashMap();
            input.put("firstName", "Keith");
            input.put("lastName", "Donald");
			
            // submit with valid input
            ViewSelection view = signalEvent("submit", input);
			
            assertCurrentStateEquals("displayResults");
            assertViewNameEquals("searchResults", view);
            assertModelAttributeCollectionSize(1, "results", view);
        }
	
        public void testCriteriaSubmitError() {
            startFlow();
			
            // submit with no input
            signalEvent("submit");
			
            assertCurrentStateEquals("enterCriteria");
        }
	
        public void testNewSearch() {
            testCriteriaSubmitSuccess();
            ViewSelection view = signalEvent("newSearch");
            assertCurrentStateEquals("enterCriteria");
            assertViewNameEquals("searchCriteria", view);
        }
	
        public void testSelectValidResult() {
            testCriteriaSubmitSuccess();
			
            Map input = new HashMap();
            input.put("id", "1");
            // select with valid input
            ViewSelection view = signalEvent("select", input);
			
            assertCurrentStateEquals("displayResults");
            assertViewNameEquals("searchResults", view);
            assertModelAttributeCollectionSize(1, "results", view);
        }
	
        /**
         * A stub for testing.
         */
        private PhoneBook phonebook = new ArrayListPhoneBook();
	
        @Override
        protected ExternalizedFlowDefinition getFlowDefinition() {
            File flowDir = new File("src/webapp/WEB-INF/flows");
            File file = new File(flowDir, "search-flow.xml");
            return new ExternalizedFlowDefinition(new FileSystemResource(file));
        }
		
        @Override
        protected FlowArtifactFactory createFlowArtifactFactory() {
            return new TestFlowArtifactFactory();
        }
	
        /**
         * Used to wire in test implementations of artifacts used by the flow, such
         * as the phonebook and the detail subflow
         */
        protected class TestFlowArtifactFactory extends FlowArtifactFactoryAdapter {
        
            public Action getAction(FlowArtifactParameters parameters){
                // there is only one global action used by this flow, phonebook
                return new LocalBeanInvokingAction(phonebook);
            }
	
            public Flow getSubflow(String id) {
                // there is only one subflow, detail
                Flow detail = new Flow(id);
                // test responding to finish result by stubbing detail flow
                EndState finish = new EndState(detail, "finish");
                finish.addEntryAction(new AbstractAction() {
                    public Event doExecute(RequestContext context) {
                        // test attribute mapping behavior
                        assertEquals(new Long(1), context.getFlowScope().getAttribute("id"));
                        return success();
                    }
                });
                return detail;
            }
        }
    }	    	
	    	

With a well-written flow execution test passing that covers the controller behavior scenarios possible for your flow you have concrete evidence the flow will execute as expected when deployed in container.

Go for Green