Friday, January 2, 2015

Extending PrimeFaces CSV with Bean Validation

Some of you already know that me and my co-author Mert Çalışkan are working on the 2. edition of the PrimeFaces Cookbook. The Packt Publishing allowed me to publish a small excerpt from one recipe of the new chapter "Client Side Validation". It would help in letting the readers know about the book's content. In this blog post, I would like to discuss the extending PrimeFaces Client Side Validation (CSV) with Bean Validation.

Bean Validation is a validation model available as part of Java EE 6 platform, which allows the validation by constraints in the form of annotations placed on a field, method, or class. JSF 2.2 supports the validation placed on fields (properties and their getters / setters) in managed beans, as well as Spring or CDI beans. The validation on class level is not supported yet, as long you do not use utilities such as OmniFaces.

PrimeFaces’ CSV has a built-in integration with Bean Validation. Constraints defined with annotations can be validated on the client-side by the CSV framework. Though 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 are able to create custom constraints for specific validation requirements. Client Side Validation API in PrimeFaces works seamlessly with custom constraints.

In this recipe, we will develop a special custom constraint and validators for validating a card verification code (CVC). CVC is used as security feature with bank card number. It is a number with a length between three and four digits. For instance, MasterCard or Visa require three digits and American Express requires four digits. Therefore, the CVC validation will depend on the selected bank card. User can select a bank card by a p:selectOneMenu, type a CVC into a p:inputText and submit the inputs after that.

How to do it...

We will start with a custom annotation used for CVC field.
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 the PrimeFaces CSV Framework, which helps to resolve metadata. The developed annotation defines the message key invalid.cvc.message and has the custom property forCardMenu. 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.
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 the CvcClientConstraint is the preparing of metadata.
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 us 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 namespace PrimeFaces.validator with the name ValidCVC. This name is an unique ID returned by the method getValidatorId() (see the class CvcClientConstraint). The function to be implemented is called validate(). It has two parameters: the element itself and the current input value to be validated.
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);
        }
    }
};
Second, we have to create a JavaScript file for localized messages, e.g. lang_en.js.
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 property cvc is annotated with our custom annotation @ValidCVC. The value of the attribute forCardMenu points to the style class of the p:selectOneMenu that lists available bank cards.
@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 validates the fields and executes the method save() on postback.
<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}"/>
Note: As you can see, 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 context parameter primefaces.TRANSFORM_METADATA to true.

In the last step, all required JavaScript files have to be included on the page.
<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 user.


How it works...

The message key invalid.cvc.message and the text should be put in resource bundles named ValidationMessages, e.g. 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 the server-side validation.

The method getMetadata() in the class CvcClientConstraint provides a map with name, value pairs. The metadata are exposed in the rendered HTML. The values can be accessed on the client-side via element.data(name), where element is an jQuery object for the underlying native HTML element. The CVC field with the metadata is rendered as
<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 the PrimeFaces’ built-in client-side converter for the data type java.lang.Integer. We only have to check if the value is positive and has a valid length. A valid length depends on the selected card in the menu p:selectOneMenu that can be accessed by the PrimeFaces JavaScript API as PrimeFaces.expressions.SearchExpressionFacade.resolveComponentsAsSelector(selector), where selector is any PrimeFaces selector, in our case @(.card). If the validation fails, we throw an exception by invoking throw PrimeFaces.util.ValidationContext.getMessage(text, parameter).

The client-side validation is triggered by setting validateClient=”true” on the p:commandButton.

2 comments:

  1. Well explained content and very informative and rich content which is very useful in my website development. Rich content have always given priority.

    ReplyDelete

Note: Only a member of this blog may post a comment.