Validación de campo cruzado con Hibernate Validator (JSR 303)


236

¿Existe una implementación de (o implementación de terceros para) validación de campo cruzado en Hibernate Validator 4.x? Si no, ¿cuál es la forma más limpia de implementar un validador de campo cruzado?

Como ejemplo, ¿cómo puede usar la API para validar dos propiedades de bean? (Como validar un campo de contraseña coincide con el campo de verificación de contraseña).

En anotaciones, esperaría algo como:

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

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

1
Consulte stackoverflow.com/questions/2781771/… para obtener una solución segura de tipo y libre de reflejos API (imo más elegante) en el nivel de clase.
Karl Richter

Respuestas:


282

Cada restricción de campo debe ser manejada por una anotación de validador distinta, o en otras palabras, no es una práctica sugerida que la anotación de validación de un campo verifique contra otros campos; La validación de campo cruzado debe hacerse a nivel de clase. Además, la forma preferida de JSR-303 Sección 2.2 para expresar múltiples validaciones del mismo tipo es a través de una lista de anotaciones. Esto permite que el mensaje de error se especifique por partido.

Por ejemplo, validando una forma común:

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

La anotación:

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

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

8
@AndyT: Existe una dependencia externa de Apache Commons BeanUtils.
GaryF

77
@ScriptAssert no le permite crear un mensaje de validación con una ruta personalizada. context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()).addNode(secondFieldName).addConstraintViolation().disableDefaultConstraintViolation(); Da la posibilidad de resaltar el campo correcto (si solo JSF lo admitiera).
Peter Davis el

8
utilicé el ejemplo anterior pero no muestra un mensaje de error, ¿cuál es el enlace que debe estar en el jsp? Tengo un enlace para la contraseña y solo confirmo, ¿se necesita algo más? <formulario: contraseña ruta = "contraseña" /> <formulario: errores ruta = "contraseña" cssClass = "errorz" /> <formulario: contraseña ruta = "confirmar Contraseña" /> <formulario: errores ruta = "confirmar Contraseña" cssClass = " errorz "/>
Mahmoud Saleh

77
BeanUtils.getPropertydevuelve una cadena El ejemplo probablemente significaba usar PropertyUtils.getPropertyque devuelve un objeto.
SingleShot

2
Buena respuesta, pero la he completado con la respuesta a esta pregunta: stackoverflow.com/questions/11890334/…
maxivis

164

Te sugiero otra posible solución. ¡Quizás menos elegante, pero más 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);
  }
}

El isValidvalidador invoca el método automáticamente.


12
Creo que esto es una mezcla de preocupaciones nuevamente. El objetivo de Bean Validation es externalizar la validación en ConstraintValidators. En este caso, tiene parte de la lógica de validación en el propio bean y parte en el marco Validator. El camino a seguir es una restricción de nivel de clase. Hibernate Validator también ofrece ahora un @ScriptAssert que facilita la implementación de dependencias internas de bean.
Hardy

10
Yo diría que esto es más elegante, ¡no menos!
NickJ

8
Mi opinión hasta ahora es que el JSR de validación de frijoles es una mezcla de preocupaciones.
Dmitry Minkovsky

3
@GaneshKrishnan ¿Qué pasa si queremos tener varios @AssertTruemétodos similares ? ¿Se cumple alguna convención de nomenclatura?
Stephane

3
¿Por qué esta no es la mejor respuesta
Funky-nd

32

Me sorprende que esto no esté disponible de fábrica. De todos modos, aquí hay una posible solución.

He creado un validador de nivel de clase, no el nivel de campo como se describe en la pregunta original.

Aquí está el código de anotación:

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

Y el validador en sí mismo:

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

Tenga en cuenta que he usado MVEL para inspeccionar las propiedades del objeto que se valida. Esto podría reemplazarse con las API de reflexión estándar o, si está validando una clase específica, los métodos de acceso en sí.

La anotación @Matches se puede usar en un bean de la siguiente manera:

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

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

  ...
}

Como descargo de responsabilidad, escribí esto en los últimos 5 minutos, por lo que probablemente todavía no haya solucionado todos los errores. Actualizaré la respuesta si algo sale mal.


1
Esto es genial y está funcionando para mí, excepto que addNote está en desuso y obtengo AbstractMethodError si uso addPropertyNode en su lugar. Google no me está ayudando aquí. Cual es la solucion? ¿Falta alguna dependencia en alguna parte?
Paul Grenyer

29

Con Hibernate Validator 4.1.0.Final recomiendo usar @ScriptAssert . Excepto de su JavaDoc:

Las expresiones de script se pueden escribir en cualquier lenguaje de script o expresión, para lo cual se puede encontrar un motor compatible con JSR 223 ("Scripting for the JavaTM Platform") en el classpath.

Nota: la evaluación se realiza mediante un script " motor " de que se ejecuta en la máquina virtual Java, por lo tanto, en el "lado del servidor" de Java, no en el "lado del cliente" como se indica en algunos comentarios.

Ejemplo:

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

  private String passVerify;
}

o con alias más corto y nulo-seguro:

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

  private String passVerify;
}

o con Java 7+ null-safe 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;
}

Sin embargo, no hay nada de malo con una solución de validación de nivel de clase personalizada @Matches .


1
Solución interesante, ¿realmente estamos empleando JavaScript aquí para lograr esta validación? Eso parece excesivo para lo que una anotación basada en Java debería poder lograr. A mis ojos vírgenes, la solución de Nicko propuesta anteriormente todavía parece más limpia tanto desde el punto de vista de la usabilidad (su anotación es fácil de leer y bastante funcional frente a las referencias poco elegantes de javascript-> java), como desde el punto de vista de la escalabilidad (supongo que hay una sobrecarga razonable para manejar el javascript, pero ¿tal vez Hibernate está almacenando en caché el código compilado al menos?). Tengo curiosidad por entender por qué esto sería preferible.
David Parks

2
Estoy de acuerdo en que la implementación de Nicko es buena, pero no veo nada objetable sobre el uso de JS como lenguaje de expresión. Java 6 incluye Rhino para exactamente esas aplicaciones. Me gusta @ScriptAssert, ya que simplemente funciona sin tener que crear una anotación y un validador cada vez que tengo que realizar un nuevo tipo de prueba.

44
Como se dijo, no hay nada malo con el validador de nivel de clase. ScriptAssert es solo una alternativa que no requiere que escriba código personalizado. No dije que es la solución preferida ;-)
Hardy

Gran respuesta porque la confirmación de la contraseña no es una validación crítica, por lo tanto, se puede hacer del lado del cliente
peterchaula

19

Las validaciones de campos cruzados se pueden realizar creando restricciones personalizadas.

Ejemplo: - Compare la contraseña y confirme los campos Contraseña de la instancia de Usuario.

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

Restricción Validador Ayudante

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

Usuario

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

Prueba

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

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

Al usar la restricción de validación CompareStrings, también podemos comparar más de dos propiedades y podemos mezclar cualquiera de los cuatro métodos de comparación de cadenas.

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

Prueba

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

Salida Message:- Please choose three different colors.

Del mismo modo, podemos tener restricciones de validación de campos cruzados CompareNumbers, CompareDates, etc.

PD: No he probado este código en un entorno de producción (aunque lo probé en un entorno de desarrollo), así que considere este código como lanzamiento de Milestone. Si encuentra un error, por favor escriba un buen comentario. :)


Me gusta este enfoque, ya que es más flexible que los demás. Me permite validar más de 2 campos para la igualdad. ¡Buen trabajo!
Tauren

9

He probado el ejemplo de Alberthoven (hibernate-validator 4.0.2.GA) y obtengo una ValidationException: „Los métodos anotados deben seguir la convención de nomenclatura JavaBeans. match () no. "también. Después de cambiar el nombre del método de "partido" a "isValid", 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;
    }

}

Funcionó correctamente para mí pero no mostró el mensaje de error. ¿Funcionó y le mostró el mensaje de error? ¿Cómo?
Diminuto

1
@Tiny: el mensaje debe estar en las violaciones devueltas por el validador. (Escriba una prueba de Unidad: stackoverflow.com/questions/5704743/… ). PERO el mensaje de validación pertenece a la propiedad "isValid". Por lo tanto, el mensaje solo se mostrará en la GUI si la GUI muestra los problemas para retypedPassword AND isValid (junto a la contraseña retypeada).
Ralph

8

Si está utilizando Spring Framework, puede usar Spring Expression Language (SpEL) para eso. He escrito una pequeña biblioteca que proporciona el validador JSR-303 basado en SpEL, ¡hace que las validaciones de campo cruzado sean muy fáciles! Echa un vistazo a https://github.com/jirutka/validator-spring .

Esto validará la longitud y la igualdad de los campos de contraseña.

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

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

    private String passVerify;
}

También puede modificar esto fácilmente para validar los campos de contraseña solo cuando ambos no estén vacíos.

@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

Me gusta la idea de Jakub Jirutka. de usar Spring Expression Language. Si no desea agregar otra biblioteca / dependencia (suponiendo que ya usa Spring), aquí hay una implementación simplificada de su idea.

La restricción:

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

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

Aplicar de esta manera:

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

3

No tengo la reputación de comentar sobre la primera respuesta, pero quería agregar que he agregado pruebas unitarias para la respuesta ganadora y tengo las siguientes observaciones:

  • Si obtiene el nombre o los nombres de campo incorrectos, recibirá un error de validación como si los valores no coincidieran. No se tropiece con errores ortográficos, por ejemplo

@FieldMatch (first = " FieldName1 no válido, segundo =" validFieldName2 ")

  • El validador será aceptar tipos de datos equivalentes, es decir, estos todo pasa con FieldMatch:

private String stringField = "1";

entero privado integerField = nuevo entero (1)

private int intField = 1;

  • Si los campos son de un tipo de objeto que no implementa iguales, la validación fallará.

2

Muy buena solución Bradhouse. ¿Hay alguna forma de aplicar la anotación @Matches a más de un campo?

EDITAR: Aquí está la solución que se me ocurrió para responder a esta pregunta, modifiqué la restricción para aceptar una matriz en lugar de un solo 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;
}

El código para la anotación:

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

Y la implementación:

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

Hmm No estoy seguro. Puede intentar crear validadores específicos para cada campo de confirmación (para que tengan anotaciones diferentes) o actualizar la anotación @Matches para aceptar múltiples pares de campos.
Braddhouse

Gracias Bradhouse, se me ocurrió una solución y la he publicado arriba. Necesita un poco de trabajo para atender cuando se pasa un número diferente de argumentos para que no obtenga IndexOutOfBoundsExceptions, pero los conceptos básicos están ahí.
McGin

1

Necesitas llamarlo explícitamente. En el ejemplo anterior, bradhouse le ha dado todos los pasos para escribir una restricción personalizada.

Agregue este código en su clase de llamante.

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

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

en el caso anterior sería

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


1

Ustedes chicos son geniales Ideas realmente asombrosas. Me gustan más Alberthoven y McGin , así que decidí combinar ambas ideas. Y desarrolle alguna solución genérica para atender todos los casos. Aquí está mi solución propuesta.

@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

Hice una pequeña adaptación en la solución de Nicko para que no sea necesario usar la biblioteca Apache Commons BeanUtils y reemplazarla con la solución ya disponible en primavera, para aquellos que la usan, ya que puedo ser más simple:

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

Solución relacionada con la pregunta: Cómo acceder a un campo que se describe en la propiedad de anotación

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

Y cómo usarlo...? Me gusta esto:

@Entity
@EnableMatchConstraint
public class User {

    @NotBlank
    private String password;

    @Match(field = "password")
    private String passwordConfirmation;
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.