JSR 303 Перевірка, якщо одне поле дорівнює “чомусь”, тоді ці інші поля не повинні бути нульовими


89

Я прагну зробити невелику власну перевірку з JSR-303 javax.validation.

У мене є поле. І якщо в це поле введено певне значення, я хочу вимагати, щоб деякі інші поля не були null.

Я намагаюся це зрозуміти. Не знаю точно, як би я це назвав, щоб допомогти знайти пояснення.

Будь-яка допомога буде вдячна. Я досить новачок у цьому.

На даний момент я думаю про спеціальне обмеження. Але я не впевнений, як перевірити значення залежного поля всередині анотації. В основному я не впевнений, як отримати доступ до об’єкта панелі з анотації.

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

Це panel.status.getValue();завдає мені клопоту .. не знаю, як це зробити.

Відповіді:


106

У цьому випадку я пропоную написати власний валідатор, який перевірить на рівні класу (щоб ми могли отримати доступ до полів об'єкта), що одне поле є обов'язковим лише у тому випадку, якщо інше поле має певне значення. Зверніть увагу, що вам слід написати загальний валідатор, який отримує 2 назви полів, і працювати лише з цими 2 полями. Щоб вимагати більше одного поля, слід додати цей валідатор для кожного поля.

Використовуйте наступний код як ідею (я його не тестував).

  • Інтерфейс валідатора

    /**
     * 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();
        }
    
    }
    
  • Впровадження валідатора

    /**
     * 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;
        }
    
    }
    
  • Приклад використання валідатора (hibernate-validator> = 6 з 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
    }
    
  • Приклад використання валідатора (hibernate-validator <6; старий приклад)

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

Зверніть увагу, що в реалізаторі валідатора використовується BeanUtilsклас із commons-beanutilsбібліотеки, але ви також можете використовувати BeanWrapperImplз Spring Framework .

Дивіться також цю чудову відповідь: Перевірка між полями за допомогою Hibernate Validator (JSR 303)


1
@Benedictus Цей приклад працюватиме лише зі рядками, але ви можете змінити його для роботи з будь-якими об'єктами. Є 2 способи: 1) параметризувати валідатор з класом, який потрібно перевірити (замість Object). У цьому випадку вам навіть не потрібно використовувати відображення для отримання значень, але в цьому випадку валідатор стає менш загальним 2) використання BeanWrapperImpз Spring Framework (або інших бібліотек) та його getPropertyValue()методу. У цьому випадку ви зможете отримати значення as Objectта передати будь-який тип, який вам потрібен.
Слава Семушин

Так, але ви не можете використовувати Object як параметр анотації, тому вам знадобиться купа різних анотацій для кожного типу, який ви хочете перевірити.
Бен,

1
Так, це те, що я маю на увазі, коли сказав: "у цьому випадку валідатор стає менш загальним".
Слава Семушин

Я хочу використовувати цей фокус для класів protoBuffer. це дуже корисно (:
Saeed

Гарне рішення. Дуже корисно створити власну анотацію!
Вішва

126

Визначте метод, який повинен перевірити на істину, і помістіть @AssertTrueанотацію зверху:

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

Метод повинен починатися з "є".


Я використав ваш метод, і він працює, але я не можу зрозуміти, як отримати повідомлення. Ви могли б знати?
anaBad

12
Це, безумовно, найбільш ефективний варіант. Дякую! @anaBad: Анотація AssertTrue може приймати власне повідомлення, як і інші анотації обмежень.
ernest_k

@ErnestKiwele Дякую за відповідь, але моя проблема не в налаштуванні повідомлення, а в отриманні його в моєму jsp. У мене є така функція модель: @AssertTrue(message="La reference doit etre un URL") public boolean isReferenceOk() { return origine!=Origine.Evolution||reference.contains("http://jira.bcaexpertise.org"); } І це в моєму 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> Але це видає помилку.
anaBad

@ErnestKiwele Неважливо, я це зрозумів, я створив логічний атрибут, який встановлюється при виклику setReference ().
anaBad

2
мені довелося оприлюднити метод
Тібі

20

Вам слід скористатися користувацькими 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
}

Дивіться також відповідне запитання на цю тему .


Цікавий спосіб зробити це. Відповідь міг би стосуватися більш детального пояснення того, як це працює, адже мені довелося прочитати його двічі, перш ніж я побачив, що відбувається ...
Жуль,

Привіт, я застосував ваше рішення, але зіткнувся з проблемою. Жоден об'єкт не передається getValidationGroups(MyCustomForm myCustomForm)методу. Чи можете ви тут допомогти? : Stackoverflow.com/questions/44520306 / ...
user238607

2
@ user238607 getValidationGroups (MyCustomForm myCustomForm) буде викликати багато часу на екземпляр компонента, і деякий час пройде нуль. Ви просто ігноруєте, якщо воно передає значення null.
Прамот,

7

Ось мій погляд на це, намагався зробити це якомога простішим.

Інтерфейс:

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

Впровадження перевірки:

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

}

Використання:

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

    private StateType stateType;

    private ModeType modeType;

}

Повідомлення:

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

Іншим підходом було б створення (захищеного) геттера, який повертає об’єкт, що містить усі залежні поля. Приклад:

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

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

StatusAndSomethingValidator тепер може отримати доступ до StatusAndSomething.status і StatusAndSomething.something і зробити залежну перевірку.


0

Зразок нижче:

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";
        }
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.