Skip to content

Commit 4f96bea

Browse files
committed
Merge pull request #42472 from mmoayyed
* pr/42472: Polish 'Allow common messages to be specified for message sources' Allow common messages to be specified for message sources Closes gh-42472
2 parents d966867 + 06569af commit 4f96bea

File tree

5 files changed

+76
-18
lines changed

5 files changed

+76
-18
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/context/MessageSourceAutoConfiguration.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616

1717
package org.springframework.boot.autoconfigure.context;
1818

19+
import java.io.IOException;
20+
import java.io.UncheckedIOException;
1921
import java.time.Duration;
22+
import java.util.List;
23+
import java.util.Properties;
2024

2125
import org.springframework.aot.hint.RuntimeHints;
2226
import org.springframework.aot.hint.RuntimeHintsRegistrar;
@@ -38,9 +42,11 @@
3842
import org.springframework.context.annotation.ImportRuntimeHints;
3943
import org.springframework.context.support.AbstractApplicationContext;
4044
import org.springframework.context.support.ResourceBundleMessageSource;
45+
import org.springframework.core.CollectionFactory;
4146
import org.springframework.core.Ordered;
4247
import org.springframework.core.io.Resource;
4348
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
49+
import org.springframework.core.io.support.PropertiesLoaderUtils;
4450
import org.springframework.core.type.AnnotatedTypeMetadata;
4551
import org.springframework.util.CollectionUtils;
4652
import org.springframework.util.ConcurrentReferenceHashMap;
@@ -81,9 +87,26 @@ public MessageSource messageSource(MessageSourceProperties properties) {
8187
}
8288
messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
8389
messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
90+
messageSource.setCommonMessages(loadCommonMessages(properties.getCommonMessages()));
8491
return messageSource;
8592
}
8693

94+
private Properties loadCommonMessages(List<Resource> resources) {
95+
if (CollectionUtils.isEmpty(resources)) {
96+
return null;
97+
}
98+
Properties properties = CollectionFactory.createSortedProperties(false);
99+
for (Resource resource : resources) {
100+
try {
101+
PropertiesLoaderUtils.fillProperties(properties, resource);
102+
}
103+
catch (IOException ex) {
104+
throw new UncheckedIOException("Failed to load common messages from '%s'".formatted(resource), ex);
105+
}
106+
}
107+
return properties;
108+
}
109+
87110
protected static class ResourceBundleCondition extends SpringBootCondition {
88111

89112
private static final ConcurrentReferenceHashMap<String, ConditionOutcome> cache = new ConcurrentReferenceHashMap<>();

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/context/MessageSourceProperties.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
import org.springframework.boot.context.properties.ConfigurationProperties;
2727
import org.springframework.boot.convert.DurationUnit;
28+
import org.springframework.core.io.Resource;
2829

2930
/**
3031
* Configuration properties for Message Source.
@@ -44,6 +45,11 @@ public class MessageSourceProperties {
4445
*/
4546
private List<String> basename = new ArrayList<>(List.of("messages"));
4647

48+
/**
49+
* List of locale-independent property file resources containing common messages.
50+
*/
51+
private List<Resource> commonMessages;
52+
4753
/**
4854
* Message bundles encoding.
4955
*/
@@ -123,4 +129,12 @@ public void setUseCodeAsDefaultMessage(boolean useCodeAsDefaultMessage) {
123129
this.useCodeAsDefaultMessage = useCodeAsDefaultMessage;
124130
}
125131

132+
public List<Resource> getCommonMessages() {
133+
return this.commonMessages;
134+
}
135+
136+
public void setCommonMessages(List<Resource> commonMessages) {
137+
this.commonMessages = commonMessages;
138+
}
139+
126140
}

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/MessageSourceAutoConfigurationTests.java

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -58,38 +58,38 @@ void testDefaultMessageSource() {
5858

5959
@Test
6060
void propertiesBundleWithSlashIsDetected() {
61-
this.contextRunner.withPropertyValues("spring.messages.basename:test/messages").run((context) -> {
61+
this.contextRunner.withPropertyValues("spring.messages.basename=test/messages").run((context) -> {
6262
assertThat(context).hasSingleBean(MessageSource.class);
6363
assertThat(context.getMessage("foo", null, "Foo message", Locale.UK)).isEqualTo("bar");
6464
});
6565
}
6666

6767
@Test
6868
void propertiesBundleWithDotIsDetected() {
69-
this.contextRunner.withPropertyValues("spring.messages.basename:test.messages").run((context) -> {
69+
this.contextRunner.withPropertyValues("spring.messages.basename=test.messages").run((context) -> {
7070
assertThat(context).hasSingleBean(MessageSource.class);
7171
assertThat(context.getMessage("foo", null, "Foo message", Locale.UK)).isEqualTo("bar");
7272
});
7373
}
7474

7575
@Test
7676
void testEncodingWorks() {
77-
this.contextRunner.withPropertyValues("spring.messages.basename:test/swedish")
77+
this.contextRunner.withPropertyValues("spring.messages.basename=test/swedish")
7878
.run((context) -> assertThat(context.getMessage("foo", null, "Foo message", Locale.UK))
7979
.isEqualTo("Some text with some swedish öäå!"));
8080
}
8181

8282
@Test
8383
void testCacheDurationNoUnit() {
8484
this.contextRunner
85-
.withPropertyValues("spring.messages.basename:test/messages", "spring.messages.cache-duration=10")
85+
.withPropertyValues("spring.messages.basename=test/messages", "spring.messages.cache-duration=10")
8686
.run(assertCache(10 * 1000));
8787
}
8888

8989
@Test
9090
void testCacheDurationWithUnit() {
9191
this.contextRunner
92-
.withPropertyValues("spring.messages.basename:test/messages", "spring.messages.cache-duration=1m")
92+
.withPropertyValues("spring.messages.basename=test/messages", "spring.messages.cache-duration=1m")
9393
.run(assertCache(60 * 1000));
9494
}
9595

@@ -102,7 +102,7 @@ private ContextConsumer<AssertableApplicationContext> assertCache(long expected)
102102

103103
@Test
104104
void testMultipleMessageSourceCreated() {
105-
this.contextRunner.withPropertyValues("spring.messages.basename:test/messages,test/messages2")
105+
this.contextRunner.withPropertyValues("spring.messages.basename=test/messages,test/messages2")
106106
.run((context) -> {
107107
assertThat(context.getMessage("foo", null, "Foo message", Locale.UK)).isEqualTo("bar");
108108
assertThat(context.getMessage("foo-foo", null, "Foo-Foo message", Locale.UK)).isEqualTo("bar-bar");
@@ -116,50 +116,68 @@ void testMessageSourceFromPropertySourceAnnotation() {
116116
.run((context) -> assertThat(context.getMessage("foo", null, "Foo message", Locale.UK)).isEqualTo("bar"));
117117
}
118118

119+
@Test
120+
void testCommonMessages() {
121+
this.contextRunner
122+
.withPropertyValues("spring.messages.basename=test/messages",
123+
"spring.messages.common-messages=classpath:test/common-messages.properties")
124+
.run((context) -> assertThat(context.getMessage("hello", null, "Hello!", Locale.UK)).isEqualTo("world"));
125+
}
126+
127+
@Test
128+
void testCommonMessagesWhenNotFound() {
129+
this.contextRunner
130+
.withPropertyValues("spring.messages.basename=test/messages",
131+
"spring.messages.common-messages=classpath:test/common-messages-missing.properties")
132+
.run((context) -> assertThat(context).getFailure()
133+
.hasMessageContaining(
134+
"Failed to load common messages from 'class path resource [test/common-messages-missing.properties]'"));
135+
}
136+
119137
@Test
120138
void testFallbackDefault() {
121-
this.contextRunner.withPropertyValues("spring.messages.basename:test/messages")
139+
this.contextRunner.withPropertyValues("spring.messages.basename=test/messages")
122140
.run((context) -> assertThat(context.getBean(MessageSource.class))
123141
.hasFieldOrPropertyWithValue("fallbackToSystemLocale", true));
124142
}
125143

126144
@Test
127145
void testFallbackTurnOff() {
128146
this.contextRunner
129-
.withPropertyValues("spring.messages.basename:test/messages",
147+
.withPropertyValues("spring.messages.basename=test/messages",
130148
"spring.messages.fallback-to-system-locale:false")
131149
.run((context) -> assertThat(context.getBean(MessageSource.class))
132150
.hasFieldOrPropertyWithValue("fallbackToSystemLocale", false));
133151
}
134152

135153
@Test
136154
void testFormatMessageDefault() {
137-
this.contextRunner.withPropertyValues("spring.messages.basename:test/messages")
155+
this.contextRunner.withPropertyValues("spring.messages.basename=test/messages")
138156
.run((context) -> assertThat(context.getBean(MessageSource.class))
139157
.hasFieldOrPropertyWithValue("alwaysUseMessageFormat", false));
140158
}
141159

142160
@Test
143161
void testFormatMessageOn() {
144162
this.contextRunner
145-
.withPropertyValues("spring.messages.basename:test/messages",
163+
.withPropertyValues("spring.messages.basename=test/messages",
146164
"spring.messages.always-use-message-format:true")
147165
.run((context) -> assertThat(context.getBean(MessageSource.class))
148166
.hasFieldOrPropertyWithValue("alwaysUseMessageFormat", true));
149167
}
150168

151169
@Test
152170
void testUseCodeAsDefaultMessageDefault() {
153-
this.contextRunner.withPropertyValues("spring.messages.basename:test/messages")
171+
this.contextRunner.withPropertyValues("spring.messages.basename=test/messages")
154172
.run((context) -> assertThat(context.getBean(MessageSource.class))
155173
.hasFieldOrPropertyWithValue("useCodeAsDefaultMessage", false));
156174
}
157175

158176
@Test
159177
void testUseCodeAsDefaultMessageOn() {
160178
this.contextRunner
161-
.withPropertyValues("spring.messages.basename:test/messages",
162-
"spring.messages.use-code-as-default-message:true")
179+
.withPropertyValues("spring.messages.basename=test/messages",
180+
"spring.messages.use-code-as-default-message=true")
163181
.run((context) -> assertThat(context.getBean(MessageSource.class))
164182
.hasFieldOrPropertyWithValue("useCodeAsDefaultMessage", true));
165183
}
@@ -173,13 +191,13 @@ void existingMessageSourceIsPreferred() {
173191
@Test
174192
void existingMessageSourceInParentIsIgnored() {
175193
this.contextRunner.run((parent) -> this.contextRunner.withParent(parent)
176-
.withPropertyValues("spring.messages.basename:test/messages")
194+
.withPropertyValues("spring.messages.basename=test/messages")
177195
.run((context) -> assertThat(context.getMessage("foo", null, "Foo message", Locale.UK)).isEqualTo("bar")));
178196
}
179197

180198
@Test
181199
void messageSourceWithNonStandardBeanNameIsIgnored() {
182-
this.contextRunner.withPropertyValues("spring.messages.basename:test/messages")
200+
this.contextRunner.withPropertyValues("spring.messages.basename=test/messages")
183201
.withUserConfiguration(CustomBeanNameMessageSourceConfiguration.class)
184202
.run((context) -> assertThat(context.getMessage("foo", null, Locale.US)).isEqualTo("bar"));
185203
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
hello=world

spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/internationalization.adoc

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ The basename of the resource bundle as well as several other attributes can be c
1414
----
1515
spring:
1616
messages:
17-
basename: "messages,config.i18n.messages"
17+
basename: "messages, config.i18n.messages"
18+
common-messages: "classpath:my-common-messages.properties"
1819
fallback-to-system-locale: false
1920
----
2021

21-
TIP: `spring.messages.basename` supports comma-separated list of locations, either a package qualifier or a resource resolved from the classpath root.
22+
TIP: The configprop:spring.messages.basename[] property supports a list of locations, either a package qualifier or a resource resolved from the classpath root.
23+
The configprop:spring.messages.common-messages[] property supports a list of property file resources.
2224

2325
See javadoc:org.springframework.boot.autoconfigure.context.MessageSourceProperties[] for more supported options.

0 commit comments

Comments
 (0)