Skip to content

Commit 71ec26f

Browse files
committed
#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.
1 parent d77a4b6 commit 71ec26f

File tree

7 files changed

+191
-22
lines changed

7 files changed

+191
-22
lines changed

src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013 the original author or authors.
2+
* Copyright 2013-2015 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -33,8 +33,7 @@
3333
* components will be registered:
3434
* <ul>
3535
* <li>{@link LinkDiscoverer}</li>
36-
* <li>a Jackson (1 or 2, dependning on what is on the classpath) module to correctly marshal the resource model classes
37-
* into the appropriate representation.
36+
* <li>a Jackson 2 module to correctly marshal the resource model classes into the appropriate representation.
3837
* </ul>
3938
*
4039
* @see LinkDiscoverer
@@ -44,7 +43,7 @@
4443
@Retention(RetentionPolicy.RUNTIME)
4544
@Target(ElementType.TYPE)
4645
@Documented
47-
@Import(HypermediaSupportBeanDefinitionRegistrar.class)
46+
@Import({ HypermediaSupportBeanDefinitionRegistrar.class, HateoasConfiguration.class })
4847
public @interface EnableHypermediaSupport {
4948

5049
/**
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2015 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.hateoas.config;
17+
18+
import org.springframework.beans.factory.BeanCreationException;
19+
import org.springframework.context.annotation.Bean;
20+
import org.springframework.context.annotation.Configuration;
21+
import org.springframework.context.support.MessageSourceAccessor;
22+
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
23+
24+
/**
25+
* Common HATOEAS specific configuration.
26+
*
27+
* @author Oliver Gierke
28+
* @soundtrack Elephants Crossing - Wait (Live at Stadtfest Dresden)
29+
* @since 0.19
30+
*/
31+
@Configuration
32+
class HateoasConfiguration {
33+
34+
/**
35+
* The {@link MessageSourceAccessor} to provide messages for {@link ResourceDescription}s being rendered.
36+
*
37+
* @return
38+
*/
39+
@Bean
40+
public MessageSourceAccessor linkRelationMessageSource() {
41+
42+
try {
43+
44+
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
45+
messageSource.setBasename("classpath:rest-messages");
46+
47+
return new MessageSourceAccessor(messageSource);
48+
49+
} catch (Exception o_O) {
50+
throw new BeanCreationException("resourceDescriptionMessageSourceAccessor", "", o_O);
51+
}
52+
}
53+
}

src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.springframework.beans.factory.support.RootBeanDefinition;
4040
import org.springframework.context.ApplicationContext;
4141
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
42+
import org.springframework.context.support.MessageSourceAccessor;
4243
import org.springframework.core.type.AnnotationMetadata;
4344
import org.springframework.hateoas.EntityLinks;
4445
import org.springframework.hateoas.LinkDiscoverer;
@@ -80,6 +81,7 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe
8081
private static final String DELEGATING_REL_PROVIDER_BEAN_NAME = "_relProvider";
8182
private static final String LINK_DISCOVERER_REGISTRY_BEAN_NAME = "_linkDiscovererRegistry";
8283
private static final String HAL_OBJECT_MAPPER_BEAN_NAME = "_halObjectMapper";
84+
private static final String MESSAGE_SOURCE_BEAN_NAME = "linkRelationMessageSource";
8385

8486
private static final boolean JACKSON2_PRESENT = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper",
8587
null);
@@ -288,9 +290,12 @@ private List<HttpMessageConverter<?>> potentiallyRegisterModule(List<HttpMessage
288290
CurieProvider curieProvider = getCurieProvider(beanFactory);
289291
RelProvider relProvider = beanFactory.getBean(DELEGATING_REL_PROVIDER_BEAN_NAME, RelProvider.class);
290292
ObjectMapper halObjectMapper = beanFactory.getBean(HAL_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class);
293+
MessageSourceAccessor linkRelationMessageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME,
294+
MessageSourceAccessor.class);
291295

292296
halObjectMapper.registerModule(new Jackson2HalModule());
293-
halObjectMapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, curieProvider));
297+
halObjectMapper.setHandlerInstantiator(
298+
new Jackson2HalModule.HalHandlerInstantiator(relProvider, curieProvider, linkRelationMessageSource));
294299

295300
MappingJackson2HttpMessageConverter halConverter = new TypeConstrainedMappingJackson2HttpMessageConverter(
296301
ResourceSupport.class);

src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java

Lines changed: 81 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import java.util.Map;
2727

2828
import org.springframework.beans.BeanUtils;
29+
import org.springframework.context.NoSuchMessageException;
30+
import org.springframework.context.support.MessageSourceAccessor;
2931
import org.springframework.hateoas.Link;
3032
import org.springframework.hateoas.Links;
3133
import org.springframework.hateoas.RelProvider;
@@ -34,6 +36,9 @@
3436
import org.springframework.hateoas.Resources;
3537
import org.springframework.util.Assert;
3638

39+
import com.fasterxml.jackson.annotation.JsonInclude;
40+
import com.fasterxml.jackson.annotation.JsonInclude.Include;
41+
import com.fasterxml.jackson.annotation.JsonUnwrapped;
3742
import com.fasterxml.jackson.core.JsonGenerationException;
3843
import com.fasterxml.jackson.core.JsonGenerator;
3944
import com.fasterxml.jackson.core.JsonParseException;
@@ -109,20 +114,26 @@ public static boolean isAlreadyRegisteredIn(ObjectMapper mapper) {
109114
*/
110115
public static class HalLinkListSerializer extends ContainerSerializer<List<Link>>implements ContextualSerializer {
111116

117+
private static final String RELATION_MESSAGE_TEMPLATE = "_links.%s.title";
118+
112119
private final BeanProperty property;
113120
private final CurieProvider curieProvider;
114121
private final EmbeddedMapper mapper;
122+
private final MessageSourceAccessor messageSource;
115123

116-
public HalLinkListSerializer(CurieProvider curieProvider, EmbeddedMapper mapper) {
117-
this(null, curieProvider, mapper);
124+
public HalLinkListSerializer(CurieProvider curieProvider, EmbeddedMapper mapper,
125+
MessageSourceAccessor messageSource) {
126+
this(null, curieProvider, mapper, messageSource);
118127
}
119128

120-
public HalLinkListSerializer(BeanProperty property, CurieProvider curieProvider, EmbeddedMapper mapper) {
129+
public HalLinkListSerializer(BeanProperty property, CurieProvider curieProvider, EmbeddedMapper mapper,
130+
MessageSourceAccessor messageSource) {
121131

122132
super(List.class, false);
123133
this.property = property;
124134
this.curieProvider = curieProvider;
125135
this.mapper = mapper;
136+
this.messageSource = messageSource;
126137
}
127138

128139
/*
@@ -166,7 +177,8 @@ public void serialize(List<Link> value, JsonGenerator jgen, SerializerProvider p
166177
}
167178

168179
links.add(link);
169-
sortedLinks.get(rel).add(link);
180+
181+
sortedLinks.get(rel).add(toHalLink(link));
170182
}
171183

172184
if (!skipCuries && prefixingRequired && curiedLinkPresent) {
@@ -188,14 +200,51 @@ public void serialize(List<Link> value, JsonGenerator jgen, SerializerProvider p
188200
serializer.serialize(sortedLinks, jgen, provider);
189201
}
190202

203+
/**
204+
* Wraps the given link into a HAL specifc extension.
205+
*
206+
* @param link must not be {@literal null}.
207+
* @return
208+
*/
209+
private HalLink toHalLink(Link link) {
210+
211+
String rel = link.getRel();
212+
String title = getTitle(rel);
213+
214+
if (title == null) {
215+
title = getTitle(rel.contains(":") ? rel.substring(rel.indexOf(":") + 1) : rel);
216+
}
217+
218+
return new HalLink(link, title);
219+
}
220+
221+
/**
222+
* Returns the title for the given local link relation resolved through the configured {@link MessageSourceAccessor}
223+
* .
224+
*
225+
* @param localRel must not be {@literal null} or empty.
226+
* @return
227+
*/
228+
private String getTitle(String localRel) {
229+
230+
Assert.hasText(localRel, "Local relation must not be null or empty!");
231+
232+
try {
233+
return messageSource == null ? null
234+
: messageSource.getMessage(String.format(RELATION_MESSAGE_TEMPLATE, localRel));
235+
} catch (NoSuchMessageException o_O) {
236+
return null;
237+
}
238+
}
239+
191240
/*
192241
* (non-Javadoc)
193242
* @see com.fasterxml.jackson.databind.ser.ContextualSerializer#createContextual(com.fasterxml.jackson.databind.SerializerProvider, com.fasterxml.jackson.databind.BeanProperty)
194243
*/
195244
@Override
196245
public JsonSerializer<?> createContextual(SerializerProvider provider, BeanProperty property)
197246
throws JsonMappingException {
198-
return new HalLinkListSerializer(property, curieProvider, mapper);
247+
return new HalLinkListSerializer(property, curieProvider, mapper, messageSource);
199248
}
200249

201250
/*
@@ -611,18 +660,20 @@ public static class HalHandlerInstantiator extends HandlerInstantiator {
611660

612661
private final Map<Class<?>, Object> instanceMap = new HashMap<Class<?>, Object>();
613662

614-
public HalHandlerInstantiator(RelProvider resolver, CurieProvider curieProvider) {
615-
this(resolver, curieProvider, true);
663+
public HalHandlerInstantiator(RelProvider resolver, CurieProvider curieProvider,
664+
MessageSourceAccessor messageSource) {
665+
this(resolver, curieProvider, messageSource, true);
616666
}
617667

618668
public HalHandlerInstantiator(RelProvider resolver, CurieProvider curieProvider,
619-
boolean enforceEmbeddedCollections) {
669+
MessageSourceAccessor messageSource, boolean enforceEmbeddedCollections) {
620670

621671
EmbeddedMapper mapper = new EmbeddedMapper(resolver, curieProvider, enforceEmbeddedCollections);
622672

623673
Assert.notNull(resolver, "RelProvider must not be null!");
624674
this.instanceMap.put(HalResourcesSerializer.class, new HalResourcesSerializer(mapper));
625-
this.instanceMap.put(HalLinkListSerializer.class, new HalLinkListSerializer(curieProvider, mapper));
675+
this.instanceMap.put(HalLinkListSerializer.class,
676+
new HalLinkListSerializer(curieProvider, mapper, messageSource));
626677
}
627678

628679
private Object findInstance(Class<?> type) {
@@ -806,4 +857,25 @@ public boolean hasCuriedEmbed(Iterable<?> source) {
806857
return false;
807858
}
808859
}
860+
861+
static class HalLink {
862+
863+
private final Link link;
864+
private final String title;
865+
866+
public HalLink(Link link, String title) {
867+
this.link = link;
868+
this.title = title;
869+
}
870+
871+
@JsonUnwrapped
872+
public Link getLink() {
873+
return link;
874+
}
875+
876+
@JsonInclude(Include.NON_NULL)
877+
public String getTitle() {
878+
return title;
879+
}
880+
}
809881
}

src/test/java/org/springframework/hateoas/VndErrorsMarshallingTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013-2014 the original author or authors.
2+
* Copyright 2013-2015 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -72,7 +72,7 @@ public void setUp() throws Exception {
7272

7373
jackson2Mapper = new com.fasterxml.jackson.databind.ObjectMapper();
7474
jackson2Mapper.registerModule(new Jackson2HalModule());
75-
jackson2Mapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, null));
75+
jackson2Mapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, null, null));
7676
jackson2Mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
7777

7878
JAXBContext context = JAXBContext.newInstance(VndErrors.class);

src/test/java/org/springframework/hateoas/client/Server.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public Server() {
6161

6262
this.mapper = new ObjectMapper();
6363
this.mapper.registerModule(new Jackson2HalModule());
64-
this.mapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, null));
64+
this.mapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, null, null));
6565

6666
initJadler(). //
6767
that().//

src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,14 @@
2323
import java.util.Collection;
2424
import java.util.Collections;
2525
import java.util.List;
26+
import java.util.Locale;
2627

2728
import org.junit.Before;
2829
import org.junit.Test;
30+
import org.springframework.context.MessageSource;
31+
import org.springframework.context.i18n.LocaleContextHolder;
32+
import org.springframework.context.support.MessageSourceAccessor;
33+
import org.springframework.context.support.StaticMessageSource;
2934
import org.springframework.hateoas.AbstractJackson2MarshallingIntegrationTest;
3035
import org.springframework.hateoas.Link;
3136
import org.springframework.hateoas.Links;
@@ -70,11 +75,13 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg
7075

7176
static final String LINK_TEMPLATE = "{\"_links\":{\"search\":{\"href\":\"/foo{?bar}\",\"templated\":true}}}";
7277

78+
static final String LINK_WITH_TITLE = "{\"_links\":{\"ns:foobar\":{\"href\":\"target\",\"title\":\"Foobar's title!\"}}}";
79+
7380
@Before
7481
public void setUpModule() {
7582

7683
mapper.registerModule(new Jackson2HalModule());
77-
mapper.setHandlerInstantiator(new HalHandlerInstantiator(new AnnotationRelProvider(), null));
84+
mapper.setHandlerInstantiator(new HalHandlerInstantiator(new AnnotationRelProvider(), null, null));
7885
}
7986

8087
/**
@@ -337,7 +344,7 @@ public Collection<? extends Object> getCurieInformation(Links links) {
337344
}
338345
};
339346

340-
assertThat(getCuriedObjectMapper(provider).writeValueAsString(resources), is(MULTIPLE_CURIES_DOCUMENT));
347+
assertThat(getCuriedObjectMapper(provider, null).writeValueAsString(resources), is(MULTIPLE_CURIES_DOCUMENT));
341348
}
342349

343350
/**
@@ -356,6 +363,37 @@ public void rendersEmptyEmbeddedCollections() throws Exception {
356363
assertThat(write(resources), is("{\"_embedded\":{\"pojos\":[]}}"));
357364
}
358365

366+
/**
367+
* @see #378
368+
*/
369+
@Test
370+
public void rendersTitleIfMessageSourceResolvesNamespacedKey() throws Exception {
371+
verifyResolvedTitle("_links.ns:foobar.title");
372+
}
373+
374+
/**
375+
* @see #378
376+
*/
377+
@Test
378+
public void rendersTitleIfMessageSourceResolvesLocalKey() throws Exception {
379+
verifyResolvedTitle("_links.foobar.title");
380+
}
381+
382+
private static void verifyResolvedTitle(String resourceBundleKey) throws Exception {
383+
384+
LocaleContextHolder.setLocale(Locale.US);
385+
386+
StaticMessageSource messageSource = new StaticMessageSource();
387+
messageSource.addMessage(resourceBundleKey, Locale.US, "Foobar's title!");
388+
389+
ObjectMapper objectMapper = getCuriedObjectMapper(null, messageSource);
390+
391+
ResourceSupport resource = new ResourceSupport();
392+
resource.add(new Link("target", "ns:foobar"));
393+
394+
assertThat(objectMapper.writeValueAsString(resource), is(LINK_WITH_TITLE));
395+
}
396+
359397
private static Resources<Resource<SimpleAnnotatedPojo>> setupAnnotatedPagedResources() {
360398

361399
List<Resource<SimpleAnnotatedPojo>> content = new ArrayList<Resource<SimpleAnnotatedPojo>>();
@@ -385,14 +423,16 @@ private static Resources<Resource<SimplePojo>> setupResources() {
385423

386424
private static ObjectMapper getCuriedObjectMapper() {
387425

388-
return getCuriedObjectMapper(new DefaultCurieProvider("foo", new UriTemplate("http://localhost:8080/rels/{rel}")));
426+
return getCuriedObjectMapper(new DefaultCurieProvider("foo", new UriTemplate("http://localhost:8080/rels/{rel}")),
427+
null);
389428
}
390429

391-
private static ObjectMapper getCuriedObjectMapper(CurieProvider provider) {
430+
private static ObjectMapper getCuriedObjectMapper(CurieProvider provider, MessageSource messageSource) {
392431

393432
ObjectMapper mapper = new ObjectMapper();
394433
mapper.registerModule(new Jackson2HalModule());
395-
mapper.setHandlerInstantiator(new HalHandlerInstantiator(new AnnotationRelProvider(), provider));
434+
mapper.setHandlerInstantiator(new HalHandlerInstantiator(new AnnotationRelProvider(), provider,
435+
messageSource == null ? null : new MessageSourceAccessor(messageSource)));
396436

397437
return mapper;
398438
}

0 commit comments

Comments
 (0)