Skip to content

Commit bbb8367

Browse files
nosanwilkinsona
authored andcommitted
Use MessageSource to interpolate bean validation messages
See gh-17530
1 parent ea9b155 commit bbb8367

File tree

10 files changed

+460
-5
lines changed

10 files changed

+460
-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
@@ -28,6 +28,7 @@
2828
import org.springframework.boot.validation.MessageInterpolatorFactory;
2929
import org.springframework.boot.validation.beanvalidation.FilteredMethodValidationPostProcessor;
3030
import org.springframework.boot.validation.beanvalidation.MethodValidationExcludeFilter;
31+
import org.springframework.context.ApplicationContext;
3132
import org.springframework.context.annotation.Bean;
3233
import org.springframework.context.annotation.Configuration;
3334
import org.springframework.context.annotation.Import;
@@ -54,9 +55,10 @@ public class ValidationAutoConfiguration {
5455
@Bean
5556
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
5657
@ConditionalOnMissingBean(Validator.class)
57-
public static LocalValidatorFactoryBean defaultValidator() {
58+
public static LocalValidatorFactoryBean defaultValidator(ApplicationContext applicationContext) {
5859
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
5960
MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
61+
interpolatorFactory.setMessageSource(applicationContext);
6062
factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
6163
return factoryBean;
6264
}

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: 25 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,30 @@ 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+
*/
60+
@Override
61+
public void setMessageSource(MessageSource messageSource) {
62+
this.messageSource = messageSource;
63+
}
64+
5065
@Override
5166
public MessageInterpolator getObject() throws BeansException {
67+
MessageInterpolator messageInterpolator = getMessageInterpolator();
68+
MessageSource messageSource = this.messageSource;
69+
if (messageSource != null) {
70+
return new MessageSourceInterpolatorDelegate(messageSource, messageInterpolator);
71+
}
72+
return messageInterpolator;
73+
}
74+
75+
private MessageInterpolator getMessageInterpolator() {
5276
try {
5377
return Validation.byDefaultProvider().configure().getDefaultMessageInterpolator();
5478
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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.LinkedHashSet;
20+
import java.util.Set;
21+
import java.util.function.Function;
22+
23+
/**
24+
* Utility class to extract message parameters ({@code {param}}) from the message. These
25+
* parameters can be substituted for supplied values.
26+
*
27+
* @author Dmytro Nosan
28+
*/
29+
final class MessageParameterPlaceholderHelper {
30+
31+
private static final char PREFIX = '{';
32+
33+
private static final char SUFFIX = '}';
34+
35+
private static final char ESCAPE = '\\';
36+
37+
/**
38+
* Replaces all message parameters using the given {@code parameterResolver}.
39+
* <p>
40+
* If returned value has other message parameters, they will be replaced recursively
41+
* until no replacement is performed;
42+
* <p>
43+
* Resolver can return {@code null} to signal that no further actions need to be done
44+
* and replacement should be omitted;
45+
* <p>
46+
* The message parameter can be escaped by the {@code '\'} symbol;
47+
* @param message the value containing the parameters to be replaced
48+
* @param parameterResolver the {@code parameterResolver} to use for replacement
49+
* @return the replaced message
50+
*/
51+
String replaceParameters(String message, Function<String, String> parameterResolver) {
52+
return replaceParameters(message, parameterResolver, new LinkedHashSet<>(4));
53+
}
54+
55+
private static String replaceParameters(String message, Function<String, String> parameterResolver,
56+
Set<String> visitedParameters) {
57+
StringBuilder buf = new StringBuilder(message);
58+
int parentheses = 0;
59+
int startIndex = -1;
60+
int endIndex = -1;
61+
for (int i = 0; i < buf.length(); i++) {
62+
if (buf.charAt(i) == ESCAPE) {
63+
i++;
64+
}
65+
else if (buf.charAt(i) == PREFIX) {
66+
if (startIndex == -1) {
67+
startIndex = i;
68+
}
69+
parentheses++;
70+
}
71+
else if (buf.charAt(i) == SUFFIX) {
72+
if (parentheses > 0) {
73+
parentheses--;
74+
}
75+
endIndex = i;
76+
}
77+
if (parentheses == 0 && startIndex < endIndex) {
78+
String parameter = buf.substring(startIndex + 1, endIndex);
79+
if (!visitedParameters.add(parameter)) {
80+
throw new IllegalArgumentException("Circular reference '{" + parameter + "}'");
81+
}
82+
String value = replaceParameter(parameter, parameterResolver, visitedParameters);
83+
if (value != null) {
84+
buf.replace(startIndex, endIndex + 1, value);
85+
i = startIndex + value.length() - 1;
86+
}
87+
visitedParameters.remove(parameter);
88+
startIndex = -1;
89+
endIndex = -1;
90+
}
91+
}
92+
return buf.toString();
93+
}
94+
95+
private static String replaceParameter(String parameter, Function<String, String> parameterResolver,
96+
Set<String> visitedParameters) {
97+
parameter = replaceParameters(parameter, parameterResolver, visitedParameters);
98+
String value = parameterResolver.apply(parameter);
99+
if (value != null) {
100+
return replaceParameters(value, parameterResolver, visitedParameters);
101+
}
102+
return null;
103+
}
104+
105+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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+
26+
/**
27+
* Resolves any message parameters via {@link MessageSource} and then interpolates a
28+
* message using the underlying {@link MessageInterpolator}.
29+
*
30+
* @author Dmytro Nosan
31+
*/
32+
class MessageSourceInterpolatorDelegate implements MessageInterpolator {
33+
34+
private static final MessageParameterPlaceholderHelper helper = new MessageParameterPlaceholderHelper();
35+
36+
private final MessageSource messageSource;
37+
38+
private final MessageInterpolator messageInterpolator;
39+
40+
MessageSourceInterpolatorDelegate(MessageSource messageSource, MessageInterpolator messageInterpolator) {
41+
this.messageSource = messageSource;
42+
this.messageInterpolator = messageInterpolator;
43+
}
44+
45+
@Override
46+
public String interpolate(String messageTemplate, Context context) {
47+
return interpolate(messageTemplate, context, LocaleContextHolder.getLocale());
48+
}
49+
50+
@Override
51+
public String interpolate(String messageTemplate, Context context, Locale locale) {
52+
String message = helper.replaceParameters(messageTemplate,
53+
(parameter) -> this.messageSource.getMessage(parameter, null, null, locale));
54+
return this.messageInterpolator.interpolate(message, context, locale);
55+
}
56+
57+
}

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
}

0 commit comments

Comments
 (0)