From 400a6bb7036fb81b193edd9b66225f744a6291fa Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Wed, 31 Jul 2019 18:11:26 +0200 Subject: [PATCH] #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. --- .../hateoas/config/HateoasConfiguration.java | 62 +++++++++++++++++-- ...r.java => DefaultOnlyMessageResolver.java} | 9 ++- .../hateoas/mediatype/MessageResolver.java | 7 ++- .../hal/HalMediaTypeConfiguration.java | 3 +- .../mediatype/hal/Jackson2HalModule.java | 9 ++- .../hateoas/client/Server.java | 2 +- ...nableHypermediaSupportIntegrationTest.java | 61 ++++++++++++++++++ .../hal/Jackson2HalIntegrationTest.java | 6 +- .../HalFormsMessageConverterUnitTest.java | 2 +- .../HalFormsTemplateBuilderUnitTest.java | 4 +- .../Jackson2HalFormsIntegrationTest.java | 2 +- .../vnderror/VndErrorsMarshallingTest.java | 2 +- .../hateoas/support/ContextTester.java | 26 +++++--- .../hateoas/config/rest-messages.properties | 0 14 files changed, 165 insertions(+), 30 deletions(-) rename src/main/java/org/springframework/hateoas/mediatype/{NoMessageResolver.java => DefaultOnlyMessageResolver.java} (80%) create mode 100644 src/test/resources/org/springframework/hateoas/config/rest-messages.properties diff --git a/src/main/java/org/springframework/hateoas/config/HateoasConfiguration.java b/src/main/java/org/springframework/hateoas/config/HateoasConfiguration.java index a609e1d6..26d7d5d3 100644 --- a/src/main/java/org/springframework/hateoas/config/HateoasConfiguration.java +++ b/src/main/java/org/springframework/hateoas/config/HateoasConfiguration.java @@ -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 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 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 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(); + } + } } diff --git a/src/main/java/org/springframework/hateoas/mediatype/NoMessageResolver.java b/src/main/java/org/springframework/hateoas/mediatype/DefaultOnlyMessageResolver.java similarity index 80% rename from src/main/java/org/springframework/hateoas/mediatype/NoMessageResolver.java rename to src/main/java/org/springframework/hateoas/mediatype/DefaultOnlyMessageResolver.java index 4d34d5bb..a018af56 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/NoMessageResolver.java +++ b/src/main/java/org/springframework/hateoas/mediatype/DefaultOnlyMessageResolver.java @@ -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(); } } diff --git a/src/main/java/org/springframework/hateoas/mediatype/MessageResolver.java b/src/main/java/org/springframework/hateoas/mediatype/MessageResolver.java index e0fd6a26..71c9ae87 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/MessageResolver.java +++ b/src/main/java/org/springframework/hateoas/mediatype/MessageResolver.java @@ -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); } } diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/HalMediaTypeConfiguration.java b/src/main/java/org/springframework/hateoas/mediatype/hal/HalMediaTypeConfiguration.java index a1b2ce61..7accce1f 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/HalMediaTypeConfiguration.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/HalMediaTypeConfiguration.java @@ -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; private final ObjectProvider halConfiguration; - private final MessageResolver resolver; + private final @Qualifier("messageResolver") MessageResolver resolver; /** * @param relProvider diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/Jackson2HalModule.java b/src/main/java/org/springframework/hateoas/mediatype/hal/Jackson2HalModule.java index 0b5d46a8..7d790232 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/Jackson2HalModule.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/Jackson2HalModule.java @@ -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; } diff --git a/src/test/java/org/springframework/hateoas/client/Server.java b/src/test/java/org/springframework/hateoas/client/Server.java index 460405f8..1a3b73cd 100644 --- a/src/test/java/org/springframework/hateoas/client/Server.java +++ b/src/test/java/org/springframework/hateoas/client/Server.java @@ -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()) // diff --git a/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java b/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java index 58bb6463..dfa3e1d1 100755 --- a/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java @@ -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 Function 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) diff --git a/src/test/java/org/springframework/hateoas/mediatype/hal/Jackson2HalIntegrationTest.java b/src/test/java/org/springframework/hateoas/mediatype/hal/Jackson2HalIntegrationTest.java index a89802e7..6951b961 100755 --- a/src/test/java/org/springframework/hateoas/mediatype/hal/Jackson2HalIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/mediatype/hal/Jackson2HalIntegrationTest.java @@ -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<>(); diff --git a/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsMessageConverterUnitTest.java b/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsMessageConverterUnitTest.java index 7f2fca98..5011b8da 100644 --- a/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsMessageConverterUnitTest.java +++ b/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsMessageConverterUnitTest.java @@ -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); diff --git a/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsTemplateBuilderUnitTest.java b/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsTemplateBuilderUnitTest.java index e283bc2c..cd3a527b 100644 --- a/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsTemplateBuilderUnitTest.java +++ b/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsTemplateBuilderUnitTest.java @@ -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 templates = builder.findTemplates(model); diff --git a/src/test/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsIntegrationTest.java b/src/test/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsIntegrationTest.java index 5757fee7..b8c0a38a 100644 --- a/src/test/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsIntegrationTest.java @@ -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); } diff --git a/src/test/java/org/springframework/hateoas/mediatype/vnderror/VndErrorsMarshallingTest.java b/src/test/java/org/springframework/hateoas/mediatype/vnderror/VndErrorsMarshallingTest.java index 0b72d051..da758bba 100755 --- a/src/test/java/org/springframework/hateoas/mediatype/vnderror/VndErrorsMarshallingTest.java +++ b/src/test/java/org/springframework/hateoas/mediatype/vnderror/VndErrorsMarshallingTest.java @@ -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!", // diff --git a/src/test/java/org/springframework/hateoas/support/ContextTester.java b/src/test/java/org/springframework/hateoas/support/ContextTester.java index 67a07360..397a7329 100644 --- a/src/test/java/org/springframework/hateoas/support/ContextTester.java +++ b/src/test/java/org/springframework/hateoas/support/ContextTester.java @@ -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 void withContext(Class configuration, - ConsumerWithException consumer) throws E { + ConsumerWithException consumer) throws E { try (AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext()) { @@ -36,18 +38,26 @@ public class ContextTester { } public static void withServletContext(Class configuration, - ConsumerWithException consumer) throws E { + ConsumerWithException consumer) throws E { - try (AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext()) { + withServletContext(configuration, Function.identity(), consumer); + } - context.register(configuration); - context.setServletContext(new MockServletContext()); - context.refresh(); + public static void withServletContext(Class configuration, + Function preparer, + ConsumerWithException 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 { void accept(T element) throws E; diff --git a/src/test/resources/org/springframework/hateoas/config/rest-messages.properties b/src/test/resources/org/springframework/hateoas/config/rest-messages.properties new file mode 100644 index 00000000..e69de29b