Feldübergreifende Validierung mit Hibernate Validator (JSR 303)

235

Gibt es eine Implementierung der feldübergreifenden Validierung (oder einer Implementierung durch einen Drittanbieter) in Hibernate Validator 4.x? Wenn nicht, wie lässt sich ein Cross-Field-Validator am saubersten implementieren?

Wie können Sie beispielsweise die API verwenden, um zu überprüfen, ob zwei Bean-Eigenschaften gleich sind (z. B. ob die Überprüfung eines Kennwortfelds mit dem Kennwortüberprüfungsfeld übereinstimmt)?

In Anmerkungen würde ich etwas erwarten wie:

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

  @Equals(property="pass")
  private String passVerify;
}
Bradhouse
quelle
1
Unter stackoverflow.com/questions/2781771/… finden Sie eine typsichere und reflexions-API-freie (imo elegantere) Lösung auf Klassenebene.
Karl Richter

Antworten:

282

Jede Feldbeschränkung sollte durch eine eigene Validator-Annotation behandelt werden. Mit anderen Worten, es wird nicht empfohlen, die Validierungs-Annotation eines Felds mit anderen Feldern zu vergleichen. Die feldübergreifende Validierung sollte auf Klassenebene erfolgen. Darüber hinaus wird in JSR-303, Abschnitt 2.2, die bevorzugte Methode zum Ausdrücken mehrerer Validierungen desselben Typs über eine Liste von Anmerkungen verwendet. Dadurch kann die Fehlermeldung pro Übereinstimmung angegeben werden.

Beispiel: Überprüfen eines allgemeinen Formulars:

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

Die Anmerkung:

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

Der Validator:

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
quelle
8
@AndyT: Es besteht eine externe Abhängigkeit von Apache Commons BeanUtils.
GaryF
7
Mit @ScriptAssert können Sie keine Validierungsnachricht mit einem benutzerdefinierten Pfad erstellen. context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()).addNode(secondFieldName).addConstraintViolation().disableDefaultConstraintViolation(); Gibt die Möglichkeit, das richtige Feld hervorzuheben (wenn nur JSF es unterstützen würde).
Peter Davis
8
Ich habe das obige Beispiel verwendet, aber es zeigt keine Fehlermeldung an. Wie soll die Bindung im JSP sein? Ich habe eine Bindung für das Passwort und bestätige nur, wird noch etwas benötigt? <form: password path = "password" /> <form: error path = "password" cssClass = "errorz" /> <form: password path = "verifyPassword" /> <form: error path = "verifyPassword" cssClass = " errorz "/>
Mahmoud Saleh
7
BeanUtils.getPropertygibt eine Zeichenfolge zurück. Das Beispiel sollte wahrscheinlich verwenden, PropertyUtils.getPropertywelches ein Objekt zurückgibt.
SingleShot
2
Schöne Antwort, aber ich habe sie mit der Antwort auf diese Frage abgeschlossen: stackoverflow.com/questions/11890334/…
maxivis
164

Ich schlage Ihnen eine andere mögliche Lösung vor. Vielleicht weniger elegant, aber einfacher!

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

Die isValidMethode wird vom Validator automatisch aufgerufen.

Alberthoven
quelle
12
Ich denke, dies ist wieder eine Mischung aus Bedenken. Der springende Punkt bei der Bean-Validierung ist die Externalisierung der Validierung in ConstraintValidators. In diesem Fall haben Sie einen Teil der Validierungslogik in der Bean selbst und einen Teil im Validator-Framework. Der Weg ist eine Einschränkung auf Klassenebene. Hibernate Validator bietet jetzt auch ein @ScriptAssert an, das die Implementierung von Bean-internen Abhängigkeiten erleichtert.
Hardy
10
Ich würde sagen , das ist mehr elegant, nicht weniger!
NickJ
8
Meine bisherige Meinung ist, dass der Bean Validation JSR eine Mischung aus Bedenken ist.
Dmitry Minkovsky
3
@GaneshKrishnan Was ist, wenn wir mehrere solcher @AssertTrueMethoden haben wollen? Einige Namenskonventionen gelten?
Stephane
3
Warum ist das nicht die beste Antwort
Funky-nd
32

Ich bin überrascht, dass dies nicht sofort verfügbar ist. Hier ist eine mögliche Lösung.

Ich habe einen Validator auf Klassenebene erstellt, nicht die Feldebene, wie in der ursprünglichen Frage beschrieben.

Hier ist der Anmerkungscode:

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

Und der Validator selbst:

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

Beachten Sie, dass ich MVEL verwendet habe, um die Eigenschaften des zu validierenden Objekts zu überprüfen. Dies könnte durch die Standardreflexions-APIs ersetzt werden, oder wenn es sich um eine bestimmte Klasse handelt, die Sie validieren, durch die Zugriffsmethoden selbst.

Die Annotation @Matches kann dann wie folgt für eine Bean verwendet werden:

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

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

  ...
}

Als Haftungsausschluss habe ich dies in den letzten 5 Minuten geschrieben, daher habe ich wahrscheinlich noch nicht alle Fehler ausgebügelt. Ich werde die Antwort aktualisieren, wenn etwas schief geht.

Bradhouse
quelle
1
Das ist großartig und funktioniert für mich, außer dass addNote veraltet ist und ich AbstractMethodError erhalte, wenn ich stattdessen addPropertyNode verwende. Google hilft mir hier nicht. Was ist die Lösung? Fehlt irgendwo eine Abhängigkeit?
Paul Grenyer
29

Mit Hibernate Validator 4.1.0.Final empfehle ich die Verwendung von @ScriptAssert . Ausgenommen von seinem JavaDoc:

Skriptausdrücke können in jeder Skript- oder Ausdruckssprache geschrieben werden, für die eine JSR 223- kompatible Engine ("Scripting für die JavaTM-Plattform") im Klassenpfad vorhanden ist.

Hinweis: Die Auswertung wird von einer Skript- Engine durchgeführt, die in der Java-VM ausgeführt wird, daher auf der Java-Serverseite und nicht auf der Clientseite, wie in einigen Kommentaren angegeben.

Beispiel:

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

  private String passVerify;
}

oder mit kürzerem Alias ​​und 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;
}

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

Trotzdem ist an einer benutzerdefinierten @ Levels- Lösung für Validator auf Klassenebene nichts auszusetzen .

Winterhart
quelle
1
Interessante Lösung, setzen wir hier wirklich Javascript ein, um diese Validierung durchzuführen? Das scheint ein Overkill für das zu sein, was eine Java-basierte Annotation leisten kann. Für meine jungfräulichen Augen scheint die oben vorgeschlagene Lösung von Nicko sowohl unter dem Gesichtspunkt der Benutzerfreundlichkeit (seine Anmerkung ist leicht zu lesen und recht funktionell im Vergleich zu uneleganten Javascript-> Java-Referenzen) als auch unter dem Gesichtspunkt der Skalierbarkeit (ich gehe davon aus, dass ein angemessener Aufwand erforderlich ist) immer noch sauberer zu sein Behandeln Sie das Javascript, aber vielleicht speichert Hibernate den kompilierten Code zumindest zwischen?). Ich bin gespannt, warum dies bevorzugt wird.
David Parks
2
Ich bin damit einverstanden, dass die Implementierung von Nicko nett ist, aber ich sehe nichts Unangenehmes daran, JS als Ausdruckssprache zu verwenden. Java 6 enthält Rhino für genau solche Anwendungen. Ich mag @ScriptAssert, da es einfach funktioniert, ohne dass ich jedes Mal eine Anmerkung und einen Validator erstellen muss, wenn ich einen neuartigen Test durchführen muss.
4
Wie gesagt, mit dem Klassenprüfer ist nichts falsch. ScriptAssert ist nur eine Alternative, bei der Sie keinen benutzerdefinierten Code schreiben müssen. Ich habe nicht gesagt, dass es die bevorzugte Lösung ist ;-)
Hardy
Gute
19

Feldübergreifende Validierungen können durch Erstellen benutzerdefinierter Einschränkungen durchgeführt werden.

Beispiel: - Vergleichen Sie die Felder password und verifyPassword der Benutzerinstanz.

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

Benutzer

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

Prüfung

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

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

Mithilfe der CompareStrings-Validierungsbeschränkung können wir auch mehr als zwei Eigenschaften vergleichen und eine von vier Zeichenfolgenvergleichsmethoden mischen.

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

Prüfung

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

Ausgabe Message:- Please choose three different colors.

In ähnlicher Weise können wir CompareNumbers, CompareDates usw. für die Validierung von Feldern verwenden.

PS Ich habe diesen Code nicht in einer Produktionsumgebung getestet (obwohl ich ihn in einer Entwicklungsumgebung getestet habe). Betrachten Sie diesen Code daher als Milestone Release. Wenn Sie einen Fehler finden, schreiben Sie bitte einen schönen Kommentar. :) :)

Dira
quelle
Ich mag diesen Ansatz, da er flexibler ist als die anderen. Damit kann ich mehr als 2 Felder auf Gleichheit überprüfen. Gut gemacht!
Tauren
9

Ich habe Alberthovens Beispiel (Hibernate-Validator 4.0.2.GA) ausprobiert und erhalte eine ValidationException: „Kommentierte Methoden müssen der JavaBeans-Namenskonvention folgen. match () nicht. “auch. Nachdem ich die Methode von "match" in "isValid" umbenannt habe, funktioniert es.

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
quelle
Es hat bei mir richtig funktioniert, aber die Fehlermeldung nicht angezeigt. Hat es funktioniert und die Fehlermeldung für Sie angezeigt. Wie?
Winzig
1
@Tiny: Die Nachricht sollte sich in den vom Validator zurückgegebenen Verstößen befinden. (Schreiben Sie einen Unit-Test: stackoverflow.com/questions/5704743/… ). ABER die Validierungsnachricht gehört zur Eigenschaft "isValid". Daher wird die Nachricht nur in der GUI angezeigt, wenn in der GUI die Probleme für retypedPassword AND isValid (neben retyped Password) angezeigt werden.
Ralph
8

Wenn Sie das Spring Framework verwenden, können Sie dafür die Spring Expression Language (SpEL) verwenden. Ich habe eine kleine Bibliothek geschrieben, die einen JSR-303-Validator basierend auf SpEL bereitstellt - das macht feldübergreifende Validierungen zum Kinderspiel! Schauen Sie sich https://github.com/jirutka/validator-spring an .

Dadurch werden Länge und Gleichheit der Kennwortfelder überprüft.

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

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

    private String passVerify;
}

Sie können dies auch leicht ändern, um die Kennwortfelder nur dann zu überprüfen, wenn nicht beide leer sind.

@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
quelle
4

Ich mag die Idee von Jakub Jirutka , Spring Expression Language zu verwenden. Wenn Sie keine weitere Bibliothek / Abhängigkeit hinzufügen möchten (vorausgesetzt, Sie verwenden Spring bereits), finden Sie hier eine vereinfachte Implementierung seiner Idee.

Die Einschränkung:

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

Der Validator:

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

Bewerben Sie sich wie folgt:

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

Ich habe nicht den Ruf, die erste Antwort zu kommentieren, wollte aber hinzufügen, dass ich Unit-Tests für die Gewinnerantwort hinzugefügt habe und die folgenden Beobachtungen habe:

  • Wenn der Vorname oder der Feldname falsch ist, wird ein Validierungsfehler angezeigt, als ob die Werte nicht übereinstimmen. Lassen Sie sich nicht durch Rechtschreibfehler stolpern, z

@FieldMatch (first = " ungültiger Feldname1", zweiter = "validFieldName2")

  • Der Validator wird akzeptieren gleichwertige Datentypen , dh diese werden alle Pass mit FieldMatch:

private String stringField = "1";

private Integer integerField = new Integer (1)

private int intField = 1;

  • Wenn die Felder von einem Objekttyp sind, der nicht gleich implementiert, schlägt die Validierung fehl.
Chanoch
quelle
2

Sehr schöne Lösung Bradhouse. Gibt es eine Möglichkeit, die Annotation @Matches auf mehrere Felder anzuwenden?

BEARBEITEN: Hier ist die Lösung, die ich zur Beantwortung dieser Frage gefunden habe. Ich habe die Einschränkung so geändert, dass sie ein Array anstelle eines einzelnen Werts akzeptiert:

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

Der Code für die Anmerkung:

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

Und die Umsetzung:

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
quelle
Hmm. Nicht sicher. Sie können versuchen, entweder spezifische Validatoren für jedes Bestätigungsfeld zu erstellen (damit diese unterschiedliche Anmerkungen haben) oder die @ Matches-Anmerkung zu aktualisieren, um mehrere Feldpaare zu akzeptieren.
Bradhouse
Danke Bradhouse, habe eine Lösung gefunden und sie oben gepostet. Es ist ein wenig Arbeit erforderlich, um zu berücksichtigen, wenn eine unterschiedliche Anzahl von Argumenten übergeben wird, sodass Sie keine IndexOutOfBoundsExceptions erhalten, aber die Grundlagen sind vorhanden.
McGin
1

Sie müssen es explizit aufrufen. Im obigen Beispiel hat Bradhouse Ihnen alle Schritte zum Schreiben einer benutzerdefinierten Einschränkung gegeben.

Fügen Sie diesen Code Ihrer Anruferklasse hinzu.

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

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

im obigen Fall wäre es

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

Warum probieren Sie nicht Oval: http://oval.sourceforge.net/

Ich sehe so aus, als würde es OGNL unterstützen, also könnten Sie es vielleicht natürlicher machen

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

Ihr seid fantastisch. Wirklich erstaunliche Ideen. Ich mag Alberthovens und McGins am meisten, deshalb habe ich beschlossen, beide Ideen zu kombinieren. Und entwickeln Sie eine generische Lösung für alle Fälle. Hier ist meine vorgeschlagene Lösung.

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

}
Scheich Abdul Wahid
quelle
0

Ich habe eine kleine Anpassung in Nickos Lösung vorgenommen, damit es nicht erforderlich ist, die Apache Commons BeanUtils-Bibliothek zu verwenden und sie durch die bereits im Frühjahr verfügbare Lösung zu ersetzen, für diejenigen, die sie verwenden, da ich einfacher sein kann:

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
quelle
-1

Mit der Frage realisierte Lösung: Zugriff auf ein Feld, das in der Annotationseigenschaft beschrieben ist

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

Und wie benutzt man es ...? So was:

@Entity
@EnableMatchConstraint
public class User {

    @NotBlank
    private String password;

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