diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java index aa88b18c576..060d2214eb6 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ import javax.validation.metadata.BeanDescriptor; import javax.validation.metadata.ConstraintDescriptor; import org.springframework.beans.NotReadablePropertyException; +import org.springframework.context.MessageSourceResolvable; import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.util.Assert; import org.springframework.validation.BindingResult; @@ -212,6 +213,9 @@ public class SpringValidatorAdapter implements SmartValidator, javax.validation. String attributeName = entry.getKey(); Object attributeValue = entry.getValue(); if (!internalAnnotationAttributes.contains(attributeName)) { + if (attributeValue instanceof String) { + attributeValue = new ResolvableAttribute(attributeValue.toString()); + } attributesToExpose.put(attributeName, attributeValue); } } @@ -279,4 +283,33 @@ public class SpringValidatorAdapter implements SmartValidator, javax.validation. return this.targetValidator.unwrap(type); } + + /** + * Wrapper for a String attribute which can be resolved via a {@code MessageSource}, + * falling back to the original attribute as a default value otherwise. + */ + private static class ResolvableAttribute implements MessageSourceResolvable { + + private final String resolvableString; + + public ResolvableAttribute(String resolvableString) { + this.resolvableString = resolvableString; + } + + @Override + public String[] getCodes() { + return new String[] {this.resolvableString}; + } + + @Override + public Object[] getArguments() { + return null; + } + + @Override + public String getDefaultMessage() { + return this.resolvableString; + } + } + } diff --git a/spring-context/src/test/java/org/springframework/validation/beanvalidation/SpringValidatorAdapterTests.java b/spring-context/src/test/java/org/springframework/validation/beanvalidation/SpringValidatorAdapterTests.java new file mode 100644 index 00000000000..becfbefb233 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/SpringValidatorAdapterTests.java @@ -0,0 +1,220 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.validation.beanvalidation; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.BeanWrapperImpl; +import org.springframework.context.support.StaticMessageSource; +import org.springframework.util.ObjectUtils; +import org.springframework.validation.BeanPropertyBindingResult; + +import javax.validation.*; +import javax.validation.constraints.Size; +import java.lang.annotation.*; +import java.util.Locale; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; + +/** + * @author Kazuki Shimizu + * @since 4.3 + */ +public class SpringValidatorAdapterTests { + + private SpringValidatorAdapter validatorAdapter; + + private StaticMessageSource messageSource; + + + @Before + public void setupSpringValidatorAdapter() { + validatorAdapter = new SpringValidatorAdapter(Validation.buildDefaultValidatorFactory().getValidator()); + + messageSource = new StaticMessageSource(); + messageSource.addMessage("Size", Locale.ENGLISH, "Size of {0} is must be between {2} and {1}"); + messageSource.addMessage("Same", Locale.ENGLISH, "{2} must be same value with {1}"); + messageSource.addMessage("password", Locale.ENGLISH, "Password"); + messageSource.addMessage("confirmPassword", Locale.ENGLISH, "Password(Confirm)"); + } + + + @Test // SPR-13406 + public void testApplyMessageSourceResolvableToStringArgumentValueWithResolvedLogicalFieldName() { + TestBean testBean = new TestBean(); + testBean.setPassword("password"); + testBean.setConfirmPassword("PASSWORD"); + + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(testBean, "testBean"); + validatorAdapter.validate(testBean, errors); + + assertThat(errors.getFieldErrorCount("password"), is(1)); + assertThat(messageSource.getMessage(errors.getFieldError("password"), Locale.ENGLISH), + is("Password must be same value with Password(Confirm)")); + + } + + @Test // SPR-13406 + public void testApplyMessageSourceResolvableToStringArgumentValueWithUnresolvedLogicalFieldName() { + TestBean testBean = new TestBean(); + testBean.setEmail("test@example.com"); + testBean.setConfirmEmail("TEST@EXAMPLE.IO"); + + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(testBean, "testBean"); + validatorAdapter.validate(testBean, errors); + + assertThat(errors.getFieldErrorCount("email"), is(1)); + assertThat(messageSource.getMessage(errors.getFieldError("email"), Locale.ENGLISH), + is("email must be same value with confirmEmail")); + + } + + @Test // SPR-13406 + public void testNoStringArgumentValue() { + TestBean testBean = new TestBean(); + testBean.setPassword("pass"); + testBean.setConfirmPassword("pass"); + + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(testBean, "testBean"); + validatorAdapter.validate(testBean, errors); + + assertThat(errors.getFieldErrorCount("password"), is(1)); + assertThat(messageSource.getMessage(errors.getFieldError("password"), Locale.ENGLISH), + is("Size of Password is must be between 8 and 128")); + + } + + + @Same(field = "password", comparingField = "confirmPassword") + @Same(field = "email", comparingField = "confirmEmail") + static class TestBean { + + @Size(min = 8, max = 128) + private String password; + private String confirmPassword; + + private String email; + private String confirmEmail; + + 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; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getConfirmEmail() { + return confirmEmail; + } + + public void setConfirmEmail(String confirmEmail) { + this.confirmEmail = confirmEmail; + } + } + + + @Documented + @Constraint(validatedBy = {SameValidator.class}) + @Target({TYPE, ANNOTATION_TYPE}) + @Retention(RUNTIME) + @Repeatable(SameGroup.class) + public @interface Same { + + String message() default "{org.springframework.validation.beanvalidation.Same.message}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + String field(); + + String comparingField(); + + @Target({TYPE, ANNOTATION_TYPE}) + @Retention(RUNTIME) + @Documented + @interface List { + Same[] value(); + } + } + + + @Documented + @Inherited + @Retention(RUNTIME) + @Target({TYPE, ANNOTATION_TYPE}) + public @interface SameGroup { + + Same[] value(); + } + + public static class SameValidator implements ConstraintValidator { + + private String field; + + private String comparingField; + + private String message; + + public void initialize(Same constraintAnnotation) { + field = constraintAnnotation.field(); + comparingField = constraintAnnotation.comparingField(); + message = constraintAnnotation.message(); + } + + public boolean isValid(Object value, ConstraintValidatorContext context) { + BeanWrapper beanWrapper = new BeanWrapperImpl(value); + Object fieldValue = beanWrapper.getPropertyValue(field); + Object comparingFieldValue = beanWrapper.getPropertyValue(comparingField); + boolean matched = ObjectUtils.nullSafeEquals(fieldValue, comparingFieldValue); + if (matched) { + return true; + } + else { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(message) + .addNode(field) + .addConstraintViolation(); + return false; + } + } + } + +}