Monday, July 5, 2010

Spring Web Flow Examined

MVC frameworks have been around for many years. But what do they do? Why do we prefer to write code in actions or handlers instead of in servlets or JSP's. Why we do not prefer logic in JSP's is pretty obvious. But why don't we write our own servlets? Out of the numerous advantages offered by MVC frameworks over plain servlets the most important advantage is probably flow control.

MVC frameworks hide the technical details involved in handling a request and rendering a response from the user and in doing so most of them offer some form of flow control. In a web environment this means configuring an execution path for handling a request based on its state. Showing one page for the successful execution of a request and another page for the unsuccessful execution is an example of a simple page flow.

More complex examples of page flows are wizards that span across multiple pages and reusing these wizards in different parts of the application, for example to create a relationship between two entities in the database. Although these scenarios can be implemented with any MVC framework the classic MVC approach has a number of drawbacks.

Classic MVC frameworks do not manage state across requests for you. Managing state yourself means boilerplate code inside your handlers or actions. This problem is more important in extensive applications where switching back and forth between page flows would require so much boilerplate code that is not implemented very often. Classic MVC frameworks give you a hard time if you want to build your web tier for reuse, their way of working simply does not promote this.


Introducing Spring Web Flow

Keith Donald and Erwin Vervaet created Spring Web Flow as a answer to the limited page flow functionality offered by classic MVC frameworks. Web Flow integrates nicely with Spring MVC, JSF or Struts and offers the means to define page flows consisting of views and actions and in addition allows page flow reuse.

The Web Flow philosophy says that any page flow can be drawn as a simple flow chart where each state in the page flow is either a screen (a view) or the execution of code (an action). Web Flow manages the transition between states and requires input from the actions or the views (the user) to determine the next step of the configured execution path of the page flow.

Before we take a look at an example of a page flow in Web Flow lets step back to think about what a state is in a web application means. If we take the example of a page with a form that on submission will either show a message for successful completion or an error message we can identify at least four states: the initial state where the user loads the page with the form, the submission state where the user submits the form and the two states related to the outcome: the success state and the error state.

Each state in the example above has a specific purpose or a work load. The initial state needs to render the page with the form and potentially load data to populate the form. The submission state needs to capture the form fields and do some form of validation on them. Depending on the outcome of this validation step the final state will be determined: the success state or the error state. The success state will show a new page which informs the user of the successful completion of the form submission while the error state will again show the page with the form with meaningful error messages.

Note that although the initial state and the error state both show the page with the form these states do not overlap. The specific state when rendering the page with the form depends on the context of the page flow at that point in time. The page flow will be in the initial state when the user loads the page with the form for the first time. The error state will be triggered when the form fields submitted by the user cannot be validated successfully.

Lets take a look at how the example above would be configured using Spring Web Flow. First we need to specify the four states we have identified:

 	 	 		 		  	  	 	 		  	  	  	 		 		  		  	  	 	  

Page flows are state-full objects stored in a cache - typically the HTTP session - that spans across multiple HTTP requests. Web Flow associates page flows that have been started and not yet ended with each HTTP request by means of the flow execution id. This id must be available as a HTTP request parameter called "_flowExecutionId" in every HTTP request except when the page flow is started, being the initial state. If the URI to the page flow above is "buy.html" an anchor that triggers the initial state would look like this:

Buy Now! 

A form that would trigger the submission state would look like this:

Order form "/>

In Web Flow a page flow always operates under one URI. You may already have noticed the form element above has no action attribute. The form contains a second hidden field called "_eventId". This parameter is required and lets Web Flow know which transition to make. Although this may seem like overhead in this example it allows us to associate multiple state transitions with one view, for example in case of multiple forms on one page. Remember Web Flow knows which state the page flow is currently in based on the flow execution id.

Managing state transitions is an important advantage over classic MVC frameworks where you would need to configure an URI in your web application setup per form on a page. Not only would this frustrate the readability of your setup, it would also leave you responsible to make sure only authorized state transitions are made. In plain english this means you would need to decide for each execution of your actions or handlers if the execution is permitted at that stage of the page flow. Classic MVC frameworks do not provide this kind of page flow functionality so if you're using one of those you are on your own.

Because Web Flow manages the state transitions for us and because of the action bean we are using we have yet to write our first line of code although we have not yet handled validation. In the page flow example above there are two references to a bean called "formAction" which is configured in the Spring application context. In this example org.springframework.web.flow.action.FormAction is used, the work horse action class shipped with Web Flow. FormAction mediates between the request context, typically a HTTP request, and a form object. Its "setupForm" method - which is called by the "setupForm" state - creates a form object and puts it in the scope ("request" or "flow") if none is available. Its "bindAndValidate" method maps request parameters to properties of the form object. This method will use the form object available in the scope or otherwise create one and put it in the scope. The "bindAndValidate" method calls the "setupForm" method so calling the latter method separately may be overhead in some cases.

Lets first look at the way the "formAction" bean is configured in the Spring application context:

 	order 	example.OrderForm  	 		 	  

"formObjectName" is the name with which the form object defined by "formObjectClass" will be associated in the scope. Lets take a look at the form object class:

package example;  public class OrderForm { 	private String firstName = null; 	private String lastName = null; 	private int age = 0;  	public void setFirstName(String firstName) { this.firstName = firstName; } 	public String getFirstName() { return this.firstName; }  	public void setLastName(String lastName) { this.lastName = lastName; } 	public String getLastName() { return this.lastName; }  	public void setAge(int age) { this.age = age; } 	public int getAge() { return this.age; } } 

Instances of OrderForm will hold the values entered by users in the order form. Lets take a look at the validator:

package example;  import org.springframework.validation.Errors; import org.springframework.validation.Validator;  import org.apache.commons.lang.StringUtils;  public class OrderFormValidator implements Validator { 	public boolean supports(Class clazz) { return OrderForm.class.equals(clazz); }  	public void validate(Object o, Errors errors) { 		OrderForm orderForm = (OrderForm)o;  		if (StringUtils.isBlank(orderForm.getFirstName())) { 			errors.rejectValue("firstName", null, "First name must not be empty"); 		} 		if (orderForm.getFirstName().length() > 30) { 			errors.rejectValue("firstName", null, "First name should not be longer than 30 characters"); 		} 		if (StringUtils.isBlank(orderForm.getLastName())) { 			errors.rejectValue("lastName", null, "Last name must not be empty"); 		} 		if (orderForm.getLastName().length() > 50) { 			errors.rejectValue("lastName", null, "Last name should not be longer than 50 characters"); 		} 		if (orderForm.getAge() <> 120) { 			errors.rejectValue("age", null, "We do not do business with the undead"); 		} 	} } 

OrderFormValidator will validate OrderForm instances - form objects - and provide meaningful error messages. The only thing missing is the HTML form:

Personal details "/>


The submission state will populate the OrderForm instance with the values entered by the user in the form and will have it validated by OrderFormValidator. If validation is successful FormAction will return "success" otherwise "error" will be returned. This allows Web Flow to complete the submission state by making the required state transition.

Lets take this page flow example one step further by adding a second page to the page flow to extend the order form functionality. First we extend OrderForm:

package example;  public class OrderForm { 	private String firstName = null; 	private String lastName = null; 	private int age = 0; 	private int quantity = 0; 	private String size = null; 	private double unitPrice = 20;  	public void setFirstName(String firstName) { this.firstName = firstName; } 	public String getFirstName() { return this.firstName; }  	public void setLastName(String lastName) { this.lastName = lastName; } 	public String getLastName() { return this.lastName; }  	public void setAge(int age) { this.age = age; } 	public int getAge() { return this.age; }  	public void setQuantity(int quantity) { this.quantity = quantity; } 	public int getQuantity() { return this.quantity; }  	public void setSize(String size) { this.size = size; } 	public String getSize() { return this.size; }  	public double getTotal() { return unitPrice * quantity; } } 

Next we extend OrderFormValidator. We create two validation methods: one to validate "firstName", "lastName" and "age" and one to validate "quantity" and "size".

package example;  import org.springframework.validation.Errors; import org.springframework.validation.Validator;  import org.apache.commons.lang.StringUtils;  public class OrderFormValidator implements Validator { 	public boolean supports(Class clazz) { return clazz.equals(OrderForm.class); }  	public void validate(Object o, Errors errors) { 		validatePersonalDetails((OrderForm)o, errors); 		validateOrderDetails((OrderForm)o, errorrs); 	}  	public void validatePersonalDetails(OrderForm orderForm, Errors errors) { 		if (StringUtils.isBlank(orderForm.getFirstName())) { 			errors.rejectValue("firstName", null, "First name must not be empty"); 		} 		if (orderForm.getFirstName().length() > 30) { 			errors.rejectValue("firstName", null, "First name should not be longer than 30 characters"); 		} 		if (StringUtils.isBlank(orderForm.getLastName())) { 			errors.rejectValue("lastName", null, "Last name must not be empty"); 		} 		if (orderForm.getLastName().length() > 50) { 			errors.rejectValue("lastName", null, "Last name should not be longer than 50 characters"); 		} 		if (orderForm.getAge() <> 110) { 			errors.rejectValue("age", null, "We do not do business with the undead"); 		} 	}  	public void validateOrderDetails(OrderForm orderForm, Errors errors) { 		if (!(orderForm.getSize().equals("S") 			|| orderForm.getSize().equals("M") 			|| orderForm.getSize().equals("L"); 			|| orderForm.getSize().equals("XL"))) { 			errors.rejectValue("size", null, "Size must be S, M, L or XL"); 		} 		if (orderForm.getQuantity() <>

We have split the validation of OrderForm instances into two methods. This allows us to either validate these two group separately or validate OrderFrom instances entirely. Now lets make some changes to the page flow:

 	 	 		  		  	  	 	 		   	  	 	 		 			   		 		  		  	  	 	 		  	  	  	 		 			  		 		   		  	  	 	   

Adding a page to the page flow has added a number of extra states yet the page flow remains very readable. There are now two submission states, personal detail submission state and order details submission state, that call the relevant method on the validator.

We need to change the scope of the FormAction instance since we want to keep the instances of the form object available across the two forms:

 	flow  	order 	example.OrderForm 	  		 	  

The only thing missing now is the HTML form for the second page:

Order details "/>

In just a few moments we have added a page to the page flow. The page flow mechanism provided by Web Flow does all the hard work for us. There are just two things left to complete the functionality of this example. First we want to navigate from the order details form screen to the personal details form screen. Secondly, if the user sets the quantity to "0" in the order details form we want to return to that page after submission and display a message saying that the order has been cancelled. There are still more ways to improve these screens, but we'll stick to these two enhancements in this article.

Adding a navigation from the the order details form screen to the personal details form screen is trivial. As you may have understood by now two thing need to be added to this end: a transition from the "orderDetailsView" state to the "personalDetailsView" state and an anchor in the order details view. Lets start by changing the "orderDetailsView" state:

	...  	 	 		   		  	  	... 

Next we can add this anchor to the order details view:

&_eventId=toPersonalDetails"> 

Note we're using an anchor which is more appropriate than a form. The last thing we need to add is a conditional transition in case the quantity in the order details form is "0". We only want a cancelled order state when the quantity was not "0" before. If the quantity changes afterwards the order state should no longer be cancelled. The best place to implement this logic is in the setQuantity method of the OrderForm class:

	...  	private boolean cancelled = false;  	/** 	 * 

Sets the "cancelled" property to true if quantity is "0" and was not "0" before, * otherwise "cancelled" will be set to false. * * @param quantity the quantity */ public void setQuantity(int quantity) { if (quantity != this.quantity) { this.cancelled = (quantity == 0); this.quantity = quantity; } } public boolean getCancelled() { return this.cancelled; } ...

Next we need to test the value of the "cancelled" property of OrderForm instances to determine where the page flow should go to if the order detail form is submitted:

	...  	 	 		  			  		 		  		   	  	 		 	  	...  

Adding a decision state allows us to the test values of OrderForm instances. You have to use the name of the object in the flow scope which is the only drawback of this approach. Notice the "testQuantity" state sits in between the "bindAndValidateOrderDetails" state and the "orderDetailsView" and "submissionSuccess" states. The decision state clearly offers a lot of power in page flow design. Changing the order view to display a message in case the order is cancelled is the very last step to complete this example:

 	 		You order has been cancelled.  	  

While reuse is not part of the example in this article it's a powerful feature that's also offered by Spring Web Flow. At any state transition in a page flow a new page flow can be started. The current page flow will be paused and continues when the sub page flow has finished. The flow launcher example shipped with Spring Web Flow illustrates this functionality in more detail. This feature allows you to take your page flow designs to a new level.

Conclusion

You will find Spring Web Flow a very powerful page flow management tool that circumvents limitations of classic MVC frameworks. Struts and JSF integration are shipped with Spring Web Flow making its integration in your existing environment straightforward. A number of sample applications - including a Struts and JSF integration examples - are a good starting point to get your hands dirty.

From a strategic point of view the close integration with Spring's dependency injection makes adapting Spring Web Flow even more interesting. Both Spring and Spring Web Flow provide more value for money and more functionality out of the box than any other application framework.

Ref:http://www.javalobby.org/articles/spring-webflow/

No comments:

Post a Comment