Developing Spring Web Flow 2 Application part 3 – Model Validation, Displaying Error and Custom Action

As part of “Developing Spring Web Flow 2 Application” series, this post shows the following features:

  • Model Validation
  • Display Error Message
  • Custom Action object
  • action-state flow design




I. Model Validation

There are 2 ways to validate a model programmatically.

1. Define validation logic in the model object.




(from: Spring Web Flow Reference Guide)
To do this, simply create a public method with the name validate${state}, where ${state} is the id of your view-state where you want validation to run.



public class Account implements Serializable { ... public void validateRegister(ValidationContext context) { MessageContext messages = context.getMessageContext(); if (this.getUsername() == null || this.getUsername().equals("")) { messages.addMessage(new MessageBuilder().error().source("username"). code("account.username.required").build()); } } }



2. Define a separate object called Validator to validate the model.




(from: Spring Web Flow Reference Guide)
To do this, first create a class whose name has the pattern ${model}Validator, where ${model} is the capitialized form of the model expression. Then define a public method with the name validate${state}, where ${state} is the id of your view-state, such as enterBookingDetails. The class should then be deployed as a Spring bean. Any number of validation methods can be defined.



package com.shoppingcart.util; import org.apache.log4j.Logger; import org.springframework.binding.message.MessageBuilder; import org.springframework.binding.message.MessageContext; import org.springframework.binding.validation.ValidationContext; import org.springframework.stereotype.Component; import org.springframework.validation.Errors; import com.shoppingcart.domain.Account; @Component public class AccountValidator { private Logger logger = Logger.getLogger(AccountValidator.class); public AccountValidator() { if (logger.isDebugEnabled()) { logger.debug("account validator created"); } } public void validateRegister(Account account, Errors errors) { // this method will be used if the method: // validateRegister(Account account, ValidationContext context) // is not defined. } public void validateRegister(Account account, ValidationContext context) { if (logger.isDebugEnabled()) { logger.debug("validateRegister called"); } MessageContext messages = context.getMessageContext(); if (account.getUsername() == null || account.getUsername().equals("")) { messages.addMessage(new MessageBuilder().error().source("username"). code("account.username.required").build()); } } }



Validator must be registered as a Spring bean using the naming convention
${model}Validator to be detected and invoked automatically.

<bean id="accountValidator" class="com.shoppingcart.util.AccountValidator" />



Or, if the validator is annotated with @Component, we need to enable component
scanning.

<context:component-scan base-package="com.shoppingcart" />



II. Display Error Message

If the validator detects an error (ex: missing username field), we can show the error
message on the JSP view.

To enable this, we need to create a ResourceBundleMessageSource bean.


<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"> <property name="basename" value="messages" /> </bean>



We also create a resource bundle called messages.properties. This file will be loaded
from the root of the classpath.



account.username.required=The user name is required



III. Action

There are 3 ways to implement the Action logic.

1. invoking a POJO action.



<evaluate expression="pojoAction.method(flowRequestContext)" />
public class PojoAction { public String method(RequestContext context) { ... } }



2. invoking a custom Action implementation.



<evaluate expression="customAction" />
public class CustomAction implements Action { public Event execute(RequestContext context) { ... } }



3. invoking a MultiAction implementation.

<evaluate expression="multiAction.actionMethod1" />
public class CustomMultiAction extends MultiAction { public Event actionMethod1(RequestContext context) { ... } public Event actionMethod2(RequestContext context) { ... } }



Here, a custom MultiAction class is created to handle the account registration logic.
This action class will delicate the registration process to the AccountService bean.
If backend AccountService throws an exception, the error message will be displayed
on the JSP view as well.



package com.shoppingcart.web.action; import org.springframework.binding.message.MessageBuilder; import org.springframework.webflow.action.MultiAction; import org.springframework.webflow.execution.Event; import org.springframework.webflow.execution.RequestContext; import com.shoppingcart.domain.Account; import com.shoppingcart.service.AccountService; import com.shoppingcart.service.AccountServiceException; public class AccountServiceAction extends MultiAction { private AccountService accountService; public Event addAccount(RequestContext context) { try { Account account = (Account)context.getFlowScope().get("account"); if (account == null) { throw new AccountServiceException("account is not found"); } accountService.addAccount(account); return success(); } catch (AccountServiceException e) { context.getMessageContext().addMessage( new MessageBuilder().error().defaultText(e.getMessage()).build()); return error(); } } public void setAccountService(AccountService accountService) { this.accountService = accountService; } }



IV. action-state

After user fills out the register form and clicks the Proceed button, a transition is triggered
from “register” view state to “addAccount” action state. The AccountServiceAction bean is then
called to add the account. If this action is successful, state is transitioned into “registerSuccess”
state. Otherwise, state is transitioned into “register” state, which again the register view is
presented to the user together with the error message.

Note that Web Flow 2 provides automatic model setup and binding using the model attribute
for view-state. There is no need to call “setupForm” and “bindAndValidate” methods anymore.

First, create the AccountServiceAction bean.



<bean id="accountServiceAction"> <property name="accountService" ref="accountService" /> </bean>



Then, define register flow.



<?xml version="1.0" encoding="UTF-8"?> <flow xmlns="http://www.springframework.org/schema/webflow" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd"> <on-start> <evaluate expression="accountService.createAccount()" result="flowScope.account" /> </on-start> <view-state id="register" view="account/register" model="account" > <binder> <binding property="username"/> <binding property="firstName"/> <binding property="lastName"/> <binding property="email"/> <binding property="password"/> </binder> <transition on="submitRegistration" to="addAccount" validate="true" /> <transition on="cancelRegistration" to="end" validate="false"/> </view-state> <action-state id="addAccount"> <evaluate expression="accountServiceAction.addAccount" /> <transition on="success" to="registerSuccess"> <set name="conversationScope.account" value="flowScope.account"></set> </transition> <transition on="error" to="register" /> </action-state>     <view-state id="registerSuccess" view="account/registerSuccess"> <transition on="home" to="home" /> </view-state>            <end-state id="home" view="externalRedirect:contextRelative:/index.html"/> <end-state id="end" view="externalRedirect:contextRelative:/index.html"/> </flow>



V. JSP page

In the Register page, we use form tag from Spring. At the beginning of the table, we use


<form:errors path="*" cssClass="errors" />



so that when there is either a binding error or an exception from AccountServiceAction,
all error messages will appear at the top of the form.

We also use form:errors tag next to individual input so we can show the error message
right next to it.

<td><form:errors path="username" cssClass="errors" /></td>

Here is the complete register page.




<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html> <head> <title>Register an Account</title> <link rel="stylesheet" type="text/css" href="/shopping/styles/styles.css" /> </head> <body>
<div style="padding-bottom: 20px;"> <h2>Register a new Account</h2> </div>
<div style="padding-left: 20px;"> <form:form modelAttribute="account"> <form:errors path="*" cssClass="errors" /> <table style="padding-top: 10px; padding-bottom: 20px;"> <tr> <td>User Name:</td> <td><form:input path="username" /></td> <td><form:errors path="username" cssClass="errors" /></td> </tr> <tr> <td>First Name:</td> <td><form:input path="firstName" /></td> <td><form:errors path="firstName" cssClass="errors" /></td> </tr> <tr> <td>Last Name:</td> <td><form:input path="lastName" /></td> <td><form:errors path="lastName" cssClass="errors" /></td> </tr> <tr> <td>Password:</td> <td><form:password path="password" /></td> <td><form:errors path="password" cssClass="errors" /></td> </tr> <tr> <td>Email:</td> <td><form:input path="email" /></td> <td><form:errors path="email" cssClass="errors" /></td> </tr> </table>
<div> <span style="padding-right: 10"> <input type="submit" name="_eventId_submitRegistration" value="Proceed" /> </span> <span> <input type="submit" name="_eventId_cancelRegistration" value="Cancel"/> </span> </div> </form:form> </div> </body> </html>



Advertisements