Validación JSR 303, si un campo es igual a "algo", estos otros campos no deben ser nulos

89

Estoy buscando hacer una pequeña validación personalizada con JSR-303 javax.validation.

Tengo un campo. Y si se ingresa un cierto valor en este campo, quiero requerir que algunos otros campos no lo estén null.

Estoy tratando de resolver esto. No estoy seguro de cómo llamaría esto para ayudar a encontrar una explicación.

Cualquier ayuda sería apreciada. Soy bastante nuevo en esto.

Por el momento estoy pensando en una restricción personalizada. Pero no estoy seguro de cómo probar el valor del campo dependiente desde dentro de la anotación. Básicamente, no estoy seguro de cómo acceder al objeto del panel desde la anotación.

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

Es lo panel.status.getValue();que me está dando problemas ... no estoy seguro de cómo lograr esto.

Eric
fuente

Respuestas:

106

En este caso, sugiero escribir un validador personalizado, que validará a nivel de clase (para permitirnos obtener acceso a los campos del objeto) que un campo es obligatorio solo si otro campo tiene un valor particular. Tenga en cuenta que debe escribir un validador genérico que obtenga 2 nombres de campo y trabaje solo con estos 2 campos. Para requerir más de un campo, debe agregar este validador para cada campo.

Use el siguiente código como idea (no lo he probado).

  • Interfaz de validación

    /**
     * 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();
        }
    
    }
    
  • Implementación del validador

    /**
     * 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;
        }
    
    }
    
  • Ejemplo de uso del validador (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
    }
    
  • Ejemplo de uso del validador (hibernate-validator <6; el ejemplo antiguo)

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

Tenga en cuenta que la implementación del validador usa la BeanUtilsclase de la commons-beanutilsbiblioteca, pero también puede usarla BeanWrapperImplde Spring Framework .

Vea también esta gran respuesta: Validación de campo cruzado con Hibernate Validator (JSR 303)

Slava Semushin
fuente
1
@Benedictus Este ejemplo solo funcionará con cadenas, pero puede modificarlo para que funcione con cualquier objeto. Hay 2 formas: 1) parametrizar el validador con la clase que desea validar (en lugar de Object). En este caso, ni siquiera necesita usar la reflexión para obtener los valores, pero en este caso el validador se vuelve menos genérico 2) use BeanWrapperImpde Spring Framework (u otras bibliotecas) y su getPropertyValue()método. En este caso, podrá obtener un valor Objecty convertir a cualquier tipo que necesite.
Slava Semushin
Sí, pero no puede tener Objeto como parámetro de anotación, por lo que necesitará un montón de anotaciones diferentes para cada tipo que desee validar.
Ben
1
Sí, a eso me refiero cuando dije "en este caso el validador se vuelve menos genérico".
Slava Semushin
Quiero usar este truco para las clases protoBuffer. esto es muy útil (:
Saeed
Buena solucion. ¡Muy útil para crear anotaciones personalizadas!
Vishwa
126

Defina el método que debe validar como verdadero y coloque la @AssertTrueanotación en la parte superior:

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

El método debe comenzar con 'is'.

Audrius Meskauskas
fuente
Usé tu método y funciona, pero no puedo entender cómo recibir el mensaje. ¿Lo sabrías?
anaBad
12
Ésta fue, con mucho, la opción más eficaz. ¡Gracias! @anaBad: La anotación AssertTrue puede tomar un mensaje personalizado, al igual que otras anotaciones de restricción.
ernest_k
@ErnestKiwele Gracias por responder, pero mi problema no es configurar el mensaje sino obtenerlo en mi jsp. Tengo la siguiente función del modelo: @AssertTrue(message="La reference doit etre un URL") public boolean isReferenceOk() { return origine!=Origine.Evolution||reference.contains("http://jira.bcaexpertise.org"); } Y esto en mi 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> Pero arroja un error.
anaBad
@ErnestKiwele No importa, lo descubrí, hice un atributo booleano que se establece cuando se llama a setReference ().
anaBad
2
Tuve que hacer público el método
tibi
20

Deberías hacer uso de custom 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
}

Consulte también la pregunta relacionada sobre este tema .

usuario11153
fuente
Interesante forma de hacerlo. Sin embargo, a la respuesta le vendría bien una explicación más detallada de cómo funciona, porque tuve que leerla dos veces antes de ver lo que estaba pasando ...
Jules
Hola, implementé su solución pero tengo un problema. No se pasa ningún objeto al getValidationGroups(MyCustomForm myCustomForm)método. ¿Podrías ayudarme aquí? : stackoverflow.com/questions/44520306/…
user238607
2
@ user238607 getValidationGroups (MyCustomForm myCustomForm) llamará muchas veces por instancia de bean y algún tiempo pasará nulo. Simplemente ignora si pasa nulo.
pramoth
7

Aquí está mi opinión, traté de mantenerlo lo más simple posible.

La interfaz:

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

Implementación de validación:

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

}

Uso:

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

    private StateType stateType;

    private ModeType modeType;

}

Mensajes:

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
fuente
3

Un enfoque diferente sería crear un captador (protegido) que devuelva un objeto que contenga todos los campos dependientes. Ejemplo:

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

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

StatusAndSomethingValidator ahora puede acceder a StatusAndSomething.status y StatusAndSomething.something y hacer una verificación dependiente.

Michael Wyraz
fuente
0

Muestra a continuación:

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
fuente