Convalida cross field con Hibernate Validator (JSR 303)


236

Esiste un'implementazione di (o implementazione di terze parti per) la validazione cross field in Hibernate Validator 4.x? In caso contrario, qual è il modo più pulito per implementare un validatore cross field?

Ad esempio, come è possibile utilizzare l'API per convalidare due proprietà del bean sono uguali (ad esempio la convalida di un campo password corrisponde al campo di verifica password).

Nelle annotazioni, mi aspetto qualcosa di simile:

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

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

1
Vedi stackoverflow.com/questions/2781771/… per una soluzione sicura e senza API (più elegante) a livello di classe.
Karl Richter,

Risposte:


282

Ogni vincolo di campo deve essere gestito da un'annotazione di convalida distinta o, in altre parole, non è consigliabile fare in modo che l'annotazione di convalida di un campo venga verificata rispetto ad altri campi; la convalida tra campi deve essere effettuata a livello di classe. Inoltre, JSR-303 Sezione 2.2 modo preferito dalla per esprimere convalide multiple dello stesso tipo è tramite un elenco di annotazioni. Ciò consente di specificare il messaggio di errore per corrispondenza.

Ad esempio, convalidando un modulo comune:

@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'annotazione:

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

Il validatore:

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: esiste una dipendenza esterna da Apache Commons BeanUtils.
GaryF,

7
@ScriptAssert non ti consente di creare un messaggio di convalida con un percorso personalizzato. context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()).addNode(secondFieldName).addConstraintViolation().disableDefaultConstraintViolation(); Offre la possibilità di evidenziare il campo giusto (se solo JSF lo supportasse).
Peter Davis,

8
ho usato sopra il campione ma non visualizza il messaggio di errore, qual è l'associazione dovrebbe essere nel jsp? ho un vincolo per la password e confermo solo, c'è qualcos'altro necessario? <form: password path = "password" /> <form: errors path = "password" cssClass = "errorz" /> <form: password path = "confirmPassword" /> <form: errors path = "confirmPassword" cssClass = " errorz "/>
Mahmoud Saleh,

7
BeanUtils.getPropertyrestituisce una stringa. L'esempio probabilmente intendeva usare ciò PropertyUtils.getPropertyche restituisce un oggetto.
SingleShot,

2
Bella risposta, ma ho completato con la risposta a questa domanda: stackoverflow.com/questions/11890334/...
maxivis

164

Ti suggerisco un'altra possibile soluzione. Forse meno elegante, ma più 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);
  }
}

Il isValidmetodo viene invocato automaticamente dal validatore.


12
Penso che questo sia di nuovo un misto di preoccupazioni. Il punto centrale di Bean Validation è esternalizzare la convalida in ConstraintValidators. In questo caso hai parte della logica di validazione nel bean stesso e parte nel framework Validator. La strada da percorrere è un vincolo a livello di classe. Hibernate Validator offre ora anche un @ScriptAssert che semplifica l'implementazione delle dipendenze interne del bean.
Hardy,

10
Direi che questo è più elegante, non meno!
NickJ,

8
La mia opinione finora è che Bean Validation JSR è un misto di preoccupazioni.
Dmitry Minkovsky,

3
@GaneshKrishnan E se volessimo avere diversi @AssertTruemetodi di questo tipo? Qualche convenzione sui nomi vale?
Stephane,

3
perché questa non è la risposta migliore
funky-

32

Sono sorpreso che questo non sia disponibile immediatamente. Comunque, ecco una possibile soluzione.

Ho creato un validatore a livello di classe, non il livello di campo come descritto nella domanda originale.

Ecco il codice di annotazione:

package com.moa.podium.util.constraints;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = MatchesValidator.class)
@Documented
public @interface Matches {

  String message() default "{com.moa.podium.util.constraints.matches}";

  Class<?>[] groups() default {};

  Class<? extends Payload>[] payload() default {};

  String field();

  String verifyField();
}

E lo stesso validatore:

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

Nota che ho usato MVEL per ispezionare le proprietà dell'oggetto da convalidare. Questo potrebbe essere sostituito con le API di riflessione standard o se si tratta di una classe specifica che si sta convalidando, i metodi di accesso stessi.

L'annotazione @Matches può quindi essere utilizzata su un bean come segue:

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

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

  ...
}

Come disclaimer, l'ho scritto negli ultimi 5 minuti, quindi probabilmente non ho ancora risolto tutti i bug. Aggiornerò la risposta se qualcosa va storto.


1
Questo è fantastico e funziona per me, tranne per il fatto che addNote è deprecato e ottengo AbstractMethodError se uso invece addPropertyNode. Google non mi sta aiutando qui. Qual è la soluzione? C'è una dipendenza che manca da qualche parte?
Paul Grenyer,

29

Con Hibernate Validator 4.1.0.Final raccomando di usare @ScriptAssert . Exceprt dal suo JavaDoc:

Le espressioni di script possono essere scritte in qualsiasi linguaggio di script o di espressioni, per il quale un motore compatibile con JSR 223 ("Scripting for the JavaTM Platform") è disponibile sul percorso di classe.

Nota: la valutazione viene eseguita da un " motore " di scripting in esecuzione nella VM Java, quindi sul "lato server" Java, non sul "lato client", come indicato in alcuni commenti.

Esempio:

@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 più breve e 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;
}

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

Tuttavia, non c'è nulla di sbagliato in una soluzione @Matches di validatore a livello di classe personalizzata .


1
Soluzione interessante, stiamo davvero impiegando javascript qui per realizzare questa validazione? Sembra eccessivo per ciò che un'annotazione basata su Java dovrebbe essere in grado di realizzare. Ai miei occhi vergini la soluzione di Nicko proposta sopra sembra ancora più pulita sia dal punto di vista dell'usabilità (la sua annotazione è facile da leggere e abbastanza funzionale rispetto ai riferimenti javascript-> java ineleganti), sia dal punto di vista della scalabilità (suppongo che ci sia un ragionevole sovraccarico per gestire il javascript, ma forse Hibernate sta memorizzando nella cache almeno il codice compilato?). Sono curioso di capire perché questo sarebbe preferito.
David Parks,

2
Concordo sul fatto che l'implementazione di Nicko sia buona, ma non vedo nulla di discutibile sull'uso di JS come linguaggio di espressione. Java 6 include Rhino per esattamente tali applicazioni. Mi piace @ScriptAssert perché funziona senza che debba creare un'annotazione e un validatore ogni volta che ho un nuovo tipo di test da eseguire.

4
Come detto, non c'è nulla di sbagliato nel validatore a livello di classe. ScriptAssert è solo un'alternativa che non richiede di scrivere codice personalizzato. Non ho detto che è la soluzione preferita ;-)
Hardy

Ottima risposta perché la conferma della password non è una convalida critica, quindi può essere eseguita sul lato client
peterchaula

19

Le convalide dei campi incrociati possono essere eseguite creando vincoli personalizzati.

Esempio: - Confronta i campi password e confirmPassword dell'istanza utente.

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

Utente

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

Test

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

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

Utilizzando il vincolo di convalida CompareStrings, possiamo anche confrontare più di due proprietà e possiamo combinare uno dei quattro metodi di confronto delle stringhe.

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

Test

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

Produzione Message:- Please choose three different colors.

Allo stesso modo, possiamo avere vincoli di convalida cross-field, CompareDumbers, CompareDates, ecc.

PS Non ho testato questo codice in ambiente di produzione (anche se l'ho testato in ambiente di sviluppo), quindi considera questo codice come Milestone Release. Se trovi un bug, ti preghiamo di scrivere un bel commento. :)


Mi piace questo approccio, in quanto è più flessibile degli altri. Mi consente di convalidare più di 2 campi per l'uguaglianza. Bel lavoro!
Tauren,

9

Ho provato l'esempio di Alberthoven (hibernate-validator 4.0.2.GA) e ottengo una ValidationException: „I metodi annotati devono seguire la convenzione di denominazione JavaBeans. match () no. “troppo. Dopo aver rinominato il metodo da "match" a "isValid" funziona.

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

}

Ha funzionato correttamente per me ma non ha visualizzato il messaggio di errore. Ha funzionato e visualizzato il messaggio di errore per te. Come?
Piccolo

1
@Tiny: il messaggio dovrebbe trovarsi nelle violazioni restituite dal validatore. (Scrivi un test unitario: stackoverflow.com/questions/5704743/… ). MA il messaggio di convalida appartiene alla proprietà "isValid". Pertanto, il messaggio verrà visualizzato nella GUI solo se la GUI mostra i problemi per retypedPassword AND isValid (accanto alla password ricodificata).
Ralph,

8

Se si utilizza Spring Framework, è possibile utilizzare Spring Expression Language (SpEL) per questo. Ho scritto una piccola libreria che fornisce validatore JSR-303 basato su SpEL - rende le convalide cross-field un gioco da ragazzi! Dai un'occhiata a https://github.com/jirutka/validator-spring .

Ciò convaliderà la lunghezza e l'uguaglianza dei campi della password.

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

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

    private String passVerify;
}

Puoi anche modificarlo facilmente per convalidare i campi della password solo quando non sono entrambi vuoti.

@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

Mi piace l'idea di Jakub Jirutka di usare Spring Expression Language. Se non vuoi aggiungere un'altra libreria / dipendenza (supponendo che tu usi già Spring), ecco un'implementazione semplificata della sua idea.

Il vincolo:

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

Il validatore:

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

Applicare in questo modo:

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

3

Non ho la reputazione di commentare la prima risposta, ma volevo aggiungere che ho aggiunto test unitari per la risposta vincente e ho le seguenti osservazioni:

  • Se il nome del campo o dei nomi viene errato, viene visualizzato un errore di convalida come se i valori non corrispondessero. Non farti inciampare da errori di ortografia, ad es

@FieldMatch (prima =" invalido FieldName1" , secondo = "validFieldName2")

  • Il validatore si accetterà i tipi di dati equivalenti cioè questi saranno tutti passaggio con FieldMatch:

private String stringField = "1";

intero intero privato Campo = nuovo intero (1)

private int intField = 1;

  • Se i campi sono di un tipo di oggetto che non implementa uguale, la validazione fallirà.

2

Soluzione molto bella bradhouse. Esiste un modo per applicare l'annotazione @Matches a più di un campo?

EDIT: Ecco la soluzione che ho trovato per rispondere a questa domanda, ho modificato il Vincolo per accettare un array anziché un singolo valore:

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

Il codice per l'annotazione:

package springapp.util.constraints;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = MatchesValidator.class)
@Documented
public @interface Matches {

  String message() default "{springapp.util.constraints.matches}";

  Class<?>[] groups() default {};

  Class<? extends Payload>[] payload() default {};

  String[] fields();

  String[] verifyFields();
}

E l'implementazione:

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. Non sono sicuro. Puoi provare a creare validatori specifici per ciascun campo di conferma (quindi hanno annotazioni diverse) o aggiornare l'annotazione @Matches per accettare più coppie di campi.
Bradhouse,

Grazie Bradhouse, ho trovato una soluzione e l'ho pubblicata sopra. Ha bisogno di un po 'di lavoro per soddisfare quando viene passato un numero diverso di argomenti in modo da non ottenere IndexOutOfBoundsExceptions, ma le basi ci sono.
McGin,

1

Devi chiamarlo esplicitamente. Nell'esempio sopra, bradhouse ti ha fornito tutti i passaggi per scrivere un vincolo personalizzato.

Aggiungi questo codice nella classe del chiamante.

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

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

nel caso sopra sarebbe

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


1

Voi ragazzi siete fantastici. Idee davvero straordinarie. Mi piacciono di più quelli di Alberthoven e McGin , quindi ho deciso di combinare entrambe le idee. E sviluppare una soluzione generica per soddisfare tutti i casi. Ecco la mia soluzione proposta.

@Documented
@Constraint(validatedBy = NotFalseValidator.class)
@Target({ElementType.METHOD, ElementType.FIELD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface NotFalse {


    String message() default "NotFalse";
    String[] messages();
    String[] properties();
    String[] verifiers();

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

}

public class NotFalseValidator implements ConstraintValidator<NotFalse, Object> {
    private String[] properties;
    private String[] messages;
    private String[] verifiers;
    @Override
    public void initialize(NotFalse flag) {
        properties = flag.properties();
        messages = flag.messages();
        verifiers = flag.verifiers();
    }

    @Override
    public boolean isValid(Object bean, ConstraintValidatorContext cxt) {
        if(bean == null) {
            return true;
        }

        boolean valid = true;
        BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(bean);

        for(int i = 0; i< properties.length; i++) {
           Boolean verified = (Boolean) beanWrapper.getPropertyValue(verifiers[i]);
           valid &= isValidProperty(verified,messages[i],properties[i],cxt);
        }

        return valid;
    }

    boolean isValidProperty(Boolean flag,String message, String property, ConstraintValidatorContext cxt) {
        if(flag == null || flag) {
            return true;
        } else {
            cxt.disableDefaultConstraintViolation();
            cxt.buildConstraintViolationWithTemplate(message)
                    .addPropertyNode(property)
                    .addConstraintViolation();
            return false;
        }

    }



}

@NotFalse(
        messages = {"End Date Before Start Date" , "Start Date Before End Date" } ,
        properties={"endDateTime" , "startDateTime"},
        verifiers = {"validDateRange" , "validDateRange"})
public class SyncSessionDTO implements ControllableNode {
    @NotEmpty @NotPastDate
    private Date startDateTime;

    @NotEmpty
    private Date endDateTime;



    public Date getStartDateTime() {
        return startDateTime;
    }

    public void setStartDateTime(Date startDateTime) {
        this.startDateTime = startDateTime;
    }

    public Date getEndDateTime() {
        return endDateTime;
    }

    public void setEndDateTime(Date endDateTime) {
        this.endDateTime = endDateTime;
    }


    public Boolean getValidDateRange(){
        if(startDateTime != null && endDateTime != null) {
            return startDateTime.getTime() <= endDateTime.getTime();
        }

        return null;
    }

}

0

Ho fatto un piccolo adattamento nella soluzione di Nicko in modo che non sia necessario utilizzare la libreria BeanUtils di Apache Commons e sostituirla con la soluzione già disponibile in primavera, per coloro che la usano come posso essere più semplice:

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

Soluzione realizzata con una domanda: come accedere a un campo descritto nella proprietà 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;
    }
}

E come usarlo ...? Come questo:

@Entity
@EnableMatchConstraint
public class User {

    @NotBlank
    private String password;

    @Match(field = "password")
    private String passwordConfirmation;
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.