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 predefined 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 {}; }
Compared to a property constraint annotation definition, there is
only one difference, the parameter of the @Target
annotation. While in the case of a property and method level custom
constraint annotation the values are ElementType.FIELD
and
ElementType.METHOD
, for the case of a class it must be
ElementType.TYPE
.
The corresponding implementation class, i.e.,
AuthorValidatorImpl
, has the same structure as in the case
of a property constraint annotation , but now, we can access all
properties of an entity bean, so we can compare two or more properties
when required. In our case, we have to compare the values of
dateOfBirth
and dateOfDeath
in the
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 class-level JPA validators in facelets requires a bit of
tweaking because they are not directly supported by JSF. For the
specific form field to be validated, we have to specify a controller
method in charge of the validation, as the value of the
@validator
attribute:
<ui:composition template="/WEB-INF/templates/page.xhtml">
<ui:define name="main">
<h:form id="createAuthorForm">
<div>
<h:outputLabel for="dateOfDeath" value="Date of death: ">
<h:inputText id="dateOfDeath" p:type="date"
value="#{author.dateOfDeath}"
validator="#{authorCtrl.checkDateOfDeath}">
<f:convertDateTime pattern="yyyy-MM-dd" />
</h:inputText>
</h:outputLabel>
<h:message for="dateOfDeath" errorClass="error" />
</div>
<div>
<h:commandButton value="Create"
action="#{authorCtrl.create( author.personId, author.name,
author.dateOfBirth, author.dateOfDeath)}"/>
</div>
</h:form>
</ui:define>
</ui:composition>
The controller method checkDateOfDeath
has to invoke the Bean Validation
API validator, catch the validation exceptions and translate them to exceptions of type
javax.faces.validator.ValidatorException
, which are then managed by JSF and
displayed in the view. Its code is as
follows:
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 with the help of the
context.getViewRoot().findComponent
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 Bean 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 selection can be made by looking for specific data in the
exception message.
As a result, the custom Bean Validation class validator is not used, and the facelet 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 approach to object-level validation is using JSF custom validators. They have the advantage that they are directly supported in facelets, but the downside of this approach is that it violates the onion architecture principle by defining business rules in the UI instead of defining them in the model..
For our example, the validator for the Author class that is responsible for validating
dateOfDeath
by comparing it with 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 facelet, 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" />