9. Possible Variations and Extensions

9.1. Object-level constraint validation

As an example of a constraint that is not bound to a specific property, but must be checked by inspecting several properties of an object, we consider the validation of the attribute Author::dateOfDeath. First, any value for this attribute must be in the past, which can be specified with the @Past Bean Validation annotation, and second, any value of dateOfDeath must be after the dateOfBirth value of the object concerned. This object-level constraint cannot be expressed with a pre-defined Bean Validation annotation. We can express it with the help of a custom class-level annotation, like the following AuthorValidator annotation interface:

@Target( ElementType.TYPE)
@Retention( RetentionPolicy.RUNTIME)
@Constraint( validatedBy=AuthorValidatorImpl.class)
public @interface AuthorValidator {
  String message() default "Author data is invalid!";
  Class<?>[] groups() default {};
  Class<? extends Payload>[] payload() default {};
}

Comparing with a property level custom constraint validation definition, there is only one difference, the parameter of @Target annotation. While in the case of a property and method level custom constraint validation the values are ElementType.FIELD and ElementType.METHOD, for the case of a class it must be ElementType.TYPE. For a better understanding of the rest of the parameter, please read Part 2 (Validation Tutorial).

The corresponding implementation class, i.e., AuthorValidatorImpl, has the same structure as for the property validation case, but now, we can get access to the class instance and access all the properties and their corresponding values, so we can compare two or many properties when required. In our case, we have to compare the values of dateOfBirth and dateOfDeath values, as shown below, by using the required isValid method:

public class AuthorValidatorImpl implements 
    ConstraintValidator<AuthorValidator, Author> {
  @Override
  public void initialize( AuthorValidator arg0) {}
  
  @Override
  public boolean isValid( Author author, 
   ConstraintValidatorContext context) {
    if (author.getDateOfDeath() != null && 
        author.getDateOfBirth().after( author.getDateOfDeath())) {
      return false;
    }
    return true;
  }
}

Using thie class level validator with JSF requires a bit of tweaking, this feature not being directly supported by JSF (as in the case of property and method validators). For this, in the JSF view, for the specific form element to be validated, we have to specify who is doing the validation, by using the @validator attribute and a JSF expression which points out to the controller method invoked to perform the validation:

<ui:composition template="/WEB-INF/templates/page.xhtml">
  <ui:define name="content">
    <h:form id="createAuthorForm">
      <h:panelGrid columns="3">
        <h:outputLabel for="dateOfDeath" value="Date of death: " />
        <h:inputText id="dateOfDeath" p:type="date" value="#{author.dateOfDeath}" 
            validator="#{authorController.checkDateOfDeath}">
          <f:convertDateTime pattern="yyyy-MM-dd" />
        </h:inputText>
        <h:message for="dateOfDeath" errorClass="error" />
      </h:panelGrid>
      <h:commandButton value="Create" 
          action="#{authorController.add( author.personId, author.name, 
              author.dateOfBirth, author.dateOfDeath)}"/>  
    </h:form>
    <h:button value="Back" outcome="index" />
  </ui:define>
</ui:composition>

The corresponding checkDateOfDeath method is responsible for manually invoking the Java Validation API validator, capture the corresponding validation exceptions and translate them to javax.faces.validator.ValidatorException exceptions which are then managed by JSF and displayed in the view. The method code is shown below:

 public void checkDateOfDeath( FacesContext context, UIComponent component,
      Object value) {
    boolean isCreateForm = (UIForm) context.getViewRoot().
        findComponent( "createAuthorForm") != null;
    String formName = isCreateForm ? "createAuthorForm:" : "updateAuthorForm:";
    UIInput personIdInput = isCreateForm ? (UIInput) context.getViewRoot().
        findComponent( formName + "personId") : null;
    UIOutput personIdOutput = isCreateForm ? null : 
        (UIOutput) context.getViewRoot().findComponent( formName + "personId");
    UIInput nameInput = (UIInput) context.getViewRoot().findComponent( formName + "name");
    UIInput dateOfBirthInput = (UIInput) context.getViewRoot().
        findComponent( formName + "dateOfBirth");
    ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
    Validator validator = factory.getValidator();
    Author author = new Author();
    if ( isCreateForm) {
      author.setPersonId( (Integer) personIdInput.getValue());
    } else {
      author.setPersonId( (Integer) personIdOutput.getValue());
    }
    author.setName( (String) nameInput.getValue());
    author.setDateOfBirth( (Date) dateOfBirthInput.getValue());
    author.setDateOfDeath( (Date) value);
    Set<ConstraintViolation<Author>> constraintViolations = validator.validate(author);
    for ( ConstraintViolation<Author> cv : constraintViolations) {
      if ( cv.getMessage().contains("date of death")) {
        throw new ValidatorException( new FacesMessage( FacesMessage.SEVERITY_ERROR, 
        cv.getMessage(), cv.getMessage()));
      }
    }
  }

While the method looks complicated, it is responsible for the following simple tasks:

  • get access to form data and extract the user input values, by using context.getViewRoot().findComponent( componentName) method. Notice that the component name has the pattern: formName:formElementName.

  • create the Author instance and set the corresponding data as extracted from the form, by using the FacesContext instance provided by the JSF specific validator method

  • manually invoke the Java Validation API validator by using the javax.validation.Validator class.

  • loop trough the validator exception, select the ones which corresponds to the custom validated field and map them to javax.faces.validator.ValidatorException exceptions. The slection can be made by looking for specific data in the exception message.

As a result, the custom Java Validation API class validator is not used, and the JSF view is able to render the corresponding error messages when the validation fails, in the same way as is possible for single property validation situations.

9.2. Alternative class level custom constraint validation

An alternative method for class level custom validation is to use JSF custom class validators. The advantage of this is that they are directly supported in the JSF views, and the dissadvantage is that if another UI engine has to be used outside JSF one, then the validation will be of no use. Also, using the Java Validation API, the validation is mainly provided at the model level, which is the desired method in most of the case, this way avoiding the duplicate logic for validations (i.e., the code duplication for the view related code and the one related to the model).

For our example, the validator for the Author class which is responsible for the validation of dateOfDeath property by comparing it with the dateOfBirth is shown below:

@FacesValidator( "AuthorValidator") 
public class AuthorValidator implements Validator  {
  @Override
  public void validate( FacesContext context, UIComponent component, 
      Object value) throws ValidatorException  {
    Date dateOfDeath  = (Date)value;
    boolean isCreateForm = (UIForm) context.getViewRoot().
        findComponent( "createAuthorForm") != null;
    String formName = isCreateForm ? "createAuthorForm:" : "updateAuthorForm:";
    UIInput dateOfBirthInput = (UIInput) context.getViewRoot().
        findComponent( formName + "dateOfBirth");
    Date dateOfBirth = (Date)dateOfBirth.getValue();  
    if (dateOfBirth.after( dateOfDeath)) {
      throw new ValidatorException ( new FacesMessage( 
          "The date of death should be after the date of birth!"));
    }
  }
}

Then, in the JSF view, for the corresponding field, the validator has to be specified:

<h:outputLabel for="dateOfDeath" value="Date of death: " />
<h:inputText id="dateOfDeath" p:type="date" value="#{author.dateOfDeath}">
  <f:validator validatorId = "AuthorValidator" />
  <f:convertDateTime pattern="yyyy-MM-dd" />
</h:inputText>
<h:message for="dateOfDeath" errorClass="error" />

As discussed before, this method only works with the JSF framework, so it must be used only if the application is not supposed to be updated in the future to other UI frameworks. Even in such case, the Java Validation API custom validation can still be used in addition, and he helps to prevent model level validation failures.