Support multi-field validation
by enabling class-level bean validation on CDI based backing
beans. This feature causes a temporary copy of the bean
referenced by the value
attribute, for the sole
purpose of populating the bean with field values already
validated by <f:validateBean />
and then
performing class-level validation on the copy. Regardless
of the result of the class-level validation, the copy is
discarded. This feature must explicitly be enabled by
setting the application parameter specified in the javadoc
for the symbolic constant
jakarta.faces.validator.BeanValidator.ENABLE_VALIDATE_WHOLE_BEAN_PARAM_NAME
.
If this parameter is not set, or is set to false, this tag
must be a no-op. A non-normative example follows the
specification of the feature.
At a high level, the feature provides for
a UIInput
subclass that maintains its own
special private Validator
that uses information
from one or more <f:validateBean />
s to
perform class-level bean validation. For discussion, this
special Validator
is called
the wholeBeanValidator.
This tag must be backed by a UIInput
component with the following specializations.
Override getSubmittedValue()
to return a
non-null non empty String. This
allows UIInput.validate()
to
call wholeBeanValidator.validate().
Override setConverter()
to be a no-op.
It does not make sense to allow a converter to be
installed.
Override addValidator()
to be a no-op
unless the argument is an instance
of wholeBeanValidator. It does not make sense to
allow additional validators to be installed.
Override validate()
to take the
following actions.
If the feature is not enabled, return immediately.
If the wholeBeanValidator has not yet
been installed, instantiate and pass it to
this.addValidator()
.
Call super.validate()
.
The wholeBeanValidator must have
a validate()
method that performs the following
actions. Due to the above specification, this method will
only ever be passed the special UIInput
component.
Resolve the value
of the component to
its Object
. Assume that
this value
is the bean whose properties are
intended to be populated by components whose values are each
validated by <f:validateBean />
tags.
For discussion, this bean is called the candidate
bean and the properties and their respective values are
called the candidate values. If the candidate
bean cannot be referenced, return immediately
from validate()
. Use the information recorded
by each of those <f:validateBean />
tags
to ensure that none of the candidate values are
invalid. If any of them are invalid, return immediately
from validate()
. This ensures class-level
validation is only performed on an instance whose fields are
all individually valid.
Otherwise it can be assumed that all field-level validations for this class-level validation have passed.
Class-level bean validation must operate on a sufficiently populated bean instance. This differs from Faces field-level validation, which prevents beans from being populated with invalid values. To accomodate this difference, the candidate bean must be copied, populated with the already-validated candidate values, and then subjected to class-level validation. The copying must proceed in the following order.
Invoke the newInstance()
method on the
bean's Class
. If this throws
any Exception
, swallow it and
continue.
If the bean implements Serializable
, use
that to copy the bean instance.
Otherwise, if the bean
implements Cloneable
, clone the bean
instance.
Otherwise, if the bean has a copy constructor, use that to copy the bean instance.
If none of these techniques yields a copy,
throw FacesException
.
Populate the copied bean with the candidate values.
Obtain a reference to
a jakarta.validation.Validator
instance using the
same steps described in the javadoc
for jakarta.faces.validator.BeanValidator.validate()
.
Let the instance be called beanValidator for
discussion.
Obtain the value of the validationGroups
attribute using the same steps described in the javadoc
for jakarta.faces.validator.BeanValidator.validate()
.
If this value is not present or not valid,
throw FacesException
.
Call the validate
method on
beanValidator, passing the populated copied bean
and the validation groups as arguments. The copied bean can
be discarded at this point.
If the
returned Set<ConstraintViolation>
is
non-empty, for each element in the Set
, create
a FacesMessage
where the summary and detail are
the return from
calling ConstraintViolation.getMessage()
.
Capture all such FacesMessage
instances into
a Collection
and pass them
to ValidatorException
. Using information
recorded by the <f:validateBean />
tag(s), call setValid(false)
on all of the
components whose values contributed to this class-level
validation. This is essential to prevent the invalid value
from being set into the model during the update model values
phase. Finally, throw the exception.
This tag must be placed in the component tree after all of the fields that are to be included in the multi-field validation. If this precondition is not met, the results of applying this tag are unspecified.
This tag must be used in concert
with <f:validateBean />
and Bean
Validation. Here is a brief example of the common case of
ensuring two password fields are individually valid and also
both the same. The feature requires the use of
the validationGroups
attribute on all of
the <f:validateBean />
tags and
the <f:validateWholeBean />
tag.
First, the ConstraintValidator
implementation.
public class PasswordValidator implements ConstraintValidator<Password, PasswordHolder> {
@Override
public void initialize(Password constraintAnnotation) { }
@Override
public boolean isValid(PasswordHolder value, ConstraintValidatorContext context) {
boolean result;
result = value.getPassword1().equals(value.getPassword2());
return result;
}
}
Note that a PasswordHolder
instance is
passed to the isValid()
method. This method
will only be called if the individual properties of
the PasswordHolder
are valid. This fact allows
the isValid()
method to inspect the properties
and perform effective class-level validtion.
Next, the Constraint
.
@Constraint(validatedBy=PasswordValidator.class)
@Target(TYPE)
@Retention(RUNTIME)
@interface Password {
String message() default "Password fields must match";
Class[] groups() default {};
Class[] payload() default {};
}
Now the backing bean constrained by
this Constraint
. Note the use
of groups
. Note the fact that the bean
implements Cloneable
.
@Named
@RequestScoped
@Password(groups = PasswordValidationGroup.class)
public class BackingBean implements PasswordHolder, Cloneable {
private String password1;
private String password2;
public BackingBean() {
password1="";
password2="";
}
@Override
protected Object clone() throws CloneNotSupportedException {
BackingBean other = (BackingBean) super.clone();
other.setPassword1(this.getPassword1());
other.setPassword2(this.getPassword2());
return other;
}
@NotNull(groups=PasswordValidationGroup.class)
@Size(max=16, min=8, message="Password must be between 8 and 16 characters long",
groups = PasswordValidationGroup.class)
@Override
public String getPassword1() {
return password1;
}
public void setPassword1(String password1) {
this.password1 = password1;
}
@NotNull(groups=PasswordValidationGroup.class)
@Size(max=16, min=8, message="Password must be between 8 and 16 characters long",
groups = PasswordValidationGroup.class)
@Override
public String getPassword2() {
return password2;
}
public void setPassword2(String password2) {
this.password2 = password2;
}
}
Finally, the Facelets view.
<h:panelGrid columns="2">
<h:outputText value="Password" />
<h:inputSecret id="password1" value='#{backingBean.password1}'>
<f:validateBean validationGroups="PasswordValidationGroup" />
</h:inputSecret>
<h:outputText value="Password again" />
<h:inputSecret id="password2" value='#{backingBean.password2}'>
<f:validateBean validationGroups="PasswordValidationGroup" />
</h:inputSecret>
</h:panelGrid>
<f:validateWholeBean value='#{backingBean}'
validationGroups="PasswordValidationGroup" />
Info | Value |
---|---|
Component Type | com.sun.faces.ext.validateWholeBean |
Handler Class | None |
Renderer Type | None |
Description | None |
Name | Required | Type | Description |
---|---|---|---|
disabled | false | jakarta.el.ValueExpression
(must evaluate to java.lang.Boolean )
|
A boolean value enabling or disabling this validation component. |
id | false | jakarta.el.ValueExpression
(must evaluate to java.lang.String )
|
Component identifier of the UIInput component to be created. |
validationGroups | true | jakarta.el.ValueExpression
(must evaluate to java.lang.String )
|
A comma-separated list of validation groups. A validation group is a fully-qualified class name. |
value | true | jakarta.el.ValueExpression
(must evaluate to java.lang.Object )
|
A ValueExpression referencing the bean to be validated. |