diff --git a/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/SpringValidatorAdapterTests.java b/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/SpringValidatorAdapterTests.java index a7980fff37..192b1d99d5 100644 --- a/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/SpringValidatorAdapterTests.java +++ b/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/SpringValidatorAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2019 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. @@ -34,6 +34,7 @@ import java.util.Set; import javax.validation.Constraint; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; +import javax.validation.ConstraintViolation; import javax.validation.Payload; import javax.validation.Valid; import javax.validation.Validation; @@ -50,11 +51,13 @@ import org.springframework.beans.BeanWrapperImpl; import org.springframework.context.support.StaticMessageSource; import org.springframework.util.ObjectUtils; import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.FieldError; import org.springframework.validation.beanvalidation.SpringValidatorAdapter; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.*; import static org.hamcrest.core.Is.*; +import static org.hamcrest.core.StringContains.*; import static org.junit.Assert.*; /** @@ -73,7 +76,7 @@ public class SpringValidatorAdapterTests { @Before public void setupSpringValidatorAdapter() { 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("Same", Locale.ENGLISH, "{2} must be same value as {1}"); messageSource.addMessage("password", Locale.ENGLISH, "Password"); messageSource.addMessage("confirmPassword", Locale.ENGLISH, "Password(Confirm)"); } @@ -96,8 +99,11 @@ public class SpringValidatorAdapterTests { assertThat(errors.getFieldErrorCount("password"), is(1)); assertThat(errors.getFieldValue("password"), is("pass")); - assertThat(messageSource.getMessage(errors.getFieldError("password"), Locale.ENGLISH), - is("Size of Password is must be between 8 and 128")); + FieldError error = errors.getFieldError("password"); + assertNotNull(error); + assertThat(messageSource.getMessage(error, Locale.ENGLISH), is("Size of Password is must be between 8 and 128")); + assertTrue(error.contains(ConstraintViolation.class)); + assertThat(error.unwrap(ConstraintViolation.class).getPropertyPath().toString(), is("password")); } @Test // SPR-13406 @@ -111,8 +117,11 @@ public class SpringValidatorAdapterTests { assertThat(errors.getFieldErrorCount("password"), is(1)); assertThat(errors.getFieldValue("password"), is("password")); - assertThat(messageSource.getMessage(errors.getFieldError("password"), Locale.ENGLISH), - is("Password must be same value with Password(Confirm)")); + FieldError error = errors.getFieldError("password"); + assertNotNull(error); + assertThat(messageSource.getMessage(error, Locale.ENGLISH), is("Password must be same value as Password(Confirm)")); + assertTrue(error.contains(ConstraintViolation.class)); + assertThat(error.unwrap(ConstraintViolation.class).getPropertyPath().toString(), is("password")); } @Test // SPR-13406 @@ -127,10 +136,16 @@ public class SpringValidatorAdapterTests { assertThat(errors.getFieldErrorCount("email"), is(1)); assertThat(errors.getFieldValue("email"), is("test@example.com")); assertThat(errors.getFieldErrorCount("confirmEmail"), is(1)); - assertThat(messageSource.getMessage(errors.getFieldError("email"), Locale.ENGLISH), - is("email must be same value with confirmEmail")); - assertThat(messageSource.getMessage(errors.getFieldError("confirmEmail"), Locale.ENGLISH), - is("Email required")); + FieldError error1 = errors.getFieldError("email"); + FieldError error2 = errors.getFieldError("confirmEmail"); + assertNotNull(error1); + assertNotNull(error2); + assertThat(messageSource.getMessage(error1, Locale.ENGLISH), is("email must be same value as confirmEmail")); + assertThat(messageSource.getMessage(error2, Locale.ENGLISH), is("Email required")); + assertTrue(error1.contains(ConstraintViolation.class)); + assertThat(error1.unwrap(ConstraintViolation.class).getPropertyPath().toString(), is("email")); + assertTrue(error2.contains(ConstraintViolation.class)); + assertThat(error2.unwrap(ConstraintViolation.class).getPropertyPath().toString(), is("confirmEmail")); } @Test // SPR-15123 @@ -147,10 +162,34 @@ public class SpringValidatorAdapterTests { assertThat(errors.getFieldErrorCount("email"), is(1)); assertThat(errors.getFieldValue("email"), is("test@example.com")); assertThat(errors.getFieldErrorCount("confirmEmail"), is(1)); - assertThat(messageSource.getMessage(errors.getFieldError("email"), Locale.ENGLISH), - is("email must be same value with confirmEmail")); - assertThat(messageSource.getMessage(errors.getFieldError("confirmEmail"), Locale.ENGLISH), - is("Email required")); + FieldError error1 = errors.getFieldError("email"); + FieldError error2 = errors.getFieldError("confirmEmail"); + assertNotNull(error1); + assertNotNull(error2); + assertThat(messageSource.getMessage(error1, Locale.ENGLISH), is("email must be same value as confirmEmail")); + assertThat(messageSource.getMessage(error2, Locale.ENGLISH), is("Email required")); + assertTrue(error1.contains(ConstraintViolation.class)); + assertThat(error1.unwrap(ConstraintViolation.class).getPropertyPath().toString(), is("email")); + assertTrue(error2.contains(ConstraintViolation.class)); + assertThat(error2.unwrap(ConstraintViolation.class).getPropertyPath().toString(), is("confirmEmail")); + } + + @Test + public void testPatternMessage() { + TestBean testBean = new TestBean(); + testBean.setEmail("X"); + testBean.setConfirmEmail("X"); + + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(testBean, "testBean"); + validatorAdapter.validate(testBean, errors); + + assertThat(errors.getFieldErrorCount("email"), is(1)); + assertThat(errors.getFieldValue("email"), is("X")); + FieldError error = errors.getFieldError("email"); + assertNotNull(error); + assertThat(messageSource.getMessage(error, Locale.ENGLISH), containsString("[\\w.'-]{1,}@[\\w.'-]{1,}")); + assertTrue(error.contains(ConstraintViolation.class)); + assertThat(error.unwrap(ConstraintViolation.class).getPropertyPath().toString(), is("email")); } @Test // SPR-16177 @@ -243,6 +282,7 @@ public class SpringValidatorAdapterTests { private String confirmPassword; + @Pattern(regexp = "[\\w.'-]{1,}@[\\w.'-]{1,}") private String email; @Pattern(regexp = "[\\p{L} -]*", message = "Email required") @@ -403,13 +443,13 @@ public class SpringValidatorAdapterTests { private Integer id; - @javax.validation.constraints.NotNull + @NotNull private String name; - @javax.validation.constraints.NotNull + @NotNull private Integer age; - @javax.validation.constraints.NotNull + @NotNull private Parent parent; public Integer getId() { diff --git a/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/ValidatorFactoryTests.java b/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/ValidatorFactoryTests.java index 4107516ea5..08a2e3427b 100644 --- a/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/ValidatorFactoryTests.java +++ b/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/ValidatorFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -284,6 +284,9 @@ public class ValidatorFactoryTests { errors.initConversion(new DefaultConversionService()); validator.validate(listContainer, errors); + FieldError fieldError = errors.getFieldError("list[1]"); + assertNotNull(fieldError); + assertEquals("X", fieldError.getRejectedValue()); assertEquals("X", errors.getFieldValue("list[1]")); } diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/AbstractMessageSource.java index 43544deb04..df4dbbf96b 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractMessageSource.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractMessageSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -287,6 +287,12 @@ public abstract class AbstractMessageSource extends MessageSourceSupport impleme String defaultMessage = resolvable.getDefaultMessage(); String[] codes = resolvable.getCodes(); if (defaultMessage != null) { + if (resolvable instanceof DefaultMessageSourceResolvable && + !((DefaultMessageSourceResolvable) resolvable).shouldRenderDefaultMessage()) { + // Given default message does not contain any argument placeholders + // (and isn't escaped for alwaysUseMessageFormat either) -> return as-is. + return defaultMessage; + } if (!ObjectUtils.isEmpty(codes) && defaultMessage.equals(codes[0])) { // Never format a code-as-default-message, even with alwaysUseMessageFormat=true return defaultMessage; diff --git a/spring-context/src/main/java/org/springframework/context/support/DefaultMessageSourceResolvable.java b/spring-context/src/main/java/org/springframework/context/support/DefaultMessageSourceResolvable.java index 1a554a0ab7..cc443eafdd 100644 --- a/spring-context/src/main/java/org/springframework/context/support/DefaultMessageSourceResolvable.java +++ b/spring-context/src/main/java/org/springframework/context/support/DefaultMessageSourceResolvable.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -129,13 +129,28 @@ public class DefaultMessageSourceResolvable implements MessageSourceResolvable, return this.defaultMessage; } + /** + * Indicate whether the specified default message needs to be rendered for + * substituting placeholders and/or {@link java.text.MessageFormat} escaping. + * @return {@code true} if the default message may contain argument placeholders; + * {@code false} if it definitely does not contain placeholders or custom escaping + * and can therefore be simply exposed as-is + * @since 5.1.7 + * @see #getDefaultMessage() + * @see #getArguments() + * @see AbstractMessageSource#renderDefaultMessage + */ + public boolean shouldRenderDefaultMessage() { + return true; + } + /** * Build a default String representation for this MessageSourceResolvable: * including codes, arguments, and default message. */ protected final String resolvableToString() { - StringBuilder result = new StringBuilder(); + StringBuilder result = new StringBuilder(64); result.append("codes [").append(StringUtils.arrayToDelimitedString(this.codes, ",")); result.append("]; arguments [").append(StringUtils.arrayToDelimitedString(this.arguments, ",")); result.append("]; default message [").append(this.defaultMessage).append(']'); 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 e10f51f600..ce974b975f 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-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -148,6 +148,7 @@ public class SpringValidatorAdapter implements SmartValidator, javax.validation. * @param violations the JSR-303 ConstraintViolation results * @param errors the Spring errors object to register to */ + @SuppressWarnings("serial") protected void processConstraintViolations(Set> violations, Errors errors) { for (ConstraintViolation violation : violations) { String field = determineField(violation); @@ -165,7 +166,12 @@ public class SpringValidatorAdapter implements SmartValidator, javax.validation. if (nestedField.isEmpty()) { String[] errorCodes = bindingResult.resolveMessageCodes(errorCode); ObjectError error = new ObjectError( - errors.getObjectName(), errorCodes, errorArgs, violation.getMessage()); + errors.getObjectName(), errorCodes, errorArgs, violation.getMessage()) { + @Override + public boolean shouldRenderDefaultMessage() { + return false; + } + }; error.wrap(violation); bindingResult.addError(error); } @@ -173,7 +179,12 @@ public class SpringValidatorAdapter implements SmartValidator, javax.validation. Object rejectedValue = getRejectedValue(field, violation, bindingResult); String[] errorCodes = bindingResult.resolveMessageCodes(errorCode, field); FieldError error = new FieldError(errors.getObjectName(), nestedField, - rejectedValue, false, errorCodes, errorArgs, violation.getMessage()); + rejectedValue, false, errorCodes, errorArgs, violation.getMessage()) { + @Override + public boolean shouldRenderDefaultMessage() { + return false; + } + }; error.wrap(violation); bindingResult.addError(error); } 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 index be955f24cc..725f4a96d9 100644 --- a/spring-context/src/test/java/org/springframework/validation/beanvalidation/SpringValidatorAdapterTests.java +++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/SpringValidatorAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -54,6 +54,7 @@ import org.springframework.validation.FieldError; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.*; import static org.hamcrest.core.Is.*; +import static org.hamcrest.core.StringContains.*; import static org.junit.Assert.*; /** @@ -170,6 +171,24 @@ public class SpringValidatorAdapterTests { assertThat(error2.unwrap(ConstraintViolation.class).getPropertyPath().toString(), is("confirmEmail")); } + @Test + public void testPatternMessage() { + TestBean testBean = new TestBean(); + testBean.setEmail("X"); + testBean.setConfirmEmail("X"); + + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(testBean, "testBean"); + validatorAdapter.validate(testBean, errors); + + assertThat(errors.getFieldErrorCount("email"), is(1)); + assertThat(errors.getFieldValue("email"), is("X")); + FieldError error = errors.getFieldError("email"); + assertNotNull(error); + assertThat(messageSource.getMessage(error, Locale.ENGLISH), containsString("[\\w.'-]{1,}@[\\w.'-]{1,}")); + assertTrue(error.contains(ConstraintViolation.class)); + assertThat(error.unwrap(ConstraintViolation.class).getPropertyPath().toString(), is("email")); + } + @Test // SPR-16177 public void testWithList() { Parent parent = new Parent(); @@ -218,6 +237,7 @@ public class SpringValidatorAdapterTests { private String confirmPassword; + @Pattern(regexp = "[\\w.'-]{1,}@[\\w.'-]{1,}") private String email; @Pattern(regexp = "[\\p{L} -]*", message = "Email required") diff --git a/spring-context/src/test/java/org/springframework/validation/beanvalidation/ValidatorFactoryTests.java b/spring-context/src/test/java/org/springframework/validation/beanvalidation/ValidatorFactoryTests.java index e53692b648..6a8655dc95 100644 --- a/spring-context/src/test/java/org/springframework/validation/beanvalidation/ValidatorFactoryTests.java +++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/ValidatorFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -284,6 +284,8 @@ public class ValidatorFactoryTests { validator.validate(listContainer, errors); FieldError fieldError = errors.getFieldError("list[1]"); + assertNotNull(fieldError); + assertEquals("X", fieldError.getRejectedValue()); assertEquals("X", errors.getFieldValue("list[1]")); }