Skip to content

Commit e6660a0

Browse files
vivinBjörn Blomqvist
authored andcommitted
Allows links under a single rel to be represented as a multiple (i.e., serialized to an array) even if there is only one.
1 parent dc90d23 commit e6660a0

File tree

5 files changed

+104
-32
lines changed

5 files changed

+104
-32
lines changed

src/main/java/org/springframework/hateoas/Link.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public class Link implements Serializable {
5555
@XmlAttribute private String rel;
5656
@XmlAttribute private String href;
5757
@XmlTransient @JsonIgnore private UriTemplate template;
58+
@XmlTransient @JsonIgnore private Class<? extends ResourceSupport> owningResource;
5859

5960
/**
6061
* Creates a new link to the given URI with the self rel.
@@ -194,10 +195,18 @@ private UriTemplate getUriTemplate() {
194195
return template;
195196
}
196197

197-
/*
198-
* (non-Javadoc)
199-
* @see java.lang.Object#equals(java.lang.Object)
200-
*/
198+
public Class<? extends ResourceSupport> getOwningResource() {
199+
return this.owningResource;
200+
}
201+
202+
void setOwningResource(Class<? extends ResourceSupport> owningResource) {
203+
this.owningResource = owningResource;
204+
}
205+
206+
/*
207+
* (non-Javadoc)
208+
* @see java.lang.Object#equals(java.lang.Object)
209+
*/
201210
@Override
202211
public boolean equals(Object obj) {
203212

src/main/java/org/springframework/hateoas/ResourceSupport.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public Link getId() {
5454
*/
5555
public void add(Link link) {
5656
Assert.notNull(link, "Link must not be null!");
57+
link.setOwningResource(this.getClass());
5758
this.links.add(link);
5859
}
5960

@@ -65,6 +66,7 @@ public void add(Link link) {
6566
public void add(Iterable<Link> links) {
6667
Assert.notNull(links, "Given links must not be null!");
6768
for (Link candidate : links) {
69+
candidate.setOwningResource(this.getClass());
6870
add(candidate);
6971
}
7072
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2012 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.hal;
17+
18+
import java.lang.annotation.Documented;
19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Inherited;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
import org.springframework.hateoas.core.ControllerEntityLinks;
26+
27+
/**
28+
* Annotation to force links under specified rels to be serialized as a JSON array regardless of cardinality.
29+
*
30+
* @author Vivin Paliath
31+
*/
32+
@Inherited
33+
@Documented
34+
@Retention(RetentionPolicy.RUNTIME)
35+
@Target(ElementType.TYPE)
36+
public @interface ForceMultipleLinksOnRels {
37+
public String[] value();
38+
}

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

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,11 @@
1717

1818
import java.io.IOException;
1919
import java.lang.reflect.Type;
20-
import java.util.ArrayList;
21-
import java.util.Collection;
22-
import java.util.HashMap;
23-
import java.util.Iterator;
24-
import java.util.LinkedHashMap;
25-
import java.util.List;
26-
import java.util.Map;
20+
import java.util.*;
2721

2822
import org.springframework.beans.BeanUtils;
29-
import org.springframework.hateoas.Link;
30-
import org.springframework.hateoas.Links;
31-
import org.springframework.hateoas.RelProvider;
32-
import org.springframework.hateoas.Resource;
33-
import org.springframework.hateoas.ResourceSupport;
34-
import org.springframework.hateoas.Resources;
23+
import org.springframework.core.annotation.AnnotationUtils;
24+
import org.springframework.hateoas.*;
3525
import org.springframework.util.Assert;
3626

3727
import com.fasterxml.jackson.core.JsonGenerationException;
@@ -130,15 +120,29 @@ public HalLinkListSerializer(BeanProperty property, CurieProvider curieProvider)
130120
public void serialize(List<Link> value, JsonGenerator jgen, SerializerProvider provider) throws IOException,
131121
JsonGenerationException {
132122

123+
124+
// keeps track of any rels that need their links to be forced as multiple (i.e., serialized to array)
125+
// regardless of cardinality
126+
List<String> forcedMultipleLinkRels = new ArrayList<String>();
127+
if(!value.isEmpty()) {
128+
Class<? extends ResourceSupport> owningResource = value.get(0).getOwningResource();
129+
ForceMultipleLinksOnRels annotation = AnnotationUtils.findAnnotation(owningResource, ForceMultipleLinksOnRels.class);
130+
if(annotation != null) {
131+
forcedMultipleLinkRels.addAll(Arrays.asList(annotation.value()));
132+
}
133+
}
134+
133135
// sort links according to their relation
134136
Map<String, List<Object>> sortedLinks = new LinkedHashMap<String, List<Object>>();
137+
135138
List<Link> links = new ArrayList<Link>();
136139

137140
boolean prefixingRequired = curieProvider != null;
138141
boolean curiedLinkPresent = false;
139142

140143
for (Link link : value) {
141144

145+
142146
String rel = prefixingRequired ? curieProvider.getNamespacedRelFrom(link) : link.getRel();
143147

144148
if (!link.getRel().equals(rel)) {
@@ -167,7 +171,7 @@ public void serialize(List<Link> value, JsonGenerator jgen, SerializerProvider p
167171
JavaType mapType = typeFactory.constructMapType(HashMap.class, keyType, valueType);
168172

169173
MapSerializer serializer = MapSerializer.construct(new String[] {}, mapType, true, null,
170-
provider.findKeySerializer(keyType, null), new OptionalListJackson2Serializer(property), null);
174+
provider.findKeySerializer(keyType, null), new OptionalListJackson2Serializer(property, forcedMultipleLinkRels), null);
171175

172176
serializer.serialize(sortedLinks, jgen, provider);
173177
}
@@ -320,6 +324,7 @@ public static class OptionalListJackson2Serializer extends ContainerSerializer<O
320324

321325
private final BeanProperty property;
322326
private final Map<Class<?>, JsonSerializer<Object>> serializers;
327+
private final List<String> forcedMultipleLinkRels;
323328

324329
public OptionalListJackson2Serializer() {
325330
this(null);
@@ -331,10 +336,24 @@ public OptionalListJackson2Serializer() {
331336
* @param property
332337
*/
333338
public OptionalListJackson2Serializer(BeanProperty property) {
339+
this(property, new ArrayList<String>());
340+
}
341+
342+
/**
343+
* Private constructor that creates a new instance using the given {@link BeanProperty}
344+
* and a list of rels whose links need to be forced into an array representation regardless of
345+
* cardinality.
346+
*
347+
* @param property
348+
* @param forcedMultipleLinkRels
349+
*/
350+
private OptionalListJackson2Serializer(BeanProperty property, List<String> forcedMultipleLinkRels) {
334351

335352
super(List.class, false);
353+
336354
this.property = property;
337355
this.serializers = new HashMap<Class<?>, JsonSerializer<Object>>();
356+
this.forcedMultipleLinkRels = forcedMultipleLinkRels;
338357
}
339358

340359
/*
@@ -360,14 +379,13 @@ public void serialize(Object value, JsonGenerator jgen, SerializerProvider provi
360379
return;
361380
}
362381

363-
if (list.size() == 1) {
382+
if(list.size() > 1 || ((list.get(0) instanceof Link) && forcedMultipleLinkRels.contains(((Link) list.get(0)).getRel()))) {
383+
jgen.writeStartArray();
384+
serializeContents(list.iterator(), jgen, provider);
385+
jgen.writeEndArray();
386+
} else {
364387
serializeContents(list.iterator(), jgen, provider);
365-
return;
366388
}
367-
368-
jgen.writeStartArray();
369-
serializeContents(list.iterator(), jgen, provider);
370-
jgen.writeEndArray();
371389
}
372390

373391
private void serializeContents(Iterator<?> value, JsonGenerator jgen, SerializerProvider provider)

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

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,8 @@
2626

2727
import org.junit.Before;
2828
import org.junit.Test;
29-
import org.springframework.hateoas.AbstractJackson2MarshallingIntegrationTest;
30-
import org.springframework.hateoas.Link;
31-
import org.springframework.hateoas.Links;
32-
import org.springframework.hateoas.PagedResources;
29+
import org.springframework.hateoas.*;
3330
import org.springframework.hateoas.PagedResources.PageMetadata;
34-
import org.springframework.hateoas.Resource;
35-
import org.springframework.hateoas.ResourceSupport;
36-
import org.springframework.hateoas.Resources;
37-
import org.springframework.hateoas.UriTemplate;
3831
import org.springframework.hateoas.core.AnnotationRelProvider;
3932
import org.springframework.hateoas.core.EmbeddedWrappers;
4033
import org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator;
@@ -50,6 +43,7 @@
5043
public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingIntegrationTest {
5144

5245
static final String SINGLE_LINK_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}}}";
46+
static final String SINGLE_LINK_REFERENCE_AS_MULTIPLE = "{\"_links\":{\"multiple\":[{\"href\":\"localhost\"}]}}";
5347
static final String LIST_LINK_REFERENCE = "{\"_links\":{\"self\":[{\"href\":\"localhost\"},{\"href\":\"localhost2\"}]}}";
5448

5549
static final String SIMPLE_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"content\":[\"first\",\"second\"]}}";
@@ -96,6 +90,13 @@ public void deserializeSingleLink() throws Exception {
9690
assertThat(read(SINGLE_LINK_REFERENCE, ResourceSupport.class), is(expected));
9791
}
9892

93+
@Test
94+
public void rendersSingleLinkAsArrayWhenForced() throws Exception {
95+
ResourceWithForcedMultipleLink expected = new ResourceWithForcedMultipleLink();
96+
expected.add(new Link("localhost").withRel("multiple"));
97+
assertThat(read(SINGLE_LINK_REFERENCE_AS_MULTIPLE, ResourceWithForcedMultipleLink.class), is(expected));
98+
}
99+
99100
/**
100101
* @see #29
101102
*/
@@ -401,4 +402,8 @@ private static ObjectMapper getCuriedObjectMapper(CurieProvider provider) {
401402

402403
return mapper;
403404
}
405+
406+
@ForceMultipleLinksOnRels({"multiple"})
407+
private static class ResourceWithForcedMultipleLink extends ResourceSupport {
408+
}
404409
}

0 commit comments

Comments
 (0)