Multi-component validation in JSF

Goal

To validate more than one field at once in JSF 2+

Description

A typical use case in the projects I usually work on are validating that one start date is prior to an end date in a given form. To do that, before JSF 2, I used to hack a bit: adding the validation to a hidden field that was put in the page, after the two date fields and look for the appropriate fields during the hidden field’s processing, or to add that logic inside the method that would be invoked in the invoke application lifecycle of JSF.

With JSF 2 and system events, no hacks are necessary anymore. This recipe tells you how to implement that correctly.

How to

The recipe consists on the following 3 basic steps:

  1. System event – define a “postValidate” system event inside a panelGroup (<h:panelGroup id=”date” layout=”block”>) that groups together the start and end date fields. Also in the f:event tag, define the method that will be invoked when that event is triggered (listener=”#{refundRequestHelper.validatePeriod}”) – shown in the first code snippet
  2. Error message – Attach a message to the wrapping panelGroup (p:message for=”date” />) – shown in the first code snippet
  3. Validator method – implement the method that checks the start and date field components and checks if their data is valid – shown in the second code snippet
<h:panelGroup id="date" layout="block">
  <f:event listener="#{refundRequestHelper.validatePeriod}" type="postValidate" />
  <h:panelGroup layout="block" styleClass="row-fluid">
    <h:panelGroup layout="block" styleClass="span6">
      <p:outputLabel for="refundRequestStartDate" value="#{i18n['refundRequest.startDate']}" />
    </h:panelGroup>
    <h:panelGroup layout="block" styleClass="span6">
      <p:outputLabel for="refundRequestEndDate" value="#{i18n['refundRequest.endDate']}" />
    </h:panelGroup>
  </h:panelGroup>
  <h:panelGroup layout="block" styleClass="row-fluid">
    <h:panelGroup layout="block" styleClass="span6">
      <p:calendar id="refundRequestStartDate" effect="slide"
        value="#{refundRequestHelper.startDate}"
        title="#{i18n['refundRequest.startDate']}" showOn="button"
        pattern="#{sigafContext.datePattern}"
        timeZone="#{sigafContext.timeZone}" navigator="true"
        required="true" locale="pt" />
    </h:panelGroup>
    <h:panelGroup layout="block" styleClass="span6">
      <p:calendar id="refundRequestEndDate" effect="slide"
        value="#{refundRequestHelper.endDate}"
        title="#{i18n['refundRequest.endDate']}" showOn="button"
        pattern="#{sigafContext.datePattern}"
        timeZone="#{sigafContext.timeZone}" navigator="true"
        required="true" locale="pt" />
    </h:panelGroup>
  </h:panelGroup>
  <br />
</h:panelGroup>
<p:message for="date" />

The validator method shown next is for a project that is using JSF inside a Liferay portal implementation. Therefore, you will see usage of a LiferayFacesContext class, which means that, for straight JSF, i.e., outside a portlet bridge implementation, some minor adjustments would be required. For instance, liferayFacesContext.getMessage() gets a message property from the application’s resource bundle – the equivalent code without Liferay would be necessary here and, instead of liferayFacesContext.renderResponse(), we would need a FacesContext.getInstance().renderResponse() call. Finally, the call to JsfUtil.addErrorMessage() although simple, is shown in the following snippet too, to clarify things.

...
public void validatePeriod(final ComponentSystemEvent event) {
  final UIComponent wrappingPanelGroup = event.getComponent();
  final UIInput startDate = (UIInput) wrappingPanelGroup.findComponent("refundRequestStartDate");
  final UIInput endDate = (UIInput) wrappingPanelGroup.findComponent("refundRequestEndDate");
  // get the components local values
  final Date start = (Date) startDate.getLocalValue();
  final Date end = (Date) endDate.getLocalValue();
  if (!end.after(start)) {
    final LiferayFacesContext liferayFacesContext = LiferayFacesContext.getInstance();
    // attach the error message to the wrapping panel group
    JsfUtil.addErrorMessage(wrappingPanelGroup.getClientId(), 
      liferayFacesContext.getMessage("message.error"),
      liferayFacesContext.getMessage("message.addRefundRequest.invalidPeriod"));
    startDate.setValid(false); // signal the input field as invalid too
    endDate.setValid(false); // signal the input field as invalid too
    liferayFacesContext.renderResponse();
  }
}
...


... 
// The JsfUtil class relevant code
/**
  * @see JsfUtil#addErrorMessage(String, FacesMessage)
  * 
  */
public static void addErrorMessage(final String clientId, final String summary, 
                                   final String detail) {
  addMessage(clientId, createFacesMessage(
                       FacesMessage.SEVERITY_ERROR, summary, detail));
}

/**
  * It adds a non global message to the faces context, associated to 
  * the component whose id matches the <code>clientId</code>.
  * 
  * @param clientId
  * @param facesMsg
  */
public static void addMessage(final String clientId, final FacesMessage facesMsg) {
  FacesContext.getCurrentInstance().addMessage(clientId, facesMsg);
}

Explanations

In the postValidate event, all individual validations have already taken place. Still inside the validation JSF phase, we are allowed to get all component values and perform a multi-component validation as we did before. Then, by associating a message to a wrapping panel group, we are allowed to present an error message to that group of components, instead of showing it directly associated to one or both date components. I hope that, now, you will use and abuse of JSF 2 system events to implement multi-component validation 🙂

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s