Extending CSV with Bean Validation

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.

How to do it…

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}"/>

Note

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:

How to do it…
How to do it…

If everything is ok, an alert box with the text Saved! is displayed to the user:

How to do it…

How it works…

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.

There's more…

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);

See also

Extending CSV with JSF validators is the topic of the previous recipe, Extending CSV with JSF.

PrimeFaces Cookbook Showcase application

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.223.170.223