-
Available since OmniFaces 1.0
The <o:validateBean>
allows the developer to control bean validation on a per-UICommand
or UIInput
component basis, as well as validating a given bean at the class level.
The standard <f:validateBean>
only allows validation control on a per-form or a per-request basis (by using multiple tags and conditional EL expressions in its attributes) which may end up in boilerplate code.
The standard <f:validateBean>
also, despite its name, does not actually have any facilities to validate a bean at all.
Usage
Some examples
Control bean validation per component
<h:commandButton value="submit" action="#{bean.submit}">
<o:validateBean validationGroups="jakarta.validation.groups.Default,com.example.MyGroup" />
</h:commandButton>
<h:selectOneMenu value="#{bean.selectedItem}">
<f:selectItems value="#{bean.availableItems}" />
<o:validateBean disabled="true" />
<f:ajax execute="@form" listener="#{bean.itemChanged}" render="@form" />
</h:selectOneMenu>
Validate a bean at the class level
<h:inputText value="#{bean.product.item}" />
<h:inputText value="#{bean.product.order}" />
<o:validateBean value="#{bean.product}" />
Since OmniFaces 3.8, nested properties are also supported with @jakarta.validation.Valid
cascade
<h:inputText value="#{bean.product.item}" />
<h:inputText value="#{bean.product.order}" />
<o:validateBean value="#{bean}" />
Whereby the product
property looks like this:
@Valid
private Product product;
When using <o:validateBean method="validateCopy" />
(which is the default), then only beans, lists, maps and arrays are considered as nested properties and the copied bean will be autopopulated with defaults. If this fails, then consider creating a custom copier as instructed in next section.
Class level validation details
In order to validate a bean at the class level, all values from input components should first be actually set on that bean and only thereafter should the bean be validated. This however does not play well with the Faces approach where a model is only updated when validation passes. But for class level validation we seemingly can not validate until the model is updated. To break this tie, a copy of the model bean is made first, and then values are stored in this copy and validated there. If validation passes, the original bean is updated.
A bean is copied using the following strategies (in the order indicated):
- Cloning - Bean must implement the
Cloneable
interface and support cloning according to the rules of that interface. SeeCloneCopier
- Serialization - Bean must implement the
Serializable
interface and support serialization according to the rules of that interface. SeeSerializationCopier
- Copy constructor - Bean must 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. See
CopyCtorCopier
- New instance - Bean should 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. See
NewInstanceCopier
If the above order is not ideal, or if an custom copy strategy is needed (e.g. when it's only needed to copy a few fields for the validation) a strategy can be supplied explicitly via the copier
attribute. The value of this attribute can be any of the build-in copier implementations given above, or can be a custom implementation of the Copier
interface.
If the copying strategy is not possible due to technical limitations, then you could set method
attribute to "validateActual"
.
<o:validateBean value="#{bean.product}" method="validateActual" />
This will update the model values and run the validation after update model values phase instead of the validations phase. The disadvantage is that the invalid values remain in the model and that the action method is anyway invoked. You would need an additional check for FacesContext.isValidationFailed()
in the action method to see if it has failed or not.
Faces messages
By default, the faces message is added with client ID of the parent UIForm
.
<h:form id="formId">
...
<h:message for="formId" />
<o:validateBean ... />
</h:form>
The faces message can also be shown for all invalidated components using showMessageFor="@all"
.
<h:form>
<h:inputText id="foo" />
<h:message for="foo" />
<h:inputText id="bar" />
<h:message for="bar" />
...
<o:validateBean ... showMessageFor="@all" />
</h:form>
The faces message can also be shown as global message using showMessageFor="@global"
.
<h:form>
...
<o:validateBean ... showMessageFor="@global" />
</h:form>
<h:messages globalOnly="true" />
The faces message can also be shown for specific components referenced by a space separated collection of their client IDs in showMessageFor
attribute.
<h:form>
<h:inputText id="foo" />
<h:message for="foo" />
<h:inputText id="bar" />
<h:message for="bar" />
...
<o:validateBean ... showMessageFor="foo bar" />
</h:form>
The faces message can also be shown for components which match Property Path of the ConstraintViolation
using showMessageFor="@violating"
, and when no matching component can be found, the message will fallback to being added with client ID of the parent UIForm
.
<h:form id="formId">
...
<!-- Unmatched messages shown here: -->
<h:message for="formId" />
...
<h:inputText id="foo" value="#{bean.product.item}" />
<!-- Messages where ConstraintViolation PropertyPath is "item" are shown here: -->
<h:message for="foo" />
...
<o:validateBean ... value="#{bean.product}" showMessageFor="@violating" />
</h:form>
The showMessageFor
attribute is new since OmniFaces 2.6 and it defaults to @form
. The showMessageFor
attribute does by design not have any effect when validateMethod="actual"
is used.
Message format
The faces message uses a predefined message format, which corresponds to the value of BeanValidator.MESSAGE_ID
in the message bundle. The default message format of {1}: {0}
prepends the labels of all the validated fields. This is useful in the case of validating a single bean property, but sometimes confusing in the case of validating a bean with many properties.
In a form containing properties like First Name, Last Name, Address, Zip Code, and Phone Number where at the bean level, at least one of the name fields must be non-null, overriding the message format can help make a more clear error message.
This can be done by overriding the BeanValidator.MESSAGE_ID
line in the message bundle:
jakarta.faces.validator.BeanValidator.MESSAGE = Errors encountered: {0}
However, this change affects all bean validation messages site-wide. In case you'd like to fine-tune the bean validation message on a per-<o:validateBean>
-basis, then you can since OmniFaces 3.12 use the messageFormat
attribute. Any {0}
placeholder will be substituted with the error message and any {1}
placeholder will be substituted with the labels of all validated fields.
<!-- Displays: "First Name, Last Name, Address, Zip Code, Phone Number: First Name and Last Name cannot both be null" -->
<o:validateBean />
<!-- Displays: "Errors encountered: First Name and Last Name cannot both be null" -->
<o:validateBean messageFormat="Errors encountered: {0}" />"
Leave either the field blank, or enter less than 6 characters
Validating at the class level that number 1 is smaller than number 2 - default (validate copy)
This uses the default validation method, which copies the object, sets the values retrieved from the input components on it, and performs class level validation on the copy. In order to make the copy, cloning, serialization, a copy constructor and a new instance will be attempted (in that order). Since our model object has a copy constructor, but is not cloneable or serializable, the copy constructor will be the one that is used.
Validating at the class level that number 1 is smaller than number 2 - validate actual bean
This uses an alternative validation method, which doesn't copy the bean but lets Faces do its normal job all the way up till the phase UPDATE MODEL VALUES, and only thereafter will it validate the actual object. This has the advantage that no copying and value collecting are needed, but atypical for Faces is that the model is updated even though there are validation errors. The bean developer is also required to manually check in the bean whether there are validation errors.
Validating at the class level that number 1 is smaller than number 2 - using custom copier
This uses the default validation method like the example above, but uses a custom copier. Such copier would be needed when the bean originates from another module, which the front end developer can't modify to be either cloneable, serializable or having a copy constructor. A custom copier also allows customization of the copy process, e.g. when a bean contains many fields but it's clear that only a few are needed for the validation.
- Demo
- ValidateBean
- MyValidationGroup
- ValidateClassLevelBean
- Product
- ValidProduct
- ProductGroup
- ProductValidator
- ProductCopier
<h3>Leave either the field blank, or enter less than 6 characters</h3>
<h:form>
<h:panelGrid columns="3">
<o:outputLabel for="value" value="Value" />
<h:inputText id="value" value="#{validateBean.value}" />
<h:message for="value"/>
<h:panelGroup />
<h:panelGroup>
<f:ajax execute="@form" render="@form">
<h:commandButton value="submit (only default validation)" />
<br/>
<h:commandButton value="submit (only MyValidationGroup validation)">
<o:validateBean validationGroups="org.omnifaces.showcase.validators.MyValidationGroup" />
</h:commandButton>
<br/>
<h:commandButton value="submit (both default and MyValidationGroup validation)">
<o:validateBean validationGroups="jakarta.validation.groups.Default,org.omnifaces.showcase.validators.MyValidationGroup" />
</h:commandButton>
<br/>
<h:commandButton value="submit (no validation)">
<o:validateBean disabled="true" />
</h:commandButton>
</f:ajax>
</h:panelGroup>
<h:outputText value="OK!" rendered="#{facesContext.postback and not facesContext.validationFailed}" />
</h:panelGrid>
</h:form>
<br/>
<h3>Validating at the class level that number 1 is smaller than number 2 - default (validate copy)</h3>
<p>
<small>
This uses the default validation method, which copies the object, sets the values retrieved from the input components
on it, and performs class level validation on the copy. In order to make the copy, cloning, serialization, a copy constructor
and a new instance will be attempted (in that order). Since our model object has a copy constructor, but is not cloneable or
serializable, the copy constructor will be the one that is used.
</small>
</p>
<h:form id="classLevelForm">
<h:messages styleClass="messages" infoClass="info" warnClass="warn" errorClass="error" />
<p>
Bean values in model:<br/>
Number 1: #{validateClassLevelBean.product.number1}<br/>
Number 2: #{validateClassLevelBean.product.number2}<br/>
</p>
<h:panelGrid columns="2">
<o:outputLabel for="number1" value="Number 1" />
<h:inputText id="number1" value="#{validateClassLevelBean.product.number1}" />
<o:outputLabel for="number2" value="Number 2" />
<h:inputText id="number2" value="#{validateClassLevelBean.product.number2}" />
</h:panelGrid>
<h:commandButton value="submit" action="#{validateClassLevelBean.action}">
<f:ajax execute="@form" render="@form"/>
</h:commandButton>
<o:validateBean value="#{validateClassLevelBean.product}" validationGroups="org.omnifaces.showcase.validators.ProductGroup" />
</h:form>
<br/>
<h3>Validating at the class level that number 1 is smaller than number 2 - validate actual bean</h3>
<p>
<small>
This uses an alternative validation method, which doesn't copy the bean but lets Faces do its normal job all the way up till
the phase UPDATE MODEL VALUES, and only thereafter will it validate the actual object. This has the advantage that no copying
and value collecting are needed, but atypical for Faces is that the model is updated even though there are validation errors.
The bean developer is also required to manually check in the bean whether there are validation errors.
</small>
</p>
<h:form id="classLevelFormActualBean">
<h:messages styleClass="messages" infoClass="info" warnClass="warn" errorClass="error" />
<p>
Bean values in model:<br/>
Number 1: #{validateClassLevelBean.product.number1}<br/>
Number 2: #{validateClassLevelBean.product.number2}<br/>
</p>
<h:panelGrid columns="2">
<o:outputLabel for="number1" value="Number 1" />
<h:inputText id="number1" value="#{validateClassLevelBean.product.number1}" />
<o:outputLabel for="number2" value="Number 2" />
<h:inputText id="number2" value="#{validateClassLevelBean.product.number2}" />
</h:panelGrid>
<h:commandButton value="submit" action="#{validateClassLevelBean.action}">
<f:ajax execute="@form" render="@form"/>
</h:commandButton>
<o:validateBean value="#{validateClassLevelBean.product}" validationGroups="org.omnifaces.showcase.validators.ProductGroup" method="validateActual" />
</h:form>
<br/>
<h3>Validating at the class level that number 1 is smaller than number 2 - using custom copier</h3>
<p>
<small>
This uses the default validation method like the example above, but uses a custom copier. Such copier would be needed when the bean
originates from another module, which the front end developer can't modify to be either cloneable, serializable or having a copy
constructor. A custom copier also allows customization of the copy process, e.g. when a bean contains many fields but it's clear
that only a few are needed for the validation.
</small>
</p>
<h:form id="classLevelFormCustomCopier">
<h:messages styleClass="messages" infoClass="info" warnClass="warn" errorClass="error" />
<p>
Bean values in model:<br/>
Number 1: #{validateClassLevelBean.product.number1}<br/>
Number 2: #{validateClassLevelBean.product.number2}<br/>
</p>
<h:panelGrid columns="2">
<o:outputLabel for="number1" value="Number 1" />
<h:inputText id="number1" value="#{validateClassLevelBean.product.number1}" />
<o:outputLabel for="number2" value="Number 2" />
<h:inputText id="number2" value="#{validateClassLevelBean.product.number2}" />
</h:panelGrid>
<h:commandButton value="submit" action="#{validateClassLevelBean.action}">
<f:ajax execute="@form" render="@form"/>
</h:commandButton>
<o:validateBean value="#{validateClassLevelBean.product}" validationGroups="org.omnifaces.showcase.validators.ProductGroup" copier="org.omnifaces.showcase.model.ProductCopier" />
</h:form>
package org.omnifaces.showcase.validators;
import java.io.Serializable;
import jakarta.inject.Named;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import org.omnifaces.cdi.ViewScoped;
@Named
@ViewScoped
public class ValidateBean implements Serializable {
private static final long serialVersionUID = 1L;
@Pattern(regexp="^(?=\\s*\\S).*$", message = "Please enter value") // not empty
@Size(min = 6, groups = MyValidationGroup.class, message = "Please enter at least 6 characters")
private String value;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
package org.omnifaces.showcase.validators;
public interface MyValidationGroup {
//
}
package org.omnifaces.showcase.validators;
import static org.omnifaces.util.Faces.isValidationFailed;
import static org.omnifaces.util.Messages.addGlobalInfo;
import static org.omnifaces.util.Messages.addGlobalWarn;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Named;
import org.omnifaces.showcase.model.Product;
@Named
@RequestScoped
public class ValidateClassLevelBean {
private Product product;
@PostConstruct
public void init() {
product = new Product();
}
public void action() {
if (isValidationFailed()) {
addGlobalWarn("Validation failed, but action method invoked.");
}
else {
addGlobalInfo("OK! Validation has not failed.");
}
}
public Product getProduct() {
return product;
}
}
package org.omnifaces.showcase.model;
import org.omnifaces.showcase.validators.ProductGroup;
import org.omnifaces.showcase.validators.ValidProduct;
@ValidProduct(groups = ProductGroup.class)
public class Product {
private String name;
private int number1;
private int number2;
public Product() {
//
}
public Product(String name) {
this.name = name;
}
public Product(Product other) {
number1 = other.number1;
number2 = other.number2;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getNumber1() {
return number1;
}
public void setNumber1(int number1) {
this.number1 = number1;
}
public int getNumber2() {
return number2;
}
public void setNumber2(int number2) {
this.number2 = number2;
}
}
package org.omnifaces.showcase.validators;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
@Constraint(validatedBy = { ProductValidator.class })
@Documented
@Target({ TYPE, METHOD, FIELD })
@Retention(RUNTIME)
public @interface ValidProduct {
String message() default "Invalid product";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
package org.omnifaces.showcase.validators;
public interface ProductGroup {
//
}
package org.omnifaces.showcase.validators;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.omnifaces.showcase.model.Product;
public class ProductValidator implements ConstraintValidator<ValidProduct, Product> {
@Override
public void initialize(ValidProduct constraintAnnotation) {
//
}
@Override
public boolean isValid(Product value, ConstraintValidatorContext context) {
return value.getNumber1() < value.getNumber2();
}
}
package org.omnifaces.showcase.model;
import org.omnifaces.util.copier.Copier;
public class ProductCopier implements Copier {
@Override
public Product copy(Object object) {
Product original = (Product) object;
// Just for the example, don't use the copy ctor, but set fields manually.
Product copy = new Product();
copy.setNumber1(original.getNumber1());
copy.setNumber2(original.getNumber2());
return copy;
}
}