diff --git a/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java b/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java
index 46879555..d5feb4a0 100644
--- a/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java
+++ b/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java
@@ -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:
*
* - {@link LinkDiscoverer}
- * - a Jackson (1 or 2, dependning on what is on the classpath) module to correctly marshal the resource model classes
- * into the appropriate representation.
+ *
- a Jackson 2 module to correctly marshal the resource model classes into the appropriate representation.
*
*
* @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 {
/**
diff --git a/src/main/java/org/springframework/hateoas/config/HateoasConfiguration.java b/src/main/java/org/springframework/hateoas/config/HateoasConfiguration.java
new file mode 100644
index 00000000..83607bf4
--- /dev/null
+++ b/src/main/java/org/springframework/hateoas/config/HateoasConfiguration.java
@@ -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);
+ }
+ }
+}
diff --git a/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java b/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java
index 0a307518..071c769b 100644
--- a/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java
+++ b/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java
@@ -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);
diff --git a/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java b/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java
index 87b5ebd3..1cffe929 100644
--- a/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java
+++ b/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java
@@ -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>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, Object> instanceMap = new HashMap, 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;
+ }
+ }
}
diff --git a/src/test/java/org/springframework/hateoas/VndErrorsMarshallingTest.java b/src/test/java/org/springframework/hateoas/VndErrorsMarshallingTest.java
index 42e7c418..473cee22 100644
--- a/src/test/java/org/springframework/hateoas/VndErrorsMarshallingTest.java
+++ b/src/test/java/org/springframework/hateoas/VndErrorsMarshallingTest.java
@@ -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);
diff --git a/src/test/java/org/springframework/hateoas/client/Server.java b/src/test/java/org/springframework/hateoas/client/Server.java
index b6f99158..580b0eae 100644
--- a/src/test/java/org/springframework/hateoas/client/Server.java
+++ b/src/test/java/org/springframework/hateoas/client/Server.java
@@ -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().//
diff --git a/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java b/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java
index 9ef8d147..df858c8d 100644
--- a/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java
+++ b/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java
@@ -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> setupAnnotatedPagedResources() {
List> content = new ArrayList>();
@@ -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;
}