#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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2014 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.
|
||||
@@ -72,7 +72,7 @@ public class VndErrorsMarshallingTest {
|
||||
|
||||
jackson2Mapper = new com.fasterxml.jackson.databind.ObjectMapper();
|
||||
jackson2Mapper.registerModule(new Jackson2HalModule());
|
||||
jackson2Mapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, null));
|
||||
jackson2Mapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, null, null));
|
||||
jackson2Mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
|
||||
|
||||
JAXBContext context = JAXBContext.newInstance(VndErrors.class);
|
||||
|
||||
@@ -61,7 +61,7 @@ public class Server implements Closeable {
|
||||
|
||||
this.mapper = new ObjectMapper();
|
||||
this.mapper.registerModule(new Jackson2HalModule());
|
||||
this.mapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, null));
|
||||
this.mapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, null, null));
|
||||
|
||||
initJadler(). //
|
||||
that().//
|
||||
|
||||
@@ -23,9 +23,14 @@ import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.context.i18n.LocaleContextHolder;
|
||||
import org.springframework.context.support.MessageSourceAccessor;
|
||||
import org.springframework.context.support.StaticMessageSource;
|
||||
import org.springframework.hateoas.AbstractJackson2MarshallingIntegrationTest;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.Links;
|
||||
@@ -70,11 +75,13 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg
|
||||
|
||||
static final String LINK_TEMPLATE = "{\"_links\":{\"search\":{\"href\":\"/foo{?bar}\",\"templated\":true}}}";
|
||||
|
||||
static final String LINK_WITH_TITLE = "{\"_links\":{\"ns:foobar\":{\"href\":\"target\",\"title\":\"Foobar's title!\"}}}";
|
||||
|
||||
@Before
|
||||
public void setUpModule() {
|
||||
|
||||
mapper.registerModule(new Jackson2HalModule());
|
||||
mapper.setHandlerInstantiator(new HalHandlerInstantiator(new AnnotationRelProvider(), null));
|
||||
mapper.setHandlerInstantiator(new HalHandlerInstantiator(new AnnotationRelProvider(), null, null));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -337,7 +344,7 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg
|
||||
}
|
||||
};
|
||||
|
||||
assertThat(getCuriedObjectMapper(provider).writeValueAsString(resources), is(MULTIPLE_CURIES_DOCUMENT));
|
||||
assertThat(getCuriedObjectMapper(provider, null).writeValueAsString(resources), is(MULTIPLE_CURIES_DOCUMENT));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -356,6 +363,37 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg
|
||||
assertThat(write(resources), is("{\"_embedded\":{\"pojos\":[]}}"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #378
|
||||
*/
|
||||
@Test
|
||||
public void rendersTitleIfMessageSourceResolvesNamespacedKey() throws Exception {
|
||||
verifyResolvedTitle("_links.ns:foobar.title");
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #378
|
||||
*/
|
||||
@Test
|
||||
public void rendersTitleIfMessageSourceResolvesLocalKey() throws Exception {
|
||||
verifyResolvedTitle("_links.foobar.title");
|
||||
}
|
||||
|
||||
private static void verifyResolvedTitle(String resourceBundleKey) throws Exception {
|
||||
|
||||
LocaleContextHolder.setLocale(Locale.US);
|
||||
|
||||
StaticMessageSource messageSource = new StaticMessageSource();
|
||||
messageSource.addMessage(resourceBundleKey, Locale.US, "Foobar's title!");
|
||||
|
||||
ObjectMapper objectMapper = getCuriedObjectMapper(null, messageSource);
|
||||
|
||||
ResourceSupport resource = new ResourceSupport();
|
||||
resource.add(new Link("target", "ns:foobar"));
|
||||
|
||||
assertThat(objectMapper.writeValueAsString(resource), is(LINK_WITH_TITLE));
|
||||
}
|
||||
|
||||
private static Resources<Resource<SimpleAnnotatedPojo>> setupAnnotatedPagedResources() {
|
||||
|
||||
List<Resource<SimpleAnnotatedPojo>> content = new ArrayList<Resource<SimpleAnnotatedPojo>>();
|
||||
@@ -385,14 +423,16 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg
|
||||
|
||||
private static ObjectMapper getCuriedObjectMapper() {
|
||||
|
||||
return getCuriedObjectMapper(new DefaultCurieProvider("foo", new UriTemplate("http://localhost:8080/rels/{rel}")));
|
||||
return getCuriedObjectMapper(new DefaultCurieProvider("foo", new UriTemplate("http://localhost:8080/rels/{rel}")),
|
||||
null);
|
||||
}
|
||||
|
||||
private static ObjectMapper getCuriedObjectMapper(CurieProvider provider) {
|
||||
private static ObjectMapper getCuriedObjectMapper(CurieProvider provider, MessageSource messageSource) {
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.registerModule(new Jackson2HalModule());
|
||||
mapper.setHandlerInstantiator(new HalHandlerInstantiator(new AnnotationRelProvider(), provider));
|
||||
mapper.setHandlerInstantiator(new HalHandlerInstantiator(new AnnotationRelProvider(), provider,
|
||||
messageSource == null ? null : new MessageSourceAccessor(messageSource)));
|
||||
|
||||
return mapper;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user