Although the Bean Validation API defines a whole set of standard constraint annotations, one can easily think of situations in which these standard annotations will not suffice. For these cases, you will be able to create custom constraints for specific validation requirements. The Client Side Validation API in PrimeFaces works seamlessly with custom constraints.
In this recipe, we will develop a special custom constraint and validators to validate a Card Verification Code (CVC). CVC is used as a security feature with a bank card number. It is a number with a length between three and four digits. For instance, MasterCard and Visa require three digits, and American Express requires four digits. Therefore, the CVC validation will depend on the selected bank card. The user can select a bank card using p:selectOneMenu
, type a CVC into p:inputText
, and submit the input after that.
We will start with a custom annotation used for the CVC field. The following code shows this:
import org.primefaces.validate.bean.ClientConstraint; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; @Constraint(validatedBy = CvcConstraintValidator.class) @ClientConstraint(resolvedBy = CvcClientConstraint.class) @Target({FIELD, METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface ValidCVC { String message() default "{invalid.cvc.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; // identifier of the select menu with cards String forCardMenu() default ""; }
@Constraint
is a regular annotation from the Bean Validation API, and @ClientConstraint
is one from PrimeFaces CSV Framework, which helps to resolve metadata. The developed annotation defines the invalid.cvc.message
message key and has the forCardMenu
custom property. The value of this property is any search expression in terms of PrimeFaces Selectors (PFS) to reference the select menu with bank cards. This is necessary because the valid CVC value depends on the selected card.
The goal of CvcConstraintValidator
is the validation of the input length. This is shown in the following code:
public class CvcConstraintValidator implements ConstraintValidator<ValidCVC, Integer> { @Override public void initialize(ValidCVC validCVC) { } @Override public boolean isValid(Integer cvc, ConstraintValidatorContext context) { if (cvc == null || cvc < 0) { return false; } int length = (int) (Math.log10(cvc) + 1); return (length >= 3 && length <= 4); } }
The goal of CvcClientConstraint
is the preparation of metadata. This is shown in the following code:
public class CvcClientConstraint implements ClientValidationConstraint { private static final String CARDMENU_METADATA = "data-forcardmenu"; @Override public Map<String, Object> getMetadata( ConstraintDescriptor constraintDescriptor) { Map<String, Object> metadata = new HashMap<String, Object>(); Map attrs = constraintDescriptor.getAttributes(); String forCardMenu = (String) attrs.get("forCardMenu"); if (StringUtils.isNotBlank(forCardMenu)) { metadata.put(CARDMENU_METADATA, forCardMenu); } return metadata; } @Override public String getValidatorId() { return ValidCVC.class.getSimpleName(); } }
Let's go to the client-side implementation. First, we have to create a JavaScript file, say validators.js
, and register there our own validator in the PrimeFaces.validator
namespace with the name ValidCVC
. This name is a unique ID returned by the getValidatorId()
method (see the CvcClientConstraint
class). The function to be implemented is called validate()
. It has two parameters—the element itself and the current input value to be validated. This is shown in the following code:
PrimeFaces.validator['ValidCVC'] = { MESSAGE_ID: 'invalid.cvc', validate: function (element, value) { // find out selected menu value var forCardMenu = element.data('forcardmenu'), var selOption = forCardMenu ? PrimeFaces.expressions.SearchExpressionFacade. resolveComponentsAsSelector(forCardMenu). find("select").val() : null; var valid = false; if (selOption && selOption === 'MCD') { // MasterCard valid = value > 0 && value.toString().length == 3; } else if (selOption && selOption === 'AMEX') { // American Express valid = value > 0 && value.toString().length == 4; } if (!valid) { throw PrimeFaces.util.ValidationContext. getMessage(this.MESSAGE_ID); } } };
Secondly, we have to create a JavaScript file for localized messages, for example, lang_en.js
. The following code shows this:
PrimeFaces.locales['en'] = { messages : PrimeFaces.locales['en_US'].messages }; $.extend(PrimeFaces.locales['en'].messages, { ... 'invalid.cvc': 'Card Validation Code is invalid' });
The bean has two required properties annotated with @NotNull
. In addition, the cvc
property is annotated with our custom annotation @ValidCVC
. The value of the forCardMenu
attribute points to the style class of p:selectOneMenu
, which lists the available bank cards. This is shown in the following code:
@Named @ViewScoped public class ExtendCsvBean implements Serializable { @NotNull private String card; @NotNull @ValidCVC(forCardMenu = "@(.card)") private Integer cvc; public void save() { RequestContext.getCurrentInstance().execute( "alert('Saved!')"); } // getters / setters ... }
In the XHTML fragment, we have a select menu with two bank cards and an input field for CVC. The p:commandButton
component validates the fields and executes the save()
method on postback. This is shown in the following code:
<h:panelGrid id="pgrid" columns="3" cellpadding="3" style="margin-bottom:10px;"> <p:outputLabel for="card" value="Card"/> <p:selectOneMenu id="card" styleClass="card" value="#{extendCsvBean.card}"> <f:selectItem itemLabel="Please select a card" itemValue="#{null}"/> <f:selectItem itemLabel="MasterCard" itemValue="MCD"/> <f:selectItem itemLabel="American Express" itemValue="AMEX"/> </p:selectOneMenu> <p:message for="card"/> <p:outputLabel for="cvc" value="CVC"/> <p:inputText id="cvc" value="#{extendCsvBean.cvc}"/> <p:message for="cvc"/> </h:panelGrid> <p:commandButton validateClient="true" value="Save" process="@this pgrid" update="pgrid" action="#{extendCsvBean.save}"/>
As illustrated, neither p:selectOneMenu
nor p:inputText
specifies the required
attribute. We can achieve the transformation of the @NotNull
annotation to the required
attribute with the value true
if we set the primefaces.TRANSFORM_METADATA
context parameter to true
. More details on this feature are available in the Bean Validation and transformation recipe.
In the last step, all required JavaScript files have to be included on the page. The following code shows this:
<h:outputScript library="js" name="chapter10/lang_en.js"/> <h:outputScript library="js" name="chapter10/validators.js"/>
The next two pictures show what happens when validations fails:
If everything is ok, an alert box with the text Saved! is displayed to the user:
The invalid.cvc.message
message key and the text should be put in resource bundles named ValidationMessages
, for example, ValidationMessages_en.properties
. ValidationMessages
is the standard name specified in the Bean Validation specification. The property files should be located in the application classpath and contain the following entry: invalid.cvc.message=Card Validation Code is invalid
. This configuration is important for server-side validation.
The getMetadata()
method in the CvcClientConstraint
class provides a map with name-value pairs. The metadata is exposed in the rendered HTML. The values can be accessed on the client side via element.data(name)
, where element
is a jQuery object for the underlying native HTML element. The CVC field with the metadata is rendered as shown here:
<input type="text" data-forcardmenu="@(.card)" data-p-con="javax.faces.Integer" data-p-required="true"...>
The most interesting part is the implementation of the client-side validator. The value to be validated is already numeric because first it gets converted by PrimeFaces' built-in client-side converter for the java.lang.Integer
data type. We only have to check whether the value is positive and has a valid length. A valid length depends on the selected card in the p:selectOneMenu
menu that can be accessed by the PrimeFaces JavaScript API as PrimeFaces.expressions.SearchExpressionFacade.resolveComponentsAsSelector(selector)
, where selector
is any PrimeFaces selector, which, in our case, is @(.card)
. If validation fails, we throw an exception by invoking throw PrimeFaces.util.ValidationContext.getMessage(text, parameter)
.
Client-side validation is triggered by setting validateClient="true"
on p:commandButton
.
You can also use third-party constraints from other libraries with CSV Framework. Use PrimeFaces' BeanValidationMetadataMapper
to register third-party annotations with ClientValidationConstraint
. Removing registered annotations is possible as well. The following code shows this:
BeanValidationMetadataMapper.registerConstraintMapping( Class<? extends Annotation> constraint, ClientValidationConstraint clientValidationConstraint); BeanValidationMetadataMapper.removeConstraintMapping( Class<? extends Annotation> constraint);
Extending CSV with JSF validators is the topic of the previous recipe, Extending CSV with JSF.
This recipe is available in the demo web application on GitHub (https://github.com/ova2/primefaces-cookbook/tree/second-edition). Clone the project if you have not done it yet, explore the project structure, and build and deploy the WAR file on application servers compatible with Servlet 3.x, such as JBoss WildFly and Apache TomEE.
The showcase for the recipe is available at http://localhost:8080/pf-cookbook/views/chapter10/extendBvCsv.jsf
.