Spring Web Flow Subflow Webapp

David Winterfeldt

2008


A Spring Web Flow 2.0 example using a flow to create and edit a Person and a subflow to create and edit a Person's Addresses. A Spring MVC annotation-based controller still handles search and deleting Person records. The example is built on Simple Spring Web Flow Webapp which can be referred to if necessary.

1. Spring Configuration

This is a basic Spring Web Flow configuration with Tiles as the view resolver and with a Spring Security flow execution listener configured. The webflow:flow-registry element registers the person flow and address subflow. The person flow XML file is stored with the person form and search page, and the address flow is stored with the address form page. A flow specific message resources file (messages.properties) could also be put in these locations.

/WEB-INF/spring/webflow-context.xml
                
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:webflow="http://www.springframework.org/schema/webflow-config"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/webflow-config
                           http://www.springframework.org/schema/webflow-config/spring-webflow-config-2.3.xsd">

    <!-- Enables FlowHandlers -->
    <bean class="org.springframework.webflow.mvc.servlet.FlowHandlerAdapter"
          p:flowExecutor-ref="flowExecutor" />
          
    <!-- Executes flows: the entry point into the Spring Web Flow system -->
    <webflow:flow-executor id="flowExecutor">
        <webflow:flow-execution-listeners>
            <webflow:listener ref="securityFlowExecutionListener" />
        </webflow:flow-execution-listeners>
    </webflow:flow-executor>

    <!-- The registry of executable flow definitions -->
    <webflow:flow-registry id="flowRegistry" flow-builder-services="flowBuilderServices">
        <webflow:flow-location path="/WEB-INF/jsp/person/person.xml" />
        <webflow:flow-location path="/WEB-INF/jsp/address/address.xml" />
    </webflow:flow-registry>

    <!-- Plugs in a custom creator for Web Flow views -->
    <webflow:flow-builder-services id="flowBuilderServices" view-factory-creator="mvcViewFactoryCreator" />

    <!-- Configures Web Flow to use Tiles to create views for rendering; Tiles allows for applying consistent layouts to your views -->
    <bean id="mvcViewFactoryCreator"
          class="org.springframework.webflow.mvc.builder.MvcViewFactoryCreator"
          p:viewResolvers-ref="tilesViewResolver" />

    <!-- Installs a listener to apply Spring Security authorities -->
    <bean id="securityFlowExecutionListener"
          class="org.springframework.webflow.security.SecurityFlowExecutionListener" />

    <!-- Used in 'create' action-state of Person Flow -->
    <bean id="personBean" 
          class="org.springbyexample.web.orm.entity.Person" 
          scope="prototype" />

    <!-- Used in 'create' action-state of Address Flow -->
    <bean id="addressBean" 
          class="org.springbyexample.web.orm.entity.Address" 
          scope="prototype" />
    
</beans>
                
            

The handlers are configured so flows and annotation-based controllers can be used together. The url '/person.html' is mapped to the person flow in the flowMappings bean and assigned a custom flow handler, which redirects to the search page at the end of the flow and if an exception not handled by the flow occurs.

The tilesViewResolver in the Spring Web Flow example is the AjaxUrlBasedViewResolver, which is able to handle rendering fragments of a Tiles context. It's viewClass property is set to use FlowAjaxDynamicTilesView. This example uses AJAX to populate just the body of the page on a form submit. Also, Spring by Example's Dynamic Tiles Spring MVC Module is used to reduce the Tiles configuration.

/WEB-INF/spring/webmvc-context.xml
                
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context 
                           http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/mvc
                           http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <context:component-scan base-package="org.springbyexample.web.servlet.mvc" />

    <!-- URL to flow mapping rules -->
    <bean id="flowMappings"
          class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping"
          p:order="0">
        <property name="mappings">
            <value>/person.html=personFlowHandler</value>
        </property>
    </bean>

    <mvc:annotation-driven />
    
    <mvc:view-controller path="/index.html" />
    <mvc:view-controller path="/login.html" />
    <mvc:view-controller path="/logoutSuccess.html" />

    <bean id="tilesConfigurer"
          class="org.springframework.web.servlet.view.tiles2.TilesConfigurer"
          p:definitions="/WEB-INF/tiles-defs/templates.xml" />
 
    <bean id="tilesViewResolver"
          class="org.springframework.web.servlet.view.UrlBasedViewResolver"
          p:viewClass="org.springbyexample.web.servlet.view.tiles2.DynamicTilesView"
          p:prefix="/WEB-INF/jsp/"
          p:suffix=".jsp" />

    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"
          p:basenames="messages" />
    
    <!-- Declare the Interceptor -->
    <mvc:interceptors>    
        <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"
              p:paramName="locale" />
    </mvc:interceptors>
    
    <!-- Declare the Resolver -->
    <bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver" />

</beans>
                
            

Custom flow for person handling create and edit. The decision-state checks if the id is null and if it is it goes to the 'createPerson' action-state, otherwise it goes to the 'editPerson' action-state. In 'createPerson' the prototype scoped personBean bean is put into 'flowScope' under 'person'. The evaluation is performed using the Spring Expression Language (Spring EL), which has been used by Spring Web Flow since version 2.1. The 'editPerson' action-state uses the Spring Data JPA person repository to look the person record based on the id in the edit URL.

Both create and edit forward to the 'personForm' view where the user has a save and cancel button. Both of these buttons are handled using the transition element. The 'save' transition saves the person using the person repository, and puts the result of the save into 'flowScope.person' along with the person id (in case of a create). The success message key is put into 'flashScope' (scope available until next view render), and then goes back to the person form. The cancel populates the latest search results and forward to end-state elements that have their view set to the person search page.

The 'personForm' view also has transitions for handling adding, editing, and deleting addresses. Adding and editing are passed to the 'address' subflow-state. The delete is handled by an evaluate element calling person repository. Both the address id and the person instance are passed into the address subflow. The person instance is used by an edit to retrieve the address to edit instead of looking it up from that database since it's already in scope. At the end of the address flow the address instance is output from the subflow and saved by the person flow's 'address' subflow-state.

Both flows are secured to the Spring Security role of 'ROLE_USER'. Which in this case is redundant since the entire webapp is secured to this role, but finer grained rules can make use of this and also it's good to secure the flow since they are reusable components (as subflows). The subflow could have only allowed only admins to access the address subflow.

Person Flow (/WEB-INF/jsp/person/person.xml)
                
<?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-2.0.xsd">

    <secured attributes="ROLE_USER" />
    
    <input name="id" />
    
    <decision-state id="createOrEdit">
        <if test="id == null" then="create" else="edit" />
    </decision-state>

    <action-state id="create">
        <evaluate expression="personBean" result="flowScope.person" />
        <transition to="personForm" />
    </action-state>

    <action-state id="edit">
        <evaluate expression="personService.findById(id)" result="flowScope.person" />
        <transition to="personForm" />
    </action-state>
    
    <view-state id="personForm" model="person" view="/person/form">
        <transition on="addAddress" to="address" bind="false">
            <set name="flashScope.addressId" value="''" />
        </transition>
        <transition on="editAddress" to="address">
            <set name="flashScope.addressId" value="requestParameters.addressId" />
        </transition>
        <transition on="deleteAddress" to="personForm">
            <evaluate expression="personService.deleteAddress(id, requestParameters.addressId)" result="flowScope.person" />
        </transition>
        
        <transition on="save" to="personForm">
            <evaluate expression="personService.save(person)" result="flowScope.person" />
            
            <set name="flowScope.id" value="person.id" />
            
            <set name="flashScope.statusMessageKey" value="'person.form.msg.success'" />
            
            <render fragments="content" />
        </transition>
        <transition on="cancel" to="cancel" bind="false">
            <evaluate expression="personService.find()" result="flowScope.persons" />
        </transition>
    </view-state>

    <subflow-state id="address" subflow="address"> 
        <input name="id" value="addressId"/>
        <input name="person" value="person"/>
        
        <output name="address" />
        
        <transition on="saveAddress" to="personForm">
            <evaluate expression="personService.saveAddress(id, address)" result="flowScope.person" />
            
            <set name="flashScope.statusMessageKey" value="'address.form.msg.success'" />
        </transition>
        <transition on="cancelAddress" to="personForm" />
    </subflow-state>
    
    <end-state id="personConfirmed" />

    <end-state id="cancel" />
    
</flow>
                
            

The flow is very similar to the person flow. The decision-state handles a create or an edit based on whether or not an id is passed into the flow. Then the action-state for the create put a new Address instance into scope and the edit gets it from the person instance. The save outputs the address instance and let's the parent flow handle saves.

Address Flow (/WEB-INF/jsp/address/address.xml)
                
<?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-2.0.xsd">

    <secured attributes="ROLE_USER" />
    
    <input name="id" />
    <input name="person" />

    <decision-state id="createOrEdit">
        <if test="id == ''" then="createAddress" else="editAddress" />
    </decision-state>
    
    <action-state id="createAddress">
        <evaluate expression="addressBean" result="flowScope.address" />
        <transition to="addressForm" />
    </action-state>

    <action-state id="editAddress">
        <evaluate expression="person.findAddressById(id)" result="flowScope.address" />
        <transition to="addressForm" />
    </action-state>
            
    <view-state id="addressForm" model="address" view="/address/form">
        <transition on="save" to="saveAddress" />
        <transition on="cancel" to="cancelAddress" bind="false" />
    </view-state>
    
    <end-state id="saveAddress">
        <output name="address" value="address"/>
    </end-state>

    <end-state id="cancelAddress" />
    
</flow>