#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.
This commit is contained in:
@@ -15,14 +15,26 @@
|
||||
*/
|
||||
package org.springframework.hateoas.config;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.PropertiesFactoryBean;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.context.support.AbstractMessageSource;
|
||||
import org.springframework.context.support.AbstractResourceBasedMessageSource;
|
||||
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.hateoas.client.LinkDiscoverer;
|
||||
import org.springframework.hateoas.client.LinkDiscoverers;
|
||||
import org.springframework.hateoas.mediatype.MessageResolver;
|
||||
@@ -50,7 +62,9 @@ import org.springframework.util.ClassUtils;
|
||||
@Configuration
|
||||
@Import(EntityLinksConfiguration.class)
|
||||
@EnablePluginRegistries({ LinkDiscoverer.class })
|
||||
class HateoasConfiguration {
|
||||
public class HateoasConfiguration {
|
||||
|
||||
private @Autowired ApplicationContext context;
|
||||
|
||||
@Bean
|
||||
public MessageResolver messageResolver() {
|
||||
@@ -104,17 +118,53 @@ class HateoasConfiguration {
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
@Nullable
|
||||
private static final MessageSource lookupMessageSource() {
|
||||
private final AbstractMessageSource lookupMessageSource() {
|
||||
|
||||
ClassPathResource resource = new ClassPathResource("rest-messages");
|
||||
List<Resource> candidates = loadProperties("rest-default-messages", false);
|
||||
|
||||
if (!resource.exists()) {
|
||||
if (candidates.isEmpty() && loadProperties("rest-messages", true).isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
AbstractResourceBasedMessageSource messageSource = new ReloadableResourceBundleMessageSource();
|
||||
messageSource.setBasename("classpath:rest-messages");
|
||||
messageSource.setDefaultEncoding(StandardCharsets.UTF_8.toString());
|
||||
|
||||
if (!candidates.isEmpty()) {
|
||||
messageSource.setCommonMessages(loadProperties(candidates));
|
||||
}
|
||||
|
||||
return messageSource;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private final Properties loadProperties(Collection<Resource> sources) {
|
||||
|
||||
Resource[] resources = loadProperties("rest-default-messages", false).stream().toArray(Resource[]::new);
|
||||
|
||||
PropertiesFactoryBean factory = new PropertiesFactoryBean();
|
||||
factory.setLocations(resources);
|
||||
|
||||
try {
|
||||
|
||||
factory.afterPropertiesSet();
|
||||
return factory.getObject();
|
||||
|
||||
} catch (IOException o_O) {
|
||||
throw new IllegalStateException("Could not load default properties from resources!", o_O);
|
||||
}
|
||||
}
|
||||
|
||||
private final List<Resource> loadProperties(String baseName, boolean withWildcard) {
|
||||
|
||||
try {
|
||||
return Arrays //
|
||||
.stream(context.getResources(String.format("classpath:%s%s.properties", baseName, withWildcard ? "*" : ""))) //
|
||||
.filter(Resource::exists) //
|
||||
.collect(Collectors.toList());
|
||||
|
||||
} catch (IOException e) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,12 @@ package org.springframework.hateoas.mediatype;
|
||||
import org.springframework.context.MessageSourceResolvable;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
enum NoMessageResolver implements MessageResolver {
|
||||
/**
|
||||
* {@link MessageResolver} to always resort to the {@link MessageSourceResolvable}'s default message.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
enum DefaultOnlyMessageResolver implements MessageResolver {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
@@ -29,6 +34,6 @@ enum NoMessageResolver implements MessageResolver {
|
||||
@Nullable
|
||||
@Override
|
||||
public String resolve(MessageSourceResolvable resolvable) {
|
||||
return null;
|
||||
return resolvable.getDefaultMessage();
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ import org.springframework.lang.Nullable;
|
||||
*/
|
||||
public interface MessageResolver {
|
||||
|
||||
public static final MessageResolver NONE = NoMessageResolver.INSTANCE;
|
||||
public static final MessageResolver DEFAULTS_ONLY = DefaultOnlyMessageResolver.INSTANCE;
|
||||
|
||||
/**
|
||||
* Resolve the given {@link MessageSourceResolvable}. Return {@literal null} if no message was found.
|
||||
@@ -46,6 +46,9 @@ public interface MessageResolver {
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public static MessageResolver of(@Nullable MessageSource messageSource) {
|
||||
return messageSource == null ? NoMessageResolver.INSTANCE : new MessageSourceResolver(messageSource);
|
||||
|
||||
return messageSource == null //
|
||||
? DefaultOnlyMessageResolver.INSTANCE //
|
||||
: new MessageSourceResolver(messageSource);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.springframework.hateoas.mediatype.hal;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.hateoas.client.LinkDiscoverer;
|
||||
@@ -42,7 +43,7 @@ public class HalMediaTypeConfiguration implements HypermediaMappingInformation {
|
||||
private final LinkRelationProvider relProvider;
|
||||
private final ObjectProvider<CurieProvider> curieProvider;
|
||||
private final ObjectProvider<HalConfiguration> halConfiguration;
|
||||
private final MessageResolver resolver;
|
||||
private final @Qualifier("messageResolver") MessageResolver resolver;
|
||||
|
||||
/**
|
||||
* @param relProvider
|
||||
|
||||
@@ -121,14 +121,19 @@ public class Jackson2HalModule extends SimpleModule {
|
||||
}
|
||||
|
||||
public HalLinkListSerializer(@Nullable BeanProperty property, CurieProvider curieProvider, EmbeddedMapper mapper,
|
||||
MessageResolver accessor, HalConfiguration halConfiguration) {
|
||||
MessageResolver resolver, HalConfiguration halConfiguration) {
|
||||
|
||||
super(TypeFactory.defaultInstance().constructType(Links.class));
|
||||
|
||||
Assert.notNull(curieProvider, "CurieProvider must not be null!");
|
||||
Assert.notNull(mapper, "EmbeddedMapper must not be null!");
|
||||
Assert.notNull(resolver, "MessageResolver must not be null!");
|
||||
Assert.notNull(halConfiguration, "HalConfiguration must not be null!");
|
||||
|
||||
this.property = property;
|
||||
this.curieProvider = curieProvider;
|
||||
this.mapper = mapper;
|
||||
this.resolver = accessor;
|
||||
this.resolver = resolver;
|
||||
this.halConfiguration = halConfiguration;
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ public class Server implements Closeable {
|
||||
this.mapper = new ObjectMapper();
|
||||
this.mapper.registerModule(new Jackson2HalModule());
|
||||
this.mapper.setHandlerInstantiator(
|
||||
new Jackson2HalModule.HalHandlerInstantiator(relProvider, CurieProvider.NONE, MessageResolver.NONE));
|
||||
new Jackson2HalModule.HalHandlerInstantiator(relProvider, CurieProvider.NONE, MessageResolver.DEFAULTS_ONLY));
|
||||
|
||||
initJadler() //
|
||||
.withDefaultResponseContentType(MediaTypes.HAL_JSON.toString()) //
|
||||
|
||||
@@ -16,9 +16,11 @@
|
||||
package org.springframework.hateoas.config;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.hateoas.mediatype.hal.HalConfiguration.RenderSingleLinks.*;
|
||||
import static org.springframework.hateoas.support.ContextTester.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
@@ -26,17 +28,21 @@ import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.MediaTypes;
|
||||
import org.springframework.hateoas.RepresentationModel;
|
||||
import org.springframework.hateoas.client.LinkDiscoverer;
|
||||
import org.springframework.hateoas.client.LinkDiscoverers;
|
||||
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
|
||||
import org.springframework.hateoas.mediatype.MessageResolver;
|
||||
import org.springframework.hateoas.mediatype.collectionjson.CollectionJsonLinkDiscoverer;
|
||||
import org.springframework.hateoas.mediatype.hal.HalConfiguration;
|
||||
import org.springframework.hateoas.mediatype.hal.HalLinkDiscoverer;
|
||||
@@ -54,6 +60,7 @@ import org.springframework.http.converter.json.MappingJackson2HttpMessageConvert
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolverComposite;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
@@ -522,6 +529,37 @@ class EnableHypermediaSupportIntegrationTest {
|
||||
);
|
||||
}
|
||||
|
||||
@Test // #1019
|
||||
void registersNoOpMessageResolverIfMessagesBundleMissing() {
|
||||
|
||||
withServletContext(HateoasConfiguration.class, //
|
||||
context -> {
|
||||
assertThat(context.getBean(MessageResolver.class)).isEqualTo(MessageResolver.of(null));
|
||||
});
|
||||
}
|
||||
|
||||
@Test // #1019
|
||||
void registersMessageResolverIfMessagesBundleAvailable() {
|
||||
|
||||
withServletContext(HateoasConfiguration.class, simulateResourceBundle(), context -> {
|
||||
assertThat(context.getBean(MessageResolver.class)).isNotEqualTo(MessageResolver.of(null));
|
||||
});
|
||||
}
|
||||
|
||||
@Test // #1019, DATAREST-686
|
||||
void defaultsEncodingOfMessageSourceToUtf8() throws Exception {
|
||||
|
||||
withServletContext(HalConfig.class, simulateResourceBundle(), context -> {
|
||||
|
||||
MessageResolver resolver = context.getBean(MessageResolver.class);
|
||||
|
||||
Object accessor = ReflectionTestUtils.getField(resolver, "accessor");
|
||||
Object messageSource = ReflectionTestUtils.getField(accessor, "messageSource");
|
||||
|
||||
assertThat((String) ReflectionTestUtils.getField(messageSource, "defaultEncoding")).isEqualTo("UTF-8");
|
||||
});
|
||||
}
|
||||
|
||||
private static void assertEntityLinksSetUp(ApplicationContext context) {
|
||||
|
||||
assertThat(context.getBeansOfType(EntityLinks.class).values()) //
|
||||
@@ -607,6 +645,29 @@ class EnableHypermediaSupportIntegrationTest {
|
||||
throw new IllegalStateException("Unexpected result when looking up argument resolvers!");
|
||||
}
|
||||
|
||||
private static <T extends AnnotationConfigWebApplicationContext> Function<T, T> simulateResourceBundle() {
|
||||
|
||||
return context -> {
|
||||
|
||||
T spy = Mockito.spy(context);
|
||||
|
||||
ClassPathResource resource = new ClassPathResource("rest-messages.properties",
|
||||
EnableHypermediaSupportIntegrationTest.class);
|
||||
assertThat(resource.exists()).isTrue();
|
||||
|
||||
try {
|
||||
|
||||
doReturn(new Resource[0]).when(spy).getResources("classpath:rest-default-messages.properties");
|
||||
doReturn(new Resource[] { resource }).when(spy).getResources("classpath:rest-messages*.properties");
|
||||
|
||||
} catch (IOException o_O) {
|
||||
fail("Couldn't mock resource lookup!", o_O);
|
||||
}
|
||||
|
||||
return spy;
|
||||
};
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
@Import(DelegateConfig.class)
|
||||
|
||||
@@ -89,7 +89,7 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg
|
||||
|
||||
mapper.registerModule(new Jackson2HalModule());
|
||||
mapper.setHandlerInstantiator(
|
||||
new HalHandlerInstantiator(provider, CurieProvider.NONE, MessageResolver.NONE, new HalConfiguration()));
|
||||
new HalHandlerInstantiator(provider, CurieProvider.NONE, MessageResolver.DEFAULTS_ONLY, new HalConfiguration()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -447,7 +447,7 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg
|
||||
void rendersSingleLinkAsArrayWhenConfigured() throws Exception {
|
||||
|
||||
mapper.setHandlerInstantiator(new HalHandlerInstantiator(new AnnotationLinkRelationProvider(), CurieProvider.NONE,
|
||||
MessageResolver.NONE, new HalConfiguration().withRenderSingleLinks(RenderSingleLinks.AS_ARRAY)));
|
||||
MessageResolver.DEFAULTS_ONLY, new HalConfiguration().withRenderSingleLinks(RenderSingleLinks.AS_ARRAY)));
|
||||
|
||||
RepresentationModel<?> resourceSupport = new RepresentationModel<>();
|
||||
resourceSupport.add(new Link("localhost").withSelfRel());
|
||||
@@ -480,7 +480,7 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg
|
||||
|
||||
AnnotationLinkRelationProvider provider = new AnnotationLinkRelationProvider();
|
||||
|
||||
mapper.setHandlerInstantiator(new HalHandlerInstantiator(provider, CurieProvider.NONE, MessageResolver.NONE,
|
||||
mapper.setHandlerInstantiator(new HalHandlerInstantiator(provider, CurieProvider.NONE, MessageResolver.DEFAULTS_ONLY,
|
||||
new HalConfiguration().withRenderSingleLinksFor("foo", RenderSingleLinks.AS_ARRAY)));
|
||||
|
||||
RepresentationModel<?> resource = new RepresentationModel<>();
|
||||
|
||||
@@ -56,7 +56,7 @@ class HalFormsMessageConverterUnitTest {
|
||||
this.mapper.registerModule(new Jackson2HalFormsModule());
|
||||
this.mapper.setHandlerInstantiator(
|
||||
new Jackson2HalFormsModule.HalFormsHandlerInstantiator(new AnnotationLinkRelationProvider(), CurieProvider.NONE,
|
||||
MessageResolver.NONE, true, new HalFormsConfiguration()));
|
||||
MessageResolver.DEFAULTS_ONLY, true, new HalFormsConfiguration()));
|
||||
|
||||
TypeConstrainedMappingJackson2HttpMessageConverter converter = new TypeConstrainedMappingJackson2HttpMessageConverter(
|
||||
RepresentationModel.class);
|
||||
|
||||
@@ -45,7 +45,7 @@ public class HalFormsTemplateBuilderUnitTest {
|
||||
HalFormsConfiguration configuration = new HalFormsConfiguration();
|
||||
configuration.registerPattern(CreditCardNumber.class, "[0-9]{16}");
|
||||
|
||||
HalFormsTemplateBuilder builder = new HalFormsTemplateBuilder(configuration, MessageResolver.NONE);
|
||||
HalFormsTemplateBuilder builder = new HalFormsTemplateBuilder(configuration, MessageResolver.DEFAULTS_ONLY);
|
||||
|
||||
PatternExample resource = new PatternExample();
|
||||
resource.add(Affordances.of(new Link("/examples")) //
|
||||
@@ -79,7 +79,7 @@ public class HalFormsTemplateBuilderUnitTest {
|
||||
.withName("post") //
|
||||
.toLink());
|
||||
|
||||
HalFormsTemplateBuilder builder = new HalFormsTemplateBuilder(new HalFormsConfiguration(), MessageResolver.NONE);
|
||||
HalFormsTemplateBuilder builder = new HalFormsTemplateBuilder(new HalFormsConfiguration(), MessageResolver.DEFAULTS_ONLY);
|
||||
|
||||
Map<String, HalFormsTemplate> templates = builder.findTemplates(model);
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra
|
||||
|
||||
mapper.registerModule(new Jackson2HalFormsModule());
|
||||
mapper.setHandlerInstantiator(new HalFormsHandlerInstantiator( //
|
||||
provider, CurieProvider.NONE, MessageResolver.NONE, true, new HalFormsConfiguration()));
|
||||
provider, CurieProvider.NONE, MessageResolver.DEFAULTS_ONLY, true, new HalFormsConfiguration()));
|
||||
mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ class VndErrorsMarshallingTest {
|
||||
jackson2Mapper = new com.fasterxml.jackson.databind.ObjectMapper();
|
||||
jackson2Mapper.registerModule(new Jackson2HalModule());
|
||||
jackson2Mapper.setHandlerInstantiator(
|
||||
new Jackson2HalModule.HalHandlerInstantiator(relProvider, CurieProvider.NONE, MessageResolver.NONE));
|
||||
new Jackson2HalModule.HalHandlerInstantiator(relProvider, CurieProvider.NONE, MessageResolver.DEFAULTS_ONLY));
|
||||
jackson2Mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
|
||||
|
||||
VndError error = new VndError("42", "Validation failed!", //
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
package org.springframework.hateoas.support;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.mock.web.MockServletContext;
|
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
||||
|
||||
@@ -24,7 +26,7 @@ import org.springframework.web.context.support.AnnotationConfigWebApplicationCon
|
||||
public class ContextTester {
|
||||
|
||||
public static <E extends Exception> void withContext(Class<?> configuration,
|
||||
ConsumerWithException<AnnotationConfigWebApplicationContext, E> consumer) throws E {
|
||||
ConsumerWithException<AnnotationConfigWebApplicationContext, E> consumer) throws E {
|
||||
|
||||
try (AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext()) {
|
||||
|
||||
@@ -36,18 +38,26 @@ public class ContextTester {
|
||||
}
|
||||
|
||||
public static <E extends Exception> void withServletContext(Class<?> configuration,
|
||||
ConsumerWithException<AnnotationConfigWebApplicationContext, E> consumer) throws E {
|
||||
ConsumerWithException<AnnotationConfigWebApplicationContext, E> consumer) throws E {
|
||||
|
||||
try (AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext()) {
|
||||
withServletContext(configuration, Function.identity(), consumer);
|
||||
}
|
||||
|
||||
context.register(configuration);
|
||||
context.setServletContext(new MockServletContext());
|
||||
context.refresh();
|
||||
public static <E extends Exception> void withServletContext(Class<?> configuration,
|
||||
Function<AnnotationConfigWebApplicationContext, AnnotationConfigWebApplicationContext> preparer,
|
||||
ConsumerWithException<AnnotationConfigWebApplicationContext, E> consumer) throws E {
|
||||
|
||||
consumer.accept(context);
|
||||
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
|
||||
context.register(configuration);
|
||||
context.setServletContext(new MockServletContext());
|
||||
|
||||
try (AnnotationConfigWebApplicationContext prepared = preparer.apply(context)) {
|
||||
|
||||
prepared.refresh();
|
||||
consumer.accept(prepared);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface ConsumerWithException<T, E extends Exception> {
|
||||
|
||||
void accept(T element) throws E;
|
||||
|
||||
Reference in New Issue
Block a user