Перевірка перехресного поля з сплячим валідатором (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-рішення (більш елегантне) рішення на рівні класу.
Карл Ріхтер

Відповіді:


282

Кожне обмеження поля має оброблятися чіткою анотацією валідатора, або іншими словами, не рекомендується застосовувати перевірку анотацій перевірки одного поля щодо інших полів; Перевірка міжпольових даних повинна проводитися на рівні класу. Крім того, 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;
    }
}

8
@AndyT: Існує зовнішня залежність від BeanUtils Apache Commons.
GaryF

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

8
Я використовував вище зразка, але він не відображає повідомлення про помилку, що має бути прив'язка у jsp? У мене є прив'язка до пароля та підтвердження, чи потрібно ще щось? <форма: пароль шлях = "пароль" /> <форма: помилки шлях = "пароль" cssClass = "помилка" /> <форма: пароль шлях = "підтвердитиPassword" /> <форма: помилки шлях = "підтвердитиPassword" 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
На сьогоднішній день, на мою думку, СРСР з валідації бобів - це змішання проблем.
Дмитро Міньковський

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.Final рекомендую використовувати @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 повинна бути спроможна виконати. На мій незайманий погляд, запропоноване вище рішення Нікко все ще здається більш чистим як з точки зору зручності користування (його анотація легко читається, так і досить функціональна порівняно з неелегантними посиланнями javas-> java), а також з точки зору масштабованості (я припускаю, що для цього є розумні витрати обробляти javascript, але, можливо, Hibernate кешує скомпільований код принаймні?). Мені цікаво зрозуміти, чому це було б кращим.
Девід Паркс

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

4
Як було сказано, з валідатором рівня класу нічого поганого. ScriptAssert - це лише альтернатива, яка не вимагає від вас написання спеціального коду. Я не сказав, що це найкраще рішення ;-)
Харді

Чудова відповідь, тому що підтвердження пароля не є критичною валідацією, тому це можна зробити на стороні клієнта
peterchaula

19

Перевірку перехресних полів можна здійснити, створивши власні обмеження.

Приклад: - Порівняйте пароль та підтвердьте поляПасса в поле екземпляра користувача.

ПорівнятиСтруси

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

StringComppareMode

public enum StringComparisonMode {
    EQUAL, EQUAL_IGNORE_CASE, NOT_EQUAL, NOT_EQUAL_IGNORE_CASE
}

ПорівнятиStringsValidator

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, ми можемо також порівняти більше двох властивостей, і ми можемо змішати будь-який із чотирьох методів порівняння рядків.

Колірний вибір

@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 Spring Expression Language (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

Мені подобається ідея Якуба Джирутки використовувати мову Spring Expression. Якщо ви не хочете додавати іншу бібліотеку / залежність (якщо припустити, що ви вже використовуєте 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 (перший = " недійсне FieldName1", друге = "validFieldName2")

  • Валідатор буде приймати еквівалентні типи даних , тобто вони будуть все пройти з FieldMatch:

приватний String stringField = "1";

приватний Integer integerField = новий цілий (1)

приватний int intField = 1;

  • Якщо поля мають тип об'єкта, який не реалізує рівних, перевірка не вдасться.

2

Дуже приємне рішення брадхаус. Чи можна застосувати анотацію @Matches до більш ніж одного поля?

EDIT: Ось рішення, яке я придумав, щоб відповісти на це запитання, я змінив обмеження, щоб прийняти масив замість одного значення:

@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, щоб прийняти кілька пар полів.
Брадхаус

Дякуємо, Брадхаус, придумав рішення і розмістив його вище. Коли потрібно прийняти різне число аргументів, потрібно трохи попрацювати, щоб не отримати IndexOutOfBoundsExceptions, але основи є.
Макгін

1

Викликати це потрібно чітко. У наведеному вище прикладі, bradhouse дав вам усі кроки для написання користувацького обмеження.

Додайте цей код у свій клас абонента.

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

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

у вищенаведеному випадку це було б

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

1

Чому б не спробувати Oval: http://oval.sourceforge.net/

Я схоже, що він підтримує OGNL, тому, можливо, ви могли б зробити це більш природним

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

1

Ви, хлопці, приголомшливі. Дійсно дивовижні ідеї. Я як Alberthoven - х і McGin - х найбільше, так що я вирішив об'єднати обидві ідеї. І розробити якесь загальне рішення для задоволення всіх справ. Ось моє запропоноване рішення.

@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.