How to implement form-level validation in JSF

Recently I was faced with the challenge of implementing form-level (or page-level) validation in a JSF-based application. What I mean by form-level validation is the need to evaluate a subset of a form's fields as a unit, rather than simply validating each field in isolation. An example of this type of validation can be found on a user registration form where one has to select a password in one text field, and then retype the same password in another text field for confirmation. Validating that these two text fields contain the same password is an example of form level validation.

In my case, I had two date selector components on my form, one for a start date/time and one for an end date/time for an event that was being scheduled. The rule I needed to validate was that the end date/time was later than the start date/time.

There are a few ways to implement validation like this, including but not limited to:


  1. Build a custom component that renders selectors for both the start and end date/time, then validate as a unit. This actually is field-level validation and doesn't truly address the form-level problem.

  2. Implement a validator method on a managed bean that will evaluate the data submitted for multiple components.



I'll address the second method in this HOWTO.

First, you'll need to bind at least n-1 of the components that you want to validate to properties on your managed bean. The simplest way is to declare properties of type UIInput:


private UIInput startDateComponent;

public UIInput getStartDateComponent() {
return startDateComponent;
}

public void setStartDateComponent(UIInput startDateComponent) {
this.startDateComponent = startDateComponent;
}


and do the actual binding in the JSP:


<t:inputDate id="eventStart" value="#{orderForm.sampleInfo.requestedStartTime}"
type="both"
popupCalendar="true"
ampm="true" binding="#{dateValidationForm.startDateComponent}"/>


Next, you'll implement the validation method, which can have any name you like, but must share the same signature as this example:

public void validateEndDate(FacesContext context, UIComponent toValidate, Object value) {
Date endDate = (Date) value;
Date startDate = (Date) getStartDateComponent().getLocalValue();

if (startDate == null) {
context.addMessage(getStartDateComponent().getClientId(context),new FacesMessage("Please specify a valid date and time."));
throw new ValidatorException(new FacesMessage());
}

long endTime = endDate.getTime();
long startTime = startDate.getTime();

if (startTime >= endTime) {
addError("errors.batchOrder.invalidEndDate");
throw new ValidatorException(new FacesMessage("Event end must be later than event start."));
}
}

And finally, you'll bind the validation method to the last component in your subset of components that need to be validated together:

<t:inputDate id="eventEnd" value="#{orderForm.sampleInfo.requestedEndTime}" type="both" popupCalendar="true" ampm="true" validator="#{dateValidationForm.validateEndDate}"/>

To understand why I say n-1 components, think of the way the validation phase occurs in JSF. Data is bound to the components in the order that they occur in the JSF component tree, which just so happens to be the order in which they appear in the JSP source. Looking at the validateEndDate method, you'll see that I only reference the startDateComponent from the binding, but I reference the endDate as the Object value reference that was passed into the method. This is why you only need to bind n-1 components, because you get the nth component from the method signature.

If you want to be more uniform and bind all of the components, you could create an extra dummy hidden value component and bind the validator method to it. You could then bind all of the components to your managed bean and access them all from the bindings rather than accessing one from the method signature.

The validateEndDate method itself is rather simple. First you access the data by getting the local value of each component (again, the endDate value is not accessed in this way - in fact, it hasn't been bound yet because it must be validated first, and that's what's happening in this method!). You then apply the business rule. You'll see that first I look to see if the startDate is null. I'm not sure why this is possible, but if the startDate was not submitting a good value on the FIRST submit, the local value was null. So, I catch that here. I add an error message to the startDateComponent and throw a ValidatorException. If the business rule is violated, throw a ValidatorException. (I'm also using the addError method provided by AppFuse to work w/ its message framework as well, but that is not necessary w/ all JSF apps).

Now, for the final problem I encountered. In Weblogic server, which we're still using for the time being, if your session cannot be serialized then it deletes your entire session. Obviously this can cause major problems in any web app. To deal with this, ANY SESSION SCOPED MANAGED BEAN must be fully serializable, meaning it and any objects referenced in its state. Herein lies the problem for JSF. Instances of UIComponent (an ancestor of UIInput) are not serializable, so if we bind our components to UIInput fields on a session-scoped managed bean (the bean backing this form is an Order Form/Shopping Cart style bean), it will not be serializable and Weblogic will kick out your session.

To deal with this problem, realize that there is no reason that you can only have one managed bean backing a form. In fact, you can reference as many managed beans as you need. Since validation is done for each request, there is no need to manage any state there across multiple requests like we need to do with a shopping cart. So, why not declare an additional managed bean that is REQUEST SCOPED, and then put the bindings and validation method there. That is exactly what I did. Here is the entire bean:

public class DateValidationForm extends BasePage {

private UIInput startDateComponent;

public UIInput getStartDateComponent() {
return startDateComponent;
}

public void setStartDateComponent(UIInput startDateComponent) {
this.startDateComponent = startDateComponent;
}

public void validateEndDate(FacesContext context, UIComponent toValidate, Object value) {
Date endDate = (Date) value;
Date startDate = (Date) getStartDateComponent().getLocalValue();

if (startDate == null) {
context.addMessage(getStartDateComponent().getClientId(context),new FacesMessage("Please specify a valid date and time."));
throw new ValidatorException(new FacesMessage());
}

long endTime = endDate.getTime();
long startTime = startDate.getTime();

if (startTime >= endTime) {
addError("errors.batchOrder.invalidEndDate");
throw new ValidatorException(new FacesMessage("Event end must be later than event start."));
}
}
}

and the declaration in faces-config.xml:

<managed-bean>
<managed-bean-name>dateValidationForm</managed-bean-name>
<managed-bean-class>org.stjude.hc.srmcti.webapp.action.ordering.DateValidationForm</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
</managed-bean>

The added bonus is that you can reuse this bean across all forms where you need this behavior. My application happens to have 2 additional forms where I would have repeated this logic, so I just reference this bean there.

Enjoy!

3 comments:

pari said...

Hi I tried using getLocalValue() , but the problem is after I have entered invalid value and then leave the field blank, the getLocalValue() evaulates to the invalid value I had entered.

Wesley said...

Thanks Matt, this works great.

FWIW, I tried using a hidden input as you hypothesized to do the binding outside the date inputs, but it doesn't fire because the input value won't change (would have to do something funky with javascript -- not worth it).

Also, regarding your serialization issue, would it work to just declare the UIInput fields as transient? I guess it depends on when the setter methods of the binding are called (once or per request) and whether you'd need them to work after deserialization.

Rui said...

HI,

I'm getting java.nullpointer exception when getting value from binding UIInput Object:
double variable = (Double getUIInputObject.getLocalValue()

Do you have any Sugestion?