Validação de campo cruzado com o Hibernate Validator (JSR 303)

236

Existe uma implementação (ou implementação de terceiros para) de validação de campo cruzado no Hibernate Validator 4.x? Caso contrário, qual é a maneira mais limpa de implementar um validador de campo cruzado?

Como exemplo, como você pode usar a API para validar que duas propriedades do bean sejam iguais (por exemplo, a validação de um campo de senha corresponde ao campo de verificação de senha).

Nas anotações, eu esperaria algo como:

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

  @Equals(property="pass")
  private String passVerify;
}
Bradhouse
fonte
1
Consulte stackoverflow.com/questions/2781771/… para obter uma solução segura de tipo e livre de API de reflexão (imo mais elegante) no nível da classe.
22915 Karl Richter

Respostas:

282

Cada restrição de campo deve ser tratada por uma anotação de validador distinta ou, em outras palavras, não é uma prática sugerida que a anotação de validação de um campo seja verificada em relação a outros campos; a validação de campo cruzado deve ser feita no nível da classe. Além disso, a maneira preferida da Seção 2.2 do JSR-303 de expressar várias validações do mesmo tipo é através de uma lista de anotações. Isso permite que a mensagem de erro seja especificada por correspondência.

Por exemplo, validando um formulário comum:

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

A anotação:

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();
    }
}

O validador:

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;
    }
}
Nicko
fonte
8
@ AndyT: Existe uma dependência externa no Apache Commons BeanUtils.
GaryF
7
O @ScriptAssert não permite criar uma mensagem de validação com um caminho personalizado. context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()).addNode(secondFieldName).addConstraintViolation().disableDefaultConstraintViolation(); Oferece a possibilidade de destacar o campo certo (se apenas o JSF o apoiar).
22613 Peter Peter
8
Eu usei acima amostra, mas ele não exibe mensagem de erro, qual é a ligação deve estar no jsp? Eu tenho ligação para senha e confirmo apenas, há mais alguma coisa necessária? <formulário: caminho da senha = "senha" /> <formulário: caminho dos erros = "senha" cssClass = "errorz" /> <formulário: caminho da senha = "confirmPassword" /> <formulário: caminho dos erros = "confirmPassword" cssClass = " errorz "/>
Mahmoud Saleh
7
BeanUtils.getPropertyretorna uma string. O exemplo provavelmente deve ser usado para PropertyUtils.getPropertyretornar um objeto.
SingleShot 13/12
2
Boa resposta, mas eu a completei com a resposta a esta pergunta: stackoverflow.com/questions/11890334/…
maxivis
164

Eu sugiro outra solução possível. Talvez menos elegante, mas mais fácil!

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

O isValidmétodo é invocado pelo validador automaticamente.

Alberthoven
fonte
12
Eu acho que isso é uma mistura de preocupações novamente. O objetivo da Validação de Bean é externalizar a validação em ConstraintValidators. Nesse caso, você tem parte da lógica de validação no próprio bean e parte na estrutura do Validator. O caminho a seguir é uma restrição de nível de classe. O Hibernate Validator também oferece agora um @ScriptAssert que facilita a implementação de dependências internas do bean.
Hardy
10
Eu diria que isso é mais elegante, não menos!
NickJ
8
Minha opinião até agora é que o JSR de Validação de Bean é uma mistura de preocupações.
Dmitry Minkovsky
3
@GaneshKrishnan E se quisermos ter vários @AssertTruemétodos desse tipo ? Alguma convenção de nomenclatura é válida?
Stephane
3
porque é que esta não melhor resposta
Funky-nd
32

Estou surpreso que isso não esteja disponível imediatamente. Enfim, aqui está uma solução possível.

Eu criei um validador no nível da classe, não no nível do campo, conforme descrito na pergunta original.

Aqui está o código da anotação:

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();
}

E o próprio validador:

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

Observe que eu usei o MVEL para inspecionar as propriedades do objeto que está sendo validado. Isso pode ser substituído pelas APIs de reflexão padrão ou, se for uma classe específica que você está validando, pelos próprios métodos acessadores.

A anotação @Matches pode ser usada em um bean da seguinte maneira:

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

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

  ...
}

Como isenção de responsabilidade, escrevi isso nos últimos 5 minutos, então provavelmente ainda não resolvi todos os erros. Atualizarei a resposta se algo der errado.

Bradhouse
fonte
1
Isso é ótimo e está funcionando para mim, exceto que o addNote está obsoleto e eu recebo AbstractMethodError se eu usar o addPropertyNode. O Google não está me ajudando aqui. Qual a solução? Existe alguma dependência faltando em algum lugar?
Paul Grenyer
29

Com o Hibernate Validator 4.1.0.Final, recomendo usar o @ScriptAssert . Exceto em seu JavaDoc:

As expressões de script podem ser escritas em qualquer linguagem de script ou expressão, para a qual um mecanismo compatível com JSR 223 ("Scripting for the JavaTM Platform") pode ser encontrado no caminho de classe.

Nota: a avaliação está sendo executada por um " mecanismo " de script em execução na Java VM, portanto, no Java "lado do servidor", não no "lado do cliente", conforme indicado em alguns comentários.

Exemplo:

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

  private String passVerify;
}

ou com alias mais curto e sem segurança:

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

  private String passVerify;
}

ou com Java 7+ seguro para nulos 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;
}

No entanto, não há nada de errado com uma solução @Matches do validador de nível de classe personalizado .

Hardy
fonte
1
Solução interessante, estamos realmente usando javascript aqui para realizar essa validação? Isso parece um exagero para o que uma anotação baseada em java deve ser capaz de realizar. Para meus olhos virgens, a solução de Nicko proposta acima ainda parece mais limpa do ponto de vista da usabilidade (sua anotação é fácil de ler e bastante funcional vs. referências deselegantes de javascript-> java) e do ponto de vista da escalabilidade (presumo que haja uma sobrecarga razoável para manipular o javascript, mas talvez o Hibernate esteja armazenando em cache o código compilado, pelo menos?). Estou curioso para entender por que isso seria preferido.
David Parks
2
Concordo que a implementação do Nicko é boa, mas não vejo nada censurável sobre o uso do JS como uma linguagem de expressão. O Java 6 inclui o Rhino para exatamente essas aplicações. Gosto do @ScriptAssert, pois ele funciona sem a necessidade de criar uma anotação e um validador toda vez que tenho um novo tipo de teste para executar.
4
Como dito, nada está errado com o validador em nível de classe. O ScriptAssert é apenas uma alternativa que não exige que você escreva um código personalizado. Eu não disse que ele é a solução preferida ;-)
Hardy
Grande resposta, porque confirmação de senha não é a validação crítica, portanto, ele pode ser feito no lado do cliente
peterchaula
19

As validações entre campos podem ser feitas criando restrições personalizadas.

Exemplo: - Compare os campos de senha e confirmPassword da instância do usuário.

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

Do utilizador

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

Teste

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

Resultado Message:- [Password, ConfirmPassword] must be equal.

Usando a restrição de validação CompareStrings, também podemos comparar mais de duas propriedades e misturar qualquer um dos quatro métodos de comparação de string.

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

Teste

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());
        }

Resultado Message:- Please choose three different colors.

Da mesma forma, podemos ter restrições de validação de campos cruzados CompareNumbers, CompareDates etc.

PS: Eu não testei esse código no ambiente de produção (apesar de tê-lo testado no ambiente de desenvolvimento), portanto, considere esse código como Milestone Release. Se você encontrar um bug, escreva um bom comentário. :)

dira
fonte
Eu gosto dessa abordagem, pois é mais flexível que as outras. Permite validar mais de 2 campos para igualdade. Bom trabalho!
Tauren
9

Tentei o exemplo de Alberthoven (hibernate-validator 4.0.2.GA) e recebo uma ValidationException: „Métodos anotados devem seguir a convenção de nomenclatura do JavaBeans. match () não. Depois que eu renomeei o método de „match“ para "isValid", ele funciona.

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

}
Ralph
fonte
Funcionou corretamente para mim, mas não exibiu a mensagem de erro. Funcionou e exibiu a mensagem de erro para você. Quão?
minúsculo
1
@ Minúsculo: A mensagem deve estar nas violações retornadas pelo validador. (Escreva um teste de unidade: stackoverflow.com/questions/5704743/… ). MAS a mensagem de validação pertence à propriedade "isValid". Portanto, a mensagem será mostrada apenas na GUI se a GUI mostrar os problemas de retypedPassword AND isValid (ao lado de Senha digitada novamente).
Ralph
8

Se você estiver usando o Spring Framework, poderá usar o Spring Expression Language (SpEL) para isso. Eu escrevi uma pequena biblioteca que fornece o validador JSR-303 baseado em SpEL - facilita muito as validações entre campos! Dê uma olhada em https://github.com/jirutka/validator-spring .

Isso validará o comprimento e a igualdade dos campos de senha.

@SpELAssert(value = "pass.equals(passVerify)",
            message = "{validator.passwords_not_same}")
public class MyBean {

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

    private String passVerify;
}

Você também pode modificar isso facilmente para validar os campos de senha apenas quando não estiverem ambos vazios.

@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;
}
Jakub Jirutka
fonte
4

Gosto da ideia de Jakub Jirutka de usar o Spring Expression Language. Se você não deseja adicionar outra biblioteca / dependência (assumindo que você já usa o Spring), aqui está uma implementação simplificada de sua ideia.

A restrição:

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

O validador:

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

Aplique assim:

@ExpressionAssert(value="pass == passVerify", message="passwords must be same")
public class MyBean {
    @Size(min=6, max=50)
    private String pass;
    private String passVerify;
}
holmis83
fonte
3

Não tenho reputação de comentar a primeira resposta, mas gostaria de acrescentar que adicionei testes de unidade para a resposta vencedora e tenho as seguintes observações:

  • Se você errar o primeiro nome ou o campo, receberá um erro de validação como se os valores não coincidissem. Não seja enganado por erros ortográficos, por exemplo

@FieldMatch (primeiro = " FieldName1 inválido ", segundo = "validFieldName2")

  • O validador irá aceitar tipos de dados equivalentes, ou seja, estes passarão todos com FieldMatch:

string privada stringField = "1";

privado Número inteiro integerField = new Número inteiro (1)

private int intField = 1;

  • Se os campos forem de um tipo de objeto que não implemente iguais, a validação falhará.
Chanoch
fonte
2

Bradhouse solução muito agradável. Existe alguma maneira de aplicar a anotação @Matches a mais de um campo?

Edição: Aqui está a solução que eu encontrei para responder a esta pergunta, eu modifiquei a restrição para aceitar uma matriz em vez de um único valor:

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

O código para a anotação:

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();
}

E a implementação:

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();
    }
}
McGin
fonte
Hmm. Não tenho certeza. Você pode tentar criar validadores específicos para cada campo de confirmação (para que tenham anotações diferentes) ou atualizar a anotação @Matches para aceitar vários pares de campos.
Bradhouse
Obrigado Bradhouse, veio com uma solução e postou acima. É necessário um pouco de trabalho para atender quando um número diferente de argumentos for aprovado, para que você não obtenha IndexOutOfBoundsExceptions, mas o básico está lá.
McGin
1

Você precisa chamá-lo explicitamente. No exemplo acima, o bradhouse forneceu todas as etapas para escrever uma restrição personalizada.

Adicione esse código à sua classe de chamada.

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

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

no caso acima, seria

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

Por que não tentar Oval: http://oval.sourceforge.net/

Parece que ele suporta OGNL, então talvez você possa fazê-lo de uma forma mais natural

@Assert(expr = "_value ==_this.pass").
Mircea D.
fonte
1

Caras, vocês são incríveis. Idéias realmente incríveis. Eu gosto mais de Alberthoven e McGin , então decidi combinar as duas idéias. E desenvolva uma solução genérica para atender a todos os casos. Aqui está a minha solução proposta.

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

}
Sheikh Abdul Wahid
fonte
0

Fiz uma pequena adaptação na solução do Nicko para que não seja necessário usar a biblioteca Apache Commons BeanUtils e substituí-la pela solução já disponível na primavera, para aqueles que a utilizam, como posso ser mais simples:

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;

    }
}
Pedro Bacchini
fonte
-1

Solução solucionada com a pergunta: Como acessar um campo descrito na propriedade de anotação

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

E como usá-lo ...? Como isso:

@Entity
@EnableMatchConstraint
public class User {

    @NotBlank
    private String password;

    @Match(field = "password")
    private String passwordConfirmation;
}
zach
fonte