#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:
@@ -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 {
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user