It is recommended that you review the Spring Web Flow sample applications included in the release distribution for best-practice illustrations of the features of this framework. A description of each sample is provided below:
Phonebook - the original sample demonstrating most core features (including subflows).
Sellitem - demonstrates a wizard with conditional transitions, flow scope, flow execution redirects, and continuations.
Sellitem-JSF - The sellitem sample in a JSF environment (notice how the flow definition is more concise because JSF components care for data binding and validation).
Shippingrate - demonstrates Spring Web Flow together with the Prototype Javascript framework (for Ajax-style flows).
NumberGuess - demonstrates use of stateful middle-tier components to carry out business logic.
Flowlauncher - demonstrates all the possible ways to launch and resume flows.
Itemlist - demonstrates REST-style URLs and inline flows.
Fileupload - demonstrates multipart file upload.
Birthdate - demonstrates Struts integration and the MultiAction.
Phonebook-Portlet - the phonebook sample in a Portlet environment (notice how the flow definitions do not change).
The samples can be built from the command line and imported as Eclipse projects - all samples come with Eclipse project settings. It is also possible to start by importing the samples into Eclipse first and then build with Ant within Eclipse.
Java 1.5 (or greater) and Ant 1.6 (or greater) are prerequisites for building the sample applications. Ensure those are present in the system path or are passed on the command line. To build Web Flow samples from the command line, open a prompt, cd to the directory where Spring Web Flow was unzipped, and run the following:
cd projects/spring-webflow/build-spring-webflow ant dist
This builds all samples preparing "target" areas within each sample project subdirectory containing webapp structures in both exploded and WAR archive forms. The build also provides basic helper targets for deploying to Tomcat from Ant; however these webapp structures can be copied to any servlet container, and each project is also a Eclipse Dynamic Web Project (DWP) for easy deployment inside Eclipse with the Eclipse Webtools Project (WTP).
Importing the sample projects into Eclipse is easy. With a new or an existing workspace select: File > Import > Existing Projects into Workspace. In the resulting dialog, browse to the project subdirectory where Spring Web Flow was unzipped and choose it as the root directory to import from. Select OK. Here Eclipse will list all projects it found including the sample application projects. Select the projects you're interested in, and select Finish.
If you previously built each project from the command line, Eclipse will compile with no errors. If not, you will need to run the Ant build once for these errors to clear.
To build all projects inside Eclipse, import and expand the build-spring-webflow project, right-click on build.xml and select Run As > Ant Build. Doing this will run the default Ant target and will build all sample projects.
To build a single project inside Eclipse, simply select the project, right-click, and select Run As > Ant Build. You can also use the convenient shortcut ALT + SHIFT + X (Execute menu), then Q (Run Ant Build).
After Ant runs and the libraries needed to compile each project are downloaded, all errors in the Eclipse problems view should go away. Try refreshing a project (F5) if you still have errors. In general, from this point on you no longer need Ant: you can rely on Eclipse's incremental compile and Eclipse's web tools (WTP) built-in JEE support for deployment. (Ant is only needed in the system for command-line usage or when the list of jar dependencies for a project changes and new jars need to be downloaded).
Each Spring Web Flow sample application project is a Eclipse Dynamic Web Project (DWP), for easy deployment to a server running inside the Eclipse IDE. To take advantage of this, you must be running Eclipse 3.2 with Web Tools 1.5.
To run a sample application as a webapp inside Eclipse, simply select the project, right-click, and select Run -> Run On Server. A convenient shortcut for this action is ALT + SHIFT + X (Execute menu), R (Run on Server). The first time you do this you will be asked to setup a Server, where you are expected to point Eclipse to a location where you have a Servlet Container such as Apache Tomcat installed. Once your container has been setup and you finish the deployment wizard, Eclipse will start the container and automatically publish your webapp to it. In addition, it will launch a embedded web browser allowing you to run the webapp fully inside the IDE.
The Sellitem example demonstrates using Web Flow to build a shopping cart wizard with a shipping rate subflow, decision states, service and data access Spring POJO beans, Spring 2.0 form tags, and a Web Flow FormAction bean for data binding, validation, and error reporting.
The Sellitem example breaks down its Spring application configuration into a number of files organized according to purpose. Although the example itself uses a small number of beans you may consider organizing a real-world application (with many more beans) according to similar principles. Before going into the specifics of each individual context, use the diagram below to get a brief overview of all configuration files including location and purpose.
The web.xml configuration maps "*.htm" requests to the sellitem servlet - a Spring MVC DispatcherServlet:
<servlet> <servlet-name>sellitem</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value> /WEB-INF/sellitem-servlet-config.xml /WEB-INF/sellitem-webflow-config.xml </param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>sellitem</servlet-name> <url-pattern>*.htm</url-pattern> </servlet-mapping>
The contextConifgLocation parameter for the DispatcherServlet indicates the Spring MVC web context for the sellitem servlet is spread over two xml files: sellitem-servlet-config.xml and sellitem-webflow-config.xml. The web.xml also requests an additional Spring context to be loaded from the classpath through the ContextLoaderListener:
<context-param> <param-name>contextConfigLocation</param-name> <param-value> classpath:org/springframework/webflow/samples/sellitem/services-config.xml </param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener>
This service layer context defines beans to be referenced from web flow definitions. The next section discusses the content of this context in more detail.
The services-config.xml loaded from the classpath through Spring MVC's ContextLoaderListener defines several beans for the service and data access layers of the application. For example, the service context defines a DAO bean ("saleProcessor") and injects it with a data source:
<bean id="saleProcessor" class="org.springframework.webflow.samples.sellitem.JdbcSaleProcessor"> <property name="dataSource" ref="dataSource"/> </bean> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="org.hsqldb.jdbcDriver"/> <property name="url" value="jdbc:hsqldb:mem:sellItem"/> <property name="username" value="sa"/> </bean>
The services context also declares a bean of type InMemoryDatabaseCreator set to autowire by type meaning that its fields will be compared against the types of beans available in the context and will be automatically set when a match is found. Hence the dataSource bean is used to set the dataSource property of InMemoryDatabaseCreator:
<bean id="databaseCreator" class="org.springframework.webflow.samples.sellitem.InMemoryDatabaseCreator" autowire="byType"/>
Looking inside the InMemoryDatabaseCreator, its initDao() method invoked during context initialization creates a table called T_SALES for use by the sample application. This table is created in an in-memory hsqldb database called sellitem (based on the url property of the dataSource bean). It's also worth noting the bean declarations related to declarative transaction management:
<tx:annotation-driven/> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean>
The "<tx:annotation-driven>"declaration indicates transaction configuration is governed by Java 5 annotations used in bean classes such as this annotation in the SaleProcessor interface:
@Transactional public interface SaleProcessor { public void process(Sale sale); }
For annotated beans the Spring container automatically creates proxies according to the transaction semantics in the annotation metadata. The "<tx:annotation-driven>" tag has a transaction-manager attribute but this attribute is not required if the transaction manager bean is named "transactionManager".
The Spring MVC web context is split over two files - sellitem-servlet-config.xml and sellitem-webflow-config.xml. The sellitem-servlet-config.xml defines a controller and a view resolver.
<bean name="/pos.htm" class="org.springframework.webflow.executor.mvc.FlowController"> <property name="flowExecutor" ref="flowExecutor" /> </bean> <!-- Maps flow view-state view names to JSP templates --> <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/" /> <property name="suffix" value=".jsp" /> </bean>
FlowController is a web flow controller extending Spring MVC's AbstractController delegating requests (in this case for the "/pos.htm" servlet path) to the flowExecutor bean it is configured with. FlowController acts as gateway to Web Flow and a single controller instance can serve the application as most of the actual control logic is encapsulated in web flow definitions.
The sellitem-webflow-config.xml defines web flow specific beans such as a flow executor, a flow registry and a flow listener beans:
<!-- Launches new flow executions and resumes existing executions --> <flow:executor id="flowExecutor" registry-ref="flowRegistry"> <flow:execution-listeners> <flow:listener ref="listener" criteria="sellitem-flow" /> </flow:execution-listeners> </flow:executor> <!-- Creates the registry of flow definitions for this application --> <flow:registry id="flowRegistry"> <flow:location path="/WEB-INF/flows/**/*-flow.xml" /> </flow:registry> <!-- Observes the lifecycle of sellitem-flow executions --> <bean id="listener" class="org.springframework.webflow.samples.sellitem.SellItemFlowExecutionListener" />
The FlowExecutor is the central entry point into the Spring Web Flow system. It drives the execution of flow definitions configured through the flowRegistry. The flowRegistry bean is configured to load definitions from files ending with "-flow.xml" in any subdirectory of /WEB-INF/flows. This matches to sellitem-flow.xml, shipping-flow.xml, sellitem-simple-flow.xml, sellitem-conversation-scope-flow.xml and shipping-conversation-scope-flow.xml.
As shown here the flow executor can also be configured with a flow listener, which is a callback mechanism for flow execution lifecycle events. The SellItemFlowExecutionListener extends FlowExecutionListenerAdapter - a default implementation of the FlowExecutionListener interface sparing the need to implement methods for all lifecycle events.
Looking inside SellItemFlowExecutionListener, it implements the stateEntering method executed for whenever a new state is about to be entered. The logic in this method checks if the current web flow state has an attribute named "role" and if so it ensures the user has that role:
String role = nextState.getAttributes().getString("role"); if (StringUtils.hasText(role)) { HttpServletRequest request = ((ServletExternalContext)context.getExternalContext()).getRequest(); if (!request.isUserInRole(role)) { throw new EnterStateVetoException(context.getActiveFlow().getId(), context.getCurrentState().getId(), nextState.getId(), "State requires role '" + role + "', but the authenticated user doesn't have it!"); } }
Based on the above definitions - web.xml, Spring MVC controller bean, and web flow registry, the sellitem-flow can be initiated with the following URI:
/swf-sellitem/pos.htm?_flowId=sellitem-flow
Note: although it is possible to invoke the shipping-flow directly as well, it expects an input attribute and is intended to be invoked as a subflow.
Before tracing the sequence of states in sellitem-flow.xml notice the import declaration at the bottom of that file:
<import resource="sellitem-beans.xml"/>
The sellitem-beans.xml located in the same directory declares a web flow FormAction bean for use in the flow definition and configures it with a SaleValidator and a SellItemPropertyEditorRegistrar:
<!-- Manages setting up, binding input to, and validating a Sale "backing wizard form object" --> <bean id="formAction" class="org.springframework.webflow.action.FormAction"> <property name="formObjectName" value="sale"/> <property name="validator"> <bean class="org.springframework.webflow.samples.sellitem.SaleValidator"/> </property> <!-- Installs property editors used to format non-String fields like 'shipDate' --> <property name="propertyEditorRegistrar"> <bean class="org.springframework.webflow.samples.sellitem.SellItemPropertyEditorRegistrar"/> </property> </bean>
The SellValidator will be used to validate form input data. The SellItemPropertyEditorRegistrar is responsible for registering custom property editors. Such editors are used to bind text data from HTML form fields to server-side Objects. For example SellItemPropertyEditorRegistrar registers a custom date editor:
public void registerCustomEditors(PropertyEditorRegistry registry) { registry.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("MM/dd/yyyy"), true)); }
This editor will bind the shipDate form field in shippingDetailsForm.jsp to the shipDate property of the Sale object on the server side.
The flow begins by declaring a "sale" variable - an object of type Sale:
<var name="sale" class="org.springframework.webflow.samples.sellitem.Sale"/>
The formAction bean will use the sale variable for form binding and validation (see sellitem-beans.xml).
The start state for the flow enterPriceAndItemCount is a view state, which resolves to the JSP page /WEB-INF/jsp/priceAndItemCountForm.jsp:
<view-state id="enterPriceAndItemCount" view="priceAndItemCountForm"> <render-actions> <!-- create the backing form object and initialize a empty errors collection --> <action bean="formAction" method="setupForm"/> </render-actions> <transition on="submit" to="enterCategory"> <action bean="formAction" method="bindAndValidate"> <attribute name="validatorMethod" value="validatePriceAndItemCount"/> </action> </transition> </view-state>
The view state uses a render action to invoke the setupForm method of the formAction bean. The setupForm method prepares a form object based on the "sale" variable declared at the top of the flow definition.
The priceAndItemCountForm.jsp page collects a price and an itemCount using Spring 2.0 form input tags binding form fields to properties in the form backing object "sale". When pressed, the submit button "_eventId_submit" causes a web flow transition for an event with the id of "submit" to the view state "enterCategory". Prior to transitioning the formAction's bindAndValidate method is called to perform binding and (partial) validation using the validatePriceAndItemCount method of the validator object.
The next view state enterCategory (based on categoryForm.jsp) collects inputs for sale category and whether shipping is required. On submit it transitions to the requiresShipping state:
<view-state id="enterCategory" view="categoryForm"> <transition on="submit" to="requiresShipping"> <action bean="formAction" method="bind"/> </transition> </view-state>
The requiresShipping state is a decision state making flow routing decisions. It evaluates a boolean expression against the executing flow and it decides where to transition to next. Here the shipping boolean property of the "sale" form backing object is checked to decide whether to go to the enterShippingDetails subflow state or proceed directly to processSale.
<decision-state id="requiresShipping"> <if test="${flowScope.sale.shipping}" then="enterShippingDetails" else="processSale"/> </decision-state>
The enterShippingDetails subflow state is based on shipping-flow.xml located in the same directory. The form backing object "sale" is passed to it as an input attribute using an attribute mapper declaration:
<subflow-state id="enterShippingDetails" flow="shipping-flow"> <attribute-mapper> <input-mapper> <input-attribute name="sale"/> </input-mapper> </attribute-mapper> <transition on="finish" to="processSale"/> </subflow-state>
The shipping-flow subflow is a simple flow with one view state. It collects the shipping details, binds the data and returns to its parent flow. The id of the subflow end state "finish" is returned to the parent subflow state causing a transition to the processSale action state.
<action-state id="processSale"> <bean-action bean="saleProcessor" method="process"> <method-arguments> <argument expression="flowScope.sale"/> </method-arguments> </bean-action> <transition on="success" to="finish"/> </action-state>
The saleProcessor bean, a POJO defined in services-config.xml is invoked using a "bean-action" declaration (as opposed to the "action" declation used to invoke a web flow Action such as FormAction). The saleProcessor (an instance of JdbcSaleProcessor) performs a database update using the values of the Sale object and upon successful completion transitions to the end view state:
<end-state id="finish" view="costOverview"> <entry-actions> <action bean="formAction" method="setupForm"/> </entry-actions> </end-state>
Then end state calls FormAction's setupForm method again. This does not re-create the "sale" form object (still in flow scope) but it does ensure any custom property editors are registered for use in rendering the JSP.
A simpler version of the sellitem-flow is available in the sellitem-simple-flow.xml file. This version uses a view state to gather shipping details instead of using a subflow. You can launch the sellitem-simple-flow using the following URI:
/swf-sellitem/pos.htm?_flowId=sellitem-simple-flow
This web flow is equivalent in functionality to the sellitem-flow definition described above. The main difference is that it uses "conversation" scope to store the form backing object declared in /WEB-INF/flows/converstation-scope/sellitem-beans.xml.
<bean id="formAction" class="org.springframework.webflow.action.FormAction"> <property name="formObjectName" value="sale"/> <property name="formObjectScope" value="CONVERSATION"/> <property name="formErrorsScope" value="CONVERSATION"/>
Conversation scope retains attributes stored in it for the life of the flow execution and is shared by all flow sessions. For example when invoking the shipping details subflow the parent flow does not need to pass the "sale" form backing object because it is now stored in conversation scope and is accessible to both flows:
<subflow-state id="enterShippingDetails" flow="shipping-conversation-scope-flow"> <transition on="finish" to="processSale"/> </subflow-state>
Also, when the "sale" object needs to be accessed it is done by referencing conversation cope:
<decision-state id="requiresShipping"> <if test="${conversationScope.sale.shipping}" then="enterShippingDetails" else="processSale"/> </decision-state>
You can launch the sellitem-conversation-scope-flow using the following URI:
/swf-sellitem/pos.htm?_flowId=sellitem-conversation-scope-flow
The Sellitem-JSF example uses Web Flow and JSF to build a shopping cart wizard. Navigation logic and supporting managed beans are supplied by Spring Web Flow, while UI views and overall servlet processing is based on JSF technology.
Note | |
---|---|
The underlying Web Flow definitions for the Sellitem and the Sellitem-JSF examples are very similar. To avoid repetition the documentation for the Sellitem-JSF example focuses primarily on the points of integration between Web Flow and JSF. For further general information on Web Flow definitions and supporting Java classes for the Sellitem example, please refer to the Sellitem example documentation. |
The web.xml contains standard JSF configuration including mappings for the JSF front servlet: it handles all requests ending with "*.faces":
<!-- Faces Servlet --> <servlet> <servlet-name>Faces Servlet</servlet-name> <servlet-class>javax.faces.webapp.FacesServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>Faces Servlet</servlet-name> <url-pattern>*.faces</url-pattern> </servlet-mapping>
In addition, the web.xml loads a Spring root web application context containing the services used by the application:
<context-param> <param-name>contextConfigLocation</param-name> <param-value> classpath:org/springframework/webflow/samples/sellitem/services-config.xml /WEB-INF/webflow-config.xml </param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener>
The services-config.xml contains POJO beans required for the services and data access layers of the application. These declarations are very similar to the Sellitem example (and explained in more detail there). The webflow-config.xml contains Web Flow related bean definitions. These definitions will be explained a little bit further on in the context of how they fit into the JSF phases lifecycle.
To plug in Web Flow, a few things must be added once to faces-config.xml. This is demonstrated in the faces-config.xml of Sellitem-JSF:
<application> <navigation-handler>org.springframework.webflow.executor.jsf.FlowNavigationHandler</navigation-handler> <variable-resolver>org.springframework.webflow.executor.jsf.DelegatingFlowVariableResolver</variable-resolver> </application> <lifecycle> <phase-listener>org.springframework.webflow.executor.jsf.FlowPhaseListener</phase-listener> </lifecycle>
The FlowNavigationHandler delegates view navigation handling to the the Web Flow system when a flow is initiated or resumed.
The DelegatingFlowVariableResolver is suitable for use along side other variable resolvers to support EL binding expressions like {#bean.property} where "bean" could be a property in any supported scope. The resolver search algorithm looks in flash scope first, then flow scope, then conversation scope. If no variable is found, this resolver delegates to the next resolver in the chain.
The FlowPhaseListener invoked during beforePhase and afterPhase JSF events is responsible for managing the lifecycle of a FlowExecution and making it available to other JSF artifacts during the lifecycle of a JSF request.
Examining the definitions in faces-config.xml highlighted the ability to use plug Web Flow in as a navigation handler and as a source for JSF managed beans. Now we can turn to the question of how to configure the web flow system itself in a JSF environment.
The Spring web context fragment /WEB-INF/webflow-config.xml contains the following configuration:
<!-- Launches new flow executions and resumes existing executions --> <flow:executor id="flowExecutor" registry-ref="flowRegistry" /> <!-- Creates the registry of flow definitions for this application --> <flow:registry id="flowRegistry"> <flow:location path="/WEB-INF/flows/sellitem-flow.xml" /> </flow:registry>
Here the flow executor is configured to support execution of a single flow definition - sellitem-flow.xml. The executor bean has been assigned the id "flowExecutor". This id is significant and is required for the JSF artifacts to detect the executor and its services.
The intro.jsp page shows how the configured web flow sellitem-flow.xml can be launched using a JSF command link component.
<h:form> <h:commandLink value="Sell Item" action="flowId:sellitem-flow"/> </h:form>
This causes the sellitem-flow to be initiated. Once a flow is initiated each subsequent JSP page can participate in the flow (the flow execution key is tracked for you).
A few notable differences between Sellitem and Sellitem-JSF to keep in mind:
The JSF version of the sellitem flow definition is simpler because JSF components care for data binding and validation.
In its web flow definition Sellitem-JSF uses actual JSP names (instead of the logical view names used in Sellitem) to be rendered by JSF. This is consistent with normal JSF-isms.
The JSP pages in Sellitem-JSF use unified EL to access the converastion scoped Sale object - e.g. #{sale.price}.
Sellitem-JSF uses JSF component tags for UI and Sellitem uses Spring form tags.
There is no need to manually track the flow execution key because it is tracked for you in the JSF view root.
The combination of delegating flow variable resolution plus automatic flow execution key management means JSF views selected a flow look like standard JSF views to JSF developers. Also, JSF components help simplify flow definition logic as the flow no longer has to worry about data binding and validation.
For more information and understanding on the Sellitem flow definition logic itself please refer to the documentation for the original Sellitem example.
The Shippingrate sample demonstrates the use of Spring Web Flow in combination with Ajaxian techniques. It consists of several wizard-style steps executed with Ajax requests and refreshing a portion of the page. The input is collected from the user in incremental steps. It is stored in a flow-scoped object and is then used to calcualte a shipping rate. The example also demonstrates invocation of a service-layer bean defined in a Spring context to perform calculations and to provide reference data such as countries and package types.
The web.xml configuration maps requests for "*.htm" to the shippingrate servlet - a regular Spring MVC DispatcherServlet:
<servlet> <servlet-name>shippingrate</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>shippingrate</servlet-name> <url-pattern>*.htm</url-pattern> </servlet-mapping>
The web.xml also ensures the following Spring context file is loaded at runtime from the web application classpath:
<context-param> <param-name>contextConfigLocation</param-name> <param-value> classpath:org/springframework/webflow/samples/shippingrate/domain/services.xml </param-value> </context-param>
The services.xml Spring context defines a "rateService" bean providing operations for making shipping rate calculations and for retrieving reference data required for display in the JSP pages of the application.
The Spring MVC servlet context for the shippingrate servlet (WEB-INF/shippingrate-servlet.xml) defines one controller bean:
<bean name="/rates.htm" class="org.springframework.webflow.executor.mvc.FlowController"> <property name="flowExecutor" ref="flowExecutor" /> </bean>
FlowController is a Web Flow controller. It is the main point of integration between Spring MVC and Spring Web Flow routing requests to one or more managed web flow executions. The FlowController is injected with flowExecutor and flowRegistry beans:
<!-- Launches new flow executions and resumes existing executions. --> <flow:executor id="flowExecutor" registry-ref="flowRegistry" repository-type="simple"/> <!-- Creates the registry of flow definitions for this application --> <flow:registry id="flowRegistry"> <flow:location path="/WEB-INF/flows/**/*-flow.xml" /> </flow:registry>
The flowExecutor and the flowRegistry beans collectively configure the FlowController with one web flow - the getRate-flow defined in /WEB-INF/flows/getRate-flow.xml. The flowExecutor uses a "simple" repository, which manages execution state in the user session.
Given the above definitions the following URI can be used to initiate the getRate-flow:
/swf-shippingrate/rates.htm?_flowId=getRate-flow
The shippingrate example consists of several wizard-style steps. After the initial index.jsp subsequent pages are loaded in an Ajax manner without reloading the entire page.
The Ajax requests are done with the help of the Prototype framework and a thin JavaScript layer over it providing convenient functions for processing Ajax form and get requests. The required Javascript libraries are included in index.jsp as follows:
<script src="prototype.js" type="text/javascript"></script> <script src="swf_ajax.js" type="text/javascript"></script>
When index.jsp is loaded the following JavaScript invokes the getRate-flow and replaces the content of the getRateWizard div tag with the response returned from the server:
<div id="getRateWizard"> <script type="text/javascript"> window.onload = function() { new SimpleRequest('getRateWizard', 'rates.htm', 'get', '_flowId=getRate-flow'); }; </script> </div>
Functions are first-class citizens and a type in JavaScript. The script above creates an instance of the SimpleRequest function defined in swf_ajax.js. This function invokes Prototype's Ajax.Updater with the specified URL and request parameters. On success the content of the getRateWizard div is replaced with the response returned from the server. On failure such as an HTTP response code other 200 (OK) an error message is displayed.
The next few pages are form-based JSP's - selectCustomer.jsp, selectReceiver.jsp, etc. Each of them contains the following JavaScript call at the bottom:
<script type="text/javascript"> formRequest('selectCustomerTypeForm'); </script>
The formRequest function is also defined in swf_ajax.js and it uses Prototype to register a handler for the form submit event:
function formRequest(formElementId) { Event.observe(formElementId, 'submit', handleSubmitEvent, true); }
The handleSubmitEvent function extracts the form parameters, stops the submit event, and posts an AJAX request via XMLHttpRequest. On success the results returned form the server replace the content of the form. On failure such as an HTTP response code other 200 (OK) an error message is displayed.
Although not demonstrated in this example a back button can be implemented in parallel with the Next button used to advance from one screen to the next. This would be necessary because the browser back button - a common issue in Ajax applications, contrary to user expectation returns to the page prior to the first Ajax request.
As a result of the Ajax requests the entire wizard is able to function within a portion of the page without refresing the remaining information on it.
The getRate-flow (/WEB-INF/jsp/flows/getRate-flow.xml) defines the following start state:
<view-state id="selectCustomerType" view="selectCustomer"> <transition on="submit" to="selectSender"> <action bean="formAction" method="bind" /> </transition> </view-state>
This is a view state, which will display the initial form using the JSP page /WEB-INF/jsp/selectCustomer.jsp. Notice, the use of a start action executed immediately before the JSP is displayed:
<start-actions> <action bean="formAction" method="setupForm" /> </start-actions>
The "formAction" bean is defined in the Spring servlet context (/WEB-INF/shippingrate-servlet.xml). It specifies a form object and a validator to use for form data binding and validation:
<!-- Performs "form backing object" data binding and validation on input submit --> <bean id="formAction" class="org.springframework.webflow.action.FormAction"> <property name="formObjectName" value="rateCriteria" /> <property name="formObjectClass" value="org.springframework.webflow.samples.shippingrate.domain.RateCriteria" /> <property name="formObjectScope" value="FLOW" /> <property name="validator"> <bean class="org.springframework.webflow.samples.shippingrate.domain.RateCriteriaValidator" /> </property> </bean>
The form object of type RateCriteria will be used to collect data from the user in several steps. The form object will be stored in FLOW scope and will not be re-created with each request as long as the flow hasn't reached its end state. The actual binding of html form fields to the RateCriteria object is based on Spring's data binding mechanism. Html form fields are surrounded with the <spring:bind> tag containing the path nested property field. FormAction's bindAndValidate method will initiate the actual binding on the server side between HTTP request parameters and RateCriteria data fields.
When the selectCustomer.jsp submits back to the FlowController via "/swf-shippingrate/rate.htm" it uses a submit button named "_eventId_submit". This indicates to Web Flow a transition to the "selectSender" view state. This view state is defined as follows:
<view-state id="selectSender" view="selectSender"> <render-actions> <bean-action bean="rateService" method="getCountries"> <method-result name="countries" /> </bean-action> </render-actions> <transition on="submit" to="selectReceiver"> <action bean="formAction" method="bindAndValidate"> <attribute name="validatorMethod" value="validateSender" /> </action> </transition> </view-state>
The selectSender view state has a render action: the "rateService" bean that was loaded through the services.xml context referenced in web.xml. The purpose of the render action is to load data required to render the JSP. In this case the rateService bean has a method called getCountries that returns a list of countries to be displayed in a drop-down by the JSP.
The "selectSender" view state also defines one transition: on event with id of "submit" a transition to the "selectReceiver" view state occurs. A pre-requisite for the transition to occur is the successful completion of formAction bean's bindAndValidate method. The attribute "validatorMethod" on the bean specifies the name of the method to invoke on the Validator object specifically for the fields of the current screen. If the bindAndValidate method does not succeed the transition does not take place and the flow remains in the "selectSender" view state where the user can review the errors and modify the selection.
The next two states in the flow - selectReceiver and selectPackageDetails use similar mechnisms. The rateSevice bean is used to retrieve countries and package types for use in the JSP. The form backing object RateCriteria stored in FLOW scope is used to collect user input with each form submit.
The "findRate" action state occurs after all user input has been provided. It is defined as follows:
<action-state id="findRate"> <bean-action bean="rateService" method="getRate"> <method-arguments> <argument expression="flowScope.rateCriteria" /> </method-arguments> <method-result name="rate" /> </bean-action> <transition on="success" to="showRate" /> </action-state>
Logic for the action state is provided by the getRate method of the rateService bean. The RateCriteria object stored in FLOW scope and containing the user input is passed to the rateService bean. The result of the method is exposed in request scope under the name "rate".
The next and final state "showRate" is a JSP page, which accesses the calculated rate information and displays it to the user.
Numberguess uses Web Flow to implement two number guessing games. For each game the user can enter multiple guesses and depending on the answer either transition back to the same screen or advance to the final screen. Logic for the guessing games is provided through FLOW-scoped beans, which also maintain state such as the total number of guesses. The example defines transitions using event pattern matching and custom exception handlers.
The web.xml configuration maps "*.htm" requests to the numberguess servlet - a regular Spring MVC DispatcherServlet:
<servlet> <servlet-name>numberguess</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/dispatcher-servlet.xml</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>numberguess</servlet-name> <url-pattern>*.htm</url-pattern> </servlet-mapping>
The Spring web context is loaded from a file called /WEB-INF/dispatcher-servlet.xml.
The Spring MVC web context (WEB-INF/dispatcher-servlet.xml) defines one controller bean:
<bean name="/play.htm" class="org.springframework.webflow.executor.mvc.FlowController"> <property name="flowExecutor" ref="flowExecutor" /> </bean>
FlowController is a Web Flow controller. It is the main point of integration between Spring MVC and Spring Web Flow routing requests to one or more managed web flow executions. The FlowController is injected with flowExecutor and flowRegistry beans:
<!-- Launches new flow executions and resumes existing executions. --> <flow:executor id="flowExecutor" registry-ref="flowRegistry" repository-type="singlekey"/> <!-- Creates the registry of flow definitions for this application --> <flow:registry id="flowRegistry"> <flow:location path="/WEB-INF/higherlower.xml" /> <flow:location path="/WEB-INF/mastermind.xml" /> </flow:registry>
The flowExecutor and the flowRegistry beans collectively configure the FlowController with two web flows - higherlower and mastermind. This flowExecutor is configured with a simple repository that assigns a single flow execution key per conversation. The key, once assigned, never changes for the duration of the conversation.
Given the above definitions the following URI's can be used to initiate each of the two flows:
/swf-numberguess/play.htm?_flowId=higherlower /swf-numberguess/play.htm?_flowId=mastermind
The Spring MVC servlet context also defines a view resolver bean for resolving logical view names. In general Web Flow does not aim to replace the flexibility of Spring MVC for view resolution. It focuses on the C in MVC.
The Higherlower flow (/WEB-INF/higherlower.xml) starts with the following flow variable declaration:
<var name="game" class="org.springframework.webflow.samples.numberguess.HigherLowerGame"/>
This variable is automatically created when an execution of the flow begins and will exist in FLOW scope throughout its duration.
The start state for the flow is defined as follows:
<view-state id="enterGuess" view="higherlower.enterGuess"> <transition on="submit" to="makeGuess"/> </view-state>
The view resolver bean of Spring MVC will resolve "higherlower.enterGuess" to /WEB-INF/jsp/higherlower.enterGuess.jsp. This JSP has a form with one input field for the guess number. The "game" variable referenced throughout the JSP is the FLOW-scoped variable that was declared at the top of the flow definition.
The name of the form submit button "_eventId_submit" indicates the event id to use for deciding where to transition to next. Given an event with id of "submit" the "enterGuess" view state transitions to the "makeGuess" action state defined as follows:
<action-state id="makeGuess"> <evaluate-action expression="flowScope.game.makeGuess(requestParameters.guess)"> <evaluation-result name="guessResult"/> </evaluate-action> <transition on="CORRECT" to="showAnswer"/> <transition on="*" to="enterGuess"/> <transition on-exception="java.lang.NumberFormatException" to="enterGuess"/> </action-state>
The makeGuess action state consists of one evaluate action and three transitions. Evaluate actions are used to invoke logic encapsulated in a FLOW-scoped object - in this case the game bean. The makeGuess method of the game bean returns one of several enum values it defines:
enum GuessResult { TOO_HIGH, TOO_LOW, CORRECT, INVALID }
Web Flow detects the returned result from the makeGuess method is a JDK 1.5 enum type and creates an Event with a String id matching the enum value. If the makeGuess method returns CORRECT a transition to the final showAnswer state occurs. For any other event (defined with the event pattern on="*") Web Flow returns to the enterGuess state. The makeGuess state also defines one on-exception transition demonstrating how specific Exceptions can be incorporated into flow transition logic.
The end-state showAnswer resolves to the JSP page /WEB-INF/jsp/higherlower.showAnswer.jsp, which simply shows the correct guess. At this point the flow has ended and the "game" bean is no longer in scope.
The mastermind flow uses a similar flow definition to implement a 4-digit guessing game:
<var name="game" class="org.springframework.webflow.samples.numberguess.MastermindGame"/> <start-state idref="enterGuess"/> <view-state id="enterGuess" view="mastermind.enterGuess"> <transition on="submit" to="makeGuess"/> </view-state> <action-state id="makeGuess"> <evaluate-action expression="flowScope.game.makeGuess(requestParameters.guess)"> <evaluation-result name="guessResult"/> </evaluate-action> <transition on="CORRECT" to="showAnswer"/> <transition on="*" to="enterGuess"/> </action-state> <end-state id="showAnswer" view="mastermind.showAnswer"/>
The MastermindGame class encapsulates the logic for the game and is stored as a FLOW-scoped bean. It returns one of three possible enum values - WRONG, CORRECT, or INVALID, which Web Flow converts to events with id's matching the enum values. If the guess is INVALID the JSP page /WEB-INF/jsp/mastermind.enterGuess.jsp will print an error message. If the guess is CORRECT the flow will transition to the showAnswer end state and complete the flow.
Flowlauncher demonstrates two different ways one web flow can launch another - by redirecting to it or by launching it as a subflow. Flowlauncher has two flows: Sample A and Sample B. As a root level flow Sample A either transitions to B through a subflow state or redirects to B in its end state.
The web.xml configuration maps "*.htm" requests to the flowlauncher servlet - a regular Spring MVC DispatcherServlet:
<servlet> <servlet-name>flowlauncher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>flowlauncher</servlet-name> <url-pattern>*.htm</url-pattern> </servlet-mapping>
The Spring MVC web context (WEB-INF/flowlauncher-servlet.xml) defines one controller bean:
<bean name="/flowController.htm" class="org.springframework.webflow.executor.mvc.FlowController"> <property name="flowExecutor" ref="flowExecutor" /> </bean>
FlowController is a Web Flow extension of Spring MVC's AbstractController. It contains a FlowExecutor and directs incoming requests for one or more managed flow executions to it. The FlowExecutor bean is configured in the same context:
<!-- Launches new flow executions and resumes existing executions. --> <flow:executor id="flowExecutor" registry-ref="flowRegistry"/> <!-- Creates the registry of flow definitions for this application --> <flow:registry id="flowRegistry"> <flow:location path="/WEB-INF/sampleA.xml" /> <flow:location path="/WEB-INF/sampleB.xml" /> </flow:registry>
A single FlowController may direct all flows for an application serving as a gateway to Web Flow. Based on the above definitions the flows sampleA and sampleB can be invoked as follows:
/swf-flowlauncher/flowController.htm?_flowId=sampleA /swf-flowlauncher/flowController.htm?_flowId=sampleB
The welcome index.html file for the web application invokes the flows and passes additional input using either a URL link or a form submit.
The Sample A web flow (/WEB-INF/sampleA.xml) begins with an input mapping declaration:
<input-mapper> <mapping source="input" target="flowScope.input" /> </input-mapper>
This declaration reads "when a new execution of this flow starts map the input attribute named input into a flowScope attribute also named input". Spring Web Flow will automatically provide the request parameters as input to the flow when launching a new flow execution. Following this declaration the input request parameter will remain available for the duration of the flow.
There are 3 states in this flow: the start state, the end state, and a subflow state. The start state is a view state - it will display a JSP page and allow the user to make a choice. The subflow state initiates Sample B as a subflow of the current flow - subflows give the ability to compose independent modules together to compose complex controller workflows. And the end state launches Sample B by redirecting to it.
The subflow state launches B with the following input attribute declaration. This declaration reads "pass the value of the flow-scoped attribute named input as an attribute also named input to subflow B.
<attribute-mapper> <input-mapper> <mapping source="flowScope.input" target="input" /> </input-mapper> </attribute-mapper>
The next line is a transition defining how to respond when the subflow ends: advance back to the start state for Sample A.
<transition on="end" to="aPage" />
The end state demonstrates how to redirect to Sample B upon completion of the root level flow Sample A:
<end-state id="endAndLaunchB" view="flowRedirect:sampleB?input=${requestParameters.input}" />
This declaration causes A to be terminated and B to start with the given requst input parameter.
The flow Sample B (/WEB-INF/sampleB.xml) - used as a subflow in Sample A has two simple states: a view state and an end state. From the view state "bPage" the flow transitions to the end state:
<view-state id="bPage" view="bPage"> <transition on="end" to="end" /> </view-state> <end-state id="end" />
The "id" attribute of the end state matches the "on" attribute of the transition in the outer flow's subflow state, which the outer flow uses to resume itself.
Also notice how bPage.jsp makes a check to detect if Sample B is running as a subflow of Sample A or if it is running as a top-level flow:
<c:if test="${!flowExecutionContext.activeSession.root}">
The FlowExecutionContext object is exposed to the views (JSPs) to make information like this available during response rendering.
Itemlist demonstrates how to configure a FlowExecutor with an argument handler enabling it to process REST-style requests where the name of the target flow is in the URL instead of a _flowId request parameter. The example also demonstrates inner flows as well as how an output parameter can be passed from a subflow to a parent flow. Finally, it serves as an illustration of how to configure Spring Web Flow using classic Spring 1.x bean definitions.
The web.xml configuration maps "/app/*" requests to the itemlist servlet - a regular Spring MVC DispatcherServlet:
<servlet> <servlet-name>itemlist</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>itemlist</servlet-name> <url-pattern>/app/*</url-pattern> </servlet-mapping>
The Spring MVC web context (/WEB-INF/itemlist-serlvet.xml) defines one controller and one URL handler mapping:
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping"> <property name="alwaysUseFullPath" value="true" /> <property name="mappings"> <value>/app/**/**=flowController</value> </property> </bean> <bean id="flowController" class="org.springframework.webflow.executor.mvc.FlowController"> <property name="flowExecutor" ref="flowExecutor" /> <property name="argumentHandler"> <bean class="org.springframework.webflow.executor.support.RequestPathFlowExecutorArgumentHandler" /> </property> </bean>
All requests with a servlet path matching "/app/**/**" are mapped to the "flowController" bean. The FlowController is a Web Flow extension of Spring MVC's AbstractController delegating requests to one or more managed web flows. It acts as gateway to Web Flow defined control logic and a single instance can serve the application.
The usual way to launch a specific web flow is to pass the _flowId request parameter. However, this example is configured with a RequestPathFlowExecutorArgumentHandler for processing REST-style URL's. Requests for services built around the REST concept are encoded in the URL and not as query string parameters. The way to invoke a web flow with this argument handler is to follow:
http://${host}/${context path}/${dispatcher path}/${flowId}
The FlowController is configured with a flowExecutor and flowRegistry beans containing two web flows - itemlist and itemlist-alternate:
<!-- Launches new flow executions and resumes existing executions: Spring 1.2 config version --> <bean id="flowExecutor" class="org.springframework.webflow.config.FlowExecutorFactoryBean"> <property name="definitionLocator" ref="flowRegistry"/> </bean> <!-- Creates the registry of flow definitions for this application: Spring 1.2 config version --> <bean id="flowRegistry" class="org.springframework.webflow.engine.builder.xml.XmlFlowRegistryFactoryBean"> <property name="flowLocations"> <list> <value>/WEB-INF/itemlist.xml</value> <value>/WEB-INF/itemlist-alternate.xml</value> </list> </property> </bean>
The FlowRegistry and FlowExecutor are defined with Spring 1.2 compatible bean definitions. However, starting with Spring 2.0 Web Flow also offers the custom tags flow:registry and flow:executor, which are more readable and less verbose.
Based on the above web context definition use the following URL's to invoke the itemlist or the itemlist-alternate web flows:
/swf-itemlist/app/itemlist /swf-itemlist/app/itemlist-alternate
Also defined in itemlist-servlet.xml are three "action" beans - createItemAction, addItemAction, and mapItemAction, which will be referenced from action states in the web flow definitions.
The itemlist flow allows adding items to a list. There are two view states - displayItemList and displayItem, and two action states - createItem and addItem.
The displayItemList view state resolves to /WEB-INF/jsp/itemList.jsp, which lists all items on the list and displays an "Add" button with the name "_eventId_add". The name of the button indicates the event id to use for deciding where to transition to next. Also, notice that instead of posting a "_flowId" parameter the JSP sets the form action to the value of flowExecutionKey - a value automatically made available in the page context by Web Flow:
<form action="${flowExecutionKey}" method="post"/>
When the form submits an event with the "_eventId_add" button the displayItemList view state transitions to the createItem action state.
<view-state id="displayItemlist" view="itemlist"> <transition on="add" to="createItem" /> </view-state> <action-state id="createItem"> <action bean="createItemAction" /> <transition on="success" to="displayItem" /> </action-state>
The "createItemAction" bean is declared in the Spring MVC context (/WEB-INF/itemlist-servlet.xml). It simply returns "success", which causes a transition to the displayItem view state.
The next two states displayItem and addItem allow adding an item to the list variable declared at the top of the flow:
<var name="list" class="java.util.ArrayList" />
The "addItemAction" bean is also declared in the Spring MVC context. It performs the add by accessing the list in flow scope and the item to be added from the request parameters as follows:
Collection list = context.getFlowScope().getRequiredCollection("list"); String data = context.getRequestParameters().get("data"); if (data != null && data.length() > 0) { list.add(data); }
For any outcome the addItem state transitions back to the initial displayItemList state using an event pattern match:
<action-state id="addItem"> <action bean="addItemAction" /> <transition on="*" to="displayItemlist" /> </action-state>
The Itemlist-alternate web flow (/WEB-INF/itemlist-alternate.xml) has functionality equivalent to that of itemlist but instead uses a subflow for selecting individual items. The "addItem" state is a subflow state invoking an inline flow called "item" (also defined in itemlist-alternate.xml) accepting an output parameter from the subflow and adding the output parameter to a flow-scoped list variable:
<subflow-state id="addItem" flow="item"> <attribute-mapper> <output-mapper> <mapping source="item" target-collection="flowScope.list" /> </output-mapper> </attribute-mapper> <transition on="finish" to="displayItemlist" /> </subflow-state>
An output-mapper is used to pass results from a subflow to a parent flow. The above declaration defines an expectation on the subflow to return an output parameter called "item". Accordingly the end state for the inline flow has this output mapping returning a parameter called "item":
<end-state id="finish"> <output-mapper> <mapping source="requestParameters.data" target="item" /> </output-mapper> </end-state>
With the above declarations we see how a subflow can pass output parameters back to its parent flow - in this case the 'data' request parameter is passed back as an output parameter.
Once the inner subflow flow has completed the item is passed to the parent flow as an output parameter, which adds it to its flow-scoped list and transitions to the initial "displayItemList" state.
Fileupload is a simple one page web application for uploading files to a server. It is based on Spring MVC, uses a Web Flow controller and one web flow with two states: a view state for displaying the initial JSP page and an action state for processing the submit.
The web.xml configuration maps requests for "*.htm" to the fileupload servlet - a regular Spring MVC DispatcherServlet:
<servlet> <servlet-name>fileupload</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>fileupload</servlet-name> <url-pattern>*.htm</url-pattern> </servlet-mapping>
The Spring MVC servlet context for the fileupload servlet (WEB-INF/fileupload-servlet.xml) defines one controller bean:
<bean name="/admin.htm" class="org.springframework.webflow.executor.mvc.FlowController"> <property name="flowExecutor" ref="flowExecutor" /> </bean>
FlowController is a Web Flow controller. It is the main point of integration between Spring MVC and Spring Web Flow routing requests to one or more managed web flow executions. The FlowController is injected with flowExecutor and flowRegistry beans containing one web flow definition:
<!-- Launches new flow executions and resumes existing executions. --> <flow:executor id="flowExecutor" registry-ref="flowRegistry" repository-type="singlekey"/> <!-- Creates the registry of flow definitions for this application --> <flow:registry id="flowRegistry"> <flow:location path="/WEB-INF/fileupload.xml" /> </flow:registry>
Given the above definitions the following URI can be used to invoke the "fileupload" flow:
/swf-fileupload/admin.htm?_flowId=fileupload
Both flowExecutor and flowRegistry beans are defined with Spring custom tags schema available in Spring 2.0. The custom tags make configuration less verbose and more readable. Regular Spring bean definitions can be used as well with earlier versions of Spring.
The Spring MVC context also defines a view resolver bean for resolving logical view names and a multipartResolver bean for the upload component. In general Web Flow does not aim to replace the flexibility of Spring MVC for view resolution. It focuses on the C in MVC.
The start state for the fileupload flow (WEB-INF/fileupload.xml) is a view state:
<start-state idref="selectFile"/> <view-state id="selectFile" view="fileForm"> <transition on="submit" to="uploadFile"/> </view-state>
View states allow a user to participate in a flow by presenting a suitable interface. The view attribute "fileForm" is a logical view name, which the Spring MVC view resolver bean will resolve to /WEB-INF/jsp/fileForm.jsp.
The fileForm.jsp has an html form that submits back to the same controller (/swf-fileupload/admin.htm) and passes a "_flowExecutionKey" parameter. The value for _flowExecutionKey is provided by the FlowController - it identifies the current instance of the flow and allows Web Flow to resume flow execution, which is paused each time a view is displayed.
The name of the form submit button "_eventId_submit" indicates the event id to use for deciding where to transition to next. Given an event with id of "submit" the "selectFile" view transitions to the "uploadFile" state:
<action-state id="uploadFile"> <action bean="uploadAction"/> <transition on="success" to="selectFile"> <set attribute="fileUploaded" scope="flash" value="true"/> </transition> <transition on="error" to="selectFile"/> </action-state>
The "uploadFile" state is an action state. Action states integrate with business application code and respond to the execution of that code by deciding what state of the flow to enter next. The code for the uploadFile state is in the "uploadAction" bean declared in the Spring web context (/WEB-INF/fileupload-servlet.xml):
<bean id="uploadAction" class="org.springframework.webflow.samples.fileupload.FileUploadAction" />
FileUploadAction has simple logic. It picks one of two Web Flow defined events - success or error, depending on whether the uploaded file size is greater than 0 or not. Both success and error transition back to the "selectFile" view state. However, a success event causes an attribute named "fileUploaded" to be set in flash scope
A flash-scoped attribute called "file" is also set programmatically in the FileUploadAction bean:
context.getFlashScope().put("file", new String(file.getBytes())); return success();
This illustrates the choice to save attributes in one of several scopes either programatically or declaratively.
Birthdate is a web application with 3 consequitive screens. The first two collect user input to populate a form object. The third presents the results of business calculations based on input provided in the first two screens.
Birthdate demonstrates Spring Web Flow's Struts integration as well as the use of FormAction, a multi-action used to do the processing required for all three screens. The sample also uses JSTL taglibs in conjunction with flows.
The web.xml configuration maps requests for "*.do" to a regular Struts ActionServlet:
<servlet> <servlet-name>action</servlet-name> <servlet-class>org.apache.struts.action.ActionServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>action</servlet-name> <url-pattern>*.do</url-pattern> </servlet-mapping>
The web.xml also sets up the loading of a Spring context at web application startup:
<context-param> <param-name>contextConfigLocation</param-name> <param-value> /WEB-INF/webflow-config.xml </param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener>
The Spring web context contains beans to set up the Web Flow runtime environment. As will be shown in the next section Struts is configured with a Web Flow action that relies on the presence of a flowExecutor and a flowRegistry beans in this context.
The Struts configuration (WEB-INF/struts-config.xml) defines the following action mapping:
<action-mappings> <action path="/flowAction" name="actionForm" scope="request" type="org.springframework.webflow.executor.struts.FlowAction"/> </action-mappings>
FlowAction is a Struts action acting as a front controller to the Web Flow system routing Struts requests to one or more managed web flow executions. To fully configure the FlowAction a Spring web context is required to define flowExecutor and flowRegistry beans (named exactly so). This is an excerpt from the Spring web context (/WEB-INF/webflow-config.xml) defining these beans:
<!-- Launches new flow executions and resumes existing executions. --> <flow:executor id="flowExecutor" registry-ref="flowRegistry"/> <!-- Creates the registry of flow definitions for this application --> <flow:registry id="flowRegistry"> <flow:location path="/WEB-INF/birthdate.xml"/> <flow:location path="/WEB-INF/birthdate-alternate.xml"/> </flow:registry>
Based on the above, Web Flow is configured with two flows - birthdate and birthdate-alternate, which can be invoked as follows:
/swf-birthdate/flowAction.do?_flowId=birthdate /swf-birthdate/flowAction.do?_flowId=birthdate-alternate
The Struts configuration file also defines several global forwards: birthdateForm, cardForm, and yourAge, which will be referenced from Web Flow definitions as logical view names (and left to Struts to resolve to actual JSP pages). In general Web Flow does not aim to replace view resolution capabilities of web frameworks such as Struts or Spring MVC. It focuses on the C in MVC.
The birthdate web flow (WEB-INF/birthdate.xml) defines the following start state:
<view-state id="enterBirthdate" view="birthdateForm"> <render-actions> <action bean="formAction" method="setupForm" /> </render-actions> <transition on="submit" to="processBirthdateFormSubmit" /> </view-state>
The setupForm action is called to perform initializations for the enterBirthdate view state. Its action bean is defined the Spring web context WEB-INF/webflow-config.xml:
<bean id="formAction" class="org.springframework.webflow.samples.birthdate.BirthDateFormAction" />
BirthDateFormAction is a FormAction - it extends Web Flow's FormAction class, which serves a purpose similar to that of Spring MVC's SimpleFormController providing common form functionality for data binding and validation.
When the BirthDateFormAction bean is instantiated it sets the name, class and scope of the form object to use for loading form data upon display and collecting form data upon submit:
public BirthDateFormAction() { // tell the superclass about the form object and validator we want to // use you could also do this in the application context XML ofcourse setFormObjectName("birthDate"); setFormObjectClass(BirthDate.class); setFormObjectScope(ScopeType.FLOW); setValidator(new BirthDateValidator()); }
The form object "birthDate" is placed in flow scope, which means it will not be re-created with each request but will be obtained from flow scope instead as long as the request remains within the same flow.
Once setupForm is done, the "birthdateForm" view will be rendered. The logical view name "birthdateForm" is a global-forward in struts-config.xml resolving to /WEB-INF/jsp/birthdateForm.jsp. This JSP collects data for the fields "name" and "date" bound to the birthDate form object and posts back to FlowAction with a submit image named "_eventId_submit". An event with the id of "submit" causes a transition to the processBirthdateFormSubmit action state defined as follows:
<action-state id="processBirthdateFormSubmit"> <action bean="formAction" method="bindAndValidate"> <attribute name="validatorMethod" value="validateBirthdateForm" /> </action> <transition on="success" to="enterCardInformation" /> <transition on="error" to="enterBirthdate" /> </action-state>
The processBirthDateFormSubmit action state uses the same formAction bean as the one already used to setup the form. This time its bindAndValidate method is used to populate and validate the html form values. Also, note the "validateMethod" attribute used to specify the name of the method to invoke on the Validator object setup in the constructor of the BirthDateFormAction. The use of this attribute allows partial validation of complex objects populated over several consecutive screens.
On error the action returns to the view state it came from. On success it transitions to the enterCardInformation view state:
<view-state id="enterCardInformation" view="cardForm"> <transition on="submit" to="processCardFormSubmit" /> </view-state>
The logical view name "cardForm" is a global-forward in struts-config.xml resolving to /WEB-INF/jsp/cardForm.jsp. This JSP collects data for the remaining fields of the birthDate form object - "sendCard" and "emailAddress", and posts back to FlowAction with a submit image named "_eventId_submit". An event with the id of "submit" causes a transition to the processCardFormSubmit action state defined as follows:
<action-state id="processCardFormSubmit"> <action bean="formAction" method="bindAndValidate"> <attribute name="validatorMethod" value="validateCardForm" /> </action> <transition on="success" to="calculateAge" /> <transition on="error" to="enterCardInformation" /> </action-state>
For this action state the bindAndValidate method of the formAction bean is used to populate and validate the remaining html form values. The "validateMethod" attribute specifies the name of the method to invoke on the Validator object specific to the fields loaded on the current screen.
On error the action returns to the view state it came from. On success it transitions to another action state called calculateAge:
<action-state id="calculateAge"> <action bean="formAction" method="calculateAge" /> <transition on="success" to="displayAge" /> </action-state>
The logic for the calculateAge action state is in the calculateAge method of the same formAction bean used for data binding and validation. This demonstrates the flexibility Web Flow allows in properly structuring control and business logic according to function.
The caculateAge method performs business calculations and adds a string in request scope with the calculated age. Upon successful completion the calculateAge action state transitions to the end view state:
<end-state id="displayAge" view="yourAge" />
Once again the logical view name "yourAge" is a global-forward in struts-config.xml resolving to /WEB-INF/jsp/yourAge.jsp. This JSP page retrieves the calculated age from request scope and displays the results for the user.
The transition to the end state indicates the end of the web flow. The flow execution is cleaned up. If the web flow is entered again a new flow execution will start, creating a new form object named "birthDate" and placing it in flow scope.
The birthdate-alternate web flow (/WEB-INF/birthdate-alternate.xml) offers an alternative way and more compact way of defining the same web flow. For example the birthdate web flow defines two independent states for the first screen - a view state (enterBirthdate) and an action state (processBirthdateFormSubmit). In birthdate-alternate those are encapsulated in the view state enterBirthdate as follows:
<view-state id="enterBirthdate" view="birthdateForm"> <render-actions> <action bean="formAction" method="setupForm" /> </render-actions> <transition on="submit" to="enterCardInformation"> <action bean="formAction" method="bindAndValidate"> <attribute name="validatorMethod" value="validateBirthdateForm" /> </action> </transition> </view-state>
Here the setupForm action state is defined as a render-action of the enterBirthdate view state while the transition to the next screen uses a nested action bean invoked before the transition occurs. Notice that success is implicitly required for the transition to occur. Similarly on error the transition does not occur and the same view state is displayed again.
The second screen is also defined with a nested transition and action bean:
<view-state id="enterCardInformation" view="cardForm"> <transition on="submit" to="calculateAge"> <action bean="formAction" method="bindAndValidate"> <attribute name="validatorMethod" value="validateCardForm" /> </action> </transition> </view-state>
The remaining two states - calculateAge and displayAge are identical.
The Phonebook-Portlet demonstrates how to run the Phonebook sample as a JSR-168 portlet. The functionality for Phonebook and Phonebook-Portlet including web flow definitions, JSP pages, and Java classes is the same and already well documented. The focus in Phonebook-Portlet is specifically on how to configure and run Phonebook in a Portal container.
Note | |
---|---|
JSR-168 defines portlets but not how portlets integrate into a portal container. This process is left open to portal vendors who have their own individual mechanisms. The Phonebook-Portlet sample is configured to run with Apache Pluto - a reference implementation of the Java Portlet Specification. However, its dependence on Pluto is limited to configuration in web.xml. Hence it should be easy to adapt for use in other Portal/Portlet implementations after learning the deployment steps specific for that implementation. |
This section provides a very brief introduction to the portal related supporting software used in the sample - namely Apache Pluto and the Portlet MVC framework. If this is not new for you feel free to skip to the next section.
For those familiar with servlet applications the process of deploying and running a portlet application can be confusing and requires some explanation. Typically an application with JSR-168 portlets runs in one webapp while a portal/portlet container runs in a separate webapp making cross-context calls to portlets. How exactly this is configured depends on each portal vendor.
Pluto is an open-source reference implementation of the Java Portlet specification. The following general steps are required to run portlets with it. First the the portlet application's web.xml is "injected" with configuration required for Pluto. Secondly Pluto's Portal web application, usually set to run at http://localhost:8080/pluto/portal is used to add or remove portlets to one or more portal container pages.
The web.xml for the Phonebook-Portlet sample has already been "injected" with the configuration required for Pluto 1.1.0. Although this enables it for use with Pluto you must still use the admin pages of Pluto's Portal web application to add the Phonebook-Portlet to a test portal page. For more information on how to do this please follow instructions from Apache Pluto.
The Portlet MVC framework represents Spring's support for JSR-168. It has many parallels with the Spring MVC framework such as the DispatcherPortlet, the Controller interface, handler mappings, view resolvers, and exception handlers. The main differences between Portlet MVC and Spring MVC have to do with the lifecycles of a portlet and its distinct phases as defined in the Porlet Specification: the action and the render phases. For more information see Chapter 16 (Portlet MVC Framework) from the Spring reference documentation.
Since the phonebook portlet was tested with Apache Pluto we've decided to documents the steps taken to deploy and run it
Download the Pluto 1.1 binary distribution named pluto-current-bundle from http://portals.apache.org/pluto
Unzip the binary distribution to any directory.
Create the directory [pluto-home]/webapps/swf-phonebook-portlet
Copy the content of [webflow-release]/spring-webflow-samples/phonebook-portlet/target/artifacts/war-expanded to the directory created in the previous step
Start Pluto with [pluto-home]/bin/startup
Go to http://localhost:8080/pluto/portal
Login as tomcat/tomcat (or any other user but see note below)
After logging in you will be taken to the Portal Test page.
Here you will see a Navigation pull-down menu at the top. Select 'Pluto Admin' from it to go to the Pluto Admin page.
On the Pluto Admin page under Portlet Applications you will see a drop-down with available portlet applications
Select '/swf-phonebook-portlet' from it, then phonebook from the drop-down next to it, and then press the 'Add Portlet' button
Use the Navigation menu at the top to go back to the Test Page. The Phonebook portlet should be present.
Note | |
---|---|
The tomcat user must have the 'pluto' role. Open [pluto-home]/conf/tomcat-users.xml and ensure the following lines are there: <role rolename="pluto"/> <user username="tomcat" password="tomcat" roles="tomcat,pluto"/>
|
Portlet.xml is a standard deployment descriptor where portlet resources are defined. The Phonebook-Portlet is based the Portlet MVC DispatcherPorlet:
<portlet-class> org.springframework.web.portlet.DispatcherPortlet </portlet-class>
The DispatcherPortlet is Spring's implementation of the Portlet interface dispatching requests for a portlet to registered Portlet MVC handlers. The phonebook portlet is configured with the following Spring contexts containing Portlet MVC handler, controller and view resolver beans:
<init-param> <name>contextConfigLocation</name> <value> /WEB-INF/phonebook-portlet-config.xml /WEB-INF/phonebook-webflow-config.xml </value> </init-param>
The above configuration defines phonebook as a portlet resource. In order to use it in a portal/portlet container additional web.xml configuration is required.
The Java Portlet Specification is defined as a layer over existing Servlet infrastructure. Therefore some sort of a servlet is required to accept servlet requests and expose portlet resources. Portal vendors provide such servlets and specific configuration varies by vendor. The Phonebook-Portlet has the following Apache Pluto servlet definition and servlet mapping:
<!-- Generated Portlet Wrapper Servlet for Apache Pluto deployment --> <servlet> <servlet-name>phonebook</servlet-name> <servlet-class>org.apache.pluto.core.PortletServlet</servlet-class> <init-param> <param-name>portlet-name</param-name> <param-value>phonebook</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>phonebook</servlet-name> <url-pattern>/PlutoInvoker/phonebook</url-pattern> </servlet-mapping>
Note | |
---|---|
The above configuration was auto generated using ant tasks from Apache Pluto 1.1.0. This configuration is included in web.xml for convenience and also as an example. For the most up-to-date information on required configuration please check Pluto's documentation. |
The web.xml configuration also contains the following servlet definition:
<servlet> <servlet-name>viewRendererServlet</servlet-name> <servlet-class> org.springframework.web.servlet.ViewRendererServlet </servlet-class> </servlet> <servlet-mapping> <servlet-name>viewRendererServlet</servlet-name> <url-pattern>/WEB-INF/servlet/view</url-pattern> </servlet-mapping>
The main purpose of this servlet is to allow reuse of Spring MVC's flexible view resolution and rendering capabilities in a Portlet application. The DispatcherPortlet converts a PortletRequest/PortletResponse to an HttpServletRequest/HttpServletResponse and then performs an include of this servlet.
The phonebook-portlet-config.xml is very similar to the Spring MVC equivalent phonebook-servlet.xml from the Phonebook sample. The main difference is in the use of a PortletModeHandlerMapping:
<bean id="portletModeControllerMapping" class="org.springframework.web.portlet.handler.PortletModeHandlerMapping"> <property name="portletModeMap"> <map> <entry key="view" value-ref="flowController"/> </map> </property> </bean>
and a PortletFlowController:
<bean id="flowController" class="org.springframework.webflow.executor.mvc.PortletFlowController"> <property name="flowExecutor" ref="flowExecutor"/> <property name="defaultFlowId" value="search-flow"/> </bean>
A PortletModeHandlerMapping allows mapping specific to each portlet mode. The VIEW mode in this case is mapped to the flowController bean, which delegates the request to Web Flow's executor for launching or resuming a flow from a flow definition. For more information on Phonebook flow definitions please refer to the Phonebook sample documentation.
One last thing to observe is the following configuration in /WEB-INF/phonebook-webflow-config.xml:
<!-- Launches new flow executions and resumes existing executions. --> <flow:executor id="flowExecutor" registry-ref="flowRegistry"> <flow:execution-attributes> <!-- execution redirects don't apply in a Portlet environment --> <flow:alwaysRedirectOnPause value="false"/> </flow:execution-attributes> </flow:executor>
As the comment indicates the default behavior of redirect after submit must be turned off in a portlet environment where there is no HTTP redirect. For more information on the alwaysRedirectOnPause refer to the following article.