Статьи

Валидация бобов в Ваадин 7

Одной из ключевых особенностей Vaadin 7 является его тесная интеграция с JSR 303, также известная как Bean Validation. Эта статья подробно расскажет о том, как добиться такой проверки.

Для начала давайте рассмотрим простой пример использования: пользователь заполняет форму. Когда возникает ошибка, пользователь визуально информируется об ошибке. Отправка формы возможна только после исправления всех ошибок. Это общий сценарий, и мы реализуем его вместе с Ваадином.

Предпосылки

Перед тем, как идти дальше, необходимо выполнить несколько условий:

  1. Во-первых, нам понадобится реализация JSR 303. В этом случае мы будем использовать Hibernate Validator (который также является эталонной реализацией JSR-303)
  2. Следующим шагом является определение базового компонента. В нашем случае мы будем делать это простым (и глупым): Person и связанные с ним атрибуты. Имя и фамилия не должны быть пустыми, в то время как электронная почта, если она существует, должна соответствовать шаблону электронной почты.
    import java.util.Date;
     
    import javax.validation.constraints.NotNull;
    import javax.validation.constraints.Size;
     
    import org.hibernate.validator.constraints.Email;
     
    public class Person {
     
        private long id;
     
        private Gender gender;
     
        @Size(max = 250)
        @NotNull
        private String firstName;
     
        @Size(max = 250)
        @NotNull
        private String lastName;
     
        @Size(max = 250)
        @Email
        private String email;
     
        private Date birthdate;
     
        // Getters and setters
    }

    Обратите внимание, что ограничения NotNull и Size исходят от самого JSR (пакет javax.validation.constraints), в то время как электронная почта поступает от Hibernate Validator (пакет org.hibernate.validator.constraints). Несмотря на связь, учитывая добавленную стоимость, у меня нет никаких сомнений в ее использовании.

Пользователи

Как только боб аннотирован, это только вопрос:

  • Создание нового бина: это довольно очевидно

    BeanItem<Person> item = new BeanItem<Person>(new Person());
  • Vaadin 7, исключив класс Form, нам нужно поместить компонент в его замену FieldGroup.

    FieldGroup group = new FieldGroup(item);
  • Затем нам нужно получить ссылку на каждое отображаемое поле (в отличие от формы, где было значение по умолчанию, и мы могли бы его настроить). FieldGroup имеет методы для создания каждого поля и привязки его к базовому свойству компонента.

    Field<?> gender = group.buildAndBind("Gender", "gender", Select.class);
    Field<?> firstName = group.buildAndBind("First name", "firstName");
    Field<?> lastName = group.buildAndBind("Last name", "lastName");
    Field<?> email = group.buildAndBind("email");
    Field<?> birthdate = group.buildAndBind("Birth date", "birthdate");
  • Каждое поле затем устанавливает свой валидатор (при необходимости)

    firstName.addValidator(new BeanValidator(Person.class, "firstName"));
    lastName.addValidator(new BeanValidator(Person.class, "lastName"));
    email.addValidator(new BeanValidator(Person.class, "email"));
  • Наконец, каждое поле должно быть установлено в нашем макете так, как нужно

    layout.addComponent(gender);
    layout.addComponent(firstName);
    layout.addComponent(lastName);
    layout.addComponent(email);
    layout.addComponent(birthdate);

Последние штрихи

Есть пара завершающих штрихов, необходимых для достижения отточенного пользовательского интерфейса.

Нет предварительной проверки

Если все предыдущие фрагменты кода выполняются в методе init (), пользователи увидят ошибки проверки имени и фамилии при отображении страницы, поскольку все атрибуты компонента изначально пусты.

Таким образом, крайне желательно устанавливать валидаторы только после первого отображения страницы без ошибок. Это может быть сделано, когда поле теряет фокус. Тем не менее, это должно быть сделано, даже если поле никогда не получает фокус. Таким образом, это должно быть сделано в двух местах:

  • В каждом проверенном поле слушателя размытия

    firstName.addListener(new InstallPersonValidatorBlurListener(firstName, "firstName"));
    lastName.addListener(new InstallPersonValidatorBlurListener(lastName, "lastName"));
    email.addListener(new InstallPersonValidatorBlurListener(email, "email"));
     
    public class InstallPersonValidatorBlurListener implements BlurListener {
     
        private Field<?> field;
        private String attribute;
     
        public InstallPersonValidatorBlurListener(Field<?> field, String attribute) {
     
            this.field = field;
            this.attribute = attribute;
        }
     
        @Override
        public void blur(BlurEvent event) {
     
            ValidatorUtils.installSingleValidator(field, attribute);
        }
    }
  • Непосредственно перед фиксацией FieldGroup

    ValidatorUtils.installSingleValidator(firstName, "firstName");
    ValidatorUtils.installSingleValidator(lastName, "lastName");
    ValidatorUtils.installSingleValidator(email, "email");
     
    group.commit();

    Обратите внимание, что поскольку мы не храним ссылку на установленный валидатор, мы должны удалить его перед установкой, иначе это приведет к двойной (или более) одинаковой валидации.

Для ясности, вот код ValidatorUtils:

public class ValidatorUtils {
 
    private ValidatorUtils() {}
     
    static void installSingleValidator(Field<?> field, String attribute) {
         
        Collection<Validator> validators = field.getValidators();
 
        if (validators == null || validators.isEmpty()) {
 
            field.addValidator(new BeanValidator(Person.class, attribute));
        }
    }
}

Правильный виджет для правильного типа данных

This point has nothing to do with validation, but it brings a nice finishing touch to the user interface. Did you notice that when we built the gender field, we passed a Select parameter and presto, the return field was a select. Wouldn’t it be better if the birthdate field would be displayed as a true DateField? We can try this:

Field<?> birthdate = group.buildAndBind("Birth date", "birthdate", DateField.class);

Unfortunately, this doesn’t work as Vaadin loudly complains with a com.vaadin.data.fieldgroup.FieldGroup$BindException: Unable to build a field of type com.vaadin.ui.DateField for editing java.util.Date. This means that in contrast to the old Form, FieldGroup doesn’t handle date fields. As both use field factories (albeit of a different type), this is easily corrected. We just have to create a field group factory that return DateField when passed Date attributes and delegates to the default field group factory otherwise.

public class EnhancedFieldGroupFieldFactory implements FieldGroupFieldFactory {
 
    private FieldGroupFieldFactory fieldFactory = new DefaultFieldGroupFieldFactory();
 
    @SuppressWarnings({ "rawtypes", "unchecked" })
    @Override
    public <T extends Field> T createField(Class<?> dataType, Class<T> fieldType) {
 
        if (Date.class.isAssignableFrom(dataType)) {
 
            return (T) createDateField();
        }
 
        return fieldFactory.createField(dataType, fieldType);
    }
 
    @SuppressWarnings({ "rawtypes", "unchecked" })
    protected <T extends Field> T createDateField() {
 
        DateField field = new DateField();
 
        field.setImmediate(true);
 
        return (T) field;
    }
}

Затем мы устанавливаем экземпляр этой фабрики в группу полей, передавая нужный нам тип поля.

Field<?> birthdate = group.buildAndBind("Birth date", "birthdate", DateField.class);

Удалить начальные уродливые нулевые значения

Finally, since the Person bean is initialized with null values, null appears when the page first loads. For a end-user, this is not particularly desirable. We have to cast the field returned by the field group to a more friendly type and call the setNullRepresentation() method.

AbstractTextField firstName = (AbstractTextField) group.buildAndBind("First name", "firstName");
 
firstName.setNullRepresentation("");

Conclusion

In this article, we used the power of Vaadin 7 to easily create a submit form, complete with validation coming from JSR 303 annotations. The full sources of this article can be found on Github.