Cross field validation with Hibernate Validator (JSR 303)

前端 未结 15 1770
渐次进展
渐次进展 2020-11-22 02:37

Is there an implementation of (or third-party implementation for) cross field validation in Hibernate Validator 4.x? If not, what is the cleanest way to implement a cross fi

相关标签:
15条回答
  • 2020-11-22 02:56

    I suggest you another possible solution. Perhaps less elegant, but easier!

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

    The isValid method is invoked by the validator automatically.

    0 讨论(0)
  • 2020-11-22 02:56

    Why not try Oval: http://oval.sourceforge.net/

    I looks like it supports OGNL so maybe you could do it by a more natural

    @Assert(expr = "_value ==_this.pass").
    
    0 讨论(0)
  • 2020-11-22 02:57

    Cross fields validations can be done by creating custom constraints.

    Example:- Compare password and confirmPassword fields of User instance.

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

    User

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

    Test

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

    Output Message:- [Password, ConfirmPassword] must be equal.

    By using the CompareStrings validation constraint, we can also compare more than two properties and we can mix any of four string comparison methods.

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

    Test

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

    Output Message:- Please choose three different colors.

    Similarly, we can have CompareNumbers, CompareDates, etc cross-fields validation constraints.

    P.S. I have not tested this code under production environment (though I tested it under dev environment), so consider this code as Milestone Release. If you find a bug, please write a nice comment. :)

    0 讨论(0)
  • 2020-11-22 02:57

    I have tried Alberthoven's example (hibernate-validator 4.0.2.GA) and i get an ValidationException: „Annotated methods must follow the JavaBeans naming convention. match() does not.“ too. After I renamed the method from „match“ to "isValid" it works.

    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;
        }
    
    }
    
    0 讨论(0)
  • 2020-11-22 02:59

    With Hibernate Validator 4.1.0.Final I recommend using @ScriptAssert. Exceprt from its JavaDoc:

    Script expressions can be written in any scripting or expression language, for which a JSR 223 ("Scripting for the JavaTM Platform") compatible engine can be found on the classpath.

    Note: the evaluation is being performed by a scripting "engine" running in the Java VM, therefore on Java "server side", not on "client side" as stated in some comments.

    Example:

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

    or with shorter alias and null-safe:

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

    or with Java 7+ null-safe 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;
    }
    

    Nevertheless, there is nothing wrong with a custom class level validator @Matches solution.

    0 讨论(0)
  • 2020-11-22 02:59

    You guys are awesome. Really amazing ideas. I like Alberthoven's and McGin's most, so I decided to combine both ideas. And develop some generic solution to cater all cases. Here is my proposed solution.

    @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 讨论(0)
提交回复
热议问题