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.
A org.springframework.webflow.execution.FlowExecution is a runtime instantiation of a flow definition. Given a single FlowDefinition any number of independent flow executions may be created, typically by a FlowExecutionFactory.
A flow execution carries out the execution of program instructions defined within its definition in response to user events.
It may be helpful to think of a flow definition as analagous to a Java Class and a flow execution as analagous to an object instance of that Class. Signaling an execution event can be considered analagous to sending an object a message.
FlowDefinition definition = ... FlowExecutionFactory factory = ... FlowExecution execution = factory.createFlowExecution(definition);
Once created, a new flow execution is initially inactive, waiting to be started. Once started a flow execution becomes active by entering its startState. From there it continues executing until it enters a state where user input is required to continue or it terminates.
MutableAttributeMap input = ... ExternalContext context = ... ViewSelection startingView = execution.start(input, context);
When a flow execution reaches a state where input is required to continue it is said to have paused, where it waits in that state for the input to be provided. 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. Attributes of the signal event request form the basis for user input. The flow execution resumes by consuming the event.
Once a flow execution has resumed it continues executing until it again enters a state where more input is needed or it terminates. Once a flow execution has terminated it becomes inactive and cannot be resumed.
ExternalContext context = ... ViewSelection nextView = execution.signalEvent("submit", context); if (execution.isActive()) { // still active but paused } else { // has ended }
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 the ability to observe the lifecycle of an executing flow by implementing a FlowExecutionListener.
The different phases of a flow execution are shown graphically below:
The Spring Web Flow flow execution implementation is org.springframework.webflow.engine.impl.FlowExecutionImpl, typically created by a FlowExecutionImplFactory (a FlowExecutionFactory implementation). The configurable properties of this flow execution implementation are summarized below:
Table 3.1. Flow Execution properties
Property name | Description | Cardinality | Default value |
---|---|---|---|
definition | The flow definition to be executed. | 1 | |
listeners | The set of observers observing the lifecycle of this flow execution. | 0..* | Empty |
attributes | Global system attributes that can be used to affect flow execution behavior | 0..* | Empty |
The configurable constructs related to flow execution are shown graphically below:
Once created, a flow execution, representing the state of a flow at a point in time, maintains contextual state about itself that can be reasoned upon by clients. In addition, a flow execution exposes several data structures, called scopes, that allow clients to set arbitrary attributes that are managed by the execution.
The contextual properties associated with a flow execution are summarized below:
Table 3.2. Flow Execution Context properties
Property name | Description | Cardinality | Default value |
---|---|---|---|
active | A flag indicating if the flow execution is active. An inactive flow execution has either ended or has never been started. | 1 | |
definition | The definition of the flow execution. The flow definition serves as the blueprint for the program. It may be helpful to think of a flow definition 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 | |
conversationScope | A data map that forms the basis for "conversation scope". Arbitrary attributes placed in this map will be retained for the life of the flow execution and correspond to the length of the logical 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 Event | Active? | Value of the activeSession property |
---|---|---|
created | false | Throws an IllegalStateException |
started | true | A FlowSession whose definition is the top-level flow definition and whose state is the definition's start state. |
state entered | true | A FlowSession whose definition is the top-level flow definition and whose state is the newly entered state. |
subflow spawned | true | A FlowSession whose definition is the subflow definition and whose state is the subflow's start state. |
subflow ended | true | A FlowSession whose definition is back to the top-level flow definition and whose state is the resuming state. |
ended | false | Throws 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 FlowSession are summarized below:
Table 3.4. Flow Session properties
Property name | Description | Cardinality | Default value |
---|---|---|---|
definition | 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. | 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 | |
flashMap | A data map that forms the basis for flash scope. Attributes placed in this map will be retained until the next external user event is signaled in the session. | 1 |
The following graphic illustrates an example flow execution context and flow session stack:
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 | |
---|---|
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. |
As alluded to, a flow execution manages several containers called scopes, which allow arbitrary attributes to be stored for a period of time. There are four scope types, each with different storage management semantics:
Table 3.5. Flow execution scope types
Scope type name | Management Semantics |
---|---|
request | Eligible for garbage collection when a single call into the flow execution completes. |
flash | Cleared when the next user event is signaled into the flow session; eligible for garbage collection when the flow session ends. |
flow | Eligible for garbage collection when the flow session ends. |
conversation | Eligible for garbage collection when the root session of the governing flow execution (logical conversation) ends. |
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:
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, for instance MockRequestContext.
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 typically selects mock or stub implementations of application services called by the flow, though it may also exercise production implementations. Both are useful, supported system test configurations. |
To help illustrate testing a flow execution, first consider the following flow definition to search a phonebook for contacts:
The corresponding XML-based flow definition implementation:
<?xml version="1.0" encoding="UTF-8"?> <flow xmlns="http://www.springframework.org/schema/webflow" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow-1.0.xsd"> <start-state idref="enterCriteria"/> <view-state id="enterCriteria" view="searchCriteria"> <render-actions> <action bean="formAction" method="setupForm"/> </render-actions> <transition on="search" to="displayResults"> <action bean="formAction" method="bindAndValidate"/> </transition> </view-state> <view-state id="displayResults" view="searchResults"> <render-actions> <bean-action bean="phonebook" method="search"> <method-arguments> <argument expression="flowScope.searchCriteria"/> </method-arguments> <method-result name="results"/> </bean-action> </render-actions> <transition on="newSearch" to="enterCriteria"/> <transition on="select" to="browseDetails"/> </view-state> <subflow-state id="browseDetails" flow="detail-flow"> <attribute-mapper> <input-mapper> <mapping source="requestParameters.id" target="id" from="string" to="long"/> </input-mapper> </attribute-mapper> <transition on="finish" to="displayResults"/> </subflow-state> </flow>
Above you see a flow with three (3) states that execute these behaviors, respectively:
The first state enterCriteria displays a search criteria form so the user can enter who he or she wishes to search for.
On form submit and successful data binding and validation the search is executed. After search execution a results view is displayed.
From the results view 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 and it's results redisplayed.
From this behavior narrative the following assertable test scenarios can be extracted:
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.
That on submit with valid input, the search is executed and a searchResults view selection is made.
That on submit with invalid input, the searchCriteria view is reselected.
That on newSearch, the searchCriteria view is selected.
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.6. Flow execution test support hierarchy
Class name | Description |
---|---|
AbstractFlowExecutionTests | The most generic base class for flow execution tests. |
AbstractExternalizedFlowExecutionTests | The base class for flow execution tests whose flow is defined within an externalized resource, such as a file. |
AbstractXmlFlowExecutionTests | The 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() { ApplicationView view = applicationView(startFlow()); assertCurrentStateEquals("enterCriteria"); assertViewNameEquals("searchCriteria", view); assertModelAttributeNotNull("searchCriteria", view); } public void testCriteriaSubmitSuccess() { startFlow(); MockParameterMap parameters = new MockParameterMap(); parameters.put("firstName", "Keith"); parameters.put("lastName", "Donald"); ApplicationView view = applicationView(signalEvent("search", parameters)); assertCurrentStateEquals("displayResults"); assertViewNameEquals("searchResults", view); assertModelAttributeCollectionSize(1, "results", view); } public void testCriteriaSubmitError() { startFlow(); signalEvent("search"); assertCurrentStateEquals("enterCriteria"); } public void testNewSearch() { testCriteriaSubmitSuccess(); ApplicationView view = applicationView(signalEvent("newSearch")); assertCurrentStateEquals("enterCriteria"); assertViewNameEquals("searchCriteria", view); } public void testSelectValidResult() { testCriteriaSubmitSuccess(); MockParameterMap parameters = new MockParameterMap(); parameters.put("id", "1"); ApplicationView view = applicationView(signalEvent("select", parameters)); assertCurrentStateEquals("displayResults"); assertViewNameEquals("searchResults", view); assertModelAttributeCollectionSize(1, "results", view); } @Override protected FlowDefinitionResource getFlowDefinitionResource() { return createFlowDefinitionResource("src/main/webapp/WEB-INF/flows/search-flow.xml"); } @Override protected void registerMockServices(MockFlowServiceLocator serviceRegistry) { Flow mockDetailFlow = new Flow("detail-flow"); mockDetailFlow.setInputMapper(new AttributeMapper() { public void map(Object source, Object target, Map context) { assertEquals("id of value 1 not provided as input by calling search flow", new Long(1), ((AttributeMap)source).get("id")); } }); // test responding to finish result new EndState(mockDetailFlow, "finish"); serviceRegistry.registerSubflow(mockDetailFlow); serviceRegistry.registerBean("phonebook", new ArrayListPhoneBook()); } }
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 a container.
The previous example shows how to test a flow execution in relative isolation with a mock service layer and mock subflows. Flow execution testing against a real service-layer and real subflows is also supported.
The next example shows how the createFlowServiceLocator method can be overridden to create the service-layer using a Spring application context:
public class SearchFlowExecutionTests extends AbstractXmlFlowExecutionTests { ... @Override protected FlowDefinitionResource getFlowDefinitionResource() { return createFlowDefinitionResource("src/main/webapp/WEB-INF/flows/search-flow.xml"); } @Override protected FlowServiceLocator createFlowServiceLocator() { // create a context to host our middle tier services ApplicationContext context = new ClassPathXmlApplicationContext(new String[] { "classpath:service-layer-config.xml", "classpath:data-access-layer-config.xml" }); // create a registry for our flow definitions being tested FlowDefinitionRegistry registry = new FlowDefinitionRegistryImpl(); // initialize the service locator DefaultFlowServiceLocator locator = new DefaultFlowServiceLocator(registry, context); // perform subflow definition registration with the help of a registrar XmlFlowRegistrar registrar = new XmlFlowRegistrar(locator); registrar.addResource(createFlowDefinitionResource("/WEB-INF/flows/search-flow.xml")); registrar.addResource(createFlowDefinitionResource("/WEB-INF/flows/detail-flow.xml")); registrar.registerFlowDefinitions(registry); return locator; } }