Перекрестная проверка полей с помощью Hibernate Validator (JSR 303)
есть ли реализация (или сторонняя реализация для) перекрестной проверки поля в Hibernate Validator 4.x? Если нет, то каков самый чистый способ реализации кросс-поля валидатора?
в качестве примера, как вы можете использовать API для проверки двух свойств компонента равны (например, проверка поля пароля соответствует полю проверки пароля).
в аннотациях, я ожидал бы что-то вроде:
public class MyBean {
@Size(min=6, max=50)
private String pass;
@Equals(property="pass")
private String passVerify;
}
15 ответов:
каждое ограничение поля должно обрабатываться отдельной аннотацией валидатора, или, другими словами, Не рекомендуется проверять аннотацию проверки одного поля на другие поля; межпольная проверка должна выполняться на уровне класса. Кроме того, JSR-303 раздел 2.2 предпочтительный способ выразить несколько проверок одного и того же типа-через список аннотаций. Это позволяет указать сообщение об ошибке для каждого соответствия.
например, проверка общей формы:
@FieldMatch.List({ @FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match"), @FieldMatch(first = "email", second = "confirmEmail", message = "The email fields must match") }) public class UserRegistrationForm { @NotNull @Size(min=8, max=25) private String password; @NotNull @Size(min=8, max=25) private String confirmPassword; @NotNull @Email private String email; @NotNull @Email private String confirmEmail; }
Аннотация:
package constraints; import constraints.impl.FieldMatchValidator; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.Documented; import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.TYPE; import java.lang.annotation.Retention; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Target; /** * Validation annotation to validate that 2 fields have the same value. * An array of fields and their matching confirmation fields can be supplied. * * Example, compare 1 pair of fields: * @FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match") * * Example, compare more than 1 pair of fields: * @FieldMatch.List({ * @FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match"), * @FieldMatch(first = "email", second = "confirmEmail", message = "The email fields must match")}) */ @Target({TYPE, ANNOTATION_TYPE}) @Retention(RUNTIME) @Constraint(validatedBy = FieldMatchValidator.class) @Documented public @interface FieldMatch { String message() default "{constraints.fieldmatch}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; /** * @return The first field */ String first(); /** * @return The second field */ String second(); /** * Defines several <code>@FieldMatch</code> annotations on the same element * * @see FieldMatch */ @Target({TYPE, ANNOTATION_TYPE}) @Retention(RUNTIME) @Documented @interface List { FieldMatch[] value(); } }
Валидатор:
package constraints.impl; import constraints.FieldMatch; import org.apache.commons.beanutils.BeanUtils; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; public class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object> { private String firstFieldName; private String secondFieldName; @Override public void initialize(final FieldMatch constraintAnnotation) { firstFieldName = constraintAnnotation.first(); secondFieldName = constraintAnnotation.second(); } @Override public boolean isValid(final Object value, final ConstraintValidatorContext context) { try { final Object firstObj = BeanUtils.getProperty(value, firstFieldName); final Object secondObj = BeanUtils.getProperty(value, secondFieldName); return firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj); } catch (final Exception ignore) { // ignore } return true; } }
Я предлагаю вам другое решение. Возможно, менее элегантно, но проще!
public class MyBean { @Size(min=6, max=50) private String pass; private String passVerify; @AssertTrue(message="passVerify field should be equal than pass field") private boolean isValid() { return this.pass.equals(this.passVerify); } }
метод isValid () вызывается валидатором автоматически.
Я удивлена, что это не доступно из коробки. В любом случае, вот возможное решение.
Я создал валидатор уровня класса, а не уровень поля, как описано в исходном вопросе.
вот код аннотация:
package com.moa.podium.util.constraints; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.*; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import javax.validation.Constraint; import javax.validation.Payload; @Target({TYPE, ANNOTATION_TYPE}) @Retention(RUNTIME) @Constraint(validatedBy = MatchesValidator.class) @Documented public @interface Matches { String message() default "{com.moa.podium.util.constraints.matches}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; String field(); String verifyField(); }
и сам валидатор:
package com.moa.podium.util.constraints; import org.mvel2.MVEL; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; public class MatchesValidator implements ConstraintValidator<Matches, Object> { private String field; private String verifyField; public void initialize(Matches constraintAnnotation) { this.field = constraintAnnotation.field(); this.verifyField = constraintAnnotation.verifyField(); } public boolean isValid(Object value, ConstraintValidatorContext context) { Object fieldObj = MVEL.getProperty(field, value); Object verifyFieldObj = MVEL.getProperty(verifyField, value); boolean neitherSet = (fieldObj == null) && (verifyFieldObj == null); if (neitherSet) { return true; } boolean matches = (fieldObj != null) && fieldObj.equals(verifyFieldObj); if (!matches) { context.disableDefaultConstraintViolation(); context.buildConstraintViolationWithTemplate("message") .addNode(verifyField) .addConstraintViolation(); } return matches; } }
обратите внимание, что я использовал MVEL для проверки свойств проверяемого объекта. Это может быть заменено стандартными API отражения или если это конкретный класс, который вы проверяете, сами методы доступа.
В @матчей, то аннотации могут быть использованы, используемых в зернах следующим образом:
@Matches(field="pass", verifyField="passRepeat") public class AccountCreateForm { @Size(min=6, max=50) private String pass; private String passRepeat; ... }
как отказ от ответственности, я написал это в последние 5 минут, так что я, вероятно, еще не сгладил все ошибки. Я обновлю ответ, если что-то пойдет не так.
С Hibernate Validator 4.1.0.Окончательный я рекомендую использовать @ScriptAssert:
@ScriptAssert(lang = "javascript", script = "_this.passVerify.equals(_this.pass)") public class MyBean { @Size(min=6, max=50) private String pass; private String passVerify; }
нет ничего плохого в пользовательском валидаторе уровня класса @Matches решение.
проверка Кросс-полей может быть выполнена путем создания пользовательских ограничений.
пример: - сравните поля password и confirmPassword экземпляра пользователя.
CompareStrings
@Target({TYPE}) @Retention(RUNTIME) @Constraint(validatedBy=CompareStringsValidator.class) @Documented public @interface CompareStrings { String[] propertyNames(); StringComparisonMode matchMode() default EQUAL; boolean allowNull() default false; String message() default ""; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
StringComparisonMode
public enum StringComparisonMode { EQUAL, EQUAL_IGNORE_CASE, NOT_EQUAL, NOT_EQUAL_IGNORE_CASE }
CompareStringsValidator
public class CompareStringsValidator implements ConstraintValidator<CompareStrings, Object> { private String[] propertyNames; private StringComparisonMode comparisonMode; private boolean allowNull; @Override public void initialize(CompareStrings constraintAnnotation) { this.propertyNames = constraintAnnotation.propertyNames(); this.comparisonMode = constraintAnnotation.matchMode(); this.allowNull = constraintAnnotation.allowNull(); } @Override public boolean isValid(Object target, ConstraintValidatorContext context) { boolean isValid = true; List<String> propertyValues = new ArrayList<String> (propertyNames.length); for(int i=0; i<propertyNames.length; i++) { String propertyValue = ConstraintValidatorHelper.getPropertyValue(String.class, propertyNames[i], target); if(propertyValue == null) { if(!allowNull) { isValid = false; break; } } else { propertyValues.add(propertyValue); } } if(isValid) { isValid = ConstraintValidatorHelper.isValid(propertyValues, comparisonMode); } if (!isValid) { /* * if custom message was provided, don't touch it, otherwise build the * default message */ String message = context.getDefaultConstraintMessageTemplate(); message = (message.isEmpty()) ? ConstraintValidatorHelper.resolveMessage(propertyNames, comparisonMode) : message; context.disableDefaultConstraintViolation(); ConstraintViolationBuilder violationBuilder = context.buildConstraintViolationWithTemplate(message); for (String propertyName : propertyNames) { NodeBuilderDefinedContext nbdc = violationBuilder.addNode(propertyName); nbdc.addConstraintViolation(); } } return isValid; } }
ConstraintValidatorHelper
public abstract class ConstraintValidatorHelper { public static <T> T getPropertyValue(Class<T> requiredType, String propertyName, Object instance) { if(requiredType == null) { throw new IllegalArgumentException("Invalid argument. requiredType must NOT be null!"); } if(propertyName == null) { throw new IllegalArgumentException("Invalid argument. PropertyName must NOT be null!"); } if(instance == null) { throw new IllegalArgumentException("Invalid argument. Object instance must NOT be null!"); } T returnValue = null; try { PropertyDescriptor descriptor = new PropertyDescriptor(propertyName, instance.getClass()); Method readMethod = descriptor.getReadMethod(); if(readMethod == null) { throw new IllegalStateException("Property '" + propertyName + "' of " + instance.getClass().getName() + " is NOT readable!"); } if(requiredType.isAssignableFrom(readMethod.getReturnType())) { try { Object propertyValue = readMethod.invoke(instance); returnValue = requiredType.cast(propertyValue); } catch (Exception e) { e.printStackTrace(); // unable to invoke readMethod } } } catch (IntrospectionException e) { throw new IllegalArgumentException("Property '" + propertyName + "' is NOT defined in " + instance.getClass().getName() + "!", e); } return returnValue; } public static boolean isValid(Collection<String> propertyValues, StringComparisonMode comparisonMode) { boolean ignoreCase = false; switch (comparisonMode) { case EQUAL_IGNORE_CASE: case NOT_EQUAL_IGNORE_CASE: ignoreCase = true; } List<String> values = new ArrayList<String> (propertyValues.size()); for(String propertyValue : propertyValues) { if(ignoreCase) { values.add(propertyValue.toLowerCase()); } else { values.add(propertyValue); } } switch (comparisonMode) { case EQUAL: case EQUAL_IGNORE_CASE: Set<String> uniqueValues = new HashSet<String> (values); return uniqueValues.size() == 1 ? true : false; case NOT_EQUAL: case NOT_EQUAL_IGNORE_CASE: Set<String> allValues = new HashSet<String> (values); return allValues.size() == values.size() ? true : false; } return true; } public static String resolveMessage(String[] propertyNames, StringComparisonMode comparisonMode) { StringBuffer buffer = concatPropertyNames(propertyNames); buffer.append(" must"); switch(comparisonMode) { case EQUAL: case EQUAL_IGNORE_CASE: buffer.append(" be equal"); break; case NOT_EQUAL: case NOT_EQUAL_IGNORE_CASE: buffer.append(" not be equal"); break; } buffer.append('.'); return buffer.toString(); } private static StringBuffer concatPropertyNames(String[] propertyNames) { //TODO improve concating algorithm StringBuffer buffer = new StringBuffer(); buffer.append('['); for(String propertyName : propertyNames) { char firstChar = Character.toUpperCase(propertyName.charAt(0)); buffer.append(firstChar); buffer.append(propertyName.substring(1)); buffer.append(", "); } buffer.delete(buffer.length()-2, buffer.length()); buffer.append("]"); return buffer; } }
пользователей
@CompareStrings(propertyNames={"password", "confirmPassword"}) public class User { private String password; private String confirmPassword; public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getConfirmPassword() { return confirmPassword; } public void setConfirmPassword(String confirmPassword) { this.confirmPassword = confirmPassword; } }
тест
public void test() { User user = new User(); user.setPassword("password"); user.setConfirmPassword("paSSword"); Set<ConstraintViolation<User>> violations = beanValidator.validate(user); for(ConstraintViolation<User> violation : violations) { logger.debug("Message:- " + violation.getMessage()); } Assert.assertEquals(violations.size(), 1); }
выход
Message:- [Password, ConfirmPassword] must be equal.
используя ограничение проверки CompareStrings, мы также можем сравнить более двух свойств, и мы можем смешать любой из четырех методов сравнения строк.
ColorChoice
@CompareStrings(propertyNames={"color1", "color2", "color3"}, matchMode=StringComparisonMode.NOT_EQUAL, message="Please choose three different colors.") public class ColorChoice { private String color1; private String color2; private String color3; ...... }
тест
ColorChoice colorChoice = new ColorChoice(); colorChoice.setColor1("black"); colorChoice.setColor2("white"); colorChoice.setColor3("white"); Set<ConstraintViolation<ColorChoice>> colorChoiceviolations = beanValidator.validate(colorChoice); for(ConstraintViolation<ColorChoice> violation : colorChoiceviolations) { logger.debug("Message:- " + violation.getMessage()); }
выход
Message:- Please choose three different colors.
аналогично, мы можем иметь CompareNumbers, CompareDates и т. д. ограничения проверки кросс-полей.
П. С. я не тестировал этот код в рабочей среде (хотя я тестировал его в среде dev), поэтому рассматривайте этот код как выпуск Milestone. Если вы нашли ошибку, пожалуйста, напишите хороший комментарий. :)
Я пробовал пример Альбертховена (hibernate-validator 4.0.2.GA) и я получаю ValidationException: "аннотированные методы должны следовать соглашению об именовании JavaBeans. совпадения () нет." слишком. После того, как я переименовал метод из "match" в "isValid", он работает.
public class Password { private String password; private String retypedPassword; public Password(String password, String retypedPassword) { super(); this.password = password; this.retypedPassword = retypedPassword; } @AssertTrue(message="password should match retyped password") private boolean isValid(){ if (password == null) { return retypedPassword == null; } else { return password.equals(retypedPassword); } } public String getPassword() { return password; } public String getRetypedPassword() { return retypedPassword; } }
Если вы используете Spring Framework, то вы можете использовать язык выражений Spring (SpEL) для этого. Я написал небольшую библиотеку, которая предоставляет валидатор JSR-303 на основе SpEL – это делает кросс-полевые проверки легким ветерком! Взгляните на https://github.com/jirutka/validator-spring.
это позволит проверить длину и равенство полей пароля.
@SpELAssert(value = "pass.equals(passVerify)", message = "{validator.passwords_not_same}") public class MyBean { @Size(min = 6, max = 50) private String pass; private String passVerify; }
вы также можете легко изменить это, чтобы проверить пароль, только если не оба пустой.
@SpELAssert(value = "pass.equals(passVerify)", applyIf = "pass || passVerify", message = "{validator.passwords_not_same}") public class MyBean { @Size(min = 6, max = 50) private String pass; private String passVerify; }
использовать
@EqualProperties
отсюда выделенная библиотека ограничений JSR-303:@EqualProperties({"pass", "passVerify"}) public class MyBean { @Size(min=6, max=50) @NotNull private String pass; @NotNull private String passVerify; }
У меня нет репутации комментирования первого ответа, но я хотел бы добавить, что я добавил модульные тесты для выигрышного ответа и имею следующие замечания:
- Если вы получаете первое или имена полей так, то вы получите ошибку проверки, как будто значения не совпадают. Не получить споткнулся орфографических ошибок, например
@FieldMatch(first="недействительнымFieldName1", second= "validFieldName2")
- оценщик будет принять эквивалентные типы данных, т. е. все они будут проходить с FieldMatch:
private String stringField = "1";
private Integer integerField = новое целое число (1)
private int intField = 1;
- Если поля имеют тип объекта, который не реализует equals, проверка будет неудача.
мне нравится идея от Якуб Jirutka использовать язык выражений Spring. Если вы не хотите добавлять другую библиотеку/зависимость (предполагая, что вы уже используете Spring), вот упрощенная реализация его идеи.
ограничения:
@Constraint(validatedBy=ExpressionAssertValidator.class) @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface ExpressionAssert { String message() default "expression must evaluate to true"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; String value(); }
валидатор:
public class ExpressionAssertValidator implements ConstraintValidator<ExpressionAssert, Object> { private Expression exp; public void initialize(ExpressionAssert annotation) { ExpressionParser parser = new SpelExpressionParser(); exp = parser.parseExpression(annotation.value()); } public boolean isValid(Object value, ConstraintValidatorContext context) { return exp.getValue(value, Boolean.class); } }
применить такой:
@ExpressionAssert(value="pass == passVerify", message="passwords must be same") public class MyBean { @Size(min=6, max=50) private String pass; private String passVerify; }
очень хорошее решение брэдхаус. Есть ли способ применить аннотацию @Matches к нескольким полям?
изменить: Вот решение, которое я придумал, чтобы ответить на этот вопрос, я изменил ограничение, чтобы принять массив вместо одного значения:
@Matches(fields={"password", "email"}, verifyFields={"confirmPassword", "confirmEmail"}) public class UserRegistrationForm { @NotNull @Size(min=8, max=25) private String password; @NotNull @Size(min=8, max=25) private String confirmPassword; @NotNull @Email private String email; @NotNull @Email private String confirmEmail; }
код для аннотации:
package springapp.util.constraints; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.*; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import javax.validation.Constraint; import javax.validation.Payload; @Target({TYPE, ANNOTATION_TYPE}) @Retention(RUNTIME) @Constraint(validatedBy = MatchesValidator.class) @Documented public @interface Matches { String message() default "{springapp.util.constraints.matches}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; String[] fields(); String[] verifyFields(); }
и реализации:
package springapp.util.constraints; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import org.apache.commons.beanutils.BeanUtils; public class MatchesValidator implements ConstraintValidator<Matches, Object> { private String[] fields; private String[] verifyFields; public void initialize(Matches constraintAnnotation) { fields = constraintAnnotation.fields(); verifyFields = constraintAnnotation.verifyFields(); } public boolean isValid(Object value, ConstraintValidatorContext context) { boolean matches = true; for (int i=0; i<fields.length; i++) { Object fieldObj, verifyFieldObj; try { fieldObj = BeanUtils.getProperty(value, fields[i]); verifyFieldObj = BeanUtils.getProperty(value, verifyFields[i]); } catch (Exception e) { //ignore continue; } boolean neitherSet = (fieldObj == null) && (verifyFieldObj == null); if (neitherSet) { continue; } boolean tempMatches = (fieldObj != null) && fieldObj.equals(verifyFieldObj); if (!tempMatches) { addConstraintViolation(context, fields[i]+ " fields do not match", verifyFields[i]); } matches = matches?tempMatches:matches; } return matches; } private void addConstraintViolation(ConstraintValidatorContext context, String message, String field) { context.disableDefaultConstraintViolation(); context.buildConstraintViolationWithTemplate(message).addNode(field).addConstraintViolation(); } }
вы должны вызвать его явно. В приведенном выше примере bradhouse дал вам все шаги для написания пользовательского ограничения.
добавьте этот код в класс вызывающего абонента.
ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); validator = factory.getValidator(); Set<ConstraintViolation<yourObjectClass>> constraintViolations = validator.validate(yourObject);
в приведенном выше случае это будет
Set<ConstraintViolation<AccountCreateForm>> constraintViolations = validator.validate(objAccountCreateForm);
почему бы не попробовать овал: http://oval.sourceforge.net/
похоже, он поддерживает OGNL, поэтому, возможно, вы могли бы сделать это более естественным
@Assert(expr = "_value ==_this.pass").
вы, ребята, удивительный. Действительно удивительные идеи. Мне нравится Alberthoven это!--5--> и Макгин большинство, поэтому я решил совместить обе идеи. И разработать какое-то общее решение для удовлетворения всех случаев. Вот мое предлагаемое решение.
@Documented @Constraint(validatedBy = NotFalseValidator.class) @Target({ElementType.METHOD, ElementType.FIELD,ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface NotFalse { String message() default "NotFalse"; String[] messages(); String[] properties(); String[] verifiers(); Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
public class NotFalseValidator implements ConstraintValidator<NotFalse, Object> { private String[] properties; private String[] messages; private String[] verifiers; @Override public void initialize(NotFalse flag) { properties = flag.properties(); messages = flag.messages(); verifiers = flag.verifiers(); } @Override public boolean isValid(Object bean, ConstraintValidatorContext cxt) { if(bean == null) { return true; } boolean valid = true; BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(bean); for(int i = 0; i< properties.length; i++) { Boolean verified = (Boolean) beanWrapper.getPropertyValue(verifiers[i]); valid &= isValidProperty(verified,messages[i],properties[i],cxt); } return valid; } boolean isValidProperty(Boolean flag,String message, String property, ConstraintValidatorContext cxt) { if(flag == null || flag) { return true; } else { cxt.disableDefaultConstraintViolation(); cxt.buildConstraintViolationWithTemplate(message) .addPropertyNode(property) .addConstraintViolation(); return false; } } }
@NotFalse( messages = {"End Date Before Start Date" , "Start Date Before End Date" } , properties={"endDateTime" , "startDateTime"}, verifiers = {"validDateRange" , "validDateRange"}) public class SyncSessionDTO implements ControllableNode { @NotEmpty @NotPastDate private Date startDateTime; @NotEmpty private Date endDateTime; public Date getStartDateTime() { return startDateTime; } public void setStartDateTime(Date startDateTime) { this.startDateTime = startDateTime; } public Date getEndDateTime() { return endDateTime; } public void setEndDateTime(Date endDateTime) { this.endDateTime = endDateTime; } public Boolean getValidDateRange(){ if(startDateTime != null && endDateTime != null) { return startDateTime.getTime() <= endDateTime.getTime(); } return null; } }
решение реализовано с вопросом: как получить доступ к полю, которое описано в аннотации собственность
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Match { String field(); String message() default ""; }
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = MatchValidator.class) @Documented public @interface EnableMatchConstraint { String message() default "Fields must match!"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
public class MatchValidator implements ConstraintValidator<EnableMatchConstraint, Object> { @Override public void initialize(final EnableMatchConstraint constraint) {} @Override public boolean isValid(final Object o, final ConstraintValidatorContext context) { boolean result = true; try { String mainField, secondField, message; Object firstObj, secondObj; final Class<?> clazz = o.getClass(); final Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { if (field.isAnnotationPresent(Match.class)) { mainField = field.getName(); secondField = field.getAnnotation(Match.class).field(); message = field.getAnnotation(Match.class).message(); if (message == null || "".equals(message)) message = "Fields " + mainField + " and " + secondField + " must match!"; firstObj = BeanUtils.getProperty(o, mainField); secondObj = BeanUtils.getProperty(o, secondField); result = firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj); if (!result) { context.disableDefaultConstraintViolation(); context.buildConstraintViolationWithTemplate(message).addPropertyNode(mainField).addConstraintViolation(); break; } } } } catch (final Exception e) { // ignore //e.printStackTrace(); } return result; } }
и как его использовать...? Вот так:
@Entity @EnableMatchConstraint public class User { @NotBlank private String password; @Match(field = "password") private String passwordConfirmation; }