Skip to content

Commit 259b61b

Browse files
committed
Provide a way to to interpolate message for JSR-303 validation
see gh-3071
1 parent 533ca06 commit 259b61b

File tree

8 files changed

+267
-5
lines changed

8 files changed

+267
-5
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfiguration.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
2626
import org.springframework.boot.autoconfigure.condition.ConditionalOnResource;
2727
import org.springframework.boot.validation.MessageInterpolatorFactory;
28+
import org.springframework.context.ApplicationContext;
2829
import org.springframework.context.annotation.Bean;
2930
import org.springframework.context.annotation.Configuration;
3031
import org.springframework.context.annotation.Import;
@@ -51,9 +52,10 @@ public class ValidationAutoConfiguration {
5152
@Bean
5253
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
5354
@ConditionalOnMissingBean(Validator.class)
54-
public static LocalValidatorFactoryBean defaultValidator() {
55+
public static LocalValidatorFactoryBean defaultValidator(ApplicationContext applicationContext) {
5556
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
5657
MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
58+
interpolatorFactory.setMessageSource(applicationContext);
5759
factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
5860
return factoryBean;
5961
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/ValidatorAdapter.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ private static Validator getExistingOrCreate(ApplicationContext applicationConte
114114
if (existing != null) {
115115
return wrap(existing, true);
116116
}
117-
return create();
117+
return create(applicationContext);
118118
}
119119

120120
private static Validator getExisting(ApplicationContext applicationContext) {
@@ -130,10 +130,11 @@ private static Validator getExisting(ApplicationContext applicationContext) {
130130
}
131131
}
132132

133-
private static Validator create() {
133+
private static Validator create(ApplicationContext applicationContext) {
134134
OptionalValidatorFactoryBean validator = new OptionalValidatorFactoryBean();
135135
try {
136136
MessageInterpolatorFactory factory = new MessageInterpolatorFactory();
137+
factory.setMessageSource(applicationContext);
137138
validator.setMessageInterpolator(factory.getObject());
138139
}
139140
catch (ValidationException ex) {

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesJsr303Validator.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ private static class Delegate extends LocalValidatorFactoryBean {
6565

6666
Delegate(ApplicationContext applicationContext) {
6767
setApplicationContext(applicationContext);
68-
setMessageInterpolator(new MessageInterpolatorFactory().getObject());
68+
MessageInterpolatorFactory factory = new MessageInterpolatorFactory();
69+
factory.setMessageSource(applicationContext);
70+
setMessageInterpolator(factory.getObject());
6971
afterPropertiesSet();
7072
}
7173

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/validation/MessageInterpolatorFactory.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import org.springframework.beans.BeanUtils;
2828
import org.springframework.beans.BeansException;
2929
import org.springframework.beans.factory.ObjectFactory;
30+
import org.springframework.context.MessageSource;
31+
import org.springframework.context.MessageSourceAware;
3032
import org.springframework.util.ClassUtils;
3133

3234
/**
@@ -37,7 +39,7 @@
3739
* @author Phillip Webb
3840
* @since 1.5.0
3941
*/
40-
public class MessageInterpolatorFactory implements ObjectFactory<MessageInterpolator> {
42+
public class MessageInterpolatorFactory implements ObjectFactory<MessageInterpolator>, MessageSourceAware {
4143

4244
private static final Set<String> FALLBACKS;
4345

@@ -47,8 +49,31 @@ public class MessageInterpolatorFactory implements ObjectFactory<MessageInterpol
4749
FALLBACKS = Collections.unmodifiableSet(fallbacks);
4850
}
4951

52+
private MessageSource messageSource;
53+
54+
/**
55+
* Sets the {@link MessageSource} used to create
56+
* {@link MessageSourceInterpolatorDelegate}.
57+
* @param messageSource the message source that resolves any message parameters before
58+
* final interpolation
59+
* @see #getObject()
60+
*/
61+
@Override
62+
public void setMessageSource(MessageSource messageSource) {
63+
this.messageSource = messageSource;
64+
}
65+
5066
@Override
5167
public MessageInterpolator getObject() throws BeansException {
68+
MessageInterpolator messageInterpolator = getMessageInterpolator();
69+
MessageSource messageSource = this.messageSource;
70+
if (messageSource != null) {
71+
return new MessageSourceInterpolatorDelegate(messageSource, messageInterpolator);
72+
}
73+
return messageInterpolator;
74+
}
75+
76+
private MessageInterpolator getMessageInterpolator() {
5277
try {
5378
return Validation.byDefaultProvider().configure().getDefaultMessageInterpolator();
5479
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright 2012-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.validation;
18+
19+
import java.util.Locale;
20+
21+
import javax.validation.MessageInterpolator;
22+
23+
import org.springframework.context.MessageSource;
24+
import org.springframework.context.i18n.LocaleContextHolder;
25+
import org.springframework.util.PropertyPlaceholderHelper;
26+
27+
/**
28+
* Resolves any message parameters via {@link MessageSource} and then interpolates a
29+
* message using the underlying {@link MessageInterpolator}.
30+
*
31+
* @author Dmytro Nosan
32+
*/
33+
class MessageSourceInterpolatorDelegate implements MessageInterpolator {
34+
35+
private final PropertyPlaceholderHelper helper;
36+
37+
private final MessageSource messageSource;
38+
39+
private final MessageInterpolator messageInterpolator;
40+
41+
MessageSourceInterpolatorDelegate(MessageSource messageSource, MessageInterpolator messageInterpolator) {
42+
this.helper = new PropertyPlaceholderHelper("{", "}", null, true);
43+
this.messageSource = messageSource;
44+
this.messageInterpolator = messageInterpolator;
45+
}
46+
47+
@Override
48+
public String interpolate(String messageTemplate, Context context) {
49+
return interpolate(messageTemplate, context, LocaleContextHolder.getLocale());
50+
}
51+
52+
@Override
53+
public String interpolate(String messageTemplate, Context context, Locale locale) {
54+
return this.messageInterpolator.interpolate(getMessage(messageTemplate, locale), context, locale);
55+
}
56+
57+
private String getMessage(String messageTemplate, Locale locale) {
58+
return this.helper.replacePlaceholders(messageTemplate,
59+
(placeholder) -> this.messageSource.getMessage(placeholder, null, null, locale));
60+
}
61+
62+
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/validation/MessageInterpolatorFactoryTests.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@
2121
import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator;
2222
import org.junit.jupiter.api.Test;
2323

24+
import org.springframework.context.MessageSource;
25+
import org.springframework.test.util.ReflectionTestUtils;
26+
2427
import static org.assertj.core.api.Assertions.assertThat;
28+
import static org.mockito.Mockito.mock;
2529

2630
/**
2731
* Tests for {@link MessageInterpolatorFactory}.
@@ -36,4 +40,16 @@ void getObjectShouldReturnResourceBundleMessageInterpolator() {
3640
assertThat(interpolator).isInstanceOf(ResourceBundleMessageInterpolator.class);
3741
}
3842

43+
@Test
44+
void getObjectShouldReturnMessageSourceMessageInterpolatorDelegateWithResourceBundleMessageInterpolator() {
45+
MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
46+
MessageSource messageSource = mock(MessageSource.class);
47+
interpolatorFactory.setMessageSource(messageSource);
48+
MessageInterpolator interpolator = interpolatorFactory.getObject();
49+
assertThat(interpolator).isInstanceOf(MessageSourceInterpolatorDelegate.class);
50+
assertThat(interpolator).hasFieldOrPropertyWithValue("messageSource", messageSource);
51+
assertThat(ReflectionTestUtils.getField(interpolator, "messageInterpolator"))
52+
.isInstanceOf(ResourceBundleMessageInterpolator.class);
53+
}
54+
3955
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/validation/MessageInterpolatorFactoryWithoutElIntegrationTests.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,12 @@
2424
import org.junit.jupiter.api.Test;
2525

2626
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
27+
import org.springframework.context.MessageSource;
28+
import org.springframework.test.util.ReflectionTestUtils;
2729

2830
import static org.assertj.core.api.Assertions.assertThat;
2931
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
32+
import static org.mockito.Mockito.mock;
3033

3134
/**
3235
* Integration tests for {@link MessageInterpolatorFactory} without EL.
@@ -50,4 +53,16 @@ void getObjectShouldUseFallback() {
5053
assertThat(interpolator).isInstanceOf(ParameterMessageInterpolator.class);
5154
}
5255

56+
@Test
57+
void getObjectShouldUseMessageSourceMessageInterpolatorDelegateWithFallback() {
58+
MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
59+
MessageSource messageSource = mock(MessageSource.class);
60+
interpolatorFactory.setMessageSource(messageSource);
61+
MessageInterpolator interpolator = interpolatorFactory.getObject();
62+
assertThat(interpolator).isInstanceOf(MessageSourceInterpolatorDelegate.class);
63+
assertThat(interpolator).hasFieldOrPropertyWithValue("messageSource", messageSource);
64+
assertThat(ReflectionTestUtils.getField(interpolator, "messageInterpolator"))
65+
.isInstanceOf(ParameterMessageInterpolator.class);
66+
}
67+
5368
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* Copyright 2012-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.validation;
18+
19+
import java.util.Locale;
20+
21+
import javax.validation.ConstraintViolation;
22+
import javax.validation.MessageInterpolator;
23+
import javax.validation.Validator;
24+
import javax.validation.constraints.NotBlank;
25+
import javax.validation.constraints.NotNull;
26+
27+
import org.assertj.core.api.Condition;
28+
import org.assertj.core.description.TextDescription;
29+
import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator;
30+
import org.junit.jupiter.api.Test;
31+
32+
import org.springframework.context.MessageSource;
33+
import org.springframework.context.i18n.LocaleContextHolder;
34+
import org.springframework.context.support.StaticMessageSource;
35+
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
36+
37+
import static org.assertj.core.api.Assertions.assertThat;
38+
39+
/**
40+
* Tests for {@link MessageSourceInterpolatorDelegate}.
41+
*
42+
* @author Dmytro Nosan
43+
*/
44+
class MessageSourceInterpolatorDelegateTests {
45+
46+
private final MessageInterpolator messageInterpolator = new ResourceBundleMessageInterpolator();
47+
48+
private final MessageSource messageSource = createMessageSource();
49+
50+
private final Validator validator = createValidator();
51+
52+
@Test
53+
void shouldResolvePlaceholdersUsingMessageSource() {
54+
hasConstraintMessage("nullValue", "value must not be <null>");
55+
}
56+
57+
@Test
58+
void shouldResolveNestedPlaceholdersUsingMessageSource() {
59+
hasConstraintMessage("nestedNull", "must not be null");
60+
}
61+
62+
@Test
63+
void shouldResolvePlaceholdersUsingMessageInterpolator() {
64+
hasConstraintMessage("blank", "must not be blank");
65+
}
66+
67+
@Test
68+
void shouldNotResolvePlaceholders() {
69+
hasConstraintMessage("unknown", "{unknown}");
70+
}
71+
72+
private Validator createValidator() {
73+
LocalValidatorFactoryBean validatorFactory = new LocalValidatorFactoryBean();
74+
validatorFactory.setMessageInterpolator(
75+
new MessageSourceInterpolatorDelegate(this.messageSource, this.messageInterpolator));
76+
validatorFactory.afterPropertiesSet();
77+
return validatorFactory.getValidator();
78+
}
79+
80+
private MessageSource createMessageSource() {
81+
Locale locale = LocaleContextHolder.getLocale();
82+
StaticMessageSource messageSource = new StaticMessageSource();
83+
messageSource.addMessage("null", locale, "<null>");
84+
messageSource.addMessage("blank", locale, "<blank>");
85+
messageSource.addMessage("nestedNull", locale, "{javax.validation.constraints.NotNull.message}");
86+
return messageSource;
87+
}
88+
89+
private void hasConstraintMessage(String field, String message) {
90+
assertThat(this.validator.validate(new Person())).has(constraintMessage(field, message));
91+
}
92+
93+
private ConstrainViolationCondition constraintMessage(String field, String message) {
94+
return new ConstrainViolationCondition(field, message);
95+
}
96+
97+
private static final class ConstrainViolationCondition
98+
extends Condition<Iterable<? extends ConstraintViolation<?>>> {
99+
100+
private final String property;
101+
102+
private final String message;
103+
104+
ConstrainViolationCondition(String property, String message) {
105+
super(new TextDescription("Property '%s' with a message '%s' ", property, message));
106+
this.property = property;
107+
this.message = message;
108+
}
109+
110+
@Override
111+
public boolean matches(Iterable<? extends ConstraintViolation<?>> constraintViolations) {
112+
for (ConstraintViolation<?> violation : constraintViolations) {
113+
if (this.property.equals(violation.getPropertyPath().toString())
114+
&& this.message.equals(violation.getMessage())) {
115+
return true;
116+
}
117+
}
118+
return false;
119+
}
120+
121+
}
122+
123+
private static final class Person {
124+
125+
@NotNull(message = "value must not be {null}")
126+
private String nullValue;
127+
128+
@NotBlank
129+
private String blank = "";
130+
131+
@NotNull(message = "{nestedNull}")
132+
private String nestedNull;
133+
134+
@NotNull(message = "{unknown}")
135+
private String unknown;
136+
137+
}
138+
139+
}

0 commit comments

Comments
 (0)