#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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user