Skip to content

Commit 56b7e73

Browse files
committed
issue #288
This change allows links behind certain rels to always be wrapped by an array regardless of cardinality.
1 parent bf9584f commit 56b7e73

File tree

5 files changed

+120
-17
lines changed

5 files changed

+120
-17
lines changed

readme.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,35 @@ Note that now the prefix `ex:` automatically appears before all rels which are n
306306

307307
Since the purpose of the `CurieProvider` API is to allow for automatic curie creation, you can define only one `CurieProvider` bean per application scope.
308308

309+
## Controlling the representation of links in HAL
310+
Links behind a link-relation in HAL are serialized into a link-object if there is only one link, and into an array of link-objects if there is more than one link. Sometimes it is desirable to always serialize links behind certain link-relations into an array, regardless of whether there is one link or many. You can do this with with the `HalMultipleLinkRels` class:
311+
312+
```java
313+
@Configuration
314+
@EnableWebMvc
315+
@EnableHypermediaSupport(type= {HypermediaType.HAL})
316+
public class Config {
317+
318+
@Bean
319+
public HalMultipleLinkRels halMultipleLinkRels() {
320+
return new HalMultipleLinkRels("order");
321+
}
322+
}
323+
```
324+
325+
Now, regardless of whether there is one link or many links behind the `order` rel, the link will always be wrapped inside an array:
326+
327+
```java
328+
{
329+
_links: {
330+
self: { href: "http://myhost/person/1" },
331+
"order": [
332+
{ href: "http://myhost/person/1/orders/1" }
333+
]
334+
}
335+
}
336+
```
337+
309338
## Traverson
310339

311340
As of version 0.11 Spring HATEOAS provides an API for client side service traversal inspired by the [Traverson](https://blog.codecentric.de/en/2013/11/traverson/) JavaScript library.

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* you may not use this file except in compliance with the License.
66
* You may obtain a copy of the License at
77
*
8-
* http://www.apache.org/licenses/LICENSE-2.0
8+
* http://www.apache.org/licenses/LICENSE-2.0
99
*
1010
* Unless required by applicable law or agreed to in writing, software
1111
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -52,6 +52,7 @@
5252
import org.springframework.hateoas.core.EvoInflectorRelProvider;
5353
import org.springframework.hateoas.hal.CurieProvider;
5454
import org.springframework.hateoas.hal.HalLinkDiscoverer;
55+
import org.springframework.hateoas.hal.HalMultipleLinkRels;
5556
import org.springframework.hateoas.hal.Jackson2HalModule;
5657
import org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter;
5758
import org.springframework.http.converter.HttpMessageConverter;
@@ -281,11 +282,12 @@ private List<HttpMessageConverter<?>> potentiallyRegisterModule(List<HttpMessage
281282
}
282283

283284
CurieProvider curieProvider = getCurieProvider(beanFactory);
285+
HalMultipleLinkRels halMultipleLinkRels = getHalMultipleLinkRels(beanFactory);
284286
RelProvider relProvider = beanFactory.getBean(DELEGATING_REL_PROVIDER_BEAN_NAME, RelProvider.class);
285287
ObjectMapper halObjectMapper = beanFactory.getBean(HAL_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class);
286288

287289
halObjectMapper.registerModule(new Jackson2HalModule());
288-
halObjectMapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, curieProvider));
290+
halObjectMapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, curieProvider, halMultipleLinkRels));
289291

290292
MappingJackson2HttpMessageConverter halConverter = new TypeConstrainedMappingJackson2HttpMessageConverter(
291293
ResourceSupport.class);
@@ -306,5 +308,13 @@ private static CurieProvider getCurieProvider(BeanFactory factory) {
306308
return null;
307309
}
308310
}
311+
312+
private static HalMultipleLinkRels getHalMultipleLinkRels(BeanFactory factory) {
313+
try {
314+
return factory.getBean(HalMultipleLinkRels.class);
315+
} catch(NoSuchBeanDefinitionException e) {
316+
return null;
317+
}
318+
}
309319
}
310320
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package org.springframework.hateoas.hal;
2+
3+
4+
import java.util.Arrays;
5+
import java.util.Collections;
6+
import java.util.HashSet;
7+
import java.util.Set;
8+
9+
/**
10+
* Class to maintain relations whose links must always be serialized as an array regardless of cardinality.
11+
*
12+
* In HAL, if there is only one link behind a rel, it can be serialized directly into a link object; otherwise
13+
* the links behind the rel are serialized into an array of link objects.
14+
*
15+
* However, the HAL specification says that links behind certain rels can always be represented as an array
16+
* regardless of cardinality. This class helps you do that. If a rel is specified here, links under it will
17+
* always be serialized into an array across all resources.
18+
*
19+
* @author Vivin Paliath
20+
*/
21+
public class HalMultipleLinkRels {
22+
23+
private final Set<String> rels;
24+
25+
public HalMultipleLinkRels(String... rels) {
26+
this.rels = new HashSet<String>(Arrays.asList(rels));
27+
}
28+
29+
public Set<String> getRels() {
30+
return Collections.unmodifiableSet(rels);
31+
}
32+
}

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

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* you may not use this file except in compliance with the License.
66
* You may obtain a copy of the License at
77
*
8-
* http://www.apache.org/licenses/LICENSE-2.0
8+
* http://www.apache.org/licenses/LICENSE-2.0
99
*
1010
* Unless required by applicable law or agreed to in writing, software
1111
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -110,16 +110,18 @@ public static class HalLinkListSerializer extends ContainerSerializer<List<Link>
110110

111111
private final BeanProperty property;
112112
private final CurieProvider curieProvider;
113+
private final HalMultipleLinkRels halMultipleLinkRels;
113114

114-
public HalLinkListSerializer(CurieProvider curieProvider) {
115-
this(null, curieProvider);
115+
public HalLinkListSerializer(CurieProvider curieProvider, HalMultipleLinkRels halMultipleLinkRels) {
116+
this(null, curieProvider, halMultipleLinkRels);
116117
}
117118

118-
public HalLinkListSerializer(BeanProperty property, CurieProvider curieProvider) {
119+
public HalLinkListSerializer(BeanProperty property, CurieProvider curieProvider, HalMultipleLinkRels halMultipleLinkRels) {
119120

120121
super(List.class, false);
121122
this.property = property;
122123
this.curieProvider = curieProvider;
124+
this.halMultipleLinkRels = halMultipleLinkRels;
123125
}
124126

125127
/*
@@ -167,7 +169,7 @@ public void serialize(List<Link> value, JsonGenerator jgen, SerializerProvider p
167169
JavaType mapType = typeFactory.constructMapType(HashMap.class, keyType, valueType);
168170

169171
MapSerializer serializer = MapSerializer.construct(new String[] {}, mapType, true, null,
170-
provider.findKeySerializer(keyType, null), new OptionalListJackson2Serializer(property), null);
172+
provider.findKeySerializer(keyType, null), new OptionalListJackson2Serializer(property, halMultipleLinkRels), null);
171173

172174
serializer.serialize(sortedLinks, jgen, provider);
173175
}
@@ -179,7 +181,7 @@ public void serialize(List<Link> value, JsonGenerator jgen, SerializerProvider p
179181
@Override
180182
public JsonSerializer<?> createContextual(SerializerProvider provider, BeanProperty property)
181183
throws JsonMappingException {
182-
return new HalLinkListSerializer(property, curieProvider);
184+
return new HalLinkListSerializer(property, curieProvider, halMultipleLinkRels);
183185
}
184186

185187
/*
@@ -320,21 +322,24 @@ public static class OptionalListJackson2Serializer extends ContainerSerializer<O
320322

321323
private final BeanProperty property;
322324
private final Map<Class<?>, JsonSerializer<Object>> serializers;
325+
private final HalMultipleLinkRels halMultipleLinkRels;
323326

324327
public OptionalListJackson2Serializer() {
325-
this(null);
328+
this(null, new HalMultipleLinkRels());
326329
}
327330

328331
/**
329-
* Creates a new {@link OptionalListJackson2Serializer} using the given {@link BeanProperty}.
330-
*
332+
* Creates a new {@link OptionalListJackson2Serializer} using the given {@link BeanProperty} and
333+
* {@link HalMultipleLinkRels}.
334+
*
331335
* @param property
332336
*/
333-
public OptionalListJackson2Serializer(BeanProperty property) {
337+
public OptionalListJackson2Serializer(BeanProperty property, HalMultipleLinkRels halMultipleLinkRels) {
334338

335339
super(List.class, false);
336340
this.property = property;
337341
this.serializers = new HashMap<Class<?>, JsonSerializer<Object>>();
342+
this.halMultipleLinkRels = halMultipleLinkRels;
338343
}
339344

340345
/*
@@ -361,6 +366,18 @@ public void serialize(Object value, JsonGenerator jgen, SerializerProvider provi
361366
}
362367

363368
if (list.size() == 1) {
369+
Object element = list.get(0);
370+
if(element instanceof Link) {
371+
Link link = (Link) element;
372+
if(halMultipleLinkRels.getRels().contains(link.getRel())) {
373+
jgen.writeStartArray();
374+
serializeContents(list.iterator(), jgen, provider);
375+
jgen.writeEndArray();
376+
377+
return;
378+
}
379+
}
380+
364381
serializeContents(list.iterator(), jgen, provider);
365382
return;
366383
}
@@ -443,7 +460,7 @@ public boolean isEmpty(Object arg0) {
443460
@Override
444461
public JsonSerializer<?> createContextual(SerializerProvider provider, BeanProperty property)
445462
throws JsonMappingException {
446-
return new OptionalListJackson2Serializer(property);
463+
return new OptionalListJackson2Serializer(property, halMultipleLinkRels);
447464
}
448465
}
449466

@@ -598,15 +615,19 @@ public static class HalHandlerInstantiator extends HandlerInstantiator {
598615
private final Map<Class<?>, Object> instanceMap = new HashMap<Class<?>, Object>();
599616

600617
public HalHandlerInstantiator(RelProvider resolver, CurieProvider curieProvider) {
601-
this(resolver, curieProvider, true);
618+
this(resolver, curieProvider, new HalMultipleLinkRels(), true);
619+
}
620+
621+
public HalHandlerInstantiator(RelProvider resolver, CurieProvider curieProvider, HalMultipleLinkRels halMultipleLinkRels) {
622+
this(resolver, curieProvider, halMultipleLinkRels, true);
602623
}
603624

604-
public HalHandlerInstantiator(RelProvider resolver, CurieProvider curieProvider, boolean enforceEmbeddedCollections) {
625+
public HalHandlerInstantiator(RelProvider resolver, CurieProvider curieProvider, HalMultipleLinkRels halMultipleLinkRels, boolean enforceEmbeddedCollections) {
605626

606627
Assert.notNull(resolver, "RelProvider must not be null!");
607628
this.instanceMap.put(HalResourcesSerializer.class, new HalResourcesSerializer(resolver, curieProvider,
608629
enforceEmbeddedCollections));
609-
this.instanceMap.put(HalLinkListSerializer.class, new HalLinkListSerializer(curieProvider));
630+
this.instanceMap.put(HalLinkListSerializer.class, new HalLinkListSerializer(curieProvider, halMultipleLinkRels));
610631
}
611632

612633
private Object findInstance(Class<?> type) {

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* you may not use this file except in compliance with the License.
66
* You may obtain a copy of the License at
77
*
8-
* http://www.apache.org/licenses/LICENSE-2.0
8+
* http://www.apache.org/licenses/LICENSE-2.0
99
*
1010
* Unless required by applicable law or agreed to in writing, software
1111
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -50,6 +50,7 @@
5050
public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingIntegrationTest {
5151

5252
static final String SINGLE_LINK_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}}}";
53+
static final String SINGLE_LINK_ARRAY_REFERENCE = "{\"_links\":{\"multiple\":[{\"href\":\"localhost\"}]}}";
5354
static final String LIST_LINK_REFERENCE = "{\"_links\":{\"self\":[{\"href\":\"localhost\"},{\"href\":\"localhost2\"}]}}";
5455

5556
static final String SIMPLE_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"content\":[\"first\",\"second\"]}}";
@@ -109,6 +110,16 @@ public void rendersMultipleLinkAsArray() throws Exception {
109110
assertThat(write(resourceSupport), is(LIST_LINK_REFERENCE));
110111
}
111112

113+
@Test
114+
public void rendersSingleLinkAsArrayWhenConfigured() throws Exception {
115+
mapper.setHandlerInstantiator(new HalHandlerInstantiator(new AnnotationRelProvider(), null, new HalMultipleLinkRels("multiple")));
116+
117+
ResourceSupport resourceSupport = new ResourceSupport();
118+
resourceSupport.add(new Link("localhost").withRel("multiple"));
119+
120+
assertThat(write(resourceSupport), is(SINGLE_LINK_ARRAY_REFERENCE));
121+
}
122+
112123
@Test
113124
public void deserializeMultipleLinks() throws Exception {
114125

0 commit comments

Comments
 (0)