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.
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.