#378 - HAL links get title attributes rendered resolved through a resource bundle.

HalLinkListSerializer now tries to obtain a link title by looking up key _links.$rel.title for both the namespaced (curied) and local link relation if the former doesn't resolve into a message.
This commit is contained in:
Oliver Gierke
2015-08-19 19:07:07 +02:00
parent d77a4b65f5
commit 71ec26fc8d
7 changed files with 191 additions and 22 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013 the original author or authors.
* Copyright 2013-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -33,8 +33,7 @@ import org.springframework.hateoas.LinkDiscoverer;
* components will be registered:
* <ul>
* <li>{@link LinkDiscoverer}</li>
* <li>a Jackson (1 or 2, dependning on what is on the classpath) module to correctly marshal the resource model classes
* into the appropriate representation.
* <li>a Jackson 2 module to correctly marshal the resource model classes into the appropriate representation.
* </ul>
*
* @see LinkDiscoverer
@@ -44,7 +43,7 @@ import org.springframework.hateoas.LinkDiscoverer;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(HypermediaSupportBeanDefinitionRegistrar.class)
@Import({ HypermediaSupportBeanDefinitionRegistrar.class, HateoasConfiguration.class })
public @interface EnableHypermediaSupport {
/**

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.config;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
/**
* Common HATOEAS specific configuration.
*
* @author Oliver Gierke
* @soundtrack Elephants Crossing - Wait (Live at Stadtfest Dresden)
* @since 0.19
*/
@Configuration
class HateoasConfiguration {
/**
* The {@link MessageSourceAccessor} to provide messages for {@link ResourceDescription}s being rendered.
*
* @return
*/
@Bean
public MessageSourceAccessor linkRelationMessageSource() {
try {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:rest-messages");
return new MessageSourceAccessor(messageSource);
} catch (Exception o_O) {
throw new BeanCreationException("resourceDescriptionMessageSourceAccessor", "", o_O);
}
}
}

View File

@@ -39,6 +39,7 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.hateoas.EntityLinks;
import org.springframework.hateoas.LinkDiscoverer;
@@ -80,6 +81,7 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe
private static final String DELEGATING_REL_PROVIDER_BEAN_NAME = "_relProvider";
private static final String LINK_DISCOVERER_REGISTRY_BEAN_NAME = "_linkDiscovererRegistry";
private static final String HAL_OBJECT_MAPPER_BEAN_NAME = "_halObjectMapper";
private static final String MESSAGE_SOURCE_BEAN_NAME = "linkRelationMessageSource";
private static final boolean JACKSON2_PRESENT = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper",
null);
@@ -288,9 +290,12 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe
CurieProvider curieProvider = getCurieProvider(beanFactory);
RelProvider relProvider = beanFactory.getBean(DELEGATING_REL_PROVIDER_BEAN_NAME, RelProvider.class);
ObjectMapper halObjectMapper = beanFactory.getBean(HAL_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class);
MessageSourceAccessor linkRelationMessageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME,
MessageSourceAccessor.class);
halObjectMapper.registerModule(new Jackson2HalModule());
halObjectMapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, curieProvider));
halObjectMapper.setHandlerInstantiator(
new Jackson2HalModule.HalHandlerInstantiator(relProvider, curieProvider, linkRelationMessageSource));
MappingJackson2HttpMessageConverter halConverter = new TypeConstrainedMappingJackson2HttpMessageConverter(
ResourceSupport.class);

View File

@@ -26,6 +26,8 @@ import java.util.List;
import java.util.Map;
import org.springframework.beans.BeanUtils;
import org.springframework.context.NoSuchMessageException;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Links;
import org.springframework.hateoas.RelProvider;
@@ -34,6 +36,9 @@ import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.Resources;
import org.springframework.util.Assert;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParseException;
@@ -109,20 +114,26 @@ public class Jackson2HalModule extends SimpleModule {
*/
public static class HalLinkListSerializer extends ContainerSerializer<List<Link>>implements ContextualSerializer {
private static final String RELATION_MESSAGE_TEMPLATE = "_links.%s.title";
private final BeanProperty property;
private final CurieProvider curieProvider;
private final EmbeddedMapper mapper;
private final MessageSourceAccessor messageSource;
public HalLinkListSerializer(CurieProvider curieProvider, EmbeddedMapper mapper) {
this(null, curieProvider, mapper);
public HalLinkListSerializer(CurieProvider curieProvider, EmbeddedMapper mapper,
MessageSourceAccessor messageSource) {
this(null, curieProvider, mapper, messageSource);
}
public HalLinkListSerializer(BeanProperty property, CurieProvider curieProvider, EmbeddedMapper mapper) {
public HalLinkListSerializer(BeanProperty property, CurieProvider curieProvider, EmbeddedMapper mapper,
MessageSourceAccessor messageSource) {
super(List.class, false);
this.property = property;
this.curieProvider = curieProvider;
this.mapper = mapper;
this.messageSource = messageSource;
}
/*
@@ -166,7 +177,8 @@ public class Jackson2HalModule extends SimpleModule {
}
links.add(link);
sortedLinks.get(rel).add(link);
sortedLinks.get(rel).add(toHalLink(link));
}
if (!skipCuries && prefixingRequired && curiedLinkPresent) {
@@ -188,6 +200,43 @@ public class Jackson2HalModule extends SimpleModule {
serializer.serialize(sortedLinks, jgen, provider);
}
/**
* Wraps the given link into a HAL specifc extension.
*
* @param link must not be {@literal null}.
* @return
*/
private HalLink toHalLink(Link link) {
String rel = link.getRel();
String title = getTitle(rel);
if (title == null) {
title = getTitle(rel.contains(":") ? rel.substring(rel.indexOf(":") + 1) : rel);
}
return new HalLink(link, title);
}
/**
* Returns the title for the given local link relation resolved through the configured {@link MessageSourceAccessor}
* .
*
* @param localRel must not be {@literal null} or empty.
* @return
*/
private String getTitle(String localRel) {
Assert.hasText(localRel, "Local relation must not be null or empty!");
try {
return messageSource == null ? null
: messageSource.getMessage(String.format(RELATION_MESSAGE_TEMPLATE, localRel));
} catch (NoSuchMessageException o_O) {
return null;
}
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.ContextualSerializer#createContextual(com.fasterxml.jackson.databind.SerializerProvider, com.fasterxml.jackson.databind.BeanProperty)
@@ -195,7 +244,7 @@ public class Jackson2HalModule extends SimpleModule {
@Override
public JsonSerializer<?> createContextual(SerializerProvider provider, BeanProperty property)
throws JsonMappingException {
return new HalLinkListSerializer(property, curieProvider, mapper);
return new HalLinkListSerializer(property, curieProvider, mapper, messageSource);
}
/*
@@ -611,18 +660,20 @@ public class Jackson2HalModule extends SimpleModule {
private final Map<Class<?>, Object> instanceMap = new HashMap<Class<?>, Object>();
public HalHandlerInstantiator(RelProvider resolver, CurieProvider curieProvider) {
this(resolver, curieProvider, true);
public HalHandlerInstantiator(RelProvider resolver, CurieProvider curieProvider,
MessageSourceAccessor messageSource) {
this(resolver, curieProvider, messageSource, true);
}
public HalHandlerInstantiator(RelProvider resolver, CurieProvider curieProvider,
boolean enforceEmbeddedCollections) {
MessageSourceAccessor messageSource, boolean enforceEmbeddedCollections) {
EmbeddedMapper mapper = new EmbeddedMapper(resolver, curieProvider, enforceEmbeddedCollections);
Assert.notNull(resolver, "RelProvider must not be null!");
this.instanceMap.put(HalResourcesSerializer.class, new HalResourcesSerializer(mapper));
this.instanceMap.put(HalLinkListSerializer.class, new HalLinkListSerializer(curieProvider, mapper));
this.instanceMap.put(HalLinkListSerializer.class,
new HalLinkListSerializer(curieProvider, mapper, messageSource));
}
private Object findInstance(Class<?> type) {
@@ -806,4 +857,25 @@ public class Jackson2HalModule extends SimpleModule {
return false;
}
}
static class HalLink {
private final Link link;
private final String title;
public HalLink(Link link, String title) {
this.link = link;
this.title = title;
}
@JsonUnwrapped
public Link getLink() {
return link;
}
@JsonInclude(Include.NON_NULL)
public String getTitle() {
return title;
}
}
}