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: * * * @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; }