Validation JSR 303, si un champ est égal à «quelque chose», alors ces autres champs ne doivent pas être nuls

89

Je cherche à faire une petite validation personnalisée avec JSR-303 javax.validation.

J'ai un champ. Et si une certaine valeur est entrée dans ce champ, je veux exiger que quelques autres champs ne le soient pas null.

J'essaye de comprendre ça. Je ne sais pas exactement comment j'appellerais cela pour aider à trouver une explication.

Toute aide serait appréciée. Je suis assez nouveau dans ce domaine.

En ce moment, je pense à une contrainte personnalisée. Mais je ne sais pas comment tester la valeur du champ dépendant à partir de l'annotation. Fondamentalement, je ne sais pas comment accéder à l'objet panneau à partir de l'annotation.

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

C'est le panel.status.getValue();qui me donne des ennuis ... je ne sais pas comment y parvenir.

Eric
la source

Réponses:

106

Dans ce cas, je suggère d'écrire un validateur personnalisé, qui validera au niveau de la classe (pour nous permettre d'accéder aux champs de l'objet) qu'un champ n'est requis que si un autre champ a une valeur particulière. Notez que vous devez écrire un validateur générique qui obtient 2 noms de champ et ne travaille qu'avec ces 2 champs. Pour exiger plus d'un champ, vous devez ajouter ce validateur pour chaque champ.

Utilisez le code suivant comme idée (je ne l'ai pas testé).

  • Interface du validateur

    /**
     * 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();
        }
    
    }
    
  • Implémentation du validateur

    /**
     * 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;
        }
    
    }
    
  • Exemple d'utilisation du validateur (hibernate-validator> = 6 avec 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
    }
    
  • Exemple d'utilisation du validateur (hibernate-validator <6; l'ancien exemple)

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

Notez que l'implémentation du validateur utilise la BeanUtilsclasse de la commons-beanutilsbibliothèque, mais vous pouvez également l'utiliser à BeanWrapperImplpartir de Spring Framework .

Voir aussi cette excellente réponse: Validation de champs croisés avec Hibernate Validator (JSR 303)

Slava Semushin
la source
1
@Benedictus Cet exemple ne fonctionnera qu'avec des chaînes, mais vous pouvez le modifier pour qu'il fonctionne avec n'importe quel objet. Il y a 2 façons: 1) paramétrer le validateur avec la classe que vous voulez valider (au lieu de Object). Dans ce cas, vous n'avez même pas besoin d'utiliser la réflexion pour obtenir les valeurs, mais dans ce cas, le validateur devient moins générique 2) utilisez BeanWrapperImpSpring Framework (ou d'autres bibliothèques) et sa getPropertyValue()méthode. Dans ce cas, vous serez en mesure d'obtenir une valeur en tant que Objectet de le convertir en n'importe quel type dont vous avez besoin.
Slava Semushin
Ouais, mais vous ne pouvez pas avoir Object comme paramètre d'annotation, vous aurez donc besoin d'un tas d'annotations différentes pour chaque type que vous souhaitez valider.
Ben
1
Oui, c'est ce que je veux dire quand j'ai dit "dans ce cas, le validateur devient moins générique".
Slava Semushin
Je veux utiliser cette astuce pour les classes protoBuffer. ceci est très utile (:
Saeed
Belle solution. Très utile pour créer une annotation personnalisée!
Vishwa le
126

Définissez la méthode qui doit valider sur true et placez l' @AssertTrueannotation par-dessus:

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

La méthode doit commencer par «est».

Audrius Meskauskas
la source
J'ai utilisé votre méthode et cela fonctionne, mais je n'arrive pas à comprendre comment faire passer le message. Le sauriez-vous?
anaBad
12
C'était de loin l'option la plus efficace. Merci! @anaBad: l'annotation AssertTrue peut prendre un message personnalisé, tout comme les autres annotations de contrainte.
ernest_k
@ErnestKiwele Merci d'avoir répondu, mais mon problème n'est pas de définir le message mais de le recevoir dans mon jsp. J'ai la fonction suivante du modèle: @AssertTrue(message="La reference doit etre un URL") public boolean isReferenceOk() { return origine!=Origine.Evolution||reference.contains("http://jira.bcaexpertise.org"); } Et cela dans mon 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> Mais cela jette une erreur.
anaBad
@ErnestKiwele Peu importe, je l'ai compris, j'ai créé un attribut booléen qui est défini lorsque setReference () est appelé.
anaBad
2
je devais rendre la méthode publique
tibi
20

Vous devez utiliser la personnalisation 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
}

Voir également la question connexe à ce sujet .

user11153
la source
Une manière intéressante de le faire. La réponse pourrait faire avec plus d'explications sur son fonctionnement, cependant, parce que j'ai dû la lire deux fois avant de voir ce qui se passait ...
Jules
Bonjour, j'ai implémenté votre solution mais je suis confronté à un problème. Aucun objet n'est transmis à la getValidationGroups(MyCustomForm myCustomForm)méthode. Pourriez-vous éventuellement aider ici? : stackoverflow.com/questions/44520306/…
user238607
2
@ user238607 getValidationGroups (MyCustomForm myCustomForm) appellera plusieurs fois par instance de bean et il passera quelque temps null. Vous ignorez simplement si elle passe null.
pramoth
7

Voici mon point de vue, j'ai essayé de le garder aussi simple que possible.

L'interface:

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

Mise en œuvre de la validation:

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

}

Usage:

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

    private StateType stateType;

    private ModeType modeType;

}

Messages:

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}
jokarl
la source
3

Une approche différente serait de créer un getter (protégé) qui retourne un objet contenant tous les champs dépendants. Exemple:

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

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

StatusAndSomethingValidator peut désormais accéder à StatusAndSomething.status et StatusAndSomething.something et effectuer une vérification dépendante.

Michael Wyraz
la source
0

Échantillon ci-dessous:

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";
        }
    }
}
Ibrahim AlTamimi
la source