Статьи

Расширение PrimeFaces CSV с проверкой бина

Некоторые из вас уже знают, что я и мой соавтор Мерт Чалышкан работаем над 2. выпуском « Поваренной книги PrimeFaces» . Издание Packt Publishing позволило мне опубликовать небольшую выдержку из одного рецепта новой главы «Проверка на стороне клиента». Это помогло бы информировать читателей о содержании книги. В этой записи блога я хотел бы обсудить расширение клиентской проверки PrimeFaces (CSV) с проверкой bean-компонентов.

Bean Validation — это модель проверки, доступная как часть платформы Java EE 6, которая позволяет выполнять проверку с помощью ограничений в форме аннотаций, размещенных в поле, методе или классе. JSF 2.2 поддерживает валидацию полей (свойств и их методов получения / установки) в управляемых bean-компонентах, а также в bean-компонентах Spring или CDI. Проверка на уровне класса еще не поддерживается, поскольку вы не используете такие утилиты, как OmniFaces .

CSV-версия PrimeFaces имеет встроенную интеграцию с Bean Validation. Ограничения, определенные с помощью аннотаций, могут быть проверены на стороне клиента средой CSV. Хотя API Bean Validation определяет целый набор стандартных аннотаций ограничений, можно легко представить себе ситуации, в которых этих стандартных аннотаций будет недостаточно. В этих случаях вы можете создавать собственные ограничения для конкретных требований проверки. API проверки на стороне клиента в PrimeFaces без проблем работает с пользовательскими ограничениями.

В этом рецепте мы разработаем специальные пользовательские ограничения и валидаторы для проверки кода подтверждения карты ( CVC ). CVC используется как функция безопасности с номером банковской карты. Это число длиной от трех до четырех цифр. Например, MasterCard или Visa требуют трех цифр, а American Express — четырехзначных. Поэтому проверка CVC будет зависеть от выбранной банковской карты. Пользователь может выбрать банковскую карту с помощью p:selectOneMenu , ввести CVC в p:inputText и после этого отправить входные данные.

Как это сделать…

Мы начнем с пользовательской аннотации, используемой для поля CVC.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 — это обычная аннотация из API-интерфейса проверки @ClientConstraint а @ClientConstraint — из PrimeFaces CSV Framework, которая помогает разрешать метаданные. Разработанная аннотация определяет ключ сообщения invalid.cvc.message и имеет настраиваемое свойство для forCardMenu . Значением этого свойства является любое поисковое выражение в терминах PrimeFaces Selectors (PFS) для ссылки на меню выбора с банковскими картами. Это необходимо, поскольку действительное значение CVC зависит от выбранной карты.

Целью CvcConstraintValidator является проверка длины ввода.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
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);
    }
}

Целью CvcClientConstraint является подготовка метаданных.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
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();
    }
}

Давайте перейдем к реализации на стороне клиента. Сначала нам нужно создать файл JavaScript, скажем validators.js , и зарегистрировать там наш собственный валидатор в пространстве имен PrimeFaces.validator с именем ValidCVC . Это имя является уникальным идентификатором, возвращаемым методом getValidatorId() (см. Класс CvcClientConstraint ). Функция для реализации называется validate() . Он имеет два параметра: сам элемент и текущее входное значение для проверки.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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);
        }
    }
};

Во-вторых, мы должны создать файл JavaScript для локализованных сообщений, например, lang_en.js .

01
02
03
04
05
06
07
08
09
10
PrimeFaces.locales['en'] = {
    messages : PrimeFaces.locales['en_US'].messages
};
 
$.extend(PrimeFaces.locales['en'].messages, {
    ...
  
    'invalid.cvc':
        'Card Validation Code is invalid'
});

@NotNull имеет два обязательных свойства, аннотированных @NotNull . Кроме того, свойство cvc нашей пользовательской аннотацией @ValidCVC . Значение атрибута forCardMenu указывает на класс стиля p:selectOneMenu котором перечислены доступные банковские карты.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
@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
    ...
}

Во фрагменте XHTML у нас есть меню выбора с двумя банковскими картами и полем ввода для CVC. p:commandButton проверяет поля и выполняет метод save() при обратной передаче.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
<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}"/>

Примечание. Как видите, ни p:selectOneMenu ни p:inputText определяют обязательный атрибут. Мы можем добиться преобразования аннотации @NotNull в обязательный атрибут со значением true если для параметра контекста primefaces.TRANSFORM_METADATA значение true .

На последнем шаге все необходимые файлы JavaScript должны быть включены на страницу.

1
2
<h:outputScript library="js" name="chapter10/lang_en.js"/>
<h:outputScript library="js" name="chapter10/validators.js"/>

На следующих двух рисунках показано, что происходит при сбое проверки

3427_10_04

3427_10_05

Если все в порядке, появится окно с текстом «Сохранено»! отображается для пользователя.

3427_10_06

Как это работает…

Ключ сообщения invalid.cvc.message и текст должны быть помещены в пакеты ресурсов с именем ValidationMessages , например, ValidationMessages_en.properties . ValidationMessages — это стандартное имя, указанное в спецификации Bean Validation. Файлы свойств должны находиться в пути к классам приложения и содержать следующую запись: invalid.cvc.message=Card Validation Code is invalid . Эта конфигурация важна для проверки на стороне сервера.

Метод getMetadata() в классе CvcClientConstraint предоставляет карту с именем, значением пары. Метаданные представлены в отображаемом HTML. Доступ к значениям на стороне клиента element.data(name) через element.data(name) , где element — это объект jQuery для базового нативного HTML-элемента. Поле CVC с метаданными отображается как

1
2
<input type="text" data-forcardmenu="@(.card)"
       data-p-con="javax.faces.Integer" data-p-required="true"...>

Наиболее интересной частью является реализация валидатора на стороне клиента. Проверяемое значение уже числовое, потому что сначала оно конвертируется встроенным в клиентский конвертер PrimeFaces для типа данных java.lang.Integer . Нам нужно только проверить, является ли значение положительным и имеет допустимую длину. Допустимая длина зависит от выбранной карты в меню p:selectOneMenu которой может обращаться JavaScript API PrimeFaces.expressions.SearchExpressionFacade.resolveComponentsAsSelector(selector) как PrimeFaces.expressions.SearchExpressionFacade.resolveComponentsAsSelector(selector) , где селектор — любой селектор PrimeFaces, в нашем случае @(.card) , Если проверка завершается неудачно, мы генерируем исключение, вызывая throw PrimeFaces.util.ValidationContext.getMessage(text, parameter) .

Проверка на стороне клиента запускается установкой validateClient=”true” в p:commandButton .