#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:
Oliver Drotbohm
2019-07-31 18:11:26 +02:00
parent b1af903de0
commit 400a6bb703
14 changed files with 165 additions and 30 deletions

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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;
}