Validação JSR 303, se um campo for igual a "algo", então esses outros campos não devem ser nulos

92

Estou procurando fazer uma pequena validação personalizada com JSR-303 javax.validation.

Eu tenho um campo. E se um determinado valor for inserido neste campo, quero exigir que alguns outros campos não sejam null.

Estou tentando descobrir isso. Não sei exatamente como chamaria isso para ajudar a encontrar uma explicação.

Qualquer ajuda seria apreciada. Eu sou muito novo nisso.

No momento, estou pensando em uma restrição personalizada. Mas não tenho certeza de como testar o valor do campo dependente de dentro da anotação. Basicamente, não tenho certeza de como acessar o objeto de painel a partir da anotação.

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

É o panel.status.getValue();que está me causando problemas ... não tenho certeza de como fazer isso.

Eric
fonte

Respostas:

107

Neste caso, sugiro escrever um validador customizado, que irá validar em nível de classe (para nos permitir acessar os campos do objeto) que um campo é obrigatório apenas se outro campo tiver um valor particular. Observe que você deve escrever um validador genérico que obtém 2 nomes de campo e trabalha apenas com esses 2 campos. Para exigir mais de um campo, você deve adicionar este validador para cada campo.

Use o código a seguir como uma ideia (não o testei).

  • Interface do validador

    /**
     * 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();
        }
    
    }
    
  • Implementação do 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;
        }
    
    }
    
  • Exemplo de uso do validador (hibernate-validator> = 6 com 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
    }
    
  • Exemplo de uso do validador (hibernate-validator <6; o exemplo antigo)

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

Observe que a implementação do validador usa BeanUtilsclasse da commons-beanutilsbiblioteca, mas você também pode usar BeanWrapperImpldo Spring Framework .

Veja também esta ótima resposta: Validação de campo cruzado com Hibernate Validator (JSR 303)

Slava Semushin
fonte
1
@Benedictus Este exemplo funcionará apenas com strings, mas você pode modificá-lo para funcionar com qualquer objeto. Existem 2 maneiras: 1) parametrizar o validador com a classe que você deseja validar (ao invés de Object). Neste caso, você nem precisa usar reflexão para obter os valores, mas neste caso o validador se torna menos genérico 2) uso BeanWrapperImpdo Spring Framework (ou outras bibliotecas) e seu getPropertyValue()método. Nesse caso, você poderá obter um valor como Objecte convertê-lo em qualquer tipo de que precisar.
Slava Semushin
Sim, mas você não pode ter Object como parâmetro de anotação, então você vai precisar de um monte de anotações diferentes para cada tipo que deseja validar.
Ben
1
Sim, é isso que quero dizer quando disse "neste caso o validador torna-se menos genérico".
Slava Semushin
Quero usar esse truque para classes de protoBuffer. isso é muito útil (:
Saeed
Ótima solução. Muito útil para construir anotações personalizadas!
Vishwa
128

Defina o método que deve ser validado como verdadeiro e coloque a @AssertTrueanotação no topo dele:

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

O método deve começar com 'é'.

Audrius Meskauskas
fonte
Usei seu método e funciona, mas não consigo descobrir como receber a mensagem. Você saberia?
anaBad
12
Esta foi de longe a mais eficiente das opções. Obrigado! @anaBad: A anotação AssertTrue pode receber uma mensagem personalizada, assim como outras anotações de restrição.
ernest_k 01 de
@ErnestKiwele Obrigado por responder, mas meu problema não é definir a mensagem, mas recebê-la no meu jsp. Tenho a seguinte função o modelo: @AssertTrue(message="La reference doit etre un URL") public boolean isReferenceOk() { return origine!=Origine.Evolution||reference.contains("http://jira.bcaexpertise.org"); } E isso no meu 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> Mas dá um erro.
anaBad de
@ErnestKiwele Não importa o que eu descobri, criei um atributo booleano que é definido quando setReference () é chamado.
anaBad de
2
Tive de tornar o método público
tibi,
22

Você deve fazer 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
}

Veja também a questão relacionada neste tópico .

user11153
fonte
Maneira interessante de fazer isso. A resposta poderia ter mais explicação de como funciona, porque eu tive que ler duas vezes antes de ver o que estava acontecendo ...
Jules
Olá, implementei sua solução, mas estou enfrentando um problema. Nenhum objeto está sendo passado para o getValidationGroups(MyCustomForm myCustomForm)método. Você poderia ajudar aqui? : stackoverflow.com/questions/44520306/…
user238607
2
@ user238607 getValidationGroups (MyCustomForm myCustomForm) chamará muitas vezes por instância de bean e algum tempo passará nulo. Você apenas ignora se passar null.
pramoth
9

Aqui está minha opinião sobre isso, tentei mantê-lo o mais simples possível.

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

Implementação de validação:

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;

}

Mensagens:

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

Uma abordagem diferente seria criar um getter (protegido) que retornasse um objeto contendo todos os campos dependentes. Exemplo:

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

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

StatusAndSomethingValidator agora pode acessar StatusAndSomething.status e StatusAndSomething.something e fazer uma verificação dependente.

Michael Wyraz
fonte
0

Amostra abaixo:

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
fonte