Skip to content

Commit 400a6bb

Browse files
committed
#1019 - Improvements to I18N optimizations.
We now properly look for resource bundles by scanning for a resource bundle *pattern* instead of a plain file named rest-messages. Added integration tests to make sure that resource bundles are not skipped even if they existed. Added the ability to define common messages in a rest-default-messages.properties, as Spring Data REST requires that and it would be too cumbersome for Spring Data REST to additionally deploy them in its own configuration.
1 parent b1af903 commit 400a6bb

File tree

14 files changed

+165
-30
lines changed

14 files changed

+165
-30
lines changed

src/main/java/org/springframework/hateoas/config/HateoasConfiguration.java

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,26 @@
1515
*/
1616
package org.springframework.hateoas.config;
1717

18-
import org.springframework.context.MessageSource;
18+
import java.io.IOException;
19+
import java.nio.charset.StandardCharsets;
20+
import java.util.Arrays;
21+
import java.util.Collection;
22+
import java.util.Collections;
23+
import java.util.List;
24+
import java.util.Properties;
25+
import java.util.stream.Collectors;
26+
27+
import org.springframework.beans.factory.annotation.Autowired;
28+
import org.springframework.beans.factory.config.PropertiesFactoryBean;
29+
import org.springframework.context.ApplicationContext;
1930
import org.springframework.context.annotation.Bean;
2031
import org.springframework.context.annotation.Configuration;
2132
import org.springframework.context.annotation.Import;
2233
import org.springframework.context.annotation.Primary;
34+
import org.springframework.context.support.AbstractMessageSource;
2335
import org.springframework.context.support.AbstractResourceBasedMessageSource;
2436
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
25-
import org.springframework.core.io.ClassPathResource;
37+
import org.springframework.core.io.Resource;
2638
import org.springframework.hateoas.client.LinkDiscoverer;
2739
import org.springframework.hateoas.client.LinkDiscoverers;
2840
import org.springframework.hateoas.mediatype.MessageResolver;
@@ -50,7 +62,9 @@
5062
@Configuration
5163
@Import(EntityLinksConfiguration.class)
5264
@EnablePluginRegistries({ LinkDiscoverer.class })
53-
class HateoasConfiguration {
65+
public class HateoasConfiguration {
66+
67+
private @Autowired ApplicationContext context;
5468

5569
@Bean
5670
public MessageResolver messageResolver() {
@@ -104,17 +118,53 @@ LinkDiscoverers linkDiscoverers(PluginRegistry<LinkDiscoverer, MediaType> discov
104118
* @return will never be {@literal null}.
105119
*/
106120
@Nullable
107-
private static final MessageSource lookupMessageSource() {
121+
private final AbstractMessageSource lookupMessageSource() {
108122

109-
ClassPathResource resource = new ClassPathResource("rest-messages");
123+
List<Resource> candidates = loadProperties("rest-default-messages", false);
110124

111-
if (!resource.exists()) {
125+
if (candidates.isEmpty() && loadProperties("rest-messages", true).isEmpty()) {
112126
return null;
113127
}
114128

115129
AbstractResourceBasedMessageSource messageSource = new ReloadableResourceBundleMessageSource();
116130
messageSource.setBasename("classpath:rest-messages");
131+
messageSource.setDefaultEncoding(StandardCharsets.UTF_8.toString());
132+
133+
if (!candidates.isEmpty()) {
134+
messageSource.setCommonMessages(loadProperties(candidates));
135+
}
117136

118137
return messageSource;
119138
}
139+
140+
@Nullable
141+
private final Properties loadProperties(Collection<Resource> sources) {
142+
143+
Resource[] resources = loadProperties("rest-default-messages", false).stream().toArray(Resource[]::new);
144+
145+
PropertiesFactoryBean factory = new PropertiesFactoryBean();
146+
factory.setLocations(resources);
147+
148+
try {
149+
150+
factory.afterPropertiesSet();
151+
return factory.getObject();
152+
153+
} catch (IOException o_O) {
154+
throw new IllegalStateException("Could not load default properties from resources!", o_O);
155+
}
156+
}
157+
158+
private final List<Resource> loadProperties(String baseName, boolean withWildcard) {
159+
160+
try {
161+
return Arrays //
162+
.stream(context.getResources(String.format("classpath:%s%s.properties", baseName, withWildcard ? "*" : ""))) //
163+
.filter(Resource::exists) //
164+
.collect(Collectors.toList());
165+
166+
} catch (IOException e) {
167+
return Collections.emptyList();
168+
}
169+
}
120170
}

src/main/java/org/springframework/hateoas/mediatype/NoMessageResolver.java renamed to src/main/java/org/springframework/hateoas/mediatype/DefaultOnlyMessageResolver.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@
1818
import org.springframework.context.MessageSourceResolvable;
1919
import org.springframework.lang.Nullable;
2020

21-
enum NoMessageResolver implements MessageResolver {
21+
/**
22+
* {@link MessageResolver} to always resort to the {@link MessageSourceResolvable}'s default message.
23+
*
24+
* @author Oliver Drotbohm
25+
*/
26+
enum DefaultOnlyMessageResolver implements MessageResolver {
2227

2328
INSTANCE;
2429

@@ -29,6 +34,6 @@ enum NoMessageResolver implements MessageResolver {
2934
@Nullable
3035
@Override
3136
public String resolve(MessageSourceResolvable resolvable) {
32-
return null;
37+
return resolvable.getDefaultMessage();
3338
}
3439
}

src/main/java/org/springframework/hateoas/mediatype/MessageResolver.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
*/
2929
public interface MessageResolver {
3030

31-
public static final MessageResolver NONE = NoMessageResolver.INSTANCE;
31+
public static final MessageResolver DEFAULTS_ONLY = DefaultOnlyMessageResolver.INSTANCE;
3232

3333
/**
3434
* Resolve the given {@link MessageSourceResolvable}. Return {@literal null} if no message was found.
@@ -46,6 +46,9 @@ public interface MessageResolver {
4646
* @return will never be {@literal null}.
4747
*/
4848
public static MessageResolver of(@Nullable MessageSource messageSource) {
49-
return messageSource == null ? NoMessageResolver.INSTANCE : new MessageSourceResolver(messageSource);
49+
50+
return messageSource == null //
51+
? DefaultOnlyMessageResolver.INSTANCE //
52+
: new MessageSourceResolver(messageSource);
5053
}
5154
}

src/main/java/org/springframework/hateoas/mediatype/hal/HalMediaTypeConfiguration.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.util.List;
1919

2020
import org.springframework.beans.factory.ObjectProvider;
21+
import org.springframework.beans.factory.annotation.Qualifier;
2122
import org.springframework.context.annotation.Bean;
2223
import org.springframework.context.annotation.Configuration;
2324
import org.springframework.hateoas.client.LinkDiscoverer;
@@ -42,7 +43,7 @@ public class HalMediaTypeConfiguration implements HypermediaMappingInformation {
4243
private final LinkRelationProvider relProvider;
4344
private final ObjectProvider<CurieProvider> curieProvider;
4445
private final ObjectProvider<HalConfiguration> halConfiguration;
45-
private final MessageResolver resolver;
46+
private final @Qualifier("messageResolver") MessageResolver resolver;
4647

4748
/**
4849
* @param relProvider

src/main/java/org/springframework/hateoas/mediatype/hal/Jackson2HalModule.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,14 +121,19 @@ public HalLinkListSerializer(CurieProvider curieProvider, EmbeddedMapper mapper,
121121
}
122122

123123
public HalLinkListSerializer(@Nullable BeanProperty property, CurieProvider curieProvider, EmbeddedMapper mapper,
124-
MessageResolver accessor, HalConfiguration halConfiguration) {
124+
MessageResolver resolver, HalConfiguration halConfiguration) {
125125

126126
super(TypeFactory.defaultInstance().constructType(Links.class));
127127

128+
Assert.notNull(curieProvider, "CurieProvider must not be null!");
129+
Assert.notNull(mapper, "EmbeddedMapper must not be null!");
130+
Assert.notNull(resolver, "MessageResolver must not be null!");
131+
Assert.notNull(halConfiguration, "HalConfiguration must not be null!");
132+
128133
this.property = property;
129134
this.curieProvider = curieProvider;
130135
this.mapper = mapper;
131-
this.resolver = accessor;
136+
this.resolver = resolver;
132137
this.halConfiguration = halConfiguration;
133138
}
134139

src/test/java/org/springframework/hateoas/client/Server.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public Server() {
6565
this.mapper = new ObjectMapper();
6666
this.mapper.registerModule(new Jackson2HalModule());
6767
this.mapper.setHandlerInstantiator(
68-
new Jackson2HalModule.HalHandlerInstantiator(relProvider, CurieProvider.NONE, MessageResolver.NONE));
68+
new Jackson2HalModule.HalHandlerInstantiator(relProvider, CurieProvider.NONE, MessageResolver.DEFAULTS_ONLY));
6969

7070
initJadler() //
7171
.withDefaultResponseContentType(MediaTypes.HAL_JSON.toString()) //

src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,33 @@
1616
package org.springframework.hateoas.config;
1717

1818
import static org.assertj.core.api.Assertions.*;
19+
import static org.mockito.Mockito.*;
1920
import static org.springframework.hateoas.mediatype.hal.HalConfiguration.RenderSingleLinks.*;
2021
import static org.springframework.hateoas.support.ContextTester.*;
2122

23+
import java.io.IOException;
2224
import java.lang.reflect.Method;
2325
import java.util.List;
2426
import java.util.Optional;
2527
import java.util.function.Function;
2628

2729
import org.junit.jupiter.api.Test;
2830
import org.junit.jupiter.api.extension.ExtendWith;
31+
import org.mockito.Mockito;
2932
import org.mockito.junit.jupiter.MockitoExtension;
3033
import org.springframework.context.ApplicationContext;
3134
import org.springframework.context.annotation.Bean;
3235
import org.springframework.context.annotation.Configuration;
3336
import org.springframework.context.annotation.Import;
37+
import org.springframework.core.io.ClassPathResource;
38+
import org.springframework.core.io.Resource;
3439
import org.springframework.hateoas.Link;
3540
import org.springframework.hateoas.MediaTypes;
3641
import org.springframework.hateoas.RepresentationModel;
3742
import org.springframework.hateoas.client.LinkDiscoverer;
3843
import org.springframework.hateoas.client.LinkDiscoverers;
3944
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
45+
import org.springframework.hateoas.mediatype.MessageResolver;
4046
import org.springframework.hateoas.mediatype.collectionjson.CollectionJsonLinkDiscoverer;
4147
import org.springframework.hateoas.mediatype.hal.HalConfiguration;
4248
import org.springframework.hateoas.mediatype.hal.HalLinkDiscoverer;
@@ -54,6 +60,7 @@
5460
import org.springframework.test.util.ReflectionTestUtils;
5561
import org.springframework.util.ReflectionUtils;
5662
import org.springframework.web.client.RestTemplate;
63+
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
5764
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
5865
import org.springframework.web.method.support.HandlerMethodArgumentResolverComposite;
5966
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@@ -522,6 +529,37 @@ void verifyRenderSingleLinkAsArrayViaOverridingBean() {
522529
);
523530
}
524531

532+
@Test // #1019
533+
void registersNoOpMessageResolverIfMessagesBundleMissing() {
534+
535+
withServletContext(HateoasConfiguration.class, //
536+
context -> {
537+
assertThat(context.getBean(MessageResolver.class)).isEqualTo(MessageResolver.of(null));
538+
});
539+
}
540+
541+
@Test // #1019
542+
void registersMessageResolverIfMessagesBundleAvailable() {
543+
544+
withServletContext(HateoasConfiguration.class, simulateResourceBundle(), context -> {
545+
assertThat(context.getBean(MessageResolver.class)).isNotEqualTo(MessageResolver.of(null));
546+
});
547+
}
548+
549+
@Test // #1019, DATAREST-686
550+
void defaultsEncodingOfMessageSourceToUtf8() throws Exception {
551+
552+
withServletContext(HalConfig.class, simulateResourceBundle(), context -> {
553+
554+
MessageResolver resolver = context.getBean(MessageResolver.class);
555+
556+
Object accessor = ReflectionTestUtils.getField(resolver, "accessor");
557+
Object messageSource = ReflectionTestUtils.getField(accessor, "messageSource");
558+
559+
assertThat((String) ReflectionTestUtils.getField(messageSource, "defaultEncoding")).isEqualTo("UTF-8");
560+
});
561+
}
562+
525563
private static void assertEntityLinksSetUp(ApplicationContext context) {
526564

527565
assertThat(context.getBeansOfType(EntityLinks.class).values()) //
@@ -607,6 +645,29 @@ private static List<HandlerMethodArgumentResolver> getResolvers(RequestMappingHa
607645
throw new IllegalStateException("Unexpected result when looking up argument resolvers!");
608646
}
609647

648+
private static <T extends AnnotationConfigWebApplicationContext> Function<T, T> simulateResourceBundle() {
649+
650+
return context -> {
651+
652+
T spy = Mockito.spy(context);
653+
654+
ClassPathResource resource = new ClassPathResource("rest-messages.properties",
655+
EnableHypermediaSupportIntegrationTest.class);
656+
assertThat(resource.exists()).isTrue();
657+
658+
try {
659+
660+
doReturn(new Resource[0]).when(spy).getResources("classpath:rest-default-messages.properties");
661+
doReturn(new Resource[] { resource }).when(spy).getResources("classpath:rest-messages*.properties");
662+
663+
} catch (IOException o_O) {
664+
fail("Couldn't mock resource lookup!", o_O);
665+
}
666+
667+
return spy;
668+
};
669+
}
670+
610671
@Configuration
611672
@EnableWebMvc
612673
@Import(DelegateConfig.class)

src/test/java/org/springframework/hateoas/mediatype/hal/Jackson2HalIntegrationTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ void setUpModule() {
8989

9090
mapper.registerModule(new Jackson2HalModule());
9191
mapper.setHandlerInstantiator(
92-
new HalHandlerInstantiator(provider, CurieProvider.NONE, MessageResolver.NONE, new HalConfiguration()));
92+
new HalHandlerInstantiator(provider, CurieProvider.NONE, MessageResolver.DEFAULTS_ONLY, new HalConfiguration()));
9393
}
9494

9595
/**
@@ -447,7 +447,7 @@ void rendersTitleIfMessageSourceResolvesLocalKey() throws Exception {
447447
void rendersSingleLinkAsArrayWhenConfigured() throws Exception {
448448

449449
mapper.setHandlerInstantiator(new HalHandlerInstantiator(new AnnotationLinkRelationProvider(), CurieProvider.NONE,
450-
MessageResolver.NONE, new HalConfiguration().withRenderSingleLinks(RenderSingleLinks.AS_ARRAY)));
450+
MessageResolver.DEFAULTS_ONLY, new HalConfiguration().withRenderSingleLinks(RenderSingleLinks.AS_ARRAY)));
451451

452452
RepresentationModel<?> resourceSupport = new RepresentationModel<>();
453453
resourceSupport.add(new Link("localhost").withSelfRel());
@@ -480,7 +480,7 @@ void rendersSpecificRelWithSingleLinkAsArrayIfConfigured() throws Exception {
480480

481481
AnnotationLinkRelationProvider provider = new AnnotationLinkRelationProvider();
482482

483-
mapper.setHandlerInstantiator(new HalHandlerInstantiator(provider, CurieProvider.NONE, MessageResolver.NONE,
483+
mapper.setHandlerInstantiator(new HalHandlerInstantiator(provider, CurieProvider.NONE, MessageResolver.DEFAULTS_ONLY,
484484
new HalConfiguration().withRenderSingleLinksFor("foo", RenderSingleLinks.AS_ARRAY)));
485485

486486
RepresentationModel<?> resource = new RepresentationModel<>();

src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsMessageConverterUnitTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ void setUp() {
5656
this.mapper.registerModule(new Jackson2HalFormsModule());
5757
this.mapper.setHandlerInstantiator(
5858
new Jackson2HalFormsModule.HalFormsHandlerInstantiator(new AnnotationLinkRelationProvider(), CurieProvider.NONE,
59-
MessageResolver.NONE, true, new HalFormsConfiguration()));
59+
MessageResolver.DEFAULTS_ONLY, true, new HalFormsConfiguration()));
6060

6161
TypeConstrainedMappingJackson2HttpMessageConverter converter = new TypeConstrainedMappingJackson2HttpMessageConverter(
6262
RepresentationModel.class);

src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsTemplateBuilderUnitTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ void detectsRegularExpressionsOnProperties(String propertyName, String expected)
4545
HalFormsConfiguration configuration = new HalFormsConfiguration();
4646
configuration.registerPattern(CreditCardNumber.class, "[0-9]{16}");
4747

48-
HalFormsTemplateBuilder builder = new HalFormsTemplateBuilder(configuration, MessageResolver.NONE);
48+
HalFormsTemplateBuilder builder = new HalFormsTemplateBuilder(configuration, MessageResolver.DEFAULTS_ONLY);
4949

5050
PatternExample resource = new PatternExample();
5151
resource.add(Affordances.of(new Link("/examples")) //
@@ -79,7 +79,7 @@ void allPropertiesAreOptionalForPatchRequests() throws Exception {
7979
.withName("post") //
8080
.toLink());
8181

82-
HalFormsTemplateBuilder builder = new HalFormsTemplateBuilder(new HalFormsConfiguration(), MessageResolver.NONE);
82+
HalFormsTemplateBuilder builder = new HalFormsTemplateBuilder(new HalFormsConfiguration(), MessageResolver.DEFAULTS_ONLY);
8383

8484
Map<String, HalFormsTemplate> templates = builder.findTemplates(model);
8585

0 commit comments

Comments
 (0)