This page will present the RSF-SWF integration from the point of view of one of the core Spring Web Flow samples, the "Phonebook" app. This is a simple application with 3 flow views which allows the user to search and browse a phonebook.
Sample Code (SVN): | original code (with maven2 build) | RSF SWF version |
---|
Phonebook application - Original SWF version#
The best starting point for users new either to Spring Web Flow or to this application is Erwin Vervaet's excellent tutorial to the original SWF version of this app held at Ervacon. This tutorial presents all the basics for defining and executing flows as part of an SWF application, and is a prerequisite for following our ports of the app on this page.The original code for this application has been checked into the RSF repository without changes, except for providing it with a Maven 2 build which is the standard build system for the RSF framework and sample apps, and will be used for our ported versions. The original Ivy build for the sample (which is standard for Spring and its samples) has been retained in this version, but Ivy builds have not been supplied for the RSF versions.
SVN check out original version of Phonebook SWF application - SWFPhonebook-orig
Versioning Note: The samples are currently updated in trunk to RSF version 0.7.2RC2, for which artefacts are in the Caret Maven 2 development repository at http://www2.caret.cam.ac.uk/maven2dev/.
The RSF-Spring Web Flow integration JAR is in SVN trunk at https://source.caret.cam.ac.uk/rsf/projects/RSF-SWF/trunk
To work with Maven 2 for RSF JARs and samples, you should consult the Maven 2 Setup page for the necessary environment.
Porting the application - RSF and SWF goals and tradeoffs#
It is no accident that this sample has been selected as the "core" sample for Spring Web Flow tutorials and illustrations, since it is an excellent showcase for the design tradeoffs that afflict web programming in general. In this tutorial we will firstly port the sample from using JSP + SpringMVC as its web framework, to using RSF and its pure templating layer, IKAT - whilst retaining all SWF definitions intact. This will demonstrate the benefits RSF can offer to SWF development by providing a Spring-all-the-way environment that lets developers enjoy all the benefits of IoC-driven programming that they have come to expect from Spring development, whilst maintaining a "clean face" of XHTML to designers, unpolluted by custom tags or strange environmental dependencies.
Secondly, we will port the application further to the equivalent pure RSF application, discarding the Spring Web Flow definitions in favour of the RSF Spring equivalents. This will illustrate the interesting tradeoffs that this application exposes underlying the designs of both RSF and SWF, and we will take stock of what has been gained and lost in this translation, and consider the scenarios in which the different strategies might be appropriate.
Porting from SWF on SpringMVC + JSP to SWF on RSF#
The first step in the design of any RSF application typically involves the construction of suitable view templates. These are either designed from scratch, or ported from an existing application either by "screen-scraping" (in the case its view layer is too far from plain HTML, for example a JSF or non-Java app) or by porting existing templates.Step 1 - porting the view layer#
For this app, our existing templates, written in JSPs, were not *too* taglib-laden and it made sense to simply copy all the .jsp files from the original sample, rename them as .html files, and clean up the tag structure.
For example, searchCriteria.jsp, at the path webapp/WEB-INF/jsp, in the RSF app becomes searchCriteria.html at the path webapp/content/templates. This is a pure XHTML file which can be previewed directly in the browser, and designed with standard web tools.
Header#
The first change is the replacement of the JSP taglib header with a standard XHTML header:
<%@ page contentType="text/html" %> <%@ page session="false" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> <html> <head> <title>Search the Phonebook</title> <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> <link rel="stylesheet" href="style.css" type="text/css"> </head>becomes
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns:rsf="http://ponder.org.uk/rsf"> <head> <title>Search the Phonebook</title> <link rel="stylesheet" href="../css/style.css" type="text/css"/> </head>Note that we have rebased the locations of css and image files slightly to fit the "standard" layout of an RSF webapp in its /content directory. None of this path structure is mandatory and can be easily changed with a Spring definition.
Form tag#
For every JSP tag, it is replaced with its standard HTML equivalent - and supplied with an rsf:id attribute, the single extension of RSF to the XHTML schema, forming a kind of "label" which lets the RSF renderer, IKAT match up Java-side and client-side definitions.
For example the JSP form tag converts from
<form:form commandName="searchCriteria" method="post">
to
<form rsf:id="search-form" action="searchResults.html" method="post">
Note that as well as removing the non-standard tag and attribute commandName, we also take the trouble to add a suitable action definition to this form for HTML previewing purposes. As it happens, in the application structure, searchResults.html is the resulting view that navigation will typically pass through on submission of this form, and so filling in this action allows the entire application structure to be previewed by users and designers more realistically, long before any code is written. Previewability of both markup and behaviour is a very strong theme in RSF development.
Input field#
The input fields convert from, for example
<form:input path="firstName" />
to
<input rsf:id="first-name" />
Submit button#
and the submission control from
<input type="hidden" name="_flowExecutionKey" value="${flowExecutionKey}"> <input type="submit" class="button" name="_eventId_search" value="Search">
to
<input rsf:id="submit" type="submit" class="button" value="Search"/>
This last represents a particularly big savings - the framework-dependent definitions _flowExecutionKey and _eventId_search were reflecting JSP's very poor level of abstraction in shielding us from the fact we are hosting an SWF application which requires various housekeeping information to be kept in its URLs. This is the sort of detail which would be confusing to a designer and could very easily become corrupted during a roundtripping process, as well as making the template fragile to alterations in the server-side environment. As we will see, once we have once drawn up our RSF templates for these views, they will remain completely unchanged in the further versions of this app, even as we remove SWF entirely, and experiment in the final RSF app with the use of GET or POST forms for this form submission.
Errors#
As a final detail, the errors tag changes from
<form:errors cssClass="error"/>to
<div rsf:id="message-for:*" class="error"/>
This is a standard RSF ID value indicating that all the error messages targetted at this view should be placed in the supplied DIV. If we were interested in previewing the use of the positioning and CSS class, we could place some sample messages within this <div>, and they would be removed at render-time in favour of the real application errors, if any.
General tidying#
Since IKAT accepts only well-formed XHTML, various other small-scale tidying was required, for example converting the index view away from the capital-letters markup style, and making sure small tags like <hr/>, <img/> and </p> were properly closed throughout the app. RSF gives accurate line and column diagnostics in each case XML is violated.
Surveying the template structure #
After converting all of our 4 views (the stub index.jsp view is promoted to becoming a proper view of the application) our app is disturbingly demonstrable. Try checking out the RSF-SWF version from SVN at SWFPhonebook-RSFSWF and open up the index.html file in your browser from the file system. Every link and submission control in the application works and navigates to the "expected" view, giving a full preview of the final application experience before a single line of code is put to paper.
At this point design cycles between users and developers could iterate, to improve the templates and review the application design, with the only cost being the possible loss of rsf:id attributes. This cannot cause any application corruption and is easy to detect, through part of the rendered view simply going missing. In the case formal verification is required, RSF features the TemplateCheckingTool, an RSF application which can be used to verify that a new set of RSF templates is equivalent in rsf:id structure to another, regardless of markup differences. In practice the rsf:id structure should be an aid to developer-designer communication and not a hindrance, since it provides almost precisely the minimum agreement (a named set of labels) required to be able to talk about a particular section of the application view reliably.
Step 2 - Packaging the application#
Rendering in RSF is performed by beans known as Producers or component producers - these are configured using standard Spring definitions, either at application scope (if possible), or as is more usual in RSF at request scope (RSAC).
RSF's request scope beans are typically configured in a file named requestContext.xml. The location of these files are configured in web.xml using a definition very similar to the standard Spring contextConfigLocation. All of RSF is built out of a set of Spring contexts, and currently these must be configured manually in web.xml. Typically each RSF JAR contributes one or two Spring context files, both at request and application scope. The definitions for our app, which would be standard for any RSF-SWF application in web.xml are as follows:
<!-- In contrast to the standard sample, we load all Spring definitions in the same application context. RSF will support the use of subcontexts in the style of the Spring "FrameworkServlet" in version 0.8 --> <context-param> <param-name>contextConfigLocation</param-name> <param-value> classpath:conf/rsf-config.xml, classpath:conf/blank-applicationContext.xml, classpath:conf/rsf-swf-applicationContext.xml, /WEB-INF/applicationContext.xml, /WEB-INF/phonebook-webflow-config.xml, classpath:org/springframework/webflow/samples/phonebook/stub/services-config.xml </param-value> </context-param> <!-- Configure "resource scope" Spring application contexts (RSAC). Be sure to mention rsf config files first, so any overrides may be processed --> <context-param> <param-name>requestContextConfigLocation</param-name> <param-value>classpath:conf/rsf-requestscope-config.xml, classpath:conf/blank-requestContext.xml, classpath:conf/rsf-swf-requestContext.xml, /WEB-INF/requestContext.xml </param-value> </context-param>
The assembly of these contexts will be automated in the next major RSF release, 0.8.
All that is left is the definition and mapping of the RSF servlet itself. The "ReasonableSpringServlet" is a short hand which as well as mounting the RSF URL space, initialises the application Spring context.
<servlet> <servlet-name>ReasonableServlet</servlet-name> <servlet-class>uk.org.ponder.rsf.servlet.ReasonableSpringServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>ReasonableServlet</servlet-name> <url-pattern>/faces/*</url-pattern> </servlet-mapping>
Settling the view structure #
An RSF ViewProducer is paired up with each of the html files which we drew up above. The linkage between a producer and its template is achieved through its ViewID. As a simple example, here is the markup and producer for the startup view of our application, index.html:<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns:rsf="http://ponder.org.uk/rsf"> <head> <title>Phonebook - A Spring Web Flow Sample (RSF Edition)</title> </head> <body> <div align="left">Phonebook - A Spring Web Flow Sample (RSF Edition)</div> <hr/> <div align="left"> <p> <a rsf:id="search-criteria" href="searchCriteria.html">Phonebook</a> </p> <p> This sample application illustrates core features of the web flow system. </p> </div> <hr/> <div align="right"></div> </body> </html>
With IndexProducer.java:
public class IndexProducer implements ViewComponentProducer, DefaultView { public static final String VIEW_ID = "index"; public String getViewID() { return VIEW_ID; } public void fillComponents(UIContainer tofill, ViewParameters viewparams, ComponentChecker checker) { UIInternalLink.make(tofill, "search-criteria", new SWFLaunchViewParams("search-flow")); } }
This would be configured in requestContext.xml as a simple, dependency-less bean (it could actually also have been placed at application scope):
<bean class="org.springframework.webflow.samples.phonebook.rsf.producers.IndexProducer" />
RSF automatically locates all ViewProducers at startup that are defined either at request or application scope.
SWFViewParams - RSF and SWF navigation schemes#
The RSF and SWF approaches to navigation are quite different. Read on at the main page on SWFViewParams, which is the standard RSF-SWF framework base class, representing all the view states within an RSF application which are participating in the SWF system.
A link to an RSF-SWF flow launch or transition is done using one of the special ViewParameters types supplied with the integration, being SWFLaunchViewParams and SWFEventViewParams.
The constructor we saw in the above view takes a single argument which is the Id of the flow to be started new SWFLaunchViewParams("search-flow"). However as we will see later, the SWFViewParams classes can also take an additional argument which is a further ViewParameters object which is intended to be "tunnelled" through the flow. Whilst the emitter of an SWF link does not know in general which view precisely will result, by virtue of trying to pass parameters to it, they almost certainly know which general category of view it is. In RSF this concept of a view category is precisely reflected in the ViewParameters object which it accepts, which summarises in a type-safe "bean-like" model the complete information it intends to receive over the request. To skip ahead a little, and show this "tunnelling" in action, let's look at a definition from SearchResultsProducer.java which renders a link to the "details" view/flow:
UIInternalLink.make(row, "details-view", person.getUserId(), new SWFEventViewParams("select", new PersonViewParams(person.getId()) ));
Here we see that whilst the results view does not in general have knowledge that the following view is to be the details view in particular (this information is held in the flow definition), it certainly knows that it is some form of view which is centred on a person, which it is able to represent in a transparent and typesafe way by passing the PersonViewParams object as the argument. This argument will be "tunnelled" through the flow, temporarily "deconstituted" into its raw URL information (in the form of a parameter named id) which will be addressible in the standard SWF representation as requestParameters.id to any flow definitions that require it, and then "reconstituted" back into a full ViewParameters object delivered to the Details view, once its view ID has been looked up from the flow.
In this case, it is the flow definition itself which makes all use of the request information from the details view, using the following render-action defined in detail-flow.xml:
<render-actions> <bean-action bean="phonebook" method="getPerson"> <method-arguments> <argument expression="flowScope.id" /> </method-arguments> <method-result name="person" /> </bean-action> </render-actions>So in fact there is no need to declare the PersonViewParams as the accepted type for DetailsView, but we could do this if there were some part of the render logic that was independently interested in knowing the id.
Accessing the flow - request-scope injection#
RSF producers are a natural fit for Spring, since their pattern for access uses just the same IoC pattern that Spring users enjoy elsewhere in the design. To access an SWF flow variable in an RSF producer, one simply injects it - for example, here is a section from search-flow.xml which define a render action performed on entering the "displayResults" state:
<render-actions> <bean-action bean="phonebook" method="search"> <method-arguments> <argument expression="flowScope.searchCriteria"/> </method-arguments> <method-result name="results"/> </bean-action> </render-actions>
investigating the target argument in the "phonebook" bean, we see it is written as follows:
public interface Phonebook { public List<Person> search(SearchCriteria criteria);
The bean called "results" is deposited in the flow scope, and to access it from our SearchResultsProducer, was can simply inject it as follows:
public class SearchResultsProducer implements ViewComponentProducer { ... private List<Person> results; public void setResults(List<Person> results) { this.results = results; } public void fillComponents(UIContainer tofill, ViewParameters viewparams, ComponentChecker checker) { ... for (Person person: results) { UIBranchContainer row = UIBranchContainer.make(tofill, "person-row:"); UIOutput.make(row, "first-name", person.getFirstName()); ... }
Being in request scope, it is safe and moral to inject this render action result into the producer and use it for rendering replicated branches in the view, like any other dependency. The definition for this producer, in requestContext.xml is as follows:
<bean class="org.springframework.webflow.samples.phonebook.rsf.producers.SearchResultsProducer"> <property name="results" ref="results" /> </bean>
On the beginning of every alteration cycle, RSF automatically transfers any beans which are in a visible Spring Web Flow scope (flow, conversation, flash, session) into its own request scope so they can be accessed in this way.
The corresponding template section appears as follows:
<tr rsf:id="person-row:"> <td rsf:id="first-name">Juergen</td> <td rsf:id="last-name">Hoeller</td> ...
Note that no logic, only labels, appear in the template - all actual looping and branching logic is written as normal Java logic in IoC-driven beans. Thus we achieve a complete separation of concerns at all levels of the architecture.
Form submissions - the SWFBindingBean and friends#
We have seen how to translate most aspects of this SWF app to SWF-RSF, except for handling of form submissions. The core SWF form submission model is based around the core framework class "FormAction", which is a broad toolkit of methods for accepting submission arguments (binding), validation and controlling responses. In this integration mode, RSF defers completely to the SWF form submission model, which is self-contained and makes no direct reference to SpringMVC definitions (although is based on its concepts). RSF form submissions, however, are encoded by means of "EL bindings", references into parts of its request addressible context. To bridge between these two models, the integration defines a special "proxy bean" called SWFBindingBean which acts as a representative via EL for any form object for which SWF is expecting submissions on at the current flow state. For example, the following definitions in SearchCriteriaProducer.javaUIForm form = UIForm.make(tofill, "search-form"); UIInput.make(form, "first-name", "SWFBindingBean.firstName"); UIInput.make(form, "last-name", "SWFBindingBean.lastName"); UICommand.make(form, "submit", "SWFEventBean.search");
encode two bindings onto the current SWF form object, at the relative paths "firstName" and "lastName", associated with two input fields on the current view.
The corresponding SWF flow definition is
<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>Here the standard bean "formAction", which is defined in search-flow-beans.xml as an instance of FormAction as follows:
<!-- Search form action that setups the form and processes form submissions --> <bean id="formAction" class="org.springframework.webflow.action.FormAction"> <property name="formObjectClass" value="org.springframework.webflow.samples.phonebook.SearchCriteria"/> <property name="validator"> <bean class="org.springframework.webflow.samples.phonebook.SearchCriteriaValidator"/> </property> </bean>
For the duration of this submission, SWFBindingBean is an alias for the bean held within formAction, which is an instance of the class SearchCriteria.
Finally we complete this form with a method binding to the SWFEventBean which will cause the form submission to be actually accepted and processed via the "bindAndValidate" method attached to the event search:
UICommand.make(form, "submit", "SWFEventBean.search");
Build issues#
For a Maven 2 build, we must take a little care to harmonise Spring Web Flow's with RSF's transitive dependencies on Spring. The current RSF distribution (of trunk/snapshot at 0.7.2M5-SNAPSHOT) transitively depends on the latest Spring release (2.0.6), whereas the Spring Web Flow release we are working with (1.0.4) binds to Spring 2.0.4. In addition SWF resolves the Spring component JARs individually, whereas RSF depends on the collected "umbrella" JAR. We resolve this conflict in favour of SWF's distribution by excluding RSF's upstream Spring dependencies, by bringing in the RSF-SWF integration JAR with the following definition in our POM:
<dependency> <groupId>uk.org.ponder.rsfutil</groupId> <artifactId>rsf-swf</artifactId> <version>${rsfutil.version}</version> <exclusions> <exclusion> <!-- The 1.0.4 webflow release binds to the 2.0.4 Spring releases. Take SWF's Spring rather than RSF's --> <groupId>org.springframework</groupId> <artifactId>spring</artifactId> </exclusion> </exclusions> </dependency>In order to complete the build, we need to add back in the spring-aop JAR which RSF depends on, but SWF does not - but at a version agreeing with SWF's dependent Spring version:
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>2.0.4</version> </dependency>
Porting from SWF on RSF to pure RSF#
We concluded porting our SWF on SpringMVC/JSP app to an RSF front-end, whilst keeping untouched
- Our flow definitions
- Our application model and services
- Our validators
Essentially all that will change in this application are our ViewProducers. The principal changes will be the ViewParameters targets for navigation. Rather than tunnelling through the SWFViewParams "proxies" for navigation destinations, our links and forms will target their resulting views directly.
Flow navigation becomes direct navigation#
For example, the index page link which leads to the criteria page changes fromUIInternalLink.make(tofill, "search-criteria", new SWFLaunchViewParams("search-flow"));
to
UIInternalLink.make(tofill, "search-criteria", new SimpleViewParameters(SearchCriteriaProducer.VIEW_ID));
becoming a direct navigation link to the SearchCriteriaProducer page, with a view id of searchCriteria.
ViewParameters, and locating service results#
Before we finish up at the searchCriteria view itself, which is certainly the most interesting view in the app, we will look at the searchResults view which reflects a slightly different strategy for locating the person list. Looking back at the SWF app, the work of fetching the search results was done via a render-action attached to the displayResults view state within the flow. In an RSF app, it is idiomatic to i) draw up the minimal state which must be put into URL state (ViewParameters) and then to ii) inject any "services" or other environmental beans into the ViewProducer (which is just a standard Spring bean) which is necessary to let it look up this minimal state to its results.This different workflow reflects the emphasis RSF places on achieving zero server state wherever possible. The framework assists you to achieve this where it is appropriate, but the side-effect is requiring a bit more thought and modelling by developers of the request cycle, and conscious choices about which state is stored where.
For our searchResults view, the incoming view state (URL state) consists of exactly the information which is held in the SearchCriteria object. Since the purpose of the RSF ViewParameters scheme is to model view state as far as possible as a "pure bean" model with proper type-safety, we may simply aggregate this type directly into the SearchCriteriaViewParams:
public class SearchCriteriaViewParams extends SimpleViewParameters { public SearchCriteria searchCriteria = new SearchCriteria(); public String getParseSpec() { return super.getParseSpec() + ",:searchCriteria.*"; } public SearchCriteriaViewParams() {} public SearchCriteriaViewParams(String viewId) { super(viewId); } }
For simple "leaf" types RSF will infer a default ParseSpec which will map them into URL attributes. To map a more complex type we need to make a declaration like the one above - more details on the ViewParameters and ParseSpec pages.
The incoming ViewParameters type of SearchResultsProducer now changes to be our SearchCriteriaViewParams:
public class SearchResultsProducer implements ViewComponentProducer, ViewParamsReporter { ... public ViewParameters getViewParameters() { return new SearchCriteriaViewParams(); }
Whereas in the SWF version, we had our search results injected directly from the flow scope, the search operation itself being triggered by the render-action, in the RSF version, we inject the underlying Phonebook service directly and fetch the results outselves.
... private Phonebook phonebook; public void setPhonebook(Phonebook phonebook) { this.phonebook = phonebook; } public void fillComponents(UIContainer tofill, ViewParameters viewparamso, ComponentChecker checker) { SearchCriteriaViewParams viewparams = (SearchCriteriaViewParams) viewparamso; UIForm form = UIForm.make(tofill, "restart-form", new SimpleViewParameters(SearchCriteriaProducer.VIEW_ID)); UICommand.make(form, "submit"); List<Person> results = phonebook.search(viewparams.searchCriteria); for (Person person: results) { UIBranchContainer row = UIBranchContainer.make(tofill, "person-row:"); UIOutput.make(row, "first-name", person.getFirstName()); UIOutput.make(row, "last-name", person.getLastName()); UIOutput.make(row, "person-phone", person.getPhone()); UIInternalLink.make(row, "details-view", person.getUserId(), new PersonViewParams(DetailsProducer.VIEW_ID, person.getId())); } }
The loop body is extremely similar to that in the SWF version, except that we use direct link destinations to the DetailsProducer rather than a flow transition.
The bean declaration of SearchResultsProducer in requestContext.xml changes to
<bean class="org.springframework.webflow.samples.phonebook.rsf.producers.SearchResultsProducer"> <property name="phonebook" ref="phonebook" /> </bean>
The search submission - GET or POST?#
The SearchCriteriaProducer is the most interesting part of the application, certainly from the point of view of this RSF port. As we mentioned earlier, the use of GET or POST transitions is the kind of implementation decision that Spring Web Flow shields the developer from. This can be considered either desirable or undesirable depending on your mindset and goals as a developer - with each gain in abstraction, there is a corresponding reduction in expressiveness, and the values of these relative tradeoffs are key determiners in the behaviour of an application or framework. RSF abstracts the concepts and physical implementation of HTTP GET and POST into its concept of a "render cycle" and "action cycle" respectively (with naming/semantics mirroring those common in the JSR-168 Portlet or WSRP world), but the distinction between them is strictly maintained at the application and design level.
In terms of trying to represent the original application as faithfully as we can in RSF, supporting the SearchCriteriaValidator in the original SWF application, which was used to reject empty search requests, is a key point. As the current RSF architecture stands, validators are only permitted to operate on action cycles (where some substantive alteration is in progress on the bean model), and so the initial implementation we'll provide (and also the default in SVN) will be of SearchCriteria via a POST form. However, search GET forms are pretty idiomatic on the web at large, and it could be argued that "validation" of search criteria doesn't always fit in with user expectations. Implementing this form as a GET transition simplifies our application a fair bit (partly due to being obliged to ditch the validation step), and reduces the server load by substituting two requests by one, and is a valid application decision that RSF supports. We will show a GET version of this producer SearchCriteriaProducerGET later - swapping this into our app only involves changing one Spring bean declaration, and results in no changes anywhere else in the application code or view template layer.
POST submission and navigation#
Firstly the rendering code for this view SearchCriteriaProducer.java is pretty straightforward:
UIForm form = UIForm.make(tofill, "search-form"); UIInput.make(form, "first-name", "searchCriteria.firstName"); UIInput.make(form, "last-name", "searchCriteria.lastName"); UICommand.make(form, "submit");
The default for any RSF UIForm is to become a POST (action) form submitting to the current ViewParameters. We set up EL bindings for our two form fields onto the searchCriteria object directly (rather than via the SWFBindingBeanProxy) before - this bean is mapped directly into our request scope with the following requestContext.xml definition:
<!-- This bean is not used in the GET version of the app --> <bean id="searchCriteria" class="org.springframework.webflow.samples.phonebook.SearchCriteria" />
In order to free this bean for access over the request, we must explicitly list it in the requestAddressibleBeans declaration - the alternative would be quite a security risk:
<bean parent="requestAddressibleParent"> <property name="value" value="searchCriteria"/> </bean>
The next difference to our SWF-RSF version is that we do not place any method binding onto our submission control, submit. This is because there is no explicit application action we wish to trigger on this cycle, other than validation, followed possibly by navigation to the following view.
Validation#
In RSF, validation semantics are expressed declaratively in the application context Spring file, in definitions known as BeanGuards. These specify triggering rules which provide for guaranteed execution logic in an action cycle, triggered by attempts to read or write bean values over the request. In this case, the rule we want to encode is that any write access to members of the searchCriteria object we set up earlier, should be followed by a trigger of the Spring Validator SearchCriteriaValidator.We can declare both the validator and the guarding rule compactly in one definition as follows:
<bean parent="writeGuardParent"> <property name="guard"> <bean class="org.springframework.webflow.samples.phonebook.SearchCriteriaValidator"/> </property> <property name="guardedPath" value="searchCriteria"/> </bean>
If the validation fails, the request is declared to be "in error", and the default navigation will be replaced by error navigation, for which the framework default is to return to the originating view. In this case, any input fields will filled in with their input values (as opposed to being refetched from the view), and a list of error/validation messages may be displayed to the user.
Normal and error action navigation#
The standard RSF framework scheme for defining action navigation rules is using the navigation case system, a declarative navigation system inherited in outline from JSF. A view exports NavigationCase rules to the framework by implementing NavigationCaseReporter and returning a list of static rules, which map action return results to resulting ViewParameters. In this case we require a single default rule - in the case of a request not in error we require navigation to the SearchResults page:
public List reportNavigationCases() { List togo = new togo.add(new NavigationCase( new SearchCriteriaViewParams(SearchResultsProducer.VIEW_ID))); return togo; }
Since these rules are static, that is, independent of runtime information, they are sometimes insufficient to fill in all the details of resulting navigation state. In RSF this is performed by the parallel action navigation scheme implemented by the ActionResultInterceptor interface, affectionately known as ARI2. NavigationCases and ARI2 cooperate to build up coarse-scale and fine-scale action navigation information, respectively. It would be possible to set up navigation using only ARI2, but since this involves Java code, the resulting navigation structure would be less transparent.
public void interceptActionResult(ARIResult result, ViewParameters incoming, Object actionReturn) { if (result.resultingView instanceof SearchCriteriaViewParams) { SearchCriteriaViewParams outgoing = (SearchCriteriaViewParams) result.resultingView; outgoing.searchCriteria = criteria; } }
The instanceof check, as well as ensuring typesafety, is implicitly testing that the navigation in progress is not for the "error case", which would be causing a navigation to the current view with ViewParameters of type SimpleViewParameters. Part of the power of RSF's ViewParameters system is its ability to make assertions about views or categories of views using standard OO type relationships.
This code block executes at the very end of the action cycle on which the form submission against searchCriteria has been successfully processed, and makes sure to transfer the submitted field values into the outgoing URL statement, by means of a simple assignment of beans.
This image shows the application state after a successful submission, following navigation to the search results view. As you can see, the page URL is both easily readable and also bookmarkable.
Submitting search arguments through GET#
The alternative implementation of SearchCriteriaProducer, SearchCriteriaProducerGET, is very much simpler, since it saves on
- NavigationCases
- The request-scope bean searchCriteria, including declaring it requestAddressible
- The validation declaration and guard
- ActionResultInterceptor navigation rule
The only changes we need make are to the producer, which now reads as follows:
public class SearchCriteriaProducerGET implements ViewComponentProducer { public static final String VIEW_ID = "searchCriteria"; public String getViewID() { return VIEW_ID; } public void fillComponents(UIContainer tofill, ViewParameters viewparams, ComponentChecker checker) { UIForm form = UIForm.make(tofill, "search-form", new SearchCriteriaViewParams(SearchResultsProducer.VIEW_ID)); UIInput.make(form, "first-name", "searchCriteria.firstName"); UIInput.make(form, "last-name", "searchCriteria.lastName"); UICommand.make(form, "submit"); } }
The 3rd argument to the UIForm implicitly identifies it as a GET form, since it is targetted at a specific ViewParameters, that of the desired resulting view SearchResultsProducer. Any EL bindings supplied within such a form (e.g. searchCriteria are implicitly assumed to be bindings against the outgoing ViewParameters for the form, rather than against the bean model as a whole which would be the default for a POST form. Once again the UICommand submission is action-free, which is now the only possibility for a GET form. Since on the resulting view we recreate *exactly* the same ViewParameters state as we did via the POST model, nothing else in the application requires to be changed, including the view template.
To switch to this version of the application, simply alter the commenting on this portion of requestContext.xml:
<!-- To switch to the POST version of this app, comment out this definition in favour of the following one for SearchCriteriaProducerPOST --> <!-- <bean class="org.springframework.webflow.samples.phonebook.rsf.producers.SearchCriteriaProducerGET" /> --> <bean class="org.springframework.webflow.samples.phonebook.rsf.producers.SearchCriteriaProducer"> <property name="searchCriteria" ref="searchCriteria" /> </bean>
What we have sacrificed here is the validation of the search arguments - however, as we see, this application doesn't misbehave too badly. The result of making no entry for first or last name is simply to return search results for the entire database, which is a fairly valid application response. This was not the design choice made by the original application, and so we must consider that the GET form version presented here is not "faithful" but it is worth highlighting it as a neighbouring and potentially desirable implementation strategy available with RSF. RSF may support GET form validation in a future version.
Application review - SWF and RSF#
We have now 3 complete versions of the original Phonebook application (as well as a further alternative for one view in the 3rd) and we should now take stock of the relationships and tradeoffs between them, as well as an inventory of the parts of the application which have been conserved.
Conservation#
SWF on SpringMVC + JSP | SWF on RSF | Pure RSF | |
---|---|---|---|
View | JSPs | IKAT XHTML | |
Application model + services | |||
Spring Validation | |||
Spring Web Flow Definition | |||
ViewParameters definitions | (partial) | (partial) |
The first key point to observe is that at no point have we needed to make any alterations in the core application artefacts (Phonebook, SearchCriteria, Person, etc.) and have preserved a clean "framework-free" pure bean model. This might seem an obvious point, but there are still a number of web frameworks that cannot achieve this easily! In addition, we have also conserved our Spring Validator definition across all application versions.
The second, more palpable point is the portability of the pure XHTML templating definitions, from our SWF+RSF application to not just one but two independent realisations of the application as pure RSF. As well as being directly usable to UI designers, this gives security to developers that they are free to migrate back and forth between different frameworks without incurring any costs in this area.