Подтвердить что ты не робот

Hibernate Проверка коллекций примитивов

Я хочу иметь возможность сделать что-то вроде:

@Email
public List<String> getEmailAddresses()
{
   return this.emailAddresses;
}

Другими словами, я хочу, чтобы каждый элемент в списке был проверен как адрес электронной почты. Конечно, недопустимо аннотировать коллекцию, подобную этой.

Есть ли способ сделать это?

4b9b3361

Ответ 1

Ни JSR-303, ни Hibernate Validator не имеют никаких готовых ограничений, которые могут проверять каждый элемент Collection.

Одним из возможных решений для решения этой проблемы является создание пользовательского ограничения @ValidCollection и соответствующей реализации валидатора ValidCollectionValidator.

Чтобы проверить каждый элемент коллекции, нам нужен экземпляр Validator внутри ValidCollectionValidator; и для получения такого экземпляра нам нужна специальная реализация ConstraintValidatorFactory.

Посмотрите, нравится ли вам следующее решение...

Просто,

  • скопировать все эти классы java (и импортировать классы relavent);
  • добавить validation-api, hibenate-validator, slf4j-log4j12 и testng jars на пути к классам;
  • запустите тестовый файл.

ValidCollection

    public @interface ValidCollection {

    Class<?> elementType();

    /* Specify constraints when collection element type is NOT constrained 
     * validator.getConstraintsForClass(elementType).isBeanConstrained(); */
    Class<?>[] constraints() default {};

    boolean allViolationMessages() default true;

    String message() default "{ValidCollection.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

}

ValidCollectionValidator

    public class ValidCollectionValidator implements ConstraintValidator<ValidCollection, Collection>, ValidatorContextAwareConstraintValidator {

    private static final Logger logger = LoggerFactory.getLogger(ValidCollectionValidator.class);

    private ValidatorContext validatorContext;

    private Class<?> elementType;
    private Class<?>[] constraints;
    private boolean allViolationMessages;

    @Override
    public void setValidatorContext(ValidatorContext validatorContext) {
        this.validatorContext = validatorContext;
    }

    @Override
    public void initialize(ValidCollection constraintAnnotation) {
        elementType = constraintAnnotation.elementType();
        constraints = constraintAnnotation.constraints();
        allViolationMessages = constraintAnnotation.allViolationMessages();
    }

    @Override
    public boolean isValid(Collection collection, ConstraintValidatorContext context) {
        boolean valid = true;

        if(collection == null) {
            //null collection cannot be validated
            return false;
        }

        Validator validator = validatorContext.getValidator();

        boolean beanConstrained = validator.getConstraintsForClass(elementType).isBeanConstrained();

        for(Object element : collection) {
            Set<ConstraintViolation<?>> violations = new HashSet<ConstraintViolation<?>> ();

            if(beanConstrained) {
                boolean hasValidCollectionConstraint = hasValidCollectionConstraint(elementType);
                if(hasValidCollectionConstraint) {
                    // elementType has @ValidCollection constraint
                    violations.addAll(validator.validate(element));
                } else {
                    violations.addAll(validator.validate(element));
                }
            } else {
                for(Class<?> constraint : constraints) {
                    String propertyName = constraint.getSimpleName();
                    propertyName = Introspector.decapitalize(propertyName);
                    violations.addAll(validator.validateValue(CollectionElementBean.class, propertyName, element));
                }
            }

            if(!violations.isEmpty()) {
                valid = false;
            }

            if(allViolationMessages) { //TODO improve
                for(ConstraintViolation<?> violation : violations) {
                    logger.debug(violation.getMessage());
                    ConstraintViolationBuilder violationBuilder = context.buildConstraintViolationWithTemplate(violation.getMessage());
                    violationBuilder.addConstraintViolation();
                }
            }

        }

        return valid;
    }

    private boolean hasValidCollectionConstraint(Class<?> beanType) {
        BeanDescriptor beanDescriptor = validatorContext.getValidator().getConstraintsForClass(beanType);
        boolean isBeanConstrained = beanDescriptor.isBeanConstrained();
        if(!isBeanConstrained) {
            return false;
        }
        Set<ConstraintDescriptor<?>> constraintDescriptors = beanDescriptor.getConstraintDescriptors(); 
        for(ConstraintDescriptor<?> constraintDescriptor : constraintDescriptors) {
            if(constraintDescriptor.getAnnotation().annotationType().getName().equals(ValidCollection.class.getName())) {
                return true;
            }
        }
        Set<PropertyDescriptor> propertyDescriptors = beanDescriptor.getConstrainedProperties();
        for(PropertyDescriptor propertyDescriptor : propertyDescriptors) {
            constraintDescriptors = propertyDescriptor.getConstraintDescriptors();
            for(ConstraintDescriptor<?> constraintDescriptor : constraintDescriptors) {
                if(constraintDescriptor.getAnnotation().annotationType().getName().equals(ValidCollection.class.getName())) {
                    return true;
                }
            }    
        }
        return false;
    }

}

ValidatorContextAwareConstraintValidator

public interface ValidatorContextAwareConstraintValidator {

    void setValidatorContext(ValidatorContext validatorContext);

}

CollectionElementBean

    public class CollectionElementBean {

    /* add more properties on-demand */
    private Object notNull;
    private String notBlank;
    private String email;

    protected CollectionElementBean() {
    }

    @NotNull
    public Object getNotNull() { return notNull; }
    public void setNotNull(Object notNull) { this.notNull = notNull; }

    @NotBlank
    public String getNotBlank() { return notBlank; }
    public void setNotBlank(String notBlank) { this.notBlank = notBlank; }

    @Email
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }

}

ConstraintValidatorFactoryImpl

public class ConstraintValidatorFactoryImpl implements ConstraintValidatorFactory {

    private ValidatorContext validatorContext;

    public ConstraintValidatorFactoryImpl(ValidatorContext nativeValidator) {
        this.validatorContext = nativeValidator;
    }

    @Override
    public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {
        T instance = null;

        try {
            instance = key.newInstance();
        } catch (Exception e) { 
            // could not instantiate class
            e.printStackTrace();
        }

        if(ValidatorContextAwareConstraintValidator.class.isAssignableFrom(key)) {
            ValidatorContextAwareConstraintValidator validator = (ValidatorContextAwareConstraintValidator) instance;
            validator.setValidatorContext(validatorContext);
        }

        return instance;
    }

}

Сотрудник

public class Employee {

    private String firstName;
    private String lastName;
    private List<String> emailAddresses;

    @NotNull
    public String getFirstName() { return firstName; }
    public void setFirstName(String firstName) { this.firstName = firstName; }

    public String getLastName() { return lastName; }
    public void setLastName(String lastName) { this.lastName = lastName; }

    @ValidCollection(elementType=String.class, constraints={Email.class})
    public List<String> getEmailAddresses() { return emailAddresses; }
    public void setEmailAddresses(List<String> emailAddresses) { this.emailAddresses = emailAddresses; }

}

Команда

public class Team {

    private String name;
    private Set<Employee> members;

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    @ValidCollection(elementType=Employee.class)
    public Set<Employee> getMembers() { return members; }
    public void setMembers(Set<Employee> members) { this.members = members; }

}

ShoppingCart

public class ShoppingCart {

    private List<String> items;

    @ValidCollection(elementType=String.class, constraints={NotBlank.class})
    public List<String> getItems() { return items; }
    public void setItems(List<String> items) { this.items = items; }

}

ValidCollectionTest

public class ValidCollectionTest {

    private static final Logger logger = LoggerFactory.getLogger(ValidCollectionTest.class);

    private ValidatorFactory validatorFactory;

    @BeforeClass
    public void createValidatorFactory() {
        validatorFactory = Validation.buildDefaultValidatorFactory();
    }

    private Validator getValidator() {
        ValidatorContext validatorContext = validatorFactory.usingContext();
        validatorContext.constraintValidatorFactory(new ConstraintValidatorFactoryImpl(validatorContext));
        Validator validator = validatorContext.getValidator();
        return validator;
    }

    @Test
    public void beanConstrained() {
        Employee se = new Employee();
        se.setFirstName("Santiago");
        se.setLastName("Ennis");
        se.setEmailAddresses(new ArrayList<String> ());
        se.getEmailAddresses().add("segmail.com");
        Employee me = new Employee();
        me.setEmailAddresses(new ArrayList<String> ());
        me.getEmailAddresses().add("[email protected]");

        Team team = new Team();
        team.setMembers(new HashSet<Employee>());
        team.getMembers().add(se);
        team.getMembers().add(me);

        Validator validator = getValidator();

        Set<ConstraintViolation<Team>> violations = validator.validate(team);
        for(ConstraintViolation<Team> violation : violations) {
            logger.info(violation.getMessage());
        }
    }

    @Test
    public void beanNotConstrained() {
        ShoppingCart cart = new ShoppingCart();
        cart.setItems(new ArrayList<String> ());
        cart.getItems().add("JSR-303 Book");
        cart.getItems().add("");

        Validator validator = getValidator();

        Set<ConstraintViolation<ShoppingCart>> violations = validator.validate(cart, Default.class);
        for(ConstraintViolation<ShoppingCart> violation : violations) {
            logger.info(violation.getMessage());
        }
    }

}

Выход

02:16:37,581  INFO main validation.ValidCollectionTest:66 - {ValidCollection.message}
02:16:38,303  INFO main validation.ValidCollectionTest:66 - may not be null
02:16:39,092  INFO main validation.ValidCollectionTest:66 - not a well-formed email address

02:17:46,460  INFO main validation.ValidCollectionTest:81 - may not be empty
02:17:47,064  INFO main validation.ValidCollectionTest:81 - {ValidCollection.message}

Примечание.. Если bean имеет ограничения, НЕ указывайте атрибут constraints ограничения @ValidCollection. Атрибут constraints необходим, если bean не имеет ограничений.

Ответ 2

Невозможно записать общую аннотацию обертки, например @EachElement, для обертывания любой аннотации ограничений - из-за ограничений самих аннотаций Java. Тем не менее, вы можете написать общий класс проверки валидатора, который делегирует фактическую проверку каждого элемента на существующий валидатор ограничений. Вы должны написать аннотацию обертки для каждого ограничения, но только один валидатор.

Ive реализовал этот подход в jirutka/validator-collection (доступен в Maven Central). Например:

@EachSize(min = 5, max = 255)
List<String> values;

Эта библиотека позволяет вам легко создать "псевдо ограничение" для любого ограничения проверки, чтобы аннотировать коллекцию простых типов, не записывая дополнительный валидатор или ненужные классы-оболочки для каждой коллекции. EachX поддерживается для всех стандартных Bean ограничений проверки и ограничений Hibernate.

Чтобы создать @EachAwesome для вашего собственного ограничения @Awesome, просто скопируйте и вставьте класс аннотации, замените аннотацию @Constraint на @Constraint(validatedBy = CommonEachValidator.class) и добавьте аннотацию @EachConstraint(validateAs = Awesome.class). Вот и все!

// common boilerplate
@Documented
@Retention(RUNTIME)
@Target({METHOD, FIELD, ANNOTATION_TYPE})
// this is important!
@EachConstraint(validateAs = Awesome.class)
@Constraint(validatedBy = CommonEachValidator.class)
public @interface EachAwesome {

    // copy&paste all attributes from Awesome annotation here
    String message() default "";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    String someAttribute();
}

EDIT: Обновлено для текущей версии библиотеки.

Ответ 3

У меня нет достаточно высокой репутации, чтобы прокомментировать это в исходном ответе, но, возможно, стоит отметить этот вопрос, что JSR-308 находится в стадии окончательного релиза и будет решать эту проблему, когда она будет выпущена! Однако, по крайней мере, потребуется Java 8.

Единственное различие заключается в том, что аннотация для проверки будет включена в объявление типа.

//@Email
public List<@Email String> getEmailAddresses()
{
   return this.emailAddresses;
}

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

P.S. Для получения дополнительной информации ознакомьтесь с этим сообщением SO.

Ответ 4

Спасибо за отличный ответ от becomputer06. Но я думаю, что в определение ValidCollection следует добавить следующие аннотации:

@Target( { ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ValidCollectionValidator.class)

И я все еще не понимаю, что делать с коллекциями примитивов примитивного типа и ограничивает аннотации, такие как @Size, @Min, @Max и т.д., потому что значение не может быть передано через способ byomputer06.

Конечно, я могу создавать пользовательские аннотации для всех случаев в моем приложении, но в любом случае мне нужно добавить свойства для этих аннотаций к CollectionElementBean. И это кажется довольно плохим решением.

Ответ 5

JSR-303 имеет возможность расширять целевые типы встроенных ограничений: см. 7.1.2. Переопределение ограничений в XML.

Вы можете реализовать ConstraintValidator<Email, List<String>>, который делает то же самое, что и заданные ответы, делегируя примитивный валидатор. Затем вы можете сохранить свое определение модели и применить @Email на List<String>.

Ответ 6

Возможно очень простое обходное решение. Вместо этого вы можете проверить коллекцию своих классов, которые обертывают свойство простого значения. Для этого вам нужно использовать аннотацию @Valid в коллекции.

Пример:

public class EmailAddress {

  @Email
  String email;

  public EmailAddress(String email){
    this.email = email;
  }
}

public class Foo {

  /* Validation that works */
  @Valid
  List<EmailAddress> getEmailAddresses(){
    return this.emails.stream().map(EmailAddress::new).collect(toList());
  }

}