dcsimg
September 29, 2016
Hot Topics:

Cross-field Validation in JSF

  • May 16, 2016
  • By Anghel Leonard
  • Send Email »
  • More Articles »

You have to be aware that, from a Java/JSF perspective, there are several limitations in using Bean Validation: JSR 303. One of them involves the fact the JSF cannot validate the class or method level constraints (so called, cross-field validation), only field constrains. Another one consists of the fact the <f:validateBean/> allows validation control on a per-form or a per-request basis, not on a per-UICommand or UIInput. In order to achieve more control, you have to be open to write boilerplate code, and to shape custom solutions that work only on specific scenarios.

In this article, we will have a brief overview of three approaches for achieving cross-field validation using JSF core and external libraries. We will pass through the approaches provided by:

  • OmniFaces
  • PrimeFaces
  • JSF 2.3

OmniFaces Approach

Let's suppose that we have a simple form that contains two input fields representing the name and the e-mail of a Web site member or admin. Next to these inputs, we have two buttons, one with the label Contact Member and another one with the label Contact Admin. When the user clicks the first button, he will "contact" the specified Web site member, and when he clicks on the second button, he will "contact" the specified Web site admin. The form is as follows:

<h:form>
   Name: <h:inputText value="#{contactBean.name}"/>
   E-mail: <h:inputText value="#{contactBean.email}"/>
   <h:commandButton value="Contact Member"
      action="contact_member"/>
   <h:commandButton value="Contact Admin"
      action="contact_admin"/>
</h:form>

For a Web site member/admin, the name input should not violate any of the constraints defined in a group named MemberContactValidationGroup. Moreover, for a Web site member/admin, the email input should not violate any of the constrains defined in the AdminContactValidationGroup group. Even more, we have a constraint over email in the default group (applicable to members and admins).

Next, we should attach these constraints to the name and email inputs, but, we need to obtain the following functionality:

  • If the user clicks the Contact Member button, the validation should take into account only constraints from the default group and from the MemberContactValidationGroup, and ignore the constraints from the AdminContactValidationGroup.
  • If the user clicks the Contact Admin button, the validation should take into account only constraints from the default group and from the AdminContactValidationGroup, and ignore the constraints from the MemberContactValidationGroup.

Finding a solution based on <f:validateBean/> will end up in some boilerplate code, because it will require a "bunch" of tags, EL expressions, conditions, server-side code, and so forth. Most likely, at the end, our form will look like a total mess. Another approach is to redesign the application, and use two forms, one for members and one for admins.

Further, let's suppose that the provided email should always start with the name (getEmail().startsWith(getName()). This is basically a cross-field constraint that can be applied via a class level constraint. But, JSF doesn't support this kind of constraints, so you have to provide another solution (not related to Bean Validation), like placing the validation condition in the action method, or in the getters (if there is no action method). Multiple components can be validated by using <f:event/> with postValidate, or, if you need to keep the validation in Process Validations phase, maybe you want to use <f:attribute/> and a JSF custom validator.

The features brought by OmniFaces via the <o:validateBean/> tag are exactly what we need to solve our use case. Although the standard <f:validateBean/> only allows validation control on a per-form or a per-request basis, <o:validateBean/> allows us to control bean validation on a per-UICommand or UIInput component basis.

For example, we can obtain the claimed functionality via <o:validateBean>, like this:

<h:form>
   Name: <h:inputText value="#{contactBean.name}"/>
   E-mail: <h:inputText value="#{contactBean.email}"/>
   <h:commandButton value="Contact Member" action="contact_member">
   <o:validateBean validationGroups="javax.validation.groups.Default,
      omnify.usecase.validateBean.MemberContactValidationGroup"/>
   </h:commandButton>
   <h:commandButton value="Contact Admin" action="contact_admin">
      <o:validateBean validationGroups="javax.validation.groups.Default,
         omnify.usecase.validateBean.AdminContactValidationGroup"/>
   </h:commandButton>
</h:form>

Listing 1: The complete application in named ValidateBean_1.

Note: By analogy, you can use <o:validateBean/> with UIInputs. Like the <f:validateBean/> tag, <o:validateBean/> supports the disabled flag attribute that can be used to enable page level determination of whether or not this validator is enabled.

Now, let's discuss the class level validation. The <f:validateBean/> does not provide anything related to bean validation, so we can "jump" directly to <o:validateBean/>. Right from the start, you should know that <o:validateBean/> supports an attribute named method, which indicates if this is a copy bean validation (default) or an actual bean validation:

  • Copy bean validation: When the method attribute is not present, this is the default validation method (method="validateCopy"). Basically, the idea is to make a copy of the object to validate at the end of the Process Validations phase, set the values retrieved from the input components on it, and perform class level validation on the copy. If the validation is a success, the original bean is updated and a potential action method is invoked; otherwise, it "jump"s directly to Render Response phase—does not update the model if validation errors occur. Of course, making a copy can be considered a drawback of this approach.
  • Actual bean validation: This method must be explicitly set via method="validateActual". In this case, the validation happens at the end of the Update Model Values, so the original bean is first updated with the values retrieved from the input components, afterwards validated. The main drawback here consists of the fact that we may pollute the model with invalid values, so the bean developer must perform a "manual" check. A potential action method will be invoked independently on the validation result because the flow continues with the Invoke Application phase.

In case of using copy bean validation, OmniFaces tries a suite of strategies for determining the copy mechanism. By default, OmniFaces comes with an interface (Copier) that is to be implement by classes that know how to copy an object, and provides four implementations (strategies) of it:

  • CloneCopier: This is an implementation of Copier capable of dealing with beans that implement the Cloneable interface and provide cloning support.
  • SerializationCopier: This is an implementation of Copier capable of dealing with beans that implement the Serializable interface and support serialization according to the rules of that interface.
  • CopyCtorCopier: This is an implementation of Copier capable of dealing with beans that have an additional constructor (next to the default constructor), taking a single argument of its own type that initializes itself with the values of that passed in type.
  • NewInstanceCopier: This is an implementation of Copier capable of dealing with beans that have a public no arguments (default) constructor. Every official JavaBean satisfies this requirement. Note that in this case no copy is made of the original bean, but just a new instance is created.

Besides these four implementations (strategies), OmniFaces comes with another one, named MultiStrategyCopier, which basically defines the order of applying the above copy strategies: CloneCopier, SerializationCopier, CopyCtorCopier, NewInstanceCopier. When one of these strategies obtains the desired copy, the process stops. If you already know the strategy that should be used (or, you have your own Copier strategy (for example, a partial object copy strategy), you can explicitly specify it via copier attribute (for example, copier="org.omnifaces.util.copier.CopyCtorCopier"). In OmniFaces Showcase, you can see an example that uses a custom copier. Moreover, you can try to find out more details about Copier on the OmniFaces Utilities ZEEF page, OmniFaces Articles block, and Copy Objects via OmniFaces Copier API article.

Now, let's focus on our cross-field validation: (getEmail().startsWith(getName()). To obtain a class level constraint based on this condition, we need to follow several steps:

1. Wrap this constraint in a custom Bean Validation validator (for example, ContactValidator).

@Override
public boolean isValid(ContactBean value,
      ConstraintValidatorContext context) {
   return value.getEmail().startsWith(value.getName());
}

2. Define a proper annotation for it (for example, ValidContact, used as @ValidContact).

3. Annotate the desired bean (optionally, add it in a group(s)).

@Named
@RequestScoped
@ValidContact(groups = ContactGroup.class)
public class ContactBean implements Serializable {
   ...
   private String name;
   private String email;
   ...
   // getters/setters
}

4. Use <o:validateBean/> to indicate the bean to be validated via value attribute (javax.el.ValueExpression that must evaluate to java.lang.Object), and the corresponding groups (this is optional). Additionally, you can specify the actual bean validation, via the method attribute, and a Copier, via the copier attribute:

<o:validateBean value="#{contactBean}"
   validationGroups="omnify.usecase.validateBean.ContactGroup" />

Listing 2: The complete application in named ValidateBean_2.

PrimeFaces Approach

As you probably know, PrimeFaces comes with a very useful support for client side validation based on JSF validation API and Bean Validation. In this post, we will focus on Bean Validation, and say that this can be successfully used as long as we don't need cross-field validation or class level validation. This means that the validation constraints placed at the class level will not be recognized by PrimeFaces client side validation.

In this post, you can see a pretty custom solution, but pretty fast to implement to obtain a cross-field client side validation for Bean Validation using PrimeFaces. We have a user contact made of a name and an e-mail, and our validation constraint is of type: e-mail must start with name (for example, name@domain.com):

<h:form>
   <p:panelGrid columns="3">
      <p:outputLabel for="nameId" value="Name"/>
      <p:inputText id="nameId" value="#{contactBean.name}"/>
      <p:message for="nameId"/>

      <p:outputLabel for="emailId" value="E-mail"/>
      <p:inputText id="emailId"
         value="#{contactBean.email}"/>
      <p:message for="emailId"/>
      <p:commandButton value="Contact Member"
         action="#{contactBean.someAction()}"
         update="@form" validateClient="true"/>
   </p:panelGrid>
   <p:messages/>
</h:form>

To accomplish this task, we will slightly adapt the PrimeFaces custom client side validation.

First, we create a ValidContact annotation:

@Documented
@Constraint(validatedBy = {ContactValidator.class})
@ClientConstraint(resolvedBy=
   ValidContactClientConstraint.class)
@Target({ANNOTATION_TYPE, METHOD, FIELD})
@Retention(RUNTIME)
public @interface ValidContact {
   String message() default "Invalid contact !";
   Class<?>[] groups() default {};
   Class<? extends Payload>[] payload() default {};
}

Further, in our bean we annotate the proper fields (name and email) with this annotation—we need to do this to indicate the fields that enter in cross-field validation; so, annotate each such field:

@Named
@RequestScoped
public class ContactBean implements Serializable {

   private static final long serialVersionUID = 1L;

   @ValidContact(message = "The name should be used
      in e-mail as name@domain.com!")
   private String name;

   @ValidContact(message = "The e-mail should be of
      type name@domain.com!")
   private String email;

   // getters and setters

   public void someAction() {
      // OmniFaces approach
      Messages.addGlobalInfo
         ("Thank you for your contacts!");
   }
}

Now, we write the validator. Here, we need to keep the name until the validator gets the e-mail also. For this, we can use the faces context attributes, as below:

public class ContactValidator implements
      ConstraintValidator<ValidContact, String> {
   @Override
   public void initialize(ValidContact constraintAnnotation) {
      // NOOP
   }

   @Override
   public boolean isValid(String value,
         ConstraintValidatorContext context) {
      if (Faces.getContextAttribute("NAME_VALUE")
         == null) {
      Faces.setContextAttribute("NAME_VALUE", value);
   } else {
      return value.startsWith(String.
         valueOf(Faces.getContextAttribute("NAME_VALUE")));
      }

      return true;
   }
}

Now, we have to accomplish the client-side validation. Again, notice that we store the name into an array (you can add more fields here) and wait for the e-mail:

<script type="text/javascript">
   var data = [];
   PrimeFaces.validator['ValidContact'] = {
      MESSAGE_ID: 'org.primefaces.examples.validate.
         contact.message',
      validate: function (element, value) {
         if (data.length == 0) {
            data.push(value);
         } else {
            if (!value.startsWith(data[0])) {
               var msgStr = element.data('p-contact-msg'),
               msg = msgStr ? {summary: msgStr,
                     detail: msgStr} :
                  vc.getMessage(this.MESSAGE_ID);
                  throw msg;
            }
         }
      }
   };
</script>

Finally, we ensure the presence of a ClientValidationConstraint implementation:

public class ValidContactClientConstraint
      implements ClientValidationConstraint {

   public static final String MESSAGE_METADATA =
      "data-p-contact-msg";

   public Map<String, Object>
         getMetadata(ConstraintDescriptor constraintDescriptor) {
      Map<String, Object> metadata =
         new HashMap<String, Object>();
      Map attrs = constraintDescriptor.getAttributes();
      Object message = attrs.get("message");
      if (message != null) {
         metadata.put(MESSAGE_METADATA, message);
      }
      return metadata;
   }
   public String getValidatorId() {
      return ValidContact.class.getSimpleName();
   }
}

Done! The complete application is named PFValidateBeanCrossField.

JSF 2.3 Approach

JSF 2.3 will come with a new tag, named <f:validateWholeBean/>. As its name suggests, this tag enables class level validation. This tag contains two important attributes (but, more will be added):

  • value: A ValueExpression referencing the bean to be validated.
  • validationGroups: A comma-separated list of validation groups. A validation group is a fully-qualified class name.

This feature causes a temporary copy of the bean referenced by the value attribute.

Here is a brief example to ensure that the provided name and e-mail fields (contacts) are individually valid and also the e-mail start with that name (for example, valid: nick, nick_ulm@yahoo.com).

ContactValidator Class:

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class ContactValidator implements
      ConstraintValidator<ValidContact, ContactBean> {

   @Override
   public void initialize(ValidContact
         constraintAnnotation) {
      // NOOP
   }

   @Override
   public boolean isValid(ContactBean value,
         ConstraintValidatorContext context) {
      return value.getEmail().startsWith(value.getName());
   }
}

ValidContact Class:

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Constraint(validatedBy = {ContactValidator.class})
@Documented
@Target(TYPE)
@Retention(RUNTIME)
public @interface ValidContact {

   String message() default "Invalid contacts
      (e-mail should start with name) !";
   Class<?>[] groups() default {};
   Class<? extends Payload>[] payload() default {};
}

ContactBean Class:

@Named
@RequestScoped
@ValidContact(groups =
   validateBean.ContactGroup.class)
public class ContactBean implements
      Serializable, Cloneable {

   private static final long serialVersionUID = 1L;

   @Size(min = 3, max = 20, message = "Please enter
         a valid name (between 3-20 characters)!",
      groups = validateBean.ContactGroup.class)
   private String name;

   @Pattern(regexp = "[a-zA-Z0-9]+@[a-zA-Z0-9]+
         \\.[a-zA-Z0-9]+",
      message = "Please enter a valid formated e-mail !",
      groups = validateBean.ContactGroup.class)
   private String email;

   // getters and setters

   @Override
   protected Object clone() throws
         CloneNotSupportedException {
      ContactBean other = (ContactBean) super.clone();
      other.setName(this.getName());
      other.setEmail(this.getEmail());
      return other;
   }
}

ContactGroup Class:

package validateBean;


public interface ContactGroup {
   // NOPE
}

JSF Page:

<h:body>
   <h:form>
      Name:
      <h:inputText value="#{contactBean.name}">
         <f:validateBean validationGroups=
            "validateBean.ContactGroup" />
      </h:inputText>

      E-mail:
      <h:inputText value="#{contactBean.email}">
         <f:validateBean validationGroups=
            "validateBean.ContactGroup" />
      </h:inputText>

      <h:commandButton value="Contact Admin"
         action="contact_admin"/>

      <f:validateWholeBean value="#{contactBean}"
         validationGroups=
            "validateBean.ContactGroup" />
   </h:form>
</h:body>
Note: This feature must explicitly be enabled by setting the following application parameter (context parameter) in web.xml: javax.faces.validator.ENABLE_VALIDATE_WHOLE_BEAN. If this parameter is not set, or is set to false, this tag must be a no-op.
<context-param>
   <param-name>javax.faces.validator.
      ENABLE_VALIDATE_WHOLE_BEAN</param-name>
   <param-value>true</param-value>
</context-param>

The complete application is named JSF23ValidateWholeBeanExample (I've tested with Mojarra 2.3.0-m04 under Payara 4). You can download the sample code from here.


Tags: Java, JSF, PrimeFaces, OmniFaces, Java Server Faces, JSR 303, JSF 2.3, bean




Comment and Contribute

 


(Maximum characters: 1200). You have characters left.

 

 


Enterprise Development Update

Don't miss an article. Subscribe to our newsletter below.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date
Rocket Fuel