Validation entre champs avec Hibernate Validator (JSR 303)


236

Existe-t-il une implémentation (ou une implémentation tierce pour) de la validation entre champs dans Hibernate Validator 4.x? Sinon, quelle est la façon la plus propre de mettre en œuvre un validateur de champ croisé?

Par exemple, comment pouvez-vous utiliser l'API pour valider deux propriétés de bean sont égales (comme la validation d'un champ de mot de passe correspond au champ de vérification de mot de passe).

Dans les annotations, je m'attendrais à quelque chose comme:

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

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

1
Voir stackoverflow.com/questions/2781771/… pour une solution sans type et sans API de réflexion (imo plus élégante) au niveau de la classe.
Karl Richter

Réponses:


282

Chaque contrainte de champ doit être gérée par une annotation de validateur distincte, ou en d'autres termes, il n'est pas recommandé de vérifier l'annotation de validation d'un champ par rapport à d'autres champs; la validation entre champs doit être effectuée au niveau de la classe. De plus, la méthode préférée JSR-303 Section 2.2 pour exprimer plusieurs validations du même type est via une liste d'annotations. Cela permet au message d'erreur d'être spécifié par correspondance.

Par exemple, valider un formulaire commun:

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

L'annotation:

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

Le validateur:

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: Il existe une dépendance externe à Apache Commons BeanUtils.
GaryF

7
@ScriptAssert ne vous permet pas de créer un message de validation avec un chemin personnalisé. context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()).addNode(secondFieldName).addConstraintViolation().disableDefaultConstraintViolation(); Donne la possibilité de mettre en évidence le bon champ (si seulement JSF le soutenait).
Peter Davis

8
J'ai utilisé l'exemple ci-dessus mais il n'affiche pas de message d'erreur, quelle doit être la liaison dans le jsp? J'ai une liaison pour le mot de passe et confirme uniquement, y a-t-il autre chose nécessaire? <form: password path = "password" /> <form: errors path = "password" cssClass = "errorz" /> <form: password path = "confirmPassword" /> <form: errors path = "confirmPassword" cssClass = " errorz "/>
Mahmoud Saleh

7
BeanUtils.getPropertyrenvoie une chaîne. L'exemple visait probablement à utiliser PropertyUtils.getPropertyce qui renvoie un objet.
SingleShot

2
Belle réponse, mais je l'ai complétée par la réponse à cette question: stackoverflow.com/questions/11890334/…
maxivis

164

Je vous propose une autre solution possible. Peut-être moins élégant, mais plus facile!

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

La isValidméthode est invoquée automatiquement par le validateur.


12
Je pense que c'est à nouveau un mélange de préoccupations. L'intérêt de Bean Validation est d'externaliser la validation en ConstraintValidators. Dans ce cas, vous avez une partie de la logique de validation dans le bean lui-même et une partie dans le framework Validator. La voie à suivre est une contrainte au niveau de la classe. Hibernate Validator propose également désormais un @ScriptAssert qui facilite l'implémentation des dépendances internes au bean.
Hardy

10
Je dirais que c'est plus élégant, pas moins!
NickJ

8
Jusqu'à présent, mon opinion est que le JSR de validation du bean est un mélange de préoccupations.
Dmitry Minkovsky

3
@GaneshKrishnan Et si nous voulons avoir plusieurs @AssertTrueméthodes de ce type? Une convention de dénomination est valable?
Stéphane

3
pourquoi est-ce pas la meilleure réponse
funky-nd

32

Je suis surpris que ce ne soit pas prêt à l'emploi. Quoi qu'il en soit, voici une solution possible.

J'ai créé un validateur de niveau classe, pas le niveau de champ comme décrit dans la question d'origine.

Voici le code d'annotation:

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

Et le validateur lui-même:

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

Notez que j'ai utilisé MVEL pour inspecter les propriétés de l'objet en cours de validation. Cela pourrait être remplacé par les API de réflexion standard ou s'il s'agit d'une classe spécifique que vous validez, les méthodes d'accesseur elles-mêmes.

L'annotation @Matches peut ensuite être utilisée sur un bean comme suit:

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

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

  ...
}

Comme avertissement, j'ai écrit cela au cours des 5 dernières minutes, donc je n'ai probablement pas encore résolu tous les bugs. Je mettrai à jour la réponse en cas de problème.


1
C'est génial et cela fonctionne pour moi, sauf que addNote est obsolète et j'obtiens AbstractMethodError si j'utilise à la place addPropertyNode. Google ne m'aide pas ici. Quelle est la solution? Y a-t-il une dépendance manquante quelque part?
Paul Grenyer

29

Avec Hibernate Validator 4.1.0.Final, je recommande d'utiliser @ScriptAssert . Exceprt de sa JavaDoc:

Les expressions de script peuvent être écrites dans n'importe quel langage de script ou d'expression, pour lequel un moteur compatible JSR 223 («Scripting for the JavaTM Platform») peut être trouvé sur le chemin de classe.

Remarque: l'évaluation est effectuée par un " moteur " de script s'exécutant dans la machine virtuelle Java, donc côté Java "côté serveur", et non pas "côté client" comme indiqué dans certains commentaires.

Exemple:

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

  private String passVerify;
}

ou avec un alias plus court et null-safe:

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

Néanmoins, il n'y a rien de mal avec une solution @Matches de validateur de niveau de classe personnalisé .


1
Solution intéressante, utilisons-nous vraiment javascript ici pour effectuer cette validation? Cela semble exagéré pour ce qu'une annotation basée sur java devrait être capable d'accomplir. À mes yeux vierges, la solution proposée par Nicko ci-dessus semble toujours plus propre à la fois du point de vue de l'utilisabilité (son annotation est facile à lire et assez fonctionnelle par rapport aux références javascript-> java inélégantes), et du point de vue de l'évolutivité (je suppose qu'il y a des frais généraux raisonnables pour gérer le javascript, mais peut-être qu'Hibernate met au moins en cache le code compilé?). Je suis curieux de comprendre pourquoi cela serait préféré.
David Parks du

2
Je suis d'accord que l'implémentation de Nicko est agréable, mais je ne vois rien de répréhensible à utiliser JS comme langage d'expression. Java 6 inclut Rhino pour exactement de telles applications. J'aime @ScriptAssert car il fonctionne sans que je doive créer une annotation et un validateur chaque fois que j'ai un nouveau type de test à effectuer.

4
Comme dit, rien ne cloche avec le validateur de niveau classe. ScriptAssert est juste une alternative qui ne vous oblige pas à écrire du code personnalisé. Je n'ai pas dit que c'était la solution préférée ;-)
Hardy

Excellente réponse car la confirmation du mot de passe n'est pas une validation critique, elle peut donc être effectuée du côté client
peterchaula

19

Les validations entre champs peuvent être effectuées en créant des contraintes personnalisées.

Exemple: - Comparez les champs mot de passe et confirmPassword de l'instance utilisateur.

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

Utilisateur

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

Tester

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

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

En utilisant la contrainte de validation CompareStrings, nous pouvons également comparer plus de deux propriétés et nous pouvons mélanger l'une des quatre méthodes de comparaison de chaînes.

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

Tester

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

Production Message:- Please choose three different colors.

De même, nous pouvons avoir des contraintes de validation inter-champs CompareNumbers, CompareDates, etc.

PS Je n'ai pas testé ce code dans un environnement de production (bien que je l'ai testé dans un environnement de développement), alors considérez ce code comme une version Milestone. Si vous trouvez un bug, merci d'écrire un joli commentaire. :)


J'aime cette approche, car elle est plus flexible que les autres. Cela me permet de valider plus de 2 champs pour l'égalité. Bon travail!
Tauren

9

J'ai essayé l'exemple d'Alberthoven (hibernate-validator 4.0.2.GA) et j'obtiens une ValidationException: „Les méthodes annotées doivent suivre la convention de nommage JavaBeans. match () ne fonctionne pas ". Après avoir renommé la méthode de «match» en «isValid», cela fonctionne.

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

}

Cela a fonctionné correctement pour moi mais n'a pas affiché le message d'erreur. Cela a-t-il fonctionné et affiché le message d'erreur pour vous? Comment?
Minuscule

1
@Tiny: Le message doit figurer dans les violations renvoyées par le validateur. (Écrivez un test unitaire: stackoverflow.com/questions/5704743/… ). MAIS le message de validation appartient à la propriété "isValid". Par conséquent, le message ne sera affiché dans l'interface graphique que si l'interface graphique affiche les problèmes pour retypedPassword ET isValid (à côté de Password retypé).
Ralph

8

Si vous utilisez Spring Framework, vous pouvez utiliser le Spring Expression Language (SpEL) pour cela. J'ai écrit une petite bibliothèque qui fournit un validateur JSR-303 basé sur SpEL - cela rend les validations inter-champs un jeu d'enfant! Jetez un œil à https://github.com/jirutka/validator-spring .

Cela validera la longueur et l'égalité des champs de mot de passe.

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

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

    private String passVerify;
}

Vous pouvez également le modifier facilement pour valider les champs de mot de passe uniquement lorsqu'ils ne sont pas tous les deux vides.

@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

J'aime l'idée de Jakub Jirutka d'utiliser Spring Expression Language. Si vous ne souhaitez pas ajouter une autre bibliothèque / dépendance (en supposant que vous utilisez déjà Spring), voici une implémentation simplifiée de son idée.

La contrainte:

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

Le validateur:

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

Appliquer comme ceci:

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

3

Je n'ai pas la réputation de commenter la première réponse, mais je voulais ajouter que j'ai ajouté des tests unitaires pour la réponse gagnante et que j'ai les observations suivantes:

  • Si vous vous trompez le premier ou les noms de champ, vous obtenez une erreur de validation comme si les valeurs ne correspondent pas. Ne vous laissez pas tromper par des fautes d'orthographe, par exemple

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

  • Le validateur va accepter les types de données équivalentes à savoir que toutes ces stratégies passe avec FieldMatch:

chaîne privée stringField = "1";

private Integer integerField = new Integer (1)

privé int intField = 1;

  • Si les champs sont d'un type d'objet qui n'implémente pas égal, la validation échouera.

2

Très belle solution bradhouse. Existe-t-il un moyen d'appliquer l'annotation @Matches à plusieurs champs?

EDIT: Voici la solution que j'ai trouvée pour répondre à cette question, j'ai modifié la contrainte pour accepter un tableau au lieu d'une seule valeur:

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

Le code de l'annotation:

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

Et la mise en œuvre:

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. Pas certain. Vous pouvez essayer de créer des validateurs spécifiques pour chaque champ de confirmation (afin qu'ils aient des annotations différentes) ou de mettre à jour l'annotation @Matches pour accepter plusieurs paires de champs.
bradhouse

Merci bradhouse, a trouvé une solution et l'a affichée ci-dessus. Il faut un peu de travail pour répondre lorsque différents nombres d'arguments sont passés afin que vous n'obteniez pas IndexOutOfBoundsExceptions, mais les bases sont là.
McGin

1

Vous devez l'appeler explicitement. Dans l'exemple ci-dessus, bradhouse vous a donné toutes les étapes pour écrire une contrainte personnalisée.

Ajoutez ce code dans votre classe d'appelant.

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

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

dans le cas ci-dessus, il serait

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

1

Pourquoi ne pas essayer Oval: http://oval.sourceforge.net/

On dirait qu'il prend en charge OGNL alors peut-être que vous pourriez le faire de manière plus naturelle

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

1

Vous êtes géniaux les gars. Des idées vraiment incroyables. J'aime le plus Alberthoven et McGin , j'ai donc décidé de combiner les deux idées. Et développer une solution générique pour répondre à tous les cas. Voici ma solution proposée.

@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

J'ai fait une petite adaptation dans la solution de Nicko pour qu'il ne soit pas nécessaire d'utiliser la bibliothèque Apache Commons BeanUtils et de la remplacer par la solution déjà disponible au printemps, pour ceux qui l'utilisent car je peux être plus 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

Solution réalisée avec une question: comment accéder à un champ qui est décrit dans la propriété d'annotation

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

Et comment l'utiliser ...? Comme ça:

@Entity
@EnableMatchConstraint
public class User {

    @NotBlank
    private String password;

    @Match(field = "password")
    private String passwordConfirmation;
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.