Межполевая проверка с помощью Hibernate Validator (JSR 303)


236

Существует ли реализация (или сторонняя реализация) перекрестной проверки в Hibernate Validator 4.x? Если нет, то каков самый чистый способ реализации валидатора между полями?

Например, как вы можете использовать API для проверки того, что два свойства бина равны (например, проверка поля пароля соответствует полю проверки пароля).

В аннотациях я бы ожидал что-то вроде:

public class MyBean {
  @Size(min=6, max=50)
  private String pass;

  @Equals(property="pass")
  private String passVerify;
}

1
См. Stackoverflow.com/questions/2781771/…, чтобы найти безопасное для типов и решение для API без рефлекса (imo более элегантное) на уровне класса.
Карл Рихтер

Ответы:


282

Каждое ограничение поля должно обрабатываться отдельной аннотацией валидатора, или, другими словами, не рекомендуется практиковать проверку аннотации одного поля для проверки других полей; межполевая проверка должна быть сделана на уровне класса. Кроме того, в разделе 2.2 JSR-303 предпочтительным способом выражения нескольких проверок одного типа является использование списка аннотаций. Это позволяет указывать сообщение об ошибке для каждого совпадения.

Например, проверка общей формы:

@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;
    }
}

8
@AndyT: существует внешняя зависимость от Apache Commons BeanUtils.
GaryF

7
@ScriptAssert не позволяет создавать сообщения проверки с настроенным путем. context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()).addNode(secondFieldName).addConstraintViolation().disableDefaultConstraintViolation(); Предоставляет возможность выделения правого поля (если только JSF поддержит его).
Питер Дэвис

8
Я использовал приведенный выше пример, но он не отображает сообщение об ошибке, какая привязка должна быть в JSP? У меня есть привязка к паролю и только подтверждение, есть ли что-нибудь еще нужно? <form: пароль путь = "пароль" /> <форма: путь ошибок = "пароль" cssClass = "errorz" /> <форма: путь пароля = "verifyPassword" /> <форма: путь ошибок = "verifyPassword" cssClass = " errorz "/>
Махмуд Салех

7
BeanUtils.getPropertyвозвращает строку Пример, вероятно, предназначен для использования, PropertyUtils.getPropertyкоторый возвращает объект.
SingleShot

2
Хороший ответ, но я завершил его ответом на этот вопрос: stackoverflow.com/questions/11890334/…
maxivis

164

Я предлагаю вам другое возможное решение. Возможно, менее элегантно, но проще!

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Метод вызывается валидатор автоматически.


12
Я думаю, что это снова смешивание проблем. Смысл проверки бина заключается в том, чтобы проверить внешний вид в ConstraintValidators. В этом случае у вас есть часть логики проверки в самом бине и часть в платформе Validator. Путь - ограничение уровня класса. Hibernate Validator также теперь предлагает @ScriptAssert, который упрощает реализацию внутренних зависимостей бина.
Харди

10
Я бы сказал , что это более элегантно, не меньше!
NickJ

8
Мое мнение до сих пор таково, что JSR Bean Validation представляет собой смесь проблем.
Дмитрий Минковский

3
@GaneshKrishnan Что если мы хотим иметь несколько таких @AssertTrueметодов? Есть какое-то соглашение об именах?
Стефан

3
почему это не лучший ответ
фанк-й

32

Я удивлен, что это не доступно из коробки. Во всяком случае, вот возможное решение.

Я создал валидатор уровня класса, а не уровень поля, как описано в исходном вопросе.

Вот код аннотации:

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 можно использовать в компоненте следующим образом:

@Matches(field="pass", verifyField="passRepeat")
public class AccountCreateForm {

  @Size(min=6, max=50)
  private String pass;
  private String passRepeat;

  ...
}

Как заявление об отказе от ответственности, я написал это за последние 5 минут, так что я, вероятно, еще не сгладил все ошибки. Я обновлю ответ, если что-то пойдет не так.


1
Это здорово, и это работает для меня, за исключением того, что addNote устарела, и я получаю AbstractMethodError, если вместо этого я использую addPropertyNode. Google не помогает мне здесь. Какое решение? Где-то отсутствует зависимость?
Пол Грениер

29

С Hibernate Validator 4.1.0. Наконец, я рекомендую использовать @ScriptAssert . Исключение из его JavaDoc:

Выражения сценариев могут быть написаны на любом языке сценариев или выражений, для которого в пути к классам можно найти совместимый механизм JSR 223 («Сценарии для платформы JavaTM»).

Примечание: оценка выполняется скриптовым « движком », работающим на Java VM, поэтому на Java «на стороне сервера», а не на «стороне клиента», как указано в некоторых комментариях.

Пример:

@ScriptAssert(lang = "javascript", script = "_this.passVerify.equals(_this.pass)")
public class MyBean {
  @Size(min=6, max=50)
  private String pass;

  private String passVerify;
}

или с более коротким псевдонимом и нулевым:

@ScriptAssert(lang = "javascript", alias = "_",
    script = "_.passVerify != null && _.passVerify.equals(_.pass)")
public class MyBean {
  @Size(min=6, max=50)
  private String pass;

  private String passVerify;
}

или с Java 7+ нулевым Objects.equals():

@ScriptAssert(lang = "javascript", script = "Objects.equals(_this.passVerify, _this.pass)")
public class MyBean {
  @Size(min=6, max=50)
  private String pass;

  private String passVerify;
}

Тем не менее, нет ничего плохого в настраиваемом решении проверки класса @Matches .


1
Интересное решение, действительно ли мы используем javascript здесь для выполнения этой проверки? Это кажется излишним для того, что должна быть в состоянии сделать Java-аннотация. На мой взгляд, предложенное выше решение Нико все еще кажется более чистым как с точки зрения удобства использования (его аннотация легко читаема и довольно функциональна по сравнению с не элегантными ссылками javascript-> java), так и с точки зрения масштабируемости (я полагаю, что для обрабатывать JavaScript, но, может быть, Hibernate кэширует скомпилированный код, по крайней мере?). Мне любопытно понять, почему это было бы предпочтительным.
Дэвид Паркс

2
Я согласен, что реализация Nicko хороша, но я не вижу ничего нежелательного в использовании JS в качестве языка выражений. Java 6 включает Rhino именно для таких приложений. Мне нравится @ScriptAssert, так как он работает без необходимости создавать аннотации и валидатор каждый раз, когда мне нужно выполнить новый тип теста.

4
Как уже говорилось, с валидатором уровня класса все в порядке. ScriptAssert - это просто альтернатива, которая не требует написания собственного кода. Я не сказал, что это предпочтительное решение ;-)
Hardy

Отличный ответ, потому что подтверждение пароля не является критической проверкой, поэтому это может быть сделано на стороне клиента
peterchaula

19

Кросс-поля проверки могут быть сделаны путем создания пользовательских ограничений.

Пример: - Сравнить поля пароля и пароля подтверждения экземпляра пользователя.

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 и т. Д.

PS Я не тестировал этот код в рабочей среде (хотя я тестировал его в среде разработки), поэтому рассмотрим этот код как Milestone Release. Если вы нашли ошибку, пожалуйста, напишите хороший комментарий. :)


Мне нравится этот подход, так как он более гибкий, чем другие. Это позволяет мне проверить более 2 полей на равенство. Хорошая работа!
Таурен

9

Я попробовал пример Альбертховена (hibernate-validator 4.0.2.GA), и я получил ValidationException: «Аннотированные методы должны следовать соглашению об именах JavaBeans. match () - нет. После того, как я переименовал метод из «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;
    }

}

Он работал правильно для меня, но не отображал сообщение об ошибке. Работало ли и отображалось ли сообщение об ошибке для вас. Как?
Крошечный

1
@Tiny: сообщение должно содержать нарушения, возвращенные валидатором. (Напишите юнит-тест: stackoverflow.com/questions/5704743/… ). НО сообщение проверки принадлежит свойству isValid. Поэтому сообщение будет отображаться только в графическом интерфейсе, если в графическом интерфейсе отображаются проблемы для retypedPassword AND isValid (рядом с повторно введенным паролем).
Ральф

8

Если вы используете 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;
}

4

Мне нравится идея от Якуба Jirutka использовать Spring Expression Language. Если вы не хотите добавлять другую библиотеку / зависимость (при условии, что вы уже используете 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;
}

3

У меня нет репутации комментировать первый ответ, но я хотел добавить, что я добавил юнит-тесты для победившего ответа и имею следующие замечания:

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

@FieldMatch (first = " invalid FieldName1", second = "validFieldName2")

  • Валидатор будет принимать эквивалентные типы данных, т.е. все они будут передаваться с FieldMatch:

private String stringField = "1";

private Integer integerField = new Integer (1)

private int intField = 1;

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

2

Очень хорошее решение Bradhouse. Есть ли способ применить аннотацию @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();
    }
}

Хм. Точно сказать не могу. Вы можете попробовать создать определенные валидаторы для каждого поля подтверждения (чтобы они имели разные аннотации) или обновить аннотацию @Matches, чтобы принимать несколько пар полей.
Брэдхаус

Спасибо Bradhouse, придумали решение и разместили его выше. Нужно немного поработать, когда передается разное количество аргументов, поэтому вы не получаете исключений IndexOutOfBoundsException, но есть основы.
Макгин

1

Вы должны назвать это явно. В приведенном выше примере, Bradhouse дал вам все шаги, чтобы написать пользовательское ограничение.

Добавьте этот код в свой класс абонента.

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();

Set<ConstraintViolation<yourObjectClass>> constraintViolations = validator.validate(yourObject);

в приведенном выше случае это будет

Set<ConstraintViolation<AccountCreateForm>> constraintViolations = validator.validate(objAccountCreateForm);

1

Почему бы не попробовать Овал: http://oval.sourceforge.net/

Похоже, он поддерживает OGNL, так что, возможно, вы могли бы сделать это более естественным

@Assert(expr = "_value ==_this.pass").

1

Вы, ребята, потрясающие. Действительно потрясающие идеи. Больше всего мне нравятся Альбертховен и Макгин , поэтому я решил объединить обе идеи. И разработать какое-то общее решение для удовлетворения всех случаев. Вот мое предлагаемое решение.

@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;
    }

}

0

Я сделал небольшую адаптацию в решении Nicko, так что нет необходимости использовать библиотеку Apache Commons BeanUtils и заменить ее решением, уже доступным весной, для тех, кто использует его, поскольку я могу быть проще:

import org.springframework.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessorFactory;

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 object, final ConstraintValidatorContext context) {

        BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(object);
        final Object firstObj = beanWrapper.getPropertyValue(firstFieldName);
        final Object secondObj = beanWrapper.getPropertyValue(secondFieldName);

        boolean isValid = firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);

        if (!isValid) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
                .addPropertyNode(firstFieldName)
                .addConstraintViolation();
        }

        return isValid;

    }
}

-1

Решение, основанное на вопросе: Как получить доступ к полю, описанному в свойстве аннотации

@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;
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.