Hazırda Bekletme Doğrulayıcı (JSR 303) ile alanlar arası doğrulama


236

Hazırda Bekletme Doğrulayıcı 4.x'te alanlar arası doğrulamanın bir uygulaması (veya üçüncü taraf uygulaması) var mı? Değilse, bir çapraz alan doğrulayıcıyı uygulamanın en temiz yolu nedir?

Örnek olarak, iki fasulye özelliğini eşitlemek için API'yi nasıl kullanabilirsiniz (bir şifre alanını şifre doğrulama alanıyla eşleştirmek gibi).

Ek açıklamalarda şöyle bir şey beklerdim:

public class MyBean {
  @Size(min=6, max=50)
  private String pass;

  @Equals(property="pass")
  private String passVerify;
}

1
Bkz stackoverflow.com/questions/2781771/... sınıf düzeyinde bir tip-güvenli ve yansıma API içermeyen (IMO daha şık) çözümü için.
Karl Richter

Yanıtlar:


282

Her alan kısıtlaması ayrı bir doğrulayıcı notuyla ele alınmalı veya başka bir deyişle, bir alanın diğer alanlara karşı doğrulama notu kontrolüne sahip olması önerilmemektedir; alanlar arası doğrulama sınıf düzeyinde yapılmalıdır. Ek olarak, JSR-303 Bölüm 2.2 , aynı türden birden fazla doğrulamayı ifade etmenin tercih edilen yolu, ek açıklamalar listesidir. Bu, hata mesajının eşleşme başına belirtilmesini sağlar.

Örneğin, ortak bir formun doğrulanması:

@FieldMatch.List({
        @FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match"),
        @FieldMatch(first = "email", second = "confirmEmail", message = "The email fields must match")
})
public class UserRegistrationForm  {
    @NotNull
    @Size(min=8, max=25)
    private String password;

    @NotNull
    @Size(min=8, max=25)
    private String confirmPassword;

    @NotNull
    @Email
    private String email;

    @NotNull
    @Email
    private String confirmEmail;
}

Ek Açıklama:

package constraints;

import constraints.impl.FieldMatchValidator;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Target;

/**
 * Validation annotation to validate that 2 fields have the same value.
 * An array of fields and their matching confirmation fields can be supplied.
 *
 * Example, compare 1 pair of fields:
 * @FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match")
 * 
 * Example, compare more than 1 pair of fields:
 * @FieldMatch.List({
 *   @FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match"),
 *   @FieldMatch(first = "email", second = "confirmEmail", message = "The email fields must match")})
 */
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = FieldMatchValidator.class)
@Documented
public @interface FieldMatch
{
    String message() default "{constraints.fieldmatch}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    /**
     * @return The first field
     */
    String first();

    /**
     * @return The second field
     */
    String second();

    /**
     * Defines several <code>@FieldMatch</code> annotations on the same element
     *
     * @see FieldMatch
     */
    @Target({TYPE, ANNOTATION_TYPE})
    @Retention(RUNTIME)
    @Documented
            @interface List
    {
        FieldMatch[] value();
    }
}

Doğrulayıcı:

package constraints.impl;

import constraints.FieldMatch;
import org.apache.commons.beanutils.BeanUtils;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object>
{
    private String firstFieldName;
    private String secondFieldName;

    @Override
    public void initialize(final FieldMatch constraintAnnotation)
    {
        firstFieldName = constraintAnnotation.first();
        secondFieldName = constraintAnnotation.second();
    }

    @Override
    public boolean isValid(final Object value, final ConstraintValidatorContext context)
    {
        try
        {
            final Object firstObj = BeanUtils.getProperty(value, firstFieldName);
            final Object secondObj = BeanUtils.getProperty(value, secondFieldName);

            return firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
        }
        catch (final Exception ignore)
        {
            // ignore
        }
        return true;
    }
}

8
@AndyT: Apache Commons BeanUtils'e harici bir bağımlılık var.
GaryF

7
@ScriptAssert, özelleştirilmiş bir yolla bir doğrulama mesajı oluşturmanıza izin vermez. context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()).addNode(secondFieldName).addConstraintViolation().disableDefaultConstraintViolation(); Doğru alanı vurgulama imkanı verir (yalnızca JSF destekliyorsa).
Peter Davis

8
i örnek yukarıda kullanılan ancak hata mesajı görüntülemez, bağlama jsp olmalıdır? şifre için bağlayıcı var ve sadece onaylamak, başka bir şey gerekli var mı? <form: password path = "password" /> <form: hatalar yolu = "password" cssClass = "errorz" /> <form: password path = "confirmPassword" /> <form: hatalar path = "confirmPassword" cssClass = " errorz "/>
Mahmoud Saleh

7
BeanUtils.getPropertybir dize döndürür. Örnek muhtemelen PropertyUtils.getPropertybir nesneyi döndüren nesneyi kullanmaktı .
SingleShot

2
Güzel cevap, ama ben bu sorunun cevabı ile tamamladım: stackoverflow.com/questions/11890334/…
maxivis

164

Size başka bir olası çözüm öneriyorum. Belki daha az zarif, ama daha kolay!

public class MyBean {
  @Size(min=6, max=50)
  private String pass;

  private String passVerify;

  @AssertTrue(message="passVerify field should be equal than pass field")
  private boolean isValid() {
    return this.pass.equals(this.passVerify);
  }
}

isValidYöntem otomatik olarak doğrulama tarafından çağrılır.


12
Bence bu yine endişelerin bir karışımı. Fasulye Validasyonunun tüm amacı validasyonu ConstraintValidators'a dışlamaktır. Bu durumda, çekirdeğin kendisinde doğrulama mantığının bir kısmı ve Doğrulayıcı çerçevesinde bir parçanız vardır. Gidilecek yol sınıf düzeyinde bir kısıtlamadır. Hibernate Validator artık çekirdek iç bağımlılıklarının uygulanmasını kolaylaştıran bir @ScriptAssert sunuyor.
Hardy

10
Bunun daha zarif olduğunu söyleyebilirim, daha az değil!
NickJ

8
Benim düşüncem şu ana kadar Bean Validation JSR'nin endişelerin bir karışımı olduğu.
Dmitry Minkovsky

3
@GaneshKrishnan Ya böyle birkaç @AssertTrueyöntem kullanmak istersek ? Bazı isimlendirme sözleşmesi geçerli mi?
Stephane

3
neden bu en iyi cevap değil
funky-nd

32

Bunun kutudan çıkmadığına şaşırdım. Her neyse, burada olası bir çözüm var.

Orijinal soruda açıklandığı gibi alan düzeyinde değil, sınıf düzeyinde bir doğrulayıcı oluşturdum.

Ek açıklama kodu şöyledir:

package com.moa.podium.util.constraints;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = MatchesValidator.class)
@Documented
public @interface Matches {

  String message() default "{com.moa.podium.util.constraints.matches}";

  Class<?>[] groups() default {};

  Class<? extends Payload>[] payload() default {};

  String field();

  String verifyField();
}

Ve doğrulayıcının kendisi:

package com.moa.podium.util.constraints;

import org.mvel2.MVEL;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class MatchesValidator implements ConstraintValidator<Matches, Object> {

  private String field;
  private String verifyField;


  public void initialize(Matches constraintAnnotation) {
    this.field = constraintAnnotation.field();
    this.verifyField = constraintAnnotation.verifyField();
  }

  public boolean isValid(Object value, ConstraintValidatorContext context) {
    Object fieldObj = MVEL.getProperty(field, value);
    Object verifyFieldObj = MVEL.getProperty(verifyField, value);

    boolean neitherSet = (fieldObj == null) && (verifyFieldObj == null);

    if (neitherSet) {
      return true;
    }

    boolean matches = (fieldObj != null) && fieldObj.equals(verifyFieldObj);

    if (!matches) {
      context.disableDefaultConstraintViolation();
      context.buildConstraintViolationWithTemplate("message")
          .addNode(verifyField)
          .addConstraintViolation();
    }

    return matches;
  }
}

Doğrulanan nesnenin özelliklerini incelemek için MVEL kullandığımı unutmayın. Bu, standart yansıma API'leriyle değiştirilebilir veya doğruladığınız belirli bir sınıfsa, erişimci yöntemlerinin kendisidir.

@Matches açıklaması daha sonra bir fasülyede aşağıdaki gibi kullanılabilir:

@Matches(field="pass", verifyField="passRepeat")
public class AccountCreateForm {

  @Size(min=6, max=50)
  private String pass;
  private String passRepeat;

  ...
}

Bir feragatname olarak, bunu son 5 dakika içinde yazdım, bu yüzden muhtemelen tüm hataları ütülemedim. Bir şeyler ters giderse cevabı güncelleyeceğim.


1
Bu harika ve benim için çalışıyor, ancak addNote kullanımdan kaldırıldı ve bunun yerine addPropertyNode kullanırsam AbstractMethodError alıyorum. Google burada bana yardım etmiyor. Çözüm ne? Bir yerde eksik olan bir bağımlılık var mı?
Paul Grenyer

29

Hazırda Validator 4.1.0.Final I ile kullanmanızı tavsiye @ScriptAssert . JavaDoc'undan istisna:

Komut dosyası ifadeleri , sınıf yolunda JSR 223 ("JavaTM Platformu için Komut Dosyası") uyumlu bir motorun bulunabileceği herhangi bir komut dosyası veya ifade dilinde yazılabilir .

Not: değerlendirme, bazı yorumlarda belirtildiği gibi "istemci tarafında" değil , Java VM'de çalışan bir komut dosyası " motoru " tarafından gerçekleştirilir , bu nedenle Java "sunucu tarafında" yapılır .

Misal:

@ScriptAssert(lang = "javascript", script = "_this.passVerify.equals(_this.pass)")
public class MyBean {
  @Size(min=6, max=50)
  private String pass;

  private String passVerify;
}

veya daha kısa bir takma adla ve sıfır güvenli:

@ScriptAssert(lang = "javascript", alias = "_",
    script = "_.passVerify != null && _.passVerify.equals(_.pass)")
public class MyBean {
  @Size(min=6, max=50)
  private String pass;

  private String passVerify;
}

veya Java 7+ null-safe ile Objects.equals():

@ScriptAssert(lang = "javascript", script = "Objects.equals(_this.passVerify, _this.pass)")
public class MyBean {
  @Size(min=6, max=50)
  private String pass;

  private String passVerify;
}

Bununla birlikte, özel sınıf düzeyinde bir validator @Matches çözümü ile ilgili yanlış bir şey yoktur .


1
İlginç bir çözüm, bu doğrulamayı gerçekleştirmek için gerçekten javascript kullanıyor muyuz? Bu, java tabanlı bir ek açıklamanın neyi başarabileceği için aşırıya kaçmış gibi görünüyor. Bakire gözlerime göre, yukarıda önerilen Nicko'nun çözümü hem kullanılabilirlik açısından daha temiz görünüyor (ek açıklamasının okunması kolay ve oldukça işlevsel değil, yetersiz javascript-> java referansları) hem de ölçeklenebilirlik açısından (makul bir ek yük var Javascript işlemek, ancak Hibernate derlenmiş kodu en azından önbelleğe alıyor olabilir?). Bunun neden tercih edileceğini merak ediyorum.
David Parks

2
Nicko'nun uygulamasının güzel olduğunu kabul ediyorum, ancak JS'yi bir ifade dili olarak kullanma konusunda sakıncalı bir şey görmüyorum. Java 6, tam olarak bu tür uygulamalar için Rhino içerir. @ScriptAssert'i seviyorum, çünkü her zaman yeni bir test türüm olduğunda bir ek açıklama ve bir doğrulayıcı oluşturmak zorunda kalmadan çalışıyor.

4
Söylendiği gibi, sınıf düzeyi doğrulayıcısında hiçbir şey yanlış değil. ScriptAssert sadece özel kod yazmanızı gerektirmeyen bir alternatiftir. Ben tercih edilen çözüm olduğunu söylemedim ;-)
Hardy

Harika bir yanıt, çünkü şifre onayı kritik bir doğrulama olmadığı için istemci tarafında yapılabilir
peterchaula

19

Çapraz alanlar doğrulamaları özel kısıtlamalar oluşturularak yapılabilir.

Örnek: - Kullanıcı örneğinin parolasını karşılaştırın ve confirmPassword alanları.

CompareStrings

@Target({TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy=CompareStringsValidator.class)
@Documented
public @interface CompareStrings {
    String[] propertyNames();
    StringComparisonMode matchMode() default EQUAL;
    boolean allowNull() default false;
    String message() default "";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

StringComparisonMode

public enum StringComparisonMode {
    EQUAL, EQUAL_IGNORE_CASE, NOT_EQUAL, NOT_EQUAL_IGNORE_CASE
}

CompareStringsValidator

public class CompareStringsValidator implements ConstraintValidator<CompareStrings, Object> {

    private String[] propertyNames;
    private StringComparisonMode comparisonMode;
    private boolean allowNull;

    @Override
    public void initialize(CompareStrings constraintAnnotation) {
        this.propertyNames = constraintAnnotation.propertyNames();
        this.comparisonMode = constraintAnnotation.matchMode();
        this.allowNull = constraintAnnotation.allowNull();
    }

    @Override
    public boolean isValid(Object target, ConstraintValidatorContext context) {
        boolean isValid = true;
        List<String> propertyValues = new ArrayList<String> (propertyNames.length);
        for(int i=0; i<propertyNames.length; i++) {
            String propertyValue = ConstraintValidatorHelper.getPropertyValue(String.class, propertyNames[i], target);
            if(propertyValue == null) {
                if(!allowNull) {
                    isValid = false;
                    break;
                }
            } else {
                propertyValues.add(propertyValue);
            }
        }

        if(isValid) {
            isValid = ConstraintValidatorHelper.isValid(propertyValues, comparisonMode);
        }

        if (!isValid) {
          /*
           * if custom message was provided, don't touch it, otherwise build the
           * default message
           */
          String message = context.getDefaultConstraintMessageTemplate();
          message = (message.isEmpty()) ?  ConstraintValidatorHelper.resolveMessage(propertyNames, comparisonMode) : message;

          context.disableDefaultConstraintViolation();
          ConstraintViolationBuilder violationBuilder = context.buildConstraintViolationWithTemplate(message);
          for (String propertyName : propertyNames) {
            NodeBuilderDefinedContext nbdc = violationBuilder.addNode(propertyName);
            nbdc.addConstraintViolation();
          }
        }    

        return isValid;
    }
}

ConstraintValidatorHelper

public abstract class ConstraintValidatorHelper {

public static <T> T getPropertyValue(Class<T> requiredType, String propertyName, Object instance) {
        if(requiredType == null) {
            throw new IllegalArgumentException("Invalid argument. requiredType must NOT be null!");
        }
        if(propertyName == null) {
            throw new IllegalArgumentException("Invalid argument. PropertyName must NOT be null!");
        }
        if(instance == null) {
            throw new IllegalArgumentException("Invalid argument. Object instance must NOT be null!");
        }
        T returnValue = null;
        try {
            PropertyDescriptor descriptor = new PropertyDescriptor(propertyName, instance.getClass());
            Method readMethod = descriptor.getReadMethod();
            if(readMethod == null) {
                throw new IllegalStateException("Property '" + propertyName + "' of " + instance.getClass().getName() + " is NOT readable!");
            }
            if(requiredType.isAssignableFrom(readMethod.getReturnType())) {
                try {
                    Object propertyValue = readMethod.invoke(instance);
                    returnValue = requiredType.cast(propertyValue);
                } catch (Exception e) {
                    e.printStackTrace(); // unable to invoke readMethod
                }
            }
        } catch (IntrospectionException e) {
            throw new IllegalArgumentException("Property '" + propertyName + "' is NOT defined in " + instance.getClass().getName() + "!", e);
        }
        return returnValue; 
    }

    public static boolean isValid(Collection<String> propertyValues, StringComparisonMode comparisonMode) {
        boolean ignoreCase = false;
        switch (comparisonMode) {
        case EQUAL_IGNORE_CASE:
        case NOT_EQUAL_IGNORE_CASE:
            ignoreCase = true;
        }

        List<String> values = new ArrayList<String> (propertyValues.size());
        for(String propertyValue : propertyValues) {
            if(ignoreCase) {
                values.add(propertyValue.toLowerCase());
            } else {
                values.add(propertyValue);
            }
        }

        switch (comparisonMode) {
        case EQUAL:
        case EQUAL_IGNORE_CASE:
            Set<String> uniqueValues = new HashSet<String> (values);
            return uniqueValues.size() == 1 ? true : false;
        case NOT_EQUAL:
        case NOT_EQUAL_IGNORE_CASE:
            Set<String> allValues = new HashSet<String> (values);
            return allValues.size() == values.size() ? true : false;
        }

        return true;
    }

    public static String resolveMessage(String[] propertyNames, StringComparisonMode comparisonMode) {
        StringBuffer buffer = concatPropertyNames(propertyNames);
        buffer.append(" must");
        switch(comparisonMode) {
        case EQUAL:
        case EQUAL_IGNORE_CASE:
            buffer.append(" be equal");
            break;
        case NOT_EQUAL:
        case NOT_EQUAL_IGNORE_CASE:
            buffer.append(" not be equal");
            break;
        }
        buffer.append('.');
        return buffer.toString();
    }

    private static StringBuffer concatPropertyNames(String[] propertyNames) {
        //TODO improve concating algorithm
        StringBuffer buffer = new StringBuffer();
        buffer.append('[');
        for(String propertyName : propertyNames) {
            char firstChar = Character.toUpperCase(propertyName.charAt(0));
            buffer.append(firstChar);
            buffer.append(propertyName.substring(1));
            buffer.append(", ");
        }
        buffer.delete(buffer.length()-2, buffer.length());
        buffer.append("]");
        return buffer;
    }
}

kullanıcı

@CompareStrings(propertyNames={"password", "confirmPassword"})
public class User {
    private String password;
    private String confirmPassword;

    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }
    public String getConfirmPassword() { return confirmPassword; }
    public void setConfirmPassword(String confirmPassword) { this.confirmPassword =  confirmPassword; }
}

Ölçek

    public void test() {
        User user = new User();
        user.setPassword("password");
        user.setConfirmPassword("paSSword");
        Set<ConstraintViolation<User>> violations = beanValidator.validate(user);
        for(ConstraintViolation<User> violation : violations) {
            logger.debug("Message:- " + violation.getMessage());
        }
        Assert.assertEquals(violations.size(), 1);
    }

Çıktı Message:- [Password, ConfirmPassword] must be equal.

CompareStrings doğrulama kısıtlamasını kullanarak, ikiden fazla özelliği de karşılaştırabiliriz ve dört dize karşılaştırma yönteminden herhangi birini karıştırabiliriz.

ColorChoice

@CompareStrings(propertyNames={"color1", "color2", "color3"}, matchMode=StringComparisonMode.NOT_EQUAL, message="Please choose three different colors.")
public class ColorChoice {

    private String color1;
    private String color2;
    private String color3;
        ......
}

Ölçek

ColorChoice colorChoice = new ColorChoice();
        colorChoice.setColor1("black");
        colorChoice.setColor2("white");
        colorChoice.setColor3("white");
        Set<ConstraintViolation<ColorChoice>> colorChoiceviolations = beanValidator.validate(colorChoice);
        for(ConstraintViolation<ColorChoice> violation : colorChoiceviolations) {
            logger.debug("Message:- " + violation.getMessage());
        }

Çıktı Message:- Please choose three different colors.

Benzer şekilde, CompareNumbers, CompareDates, vb. Alanlar arası doğrulama kısıtlamalarına sahip olabiliriz.

PS Bu kodu üretim ortamında test etmedim (geliştirici ortamında test ettim), bu yüzden bu kodu Milestone Release olarak düşünün. Bir hata bulursanız, lütfen güzel bir yorum yazın. :)


Diğerlerinden daha esnek olduğu için bu yaklaşımı seviyorum. Eşitlik için 2'den fazla alanı doğrulamama izin veriyor. İyi iş!
Tauren

9

Alberthoven örneğini (hazırda bekleme doğrulayıcısı 4.0.2.GA) denedim ve bir ValidationException alıyorum: “Açıklamalı yöntemler JavaBeans adlandırma kuralına uymalıdır. match () bunu yapmaz. “ "Eşleme" den "isValid" yöntemini yeniden adlandırdıktan sonra çalışır.

public class Password {

    private String password;

    private String retypedPassword;

    public Password(String password, String retypedPassword) {
        super();
        this.password = password;
        this.retypedPassword = retypedPassword;
    }

    @AssertTrue(message="password should match retyped password")
    private boolean isValid(){
        if (password == null) {
            return retypedPassword == null;
        } else {
            return password.equals(retypedPassword);
        }
    }

    public String getPassword() {
        return password;
    }

    public String getRetypedPassword() {
        return retypedPassword;
    }

}

Benim için doğru çalıştı ama hata mesajını göstermedi. İşe yaradı ve sizin için hata mesajını gösterdi. Nasıl?
Küçük

1
@Tiny: Mesaj, doğrulayıcı tarafından döndürülen ihlallerde olmalıdır. (Bir Birim testi yazın: stackoverflow.com/questions/5704743/… ). AMA doğrulama iletisi "isValid" özelliğine aittir. Bu nedenle, GUI yalnızca GUI'nin retypedPassword AND isValid (yeniden yazılan Parolanın yanında) sorunlarını göstermesi durumunda GUI'de gösterilir.
Ralph

8

Spring Framework kullanıyorsanız, bunun için Bahar İfade Dilini (SpEL) kullanabilirsiniz. SpEL tabanlı JSR-303 doğrulayıcı sağlayan küçük bir kütüphane yazdım - çapraz alan doğrulamaları bir esinti yapar! Https://github.com/jirutka/validator-spring adresine bir göz atın .

Bu parola alanlarının uzunluğunu ve eşitliğini doğrular.

@SpELAssert(value = "pass.equals(passVerify)",
            message = "{validator.passwords_not_same}")
public class MyBean {

    @Size(min = 6, max = 50)
    private String pass;

    private String passVerify;
}

Bunu, şifre alanlarını yalnızca her ikisi de boş olmadığında doğrulamak için kolayca değiştirebilirsiniz.

@SpELAssert(value = "pass.equals(passVerify)",
            applyIf = "pass || passVerify",
            message = "{validator.passwords_not_same}")
public class MyBean {

    @Size(min = 6, max = 50)
    private String pass;

    private String passVerify;
}

4

Jakub Jirutka'nın Bahar İfade Dilini kullanma fikrini seviyorum . Başka bir kütüphane / bağımlılık eklemek istemiyorsanız (zaten Spring'i kullandığınızı varsayarak), burada fikrinin basitleştirilmiş bir uygulaması.

Kısıtlama:

@Constraint(validatedBy=ExpressionAssertValidator.class)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExpressionAssert {
    String message() default "expression must evaluate to true";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    String value();
}

Doğrulayıcı:

public class ExpressionAssertValidator implements ConstraintValidator<ExpressionAssert, Object> {
    private Expression exp;

    public void initialize(ExpressionAssert annotation) {
        ExpressionParser parser = new SpelExpressionParser();
        exp = parser.parseExpression(annotation.value());
    }

    public boolean isValid(Object value, ConstraintValidatorContext context) {
        return exp.getValue(value, Boolean.class);
    }
}

Bu şekilde uygulayın:

@ExpressionAssert(value="pass == passVerify", message="passwords must be same")
public class MyBean {
    @Size(min=6, max=50)
    private String pass;
    private String passVerify;
}

3

İlk yanıta yorum yapma konusunda bir üne sahip değilim ama kazanan cevap için birim testleri eklediğimi ve aşağıdaki gözlemlere sahip olduğumu eklemek istedim:

  • İlk veya alan adlarını yanlış alırsanız, değerler eşleşmiyormuş gibi bir doğrulama hatası alırsınız. Yazım hatalarıyla uğraşmayın, ör.

@FieldMatch (first = " geçersiz FieldName1", ikinci = "validFieldName2")

  • Doğrulayıcı olacak eşdeğer veri tipleri FieldMatch bu irade bütün geçiş yani kabul ediyoruz:

private String stringField = "1";

private Integer integerField = yeni Tamsayı (1)

özel int intField = 1;

  • Alanlar eşit olmayan bir nesne türündeyse, doğrulama başarısız olur.

2

Çok güzel bir çözüm bradhouse. @Matches ek açıklamasını birden fazla alana uygulamanın herhangi bir yolu var mı?

EDIT: İşte bu soruyu cevaplamak için geldi çözüm, ben tek bir değer yerine bir dizi kabul etmek için Kısıtlama değiştirdim:

@Matches(fields={"password", "email"}, verifyFields={"confirmPassword", "confirmEmail"})
public class UserRegistrationForm  {

    @NotNull
    @Size(min=8, max=25)
    private String password;

    @NotNull
    @Size(min=8, max=25)
    private String confirmPassword;


    @NotNull
    @Email
    private String email;

    @NotNull
    @Email
    private String confirmEmail;
}

Ek açıklama kodu:

package springapp.util.constraints;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = MatchesValidator.class)
@Documented
public @interface Matches {

  String message() default "{springapp.util.constraints.matches}";

  Class<?>[] groups() default {};

  Class<? extends Payload>[] payload() default {};

  String[] fields();

  String[] verifyFields();
}

Ve uygulama:

package springapp.util.constraints;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import org.apache.commons.beanutils.BeanUtils;

public class MatchesValidator implements ConstraintValidator<Matches, Object> {

    private String[] fields;
    private String[] verifyFields;

    public void initialize(Matches constraintAnnotation) {
        fields = constraintAnnotation.fields();
        verifyFields = constraintAnnotation.verifyFields();
    }

    public boolean isValid(Object value, ConstraintValidatorContext context) {

        boolean matches = true;

        for (int i=0; i<fields.length; i++) {
            Object fieldObj, verifyFieldObj;
            try {
                fieldObj = BeanUtils.getProperty(value, fields[i]);
                verifyFieldObj = BeanUtils.getProperty(value, verifyFields[i]);
            } catch (Exception e) {
                //ignore
                continue;
            }
            boolean neitherSet = (fieldObj == null) && (verifyFieldObj == null);
            if (neitherSet) {
                continue;
            }

            boolean tempMatches = (fieldObj != null) && fieldObj.equals(verifyFieldObj);

            if (!tempMatches) {
                addConstraintViolation(context, fields[i]+ " fields do not match", verifyFields[i]);
            }

            matches = matches?tempMatches:matches;
        }
        return matches;
    }

    private void addConstraintViolation(ConstraintValidatorContext context, String message, String field) {
        context.disableDefaultConstraintViolation();
        context.buildConstraintViolationWithTemplate(message).addNode(field).addConstraintViolation();
    }
}

Hmm. Emin değil. Her onay alanı için belirli doğrulayıcılar oluşturmayı (böylece farklı ek açıklamalara sahip olmalarını) veya birden fazla alan çiftini kabul etmek için @Matches ek açıklamasını güncellemeyi deneyebilirsiniz.
bradhouse

Teşekkürler bradhouse, bir çözüm buldu ve yukarıda gönderdi. IndexOutOfBoundsExceptions elde edemezsiniz, ancak farklı sayıda argümanlar geçtiğinde karşılamak için biraz çalışma gerekir, ancak temelleri vardır.
McGin

1

Açıkça çağırmanız gerekir. Yukarıdaki örnekte, bradhouse size özel bir kısıtlama yazmak için tüm adımları verdi.

Bu kodu arayan sınıfınıza ekleyin.

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();

Set<ConstraintViolation<yourObjectClass>> constraintViolations = validator.validate(yourObject);

yukarıdaki durumda bu olurdu

Set<ConstraintViolation<AccountCreateForm>> constraintViolations = validator.validate(objAccountCreateForm);


1

Siz harikasınız. Gerçekten harika fikirler. Alberthoven ve McGin'in en çok hoşuma gitti , bu yüzden her iki fikri de birleştirmeye karar verdim. Ve tüm vakaları karşılamak için bazı genel bir çözüm geliştirin. İşte benim önerdiğim çözüm.

@Documented
@Constraint(validatedBy = NotFalseValidator.class)
@Target({ElementType.METHOD, ElementType.FIELD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface NotFalse {


    String message() default "NotFalse";
    String[] messages();
    String[] properties();
    String[] verifiers();

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

}

public class NotFalseValidator implements ConstraintValidator<NotFalse, Object> {
    private String[] properties;
    private String[] messages;
    private String[] verifiers;
    @Override
    public void initialize(NotFalse flag) {
        properties = flag.properties();
        messages = flag.messages();
        verifiers = flag.verifiers();
    }

    @Override
    public boolean isValid(Object bean, ConstraintValidatorContext cxt) {
        if(bean == null) {
            return true;
        }

        boolean valid = true;
        BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(bean);

        for(int i = 0; i< properties.length; i++) {
           Boolean verified = (Boolean) beanWrapper.getPropertyValue(verifiers[i]);
           valid &= isValidProperty(verified,messages[i],properties[i],cxt);
        }

        return valid;
    }

    boolean isValidProperty(Boolean flag,String message, String property, ConstraintValidatorContext cxt) {
        if(flag == null || flag) {
            return true;
        } else {
            cxt.disableDefaultConstraintViolation();
            cxt.buildConstraintViolationWithTemplate(message)
                    .addPropertyNode(property)
                    .addConstraintViolation();
            return false;
        }

    }



}

@NotFalse(
        messages = {"End Date Before Start Date" , "Start Date Before End Date" } ,
        properties={"endDateTime" , "startDateTime"},
        verifiers = {"validDateRange" , "validDateRange"})
public class SyncSessionDTO implements ControllableNode {
    @NotEmpty @NotPastDate
    private Date startDateTime;

    @NotEmpty
    private Date endDateTime;



    public Date getStartDateTime() {
        return startDateTime;
    }

    public void setStartDateTime(Date startDateTime) {
        this.startDateTime = startDateTime;
    }

    public Date getEndDateTime() {
        return endDateTime;
    }

    public void setEndDateTime(Date endDateTime) {
        this.endDateTime = endDateTime;
    }


    public Boolean getValidDateRange(){
        if(startDateTime != null && endDateTime != null) {
            return startDateTime.getTime() <= endDateTime.getTime();
        }

        return null;
    }

}

0

Nicko'nun çözümünde, Apache Commons BeanUtils kütüphanesini kullanmak ve ilkbaharda zaten mevcut olan çözümle değiştirmek gerekmemesi için küçük bir uyarlama yaptım, daha basit olabileceğim için kullananlar için:

import org.springframework.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessorFactory;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object> {

    private String firstFieldName;
    private String secondFieldName;

    @Override
    public void initialize(final FieldMatch constraintAnnotation) {
        firstFieldName = constraintAnnotation.first();
        secondFieldName = constraintAnnotation.second();
    }

    @Override
    public boolean isValid(final Object object, final ConstraintValidatorContext context) {

        BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(object);
        final Object firstObj = beanWrapper.getPropertyValue(firstFieldName);
        final Object secondObj = beanWrapper.getPropertyValue(secondFieldName);

        boolean isValid = firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);

        if (!isValid) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
                .addPropertyNode(firstFieldName)
                .addConstraintViolation();
        }

        return isValid;

    }
}

-1

Soruyla ilgili çözüm: Ek açıklama özelliğinde açıklanan alana nasıl erişilir ?

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Match {

    String field();

    String message() default "";
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MatchValidator.class)
@Documented
public @interface EnableMatchConstraint {

    String message() default "Fields must match!";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

public class MatchValidator implements  ConstraintValidator<EnableMatchConstraint, Object> {

    @Override
    public void initialize(final EnableMatchConstraint constraint) {}

    @Override
    public boolean isValid(final Object o, final ConstraintValidatorContext context) {
        boolean result = true;
        try {
            String mainField, secondField, message;
            Object firstObj, secondObj;

            final Class<?> clazz = o.getClass();
            final Field[] fields = clazz.getDeclaredFields();

            for (Field field : fields) {
                if (field.isAnnotationPresent(Match.class)) {
                    mainField = field.getName();
                    secondField = field.getAnnotation(Match.class).field();
                    message = field.getAnnotation(Match.class).message();

                    if (message == null || "".equals(message))
                        message = "Fields " + mainField + " and " + secondField + " must match!";

                    firstObj = BeanUtils.getProperty(o, mainField);
                    secondObj = BeanUtils.getProperty(o, secondField);

                    result = firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
                    if (!result) {
                        context.disableDefaultConstraintViolation();
                        context.buildConstraintViolationWithTemplate(message).addPropertyNode(mainField).addConstraintViolation();
                        break;
                    }
                }
            }
        } catch (final Exception e) {
            // ignore
            //e.printStackTrace();
        }
        return result;
    }
}

Ve nasıl kullanılır ...? Bunun gibi:

@Entity
@EnableMatchConstraint
public class User {

    @NotBlank
    private String password;

    @Match(field = "password")
    private String passwordConfirmation;
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.