JSR 303 Validation, Jeśli jedno pole równa się „coś”, te pozostałe pola nie powinny być puste

89

Chcę przeprowadzić małą niestandardową walidację z JSR-303 javax.validation.

Mam pole. A jeśli w tym polu zostanie wprowadzona pewna wartość, chcę wymagać, aby kilka innych pól nie było null.

Próbuję to rozgryźć. Nie jestem pewien, jak nazwałbym to, aby znaleźć wyjaśnienie.

Każda pomoc będzie mile widziana. Jestem w tym całkiem nowy.

W tej chwili myślę o niestandardowym ograniczeniu. Ale nie jestem pewien, jak przetestować wartość pola zależnego z poziomu adnotacji. Zasadniczo nie jestem pewien, jak uzyskać dostęp do obiektu panelu z adnotacji.

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

To panel.status.getValue();sprawia mi kłopoty… nie wiem, jak to osiągnąć.

Eric
źródło

Odpowiedzi:

106

W tym przypadku proponuję napisać niestandardowy walidator, który będzie sprawdzał na poziomie klasy (aby umożliwić nam dostęp do pól obiektu), że jedno pole jest wymagane tylko wtedy, gdy inne pole ma określoną wartość. Zauważ, że powinieneś napisać ogólny walidator, który pobierze 2 nazwy pól i będzie działał tylko z tymi 2 polami. Aby wymagać więcej niż jednego pola, należy dodać ten walidator dla każdego pola.

Użyj poniższego kodu jako pomysłu (nie testowałem tego).

  • Interfejs walidatora

    /**
     * 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();
        }
    
    }
    
  • Implementacja walidatora

    /**
     * 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;
        }
    
    }
    
  • Przykład użycia walidatora (hibernate-validator> = 6 z Javą 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
    }
    
  • Przykład użycia walidatora (hibernate-validator <6; stary przykład)

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

Zwróć uwagę, że implementacja walidatora używa BeanUtilsklasy z commons-beanutilsbiblioteki, ale możesz również użyć BeanWrapperImplSpring Framework .

Zobacz także tę świetną odpowiedź: Walidacja między polami za pomocą Hibernate Validator (JSR 303)

Slava Semushin
źródło
1
@Benedictus Ten przykład będzie działał tylko z łańcuchami, ale możesz go zmodyfikować, aby działał z dowolnymi obiektami. Istnieją 2 sposoby: 1) parametryzacja walidatora za pomocą klasy, którą chcesz walidować (zamiast Object). W tym przypadku nie musisz nawet używać refleksji, aby uzyskać wartości, ale w tym przypadku walidator stanie się mniej ogólny 2) użyj BeanWrapperImpSpring Framework (lub innych bibliotek) i jego getPropertyValue()metody. W takim przypadku będziesz mógł uzyskać wartość jako Objecti rzutować na dowolny typ, którego potrzebujesz.
Slava Semushin
Tak, ale nie możesz mieć obiektu jako parametru adnotacji, więc będziesz potrzebować kilku różnych adnotacji dla każdego typu, który chcesz zweryfikować.
Ben
1
Tak, to właśnie mam na myśli, mówiąc „w tym przypadku walidator stał się mniej ogólny”.
Slava Semushin
Chcę użyć tej sztuczki dla klas protoBuffer. jest to bardzo pomocne (:
Saeed
Niezłe rozwiązanie. Bardzo pomocne w tworzeniu niestandardowych adnotacji!
Vishwa
126

Zdefiniuj metodę, która musi zostać zweryfikowana jako true i umieść @AssertTrueadnotację na jej wierzchu:

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

Metoda musi zaczynać się od „jest”.

Audrius Meskauskas
źródło
Użyłem Twojej metody i działa, ale nie mogę znaleźć sposobu, aby uzyskać wiadomość. Czy mógłbyś wiedzieć?
anaBad
12
To była zdecydowanie najbardziej efektywna opcja. Dzięki! @anaBad: Adnotacja AssertTrue może przyjąć niestandardową wiadomość, podobnie jak inne adnotacje dotyczące ograniczeń.
ernest_k
@ErnestKiwele Dziękuję za odpowiedź, ale moim problemem nie jest ustawienie wiadomości, ale umieszczenie jej w moim jsp. Mam następującą funkcję modelu: @AssertTrue(message="La reference doit etre un URL") public boolean isReferenceOk() { return origine!=Origine.Evolution||reference.contains("http://jira.bcaexpertise.org"); } A to w moim 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> Ale wyrzuca błąd.
anaBad
@ErnestKiwele Nieważne, zrozumiałem, utworzyłem atrybut boolowski, który jest ustawiany, gdy wywoływana jest metoda setReference ().
anaBad
2
musiałem upublicznić tę metodę
tibi
20

Powinieneś skorzystać z niestandardowych 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
}

Zobacz także powiązane pytanie na ten temat .

user11153
źródło
Ciekawy sposób na zrobienie tego. Odpowiedź przydałaby się jednak, gdybyśmy dokładniej wyjaśnili, jak to działa, ponieważ musiałem go przeczytać dwa razy, zanim zobaczyłem, co się dzieje ...
Jules
Cześć, wdrożyłem Twoje rozwiązanie, ale mam problem. Żaden obiekt nie jest przekazywany do getValidationGroups(MyCustomForm myCustomForm)metody. Czy mógłbyś tu pomóc? : stackoverflow.com/questions/44520306/ ...
user238607
2
@ user238607 getValidationGroups (MyCustomForm myCustomForm) będzie wywoływać wiele razy dla każdej instancji fasoli i po pewnym czasie minie wartość null. Po prostu ignorujesz, jeśli przejdzie zero.
pramoth
7

Oto moje podejście, starałem się, aby było to tak proste, jak to tylko możliwe.

Interfejs:

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

Wdrożenie walidacji:

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

}

Stosowanie:

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

    private StateType stateType;

    private ModeType modeType;

}

Wiadomości:

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
źródło
3

Innym podejściem byłoby utworzenie (chronionego) modułu pobierającego, który zwraca obiekt zawierający wszystkie zależne pola. Przykład:

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

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

StatusAndSomethingValidator może teraz uzyskać dostęp do StatusAndSomething.status i StatusAndSomething.something i dokonać zależnego sprawdzenia.

Michael Wyraz
źródło
0

Próbka poniżej:

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
źródło