Convalida JSR 303, se un campo è uguale a "qualcosa", questi altri campi non devono essere nulli


92

Sto cercando di fare una piccola convalida personalizzata con JSR-303 javax.validation.

Ho un campo. E se un certo valore viene inserito in questo campo, voglio richiedere che alcuni altri campi non lo siano null.

Sto cercando di capirlo. Non sono sicuro di come lo chiamerei per trovare una spiegazione.

Qualsiasi aiuto sarebbe apprezzato. Sono abbastanza nuovo in questo.

Al momento sto pensando a un vincolo personalizzato. Ma non sono sicuro di come testare il valore del campo dipendente dall'interno dell'annotazione. Fondamentalmente non sono sicuro di come accedere all'oggetto del pannello dall'annotazione.

public class StatusValidator implements ConstraintValidator<NotNull, String> {

    @Override
    public void initialize(NotNull constraintAnnotation) {}

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if ("Canceled".equals(panel.status.getValue())) {
            if (value != null) {
                return true;
            }
        } else {
            return false;
        }
    }
}

Mi sta panel.status.getValue();dando problemi .. non sono sicuro di come ottenere questo risultato.

Risposte:


107

In questo caso suggerisco di scrivere un validatore personalizzato, che convaliderà a livello di classe (per permetterci di accedere ai campi dell'oggetto) che un campo è richiesto solo se un altro campo ha un valore particolare. Nota che dovresti scrivere un validatore generico che ottiene 2 nomi di campo e lavora solo con questi 2 campi. Per richiedere più di un campo è necessario aggiungere questo validatore per ogni campo.

Usa il seguente codice come idea (non l'ho testato).

  • Interfaccia del validatore

    /**
     * Validates that field {@code dependFieldName} is not null if
     * field {@code fieldName} has value {@code fieldValue}.
     **/
    @Target({TYPE, ANNOTATION_TYPE})
    @Retention(RUNTIME)
    @Repeatable(NotNullIfAnotherFieldHasValue.List.class) // only with hibernate-validator >= 6.x
    @Constraint(validatedBy = NotNullIfAnotherFieldHasValueValidator.class)
    @Documented
    public @interface NotNullIfAnotherFieldHasValue {
    
        String fieldName();
        String fieldValue();
        String dependFieldName();
    
        String message() default "{NotNullIfAnotherFieldHasValue.message}";
        Class<?>[] groups() default {};
        Class<? extends Payload>[] payload() default {};
    
        @Target({TYPE, ANNOTATION_TYPE})
        @Retention(RUNTIME)
        @Documented
        @interface List {
            NotNullIfAnotherFieldHasValue[] value();
        }
    
    }
    
  • Implementazione del validatore

    /**
     * Implementation of {@link NotNullIfAnotherFieldHasValue} validator.
     **/
    public class NotNullIfAnotherFieldHasValueValidator
        implements ConstraintValidator<NotNullIfAnotherFieldHasValue, Object> {
    
        private String fieldName;
        private String expectedFieldValue;
        private String dependFieldName;
    
        @Override
        public void initialize(NotNullIfAnotherFieldHasValue annotation) {
            fieldName          = annotation.fieldName();
            expectedFieldValue = annotation.fieldValue();
            dependFieldName    = annotation.dependFieldName();
        }
    
        @Override
        public boolean isValid(Object value, ConstraintValidatorContext ctx) {
    
            if (value == null) {
                return true;
            }
    
            try {
                String fieldValue       = BeanUtils.getProperty(value, fieldName);
                String dependFieldValue = BeanUtils.getProperty(value, dependFieldName);
    
                if (expectedFieldValue.equals(fieldValue) && dependFieldValue == null) {
                    ctx.disableDefaultConstraintViolation();
                    ctx.buildConstraintViolationWithTemplate(ctx.getDefaultConstraintMessageTemplate())
                        .addNode(dependFieldName)
                        .addConstraintViolation();
                        return false;
                }
    
            } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException ex) {
                throw new RuntimeException(ex);
            }
    
            return true;
        }
    
    }
    
  • Esempio di utilizzo del validatore (hibernate-validator> = 6 con Java 8+)

    @NotNullIfAnotherFieldHasValue(
        fieldName = "status",
        fieldValue = "Canceled",
        dependFieldName = "fieldOne")
    @NotNullIfAnotherFieldHasValue(
        fieldName = "status",
        fieldValue = "Canceled",
        dependFieldName = "fieldTwo")
    public class SampleBean {
        private String status;
        private String fieldOne;
        private String fieldTwo;
    
        // getters and setters omitted
    }
    
  • Esempio di utilizzo del validatore (hibernate-validator <6; il vecchio esempio)

    @NotNullIfAnotherFieldHasValue.List({
        @NotNullIfAnotherFieldHasValue(
            fieldName = "status",
            fieldValue = "Canceled",
            dependFieldName = "fieldOne"),
        @NotNullIfAnotherFieldHasValue(
            fieldName = "status",
            fieldValue = "Canceled",
            dependFieldName = "fieldTwo")
    })
    public class SampleBean {
        private String status;
        private String fieldOne;
        private String fieldTwo;
    
        // getters and setters omitted
    }
    

Nota che l'implementazione del validatore usa la BeanUtilsclasse dalla commons-beanutilslibreria ma puoi anche usare BeanWrapperImplda Spring Framework .

Vedi anche questa ottima risposta: convalida del campo incrociato con Hibernate Validator (JSR 303)


1
@Benedictus Questo esempio funzionerà solo con le stringhe, ma puoi modificarlo per funzionare con qualsiasi oggetto. Ci sono 2 modi: 1) parametrizzare il validatore con la classe che vuoi validare (invece di Object). In questo caso, non è nemmeno necessario utilizzare la reflection per ottenere i valori, ma in questo caso il validatore diventa meno generico 2) utilizzare BeanWrapperImpda Spring Framework (o altre librerie) e il suo getPropertyValue()metodo. In questo caso sarai in grado di ottenere un valore as Objecte di eseguire il cast su qualsiasi tipo di cui hai bisogno.
Slava Semushin

Sì, ma non puoi avere Object come parametro di annotazione, quindi avrai bisogno di un gruppo di annotazioni diverse per ogni tipo che desideri convalidare.
Ben

1
Sì, questo è quello che intendo quando ho detto "in questo caso il validatore diventa meno generico".
Slava Semushin

Voglio usare questo trucco per le classi protoBuffer. questo è molto utile (:
Saeed

Bella soluzione. Molto utile per creare annotazioni personalizzate!
Vishwa

128

Definire il metodo che deve essere convalidato su true e inserire l' @AssertTrueannotazione sopra di esso:

  @AssertTrue
  private boolean isOk() {
    return someField != something || otherField != null;
  }

Il metodo deve iniziare con "è".


Ho usato il tuo metodo e funziona, ma non riesco a capire come ricevere il messaggio. Lo sapresti per caso?
anaBad

12
Questa è stata di gran lunga la più efficiente delle opzioni. Grazie! @anaBad: l'annotazione AssertTrue può accettare un messaggio personalizzato, proprio come le altre annotazioni di vincolo.
ernest_k

@ErnestKiwele Grazie per aver risposto, ma il mio problema non è impostare il messaggio ma ottenerlo nel mio jsp. Ho la seguente funzione del modello: @AssertTrue(message="La reference doit etre un URL") public boolean isReferenceOk() { return origine!=Origine.Evolution||reference.contains("http://jira.bcaexpertise.org"); } E questo nel mio jsp: <th><form:label path="reference"><s:message code="reference"/></form:label></th><td><form:input path="reference" cssErrorClass="errorField"/><br/><form:errors path="isReferenceOk" cssClass="error"/></td> Ma genera un errore.
anaBad

@ErnestKiwele Non importa se l'ho capito, ho creato un attributo booleano che viene impostato quando viene chiamato setReference ().
anaBad

2
Ho dovuto rendere pubblico il metodo
tibi

22

Dovresti usare la personalizzazione DefaultGroupSequenceProvider<T>:

ConditionalValidation.java

// Marker interface
public interface ConditionalValidation {}

MyCustomFormSequenceProvider.java

public class MyCustomFormSequenceProvider
    implements DefaultGroupSequenceProvider<MyCustomForm> {

    @Override
    public List<Class<?>> getValidationGroups(MyCustomForm myCustomForm) {

        List<Class<?>> sequence = new ArrayList<>();

        // Apply all validation rules from ConditionalValidation group
        // only if someField has given value
        if ("some value".equals(myCustomForm.getSomeField())) {
            sequence.add(ConditionalValidation.class);
        }

        // Apply all validation rules from default group
        sequence.add(MyCustomForm.class);

        return sequence;
    }
}

MyCustomForm.java

@GroupSequenceProvider(MyCustomFormSequenceProvider.class)
public class MyCustomForm {

    private String someField;

    @NotEmpty(groups = ConditionalValidation.class)
    private String fieldTwo;

    @NotEmpty(groups = ConditionalValidation.class)
    private String fieldThree;

    @NotEmpty
    private String fieldAlwaysValidated;


    // getters, setters omitted
}

Vedi anche la domanda correlata su questo argomento .


Modo interessante per farlo. La risposta potrebbe avere più spiegazioni su come funziona, però, perché ho dovuto leggerlo due volte prima di vedere cosa stava succedendo ...
Jules

Ciao, ho implementato la tua soluzione ma sto affrontando un problema. Nessun oggetto viene passato al getValidationGroups(MyCustomForm myCustomForm)metodo. Potresti forse aiutare qui? : Stackoverflow.com/questions/44520306/...
user238607

2
@ user238607 getValidationGroups (MyCustomForm myCustomForm) chiamerà molte volte per istanza di bean e qualche volta passerà nullo. Devi solo ignorare se passa nullo.
pramoth

9

Ecco la mia opinione, ho cercato di mantenerlo il più semplice possibile.

L'interfaccia:

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

    String message() default "{one.of.message}";

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

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

    String[] value();
}

Implementazione della convalida:

public class OneOfValidator implements ConstraintValidator<OneOf, Object> {

    private String[] fields;

    @Override
    public void initialize(OneOf annotation) {
        this.fields = annotation.value();
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {

        BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(value);

        int matches = countNumberOfMatches(wrapper);

        if (matches > 1) {
            setValidationErrorMessage(context, "one.of.too.many.matches.message");
            return false;
        } else if (matches == 0) {
            setValidationErrorMessage(context, "one.of.no.matches.message");
            return false;
        }

        return true;
    }

    private int countNumberOfMatches(BeanWrapper wrapper) {
        int matches = 0;
        for (String field : fields) {
            Object value = wrapper.getPropertyValue(field);
            boolean isPresent = detectOptionalValue(value);

            if (value != null && isPresent) {
                matches++;
            }
        }
        return matches;
    }

    private boolean detectOptionalValue(Object value) {
        if (value instanceof Optional) {
            return ((Optional) value).isPresent();
        }
        return true;
    }

    private void setValidationErrorMessage(ConstraintValidatorContext context, String template) {
        context.disableDefaultConstraintViolation();
        context
            .buildConstraintViolationWithTemplate("{" + template + "}")
            .addConstraintViolation();
    }

}

Utilizzo:

@OneOf({"stateType", "modeType"})
public class OneOfValidatorTestClass {

    private StateType stateType;

    private ModeType modeType;

}

Messaggi:

one.of.too.many.matches.message=Only one of the following fields can be specified: {value}
one.of.no.matches.message=Exactly one of the following fields must be specified: {value}

3

Un approccio diverso sarebbe creare un getter (protetto) che restituisca un oggetto contenente tutti i campi dipendenti. Esempio:

public class MyBean {
  protected String status;
  protected String name;

  @StatusAndSomethingValidator
  protected StatusAndSomething getStatusAndName() {
    return new StatusAndSomething(status,name);
  }
}

StatusAndSomethingValidator può ora accedere a StatusAndSomething.status e StatusAndSomething.something ed eseguire un controllo dipendente.


0

Esempio di seguito:

package io.quee.sample.javax;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import javax.validation.ConstraintViolation;
import javax.validation.Valid;
import javax.validation.Validator;
import javax.validation.constraints.Pattern;
import java.util.Set;

/**
 * Created By [**Ibrahim Al-Tamimi **](https://www.linkedin.com/in/iloom/)
 * Created At **Wednesday **23**, September 2020**
 */
@SpringBootApplication
public class SampleJavaXValidation implements CommandLineRunner {
    private final Validator validator;

    public SampleJavaXValidation(Validator validator) {
        this.validator = validator;
    }

    public static void main(String[] args) {
        SpringApplication.run(SampleJavaXValidation.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        Set<ConstraintViolation<SampleDataCls>> validate = validator.validate(new SampleDataCls(SampleTypes.TYPE_A, null, null));
        System.out.println(validate);
    }

    public enum SampleTypes {
        TYPE_A,
        TYPE_B;
    }

    @Valid
    public static class SampleDataCls {
        private final SampleTypes type;
        private final String valueA;
        private final String valueB;

        public SampleDataCls(SampleTypes type, String valueA, String valueB) {
            this.type = type;
            this.valueA = valueA;
            this.valueB = valueB;
        }

        public SampleTypes getType() {
            return type;
        }

        public String getValueA() {
            return valueA;
        }

        public String getValueB() {
            return valueB;
        }

        @Pattern(regexp = "TRUE")
        public String getConditionalValueA() {
            if (type.equals(SampleTypes.TYPE_A)) {
                return valueA != null ? "TRUE" : "";
            }
            return "TRUE";
        }

        @Pattern(regexp = "TRUE")
        public String getConditionalValueB() {
            if (type.equals(SampleTypes.TYPE_B)) {
                return valueB != null ? "TRUE" : "";
            }
            return "TRUE";
        }
    }
}
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.