Validasi JSR 303, Jika satu bidang sama dengan "sesuatu", bidang lainnya ini tidak boleh kosong

92

Saya ingin melakukan sedikit validasi khusus dengan JSR-303 javax.validation.

Saya memiliki lapangan. Dan jika nilai tertentu dimasukkan ke dalam bidang ini, saya ingin meminta beberapa bidang lainnya tidak null.

Saya mencoba untuk mencari tahu ini. Tidak yakin persis apa yang saya sebut ini untuk membantu menemukan penjelasan.

Bantuan apa pun akan dihargai. Saya cukup baru dalam hal ini.

Saat ini saya sedang memikirkan Batasan Kustom. Tapi saya tidak yakin bagaimana cara menguji nilai field dependen dari dalam anotasi. Pada dasarnya saya tidak yakin bagaimana mengakses objek panel dari anotasi.

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

Ini panel.status.getValue();memberi saya masalah .. tidak yakin bagaimana mencapai ini.

Eric
sumber

Jawaban:

107

Dalam hal ini saya menyarankan untuk menulis validator khusus, yang akan memvalidasi di tingkat kelas (untuk memungkinkan kita mendapatkan akses ke bidang objek) bahwa satu bidang hanya diperlukan jika bidang lain memiliki nilai tertentu. Perhatikan bahwa Anda harus menulis validator generik yang mendapatkan 2 nama kolom dan hanya berfungsi dengan 2 kolom ini. Untuk membutuhkan lebih dari satu bidang, Anda harus menambahkan validator ini untuk setiap bidang.

Gunakan kode berikut sebagai ide (saya belum mengujinya).

  • Antarmuka validator

    /**
     * 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();
        }
    
    }
    
  • Implementasi validator

    /**
     * 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;
        }
    
    }
    
  • Contoh penggunaan validator (hibernate-validator> = 6 dengan 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
    }
    
  • Contoh penggunaan validator (hibernate-validator <6; contoh lama)

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

Perhatikan bahwa implementasi validator menggunakan BeanUtilskelas dari commons-beanutilspustaka tetapi Anda juga dapat menggunakan BeanWrapperImpldari Spring Framework .

Lihat juga jawaban bagus ini: Validasi lintas bidang dengan Hibernate Validator (JSR 303)

Slava Semushin
sumber
1
@Benedictus Contoh ini hanya akan bekerja dengan string tetapi Anda dapat memodifikasinya untuk bekerja dengan objek apa pun. Ada 2 cara: 1) parametrize validator dengan kelas yang ingin Anda validasi (bukan Object). Dalam hal ini, Anda bahkan tidak perlu menggunakan refleksi untuk mendapatkan nilai tetapi dalam kasus ini validator menjadi kurang umum 2) gunakan BeanWrapperImpdari Spring Framework (atau pustaka lain) dan getPropertyValue()metodenya. Dalam hal ini Anda akan bisa mendapatkan nilai as Objectdan cast ke tipe apa pun yang Anda butuhkan.
Slava Semushin
Ya, tetapi Anda tidak dapat memiliki Objek sebagai parameter anotasi, Jadi Anda akan memerlukan banyak anotasi berbeda untuk setiap jenis yang ingin Anda validasi.
Ben
1
Ya, itulah yang saya maksud ketika saya mengatakan "dalam hal ini validator menjadi kurang umum".
Slava Semushin
Saya ingin menggunakan trik ini untuk kelas protoBuffer. ini sangat membantu (:
Saeed
Solusi bagus. Sangat membantu untuk membuat anotasi khusus!
Vishwa
128

Tentukan metode yang harus divalidasi menjadi true dan letakkan @AssertTrueanotasi di atasnya:

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

Metode ini harus dimulai dengan 'adalah'.

Audrius Meskauskas
sumber
Saya menggunakan metode Anda dan berhasil, tetapi saya tidak tahu bagaimana cara mendapatkan pesannya. Apakah Anda tahu?
anaBad
12
Sejauh ini, ini adalah opsi yang paling efisien. Terima kasih! @anaBad: Anotasi AssertTrue dapat menerima pesan khusus, sama seperti anotasi pembatas lainnya.
ernest_k
@ErnestKiwele Terima kasih telah menjawab, tetapi masalah saya bukan pada pengaturan pesan tetapi mendapatkannya di jsp saya. Saya memiliki fungsi berikut model: @AssertTrue(message="La reference doit etre un URL") public boolean isReferenceOk() { return origine!=Origine.Evolution||reference.contains("http://jira.bcaexpertise.org"); } Dan ini di jsp saya: <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> Tapi itu membuat kesalahan.
anaBad
@ErnestKiwele Tidak masalah saya mengetahuinya, saya membuat atribut boolean yang disetel saat setReference () dipanggil.
anaBad
2
saya harus membuat metode ini publik
tibi
22

Anda harus memanfaatkan kebiasaan 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
}

Lihat juga pertanyaan terkait tentang topik ini .

pengguna11153
sumber
Cara yang menarik untuk melakukannya. Jawabannya bisa dilakukan dengan lebih banyak penjelasan tentang cara kerjanya, karena saya harus membacanya dua kali sebelum saya melihat apa yang sedang terjadi ...
Jules
Hai, saya menerapkan solusi Anda tetapi menghadapi masalah. Tidak ada objek yang dikirimkan ke getValidationGroups(MyCustomForm myCustomForm)metode ini. Bisakah Anda membantu di sini? : stackoverflow.com/questions/44520306/…
pengguna238607
2
@ user238607 getValidationGroups (MyCustomForm myCustomForm) akan memanggil banyak waktu per instance kacang dan beberapa kali melewatkan null. Anda hanya mengabaikan jika lulus null.
pramoth
9

Ini pendapat saya, mencoba membuatnya sesederhana mungkin.

Antarmuka:

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

Implementasi validasi:

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

}

Pemakaian:

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

    private StateType stateType;

    private ModeType modeType;

}

Pesan:

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}
bercanda
sumber
3

Pendekatan yang berbeda adalah membuat pengambil (dilindungi) yang mengembalikan objek yang berisi semua bidang dependen. Contoh:

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

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

StatusAndSomethingValidator sekarang dapat mengakses StatusAndSomething.status dan StatusAndSomething.something dan melakukan pemeriksaan dependen.

Michael Wyraz
sumber
0

Contoh di bawah ini:

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
sumber