Thursday, November 13, 2008

JSF and field validation

JSF provides some useful tools for field-level validation (<f:validate .../> and cousins). However, what if you have some externally-implemented validation whose errors you want to associate with specific fields? The "JSF way" would be to register the errors with the specific fields in question, for example:


public String someAction() {
String outcome = "success";
boolean result = someExternalValidation(...);
if (!result) {
FacesContext.getCurrentInstance().addMessage(
"formId:subviewId:fieldId",
new FacesMessage(FacesMessage.SEVERITY_ERROR, "This field is wrong");
);
outcome = null;
}
return outcome;
}




The above is not ideal, since the field IDs are hard-coded into the backing bean. An alternative is to add a binding to the backing bean so that the correct component ID can be fetched.


private UIComponent fieldComponent;
...
public setFieldComponent(UIComponent pFieldComponent) {
this.fieldComponent = pComponent;
}
...
public String someAction() {
String outcome = "success";
boolean result = someExternalValidation(...);
if (!result) {
FacesContext.getCurrentInstance().addMessage(
this.fieldComponent.getClientId(),
new FacesMessage(FacesMessage.SEVERITY_ERROR, "This field is wrong");
);
outcome = null;
}
return outcome;
}




While this works, your backing bean will need to have a whole bunch of UIComponent objects bound to it, which is a little messy.

A further problem arises when you try to integrate some other field-level validation framework into JSF, for example, the Spring validator. Suppose you had the following:


private Object dataObject;
private Validator dataObjectValidator;
...
public String someAction() {
String outcome = "success";
Errors errors = new BeanPropertyBindingResult(this.dataObject, "dataObject");
ValidationUtils.invokeValidator(this.dataObjectValidator, this.dataObject, errors);
if (errors.hasErrors()) {
// ?!? How to associate these errors with field IDs ?!?
outcome = null;
}
return outcome;
}




There is no out-of-the-box way to associate the errors generated by the Spring validation framework with actual JSF field IDs.

The spring "field id" for an error is really the JavaBean property path to access that property (e.g. dataObject.property1.property2). JSF field IDs follow allowable HTML ID syntax, so the spring "field id" can't be used directly as a JSF field ID.

It would also be nice to associate one error message with multiple fields. For example, suppose your validator lodged an object-level (i.e. not global) message such as Assigned date must be after lodged date. Clearly this error involves two fields and can be remedied by changing either of the fields. In this case, you'd want to display an interesting marker next to each field (e.g. using <rich:message .../> or some similar tag) and display the overall error associated to the general section of the form (or as a global error).

JSF purists would say that as error message display is a UI function, then you should code all mappings at the backing bean level. However, we live in the real world here!!! If you have heaps of fields for a simple domain object and an existing, tested validator for this domain object, then you're not going to re-write the validations in JSF, nor are you going to add a whole bunch of code to each backing bean solely to deal with the Spring-to-JSF error translation. Instead, you want a simple re-usable way to associate the existing error messages with the appropriate JSF field IDs.

To acheive this, we need to solve several problems. These are:

  • Translating the Spring field/object "field id" paths into valid JSF IDs. (but see below)

  • Adding the Spring field/object error messages as JSF error messages.

  • Associating those error messages with an actual JSF field.



So we might end up with markup like this:

<h:panelgrid cellspacing="2" columns="2">
...
<h:outputlabel value="Field 1" for="field1"/>
<h:panelgroup>
<h:inputtext id="field1" value="#{bean.field1}">
<f:attribute name="otherMessageIds" value="dataObject.field1"/>
</h:inputtext>
<h:message for="field1"/>
</h:panelgroup>
...
<h:panelgrid>




We want to perform this enhancement without having to write custom JSF components. This means that we need to modify the list of faces messages returned by FacesContext.getMessages(...) to include any messages for those IDs given in the otherMessageIds attribute.

One possibility is to use a PhaseListener before the render phase to map the messages. The disadvantages of this method are:

  • You need to remove the old message, otherwise you will get multiple messages for the same error.

  • You can't associate the same error with multiple fields.



For these reasons, we use an alternative method which is to override the FacesContext object with our own.

To do this, you need:

  • A FacesContextWrapper implementation which implements the adaptor pattern for a FacesContext. This is so we don't have to implement all the faces context methods ourselves!

  • A MessageMappingFacesContext implementation, which is a subclass of the wrapper.

  • A FacesContextFactory implementation which creates your special MessageMappingFacesContext instances.



The FacesContextWrapper is a simple adaptor implementation for a FacesContext class. It is an abstract class that wraps a FacesContext and delegates all requests to the wrapped class.

The MessageMappingFacesContextFactory factory implementation creates instances of the MessageMappingFacesContext.

The key part of the implementation is the MessageMappingFacesContext which overrides the getMessages(...) function. The override method:

  • Invokes the superclass to fetch the actual normal messages.

  • Adds all these messages to an internal list

  • Locates the associated component for the client ID, and if this component exists and has the "otherMessageIds" attribute, invokes the superclass to fetch all messages for each message ID and adds them to the internal list

  • Returns an iterator for the internal list



I've implemented this technique and it works fine. When I get a chance, I'll post a code example.

No comments: