Skip to content

Commit aea4c4c

Browse files
committed
#811 - Added ability to override single link render mode per link relation.
HalConfiguration now exposes a withSingleLinkRenderModeFor(…) taking a path pattern to be rendered in the also given RenderSingleLinks mode. It takes patterns as link relations are either plain strings or valid URIs. Simplified HAL link list rendering in Jackson2HalModule avoiding double nesting of collections before rendering.
1 parent 943e9af commit aea4c4c

File tree

4 files changed

+192
-33
lines changed

4 files changed

+192
-33
lines changed

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

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017 the original author or authors.
2+
* Copyright 2017-2019 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.
@@ -18,21 +18,98 @@
1818
import lombok.AccessLevel;
1919
import lombok.AllArgsConstructor;
2020
import lombok.Getter;
21-
import lombok.NoArgsConstructor;
2221
import lombok.experimental.Wither;
2322

23+
import java.util.LinkedHashMap;
24+
import java.util.Map;
25+
import java.util.Map.Entry;
26+
2427
import org.springframework.hateoas.Link;
28+
import org.springframework.hateoas.LinkRelation;
29+
import org.springframework.util.AntPathMatcher;
30+
import org.springframework.util.Assert;
31+
import org.springframework.util.PathMatcher;
2532

2633
/**
34+
* HAL specific configuration.
35+
*
2736
* @author Greg Turnquist
28-
* @author Oliver Gierke
37+
* @author Oliver Drotbohm
2938
*/
30-
@NoArgsConstructor
3139
@AllArgsConstructor(access = AccessLevel.PRIVATE)
3240
public class HalConfiguration {
3341

34-
private @Wither @Getter RenderSingleLinks renderSingleLinks = RenderSingleLinks.AS_SINGLE;
42+
private static final PathMatcher MATCHER = new AntPathMatcher();
43+
44+
/**
45+
* Configures how to render links in case there is exactly one defined for a given link relation in general. By
46+
* default, this single link will be rendered as nested document.
47+
*/
48+
private final @Wither @Getter RenderSingleLinks renderSingleLinks;
49+
private final @Wither(AccessLevel.PRIVATE) Map<String, RenderSingleLinks> singleLinksPerPattern;
50+
51+
/**
52+
* Creates a new default {@link HalConfiguration} rendering single links as immediate sub-document.
53+
*/
54+
public HalConfiguration() {
55+
56+
this.renderSingleLinks = RenderSingleLinks.AS_SINGLE;
57+
this.singleLinksPerPattern = new LinkedHashMap<>();
58+
}
59+
60+
/**
61+
* Configures how to render a single link for a given particular {@link LinkRelation}. This will override what has
62+
* been configured via {@link #withRenderSingleLinks(RenderSingleLinks)} for that particular link relation.
63+
*
64+
* @param relation must not be {@literal null}.
65+
* @param renderSingleLinks must not be {@literal null}.
66+
* @return
67+
*/
68+
public HalConfiguration withRenderSingleLinksFor(LinkRelation relation, RenderSingleLinks renderSingleLinks) {
69+
70+
Assert.notNull(relation, "Link relation must not be null!");
71+
Assert.notNull(renderSingleLinks, "RenderSingleLinks must not be null!");
72+
73+
return withRenderSingleLinksFor(relation.value(), renderSingleLinks);
74+
}
75+
76+
/**
77+
* Configures how to render a single link for the given link relation pattern, i.e. this can be either a fixed link
78+
* relation (like {@code search}), take wildcards to e.g. match links of a given curie (like {@code acme:*}) or even
79+
* complete URIs (like {@code http://api.acme.com/foo/**}).
80+
*
81+
* @param pattern must not be {@literal null}.
82+
* @param renderSingleLinks must not be {@literal null}.
83+
* @return @see PathMatcher
84+
*/
85+
public HalConfiguration withRenderSingleLinksFor(String pattern, RenderSingleLinks renderSingleLinks) {
86+
87+
Map<String, RenderSingleLinks> map = new LinkedHashMap<>(singleLinksPerPattern);
88+
map.put(pattern, renderSingleLinks);
89+
90+
return withSingleLinksPerPattern(map);
91+
}
92+
93+
/**
94+
* Returns which render mode to use to render a single link for the given {@link LinkRelation}.
95+
*
96+
* @param relation must not be {@literal null}.
97+
* @return
98+
*/
99+
RenderSingleLinks getSingleLinkRenderModeFor(LinkRelation relation) {
100+
101+
return singleLinksPerPattern.entrySet().stream() //
102+
.filter(entry -> MATCHER.match(entry.getKey(), relation.value())) //
103+
.map(Entry::getValue) //
104+
.findFirst() //
105+
.orElse(renderSingleLinks);
106+
}
35107

108+
/**
109+
* Configuration option how to render single links of a given {@link LinkRelation}.
110+
*
111+
* @author Oliver Drotbohm
112+
*/
36113
public enum RenderSingleLinks {
37114

38115
/**

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

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import java.util.Collection;
2222
import java.util.Collections;
2323
import java.util.HashMap;
24-
import java.util.Iterator;
2524
import java.util.LinkedHashMap;
2625
import java.util.List;
2726
import java.util.Map;
@@ -183,7 +182,7 @@ public void serialize(Links value, JsonGenerator jgen, SerializerProvider provid
183182
if (!skipCuries && prefixingRequired && curiedLinkPresent) {
184183

185184
ArrayList<Object> curies = new ArrayList<>();
186-
curies.add(curieProvider.getCurieInformation(Links.of(links)));
185+
curies.addAll(curieProvider.getCurieInformation(Links.of(links)));
187186

188187
sortedLinks.put(HalLinkRelation.CURIES, curies);
189188
}
@@ -420,40 +419,24 @@ public void serialize(Object value, JsonGenerator jgen, SerializerProvider provi
420419
return;
421420
}
422421

423-
if (list.size() == 1 && this.halConfiguration.getRenderSingleLinks() == RenderSingleLinks.AS_SINGLE) {
424-
serializeContents(list.iterator(), jgen, provider);
422+
Object firstElement = list.get(0);
423+
424+
if (!HalLink.class.isInstance(firstElement)) {
425+
serializeContents(list, jgen, provider);
425426
return;
426427
}
427428

428-
jgen.writeStartArray();
429-
serializeContents(list.iterator(), jgen, provider);
430-
jgen.writeEndArray();
431-
}
432-
433-
private void serializeContents(Iterator<?> value, JsonGenerator jgen, SerializerProvider provider)
434-
throws IOException {
429+
HalLink halLink = HalLink.class.cast(firstElement);
435430

436-
while (value.hasNext()) {
437-
Object elem = value.next();
438-
if (elem == null) {
439-
provider.defaultSerializeNull(jgen);
440-
} else {
441-
getOrLookupSerializerFor(elem.getClass(), provider).serialize(elem, jgen, provider);
442-
}
443-
}
444-
}
431+
if (list.size() == 1
432+
&& halConfiguration.getSingleLinkRenderModeFor(halLink.getLink().getRel()).equals(RenderSingleLinks.AS_SINGLE)) {
445433

446-
private JsonSerializer<Object> getOrLookupSerializerFor(Class<?> type, SerializerProvider provider)
447-
throws JsonMappingException {
434+
serializeContents(halLink, jgen, provider);
448435

449-
JsonSerializer<Object> serializer = serializers.get(type);
450-
451-
if (serializer == null) {
452-
serializer = provider.findValueSerializer(type, property);
453-
serializers.put(type, serializer);
436+
return;
454437
}
455438

456-
return serializer;
439+
serializeContents(list, jgen, provider);
457440
}
458441

459442
/*
@@ -504,6 +487,24 @@ public JsonSerializer<?> createContextual(SerializerProvider provider, BeanPrope
504487
throws JsonMappingException {
505488
return new OptionalListJackson2Serializer(property, halConfiguration);
506489
}
490+
491+
private void serializeContents(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
492+
getOrLookupSerializerFor(value, provider).serialize(value, jgen, provider);
493+
}
494+
495+
private JsonSerializer<Object> getOrLookupSerializerFor(Object value, SerializerProvider provider)
496+
throws JsonMappingException {
497+
498+
Class<? extends Object> type = value.getClass();
499+
JsonSerializer<Object> serializer = serializers.get(type);
500+
501+
if (serializer == null) {
502+
serializer = provider.findValueSerializer(type, property);
503+
serializers.put(type, serializer);
504+
}
505+
506+
return serializer;
507+
}
507508
}
508509

509510
public static class HalLinkListDeserializer extends ContainerDeserializerBase<List<Link>> {
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2019 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 static org.assertj.core.api.Assertions.*;
19+
20+
import org.junit.Test;
21+
import org.springframework.hateoas.LinkRelation;
22+
import org.springframework.hateoas.hal.HalConfiguration.RenderSingleLinks;
23+
24+
/**
25+
* Unit tests for {@link HalConfiguration}.
26+
*
27+
* @author Oliver Drotbohm
28+
* @soundtrack Port Cities - Montreal (Single)
29+
*/
30+
public class HalConfigurationUnitTest {
31+
32+
@Test // #811
33+
public void registersSimpleArrayLinksPattern() {
34+
35+
HalConfiguration configuration = new HalConfiguration().withRenderSingleLinksFor("foo", RenderSingleLinks.AS_ARRAY);
36+
37+
assertThat(configuration.getSingleLinkRenderModeFor(LinkRelation.of("foo"))).isEqualTo(RenderSingleLinks.AS_ARRAY);
38+
assertThat(configuration.getSingleLinkRenderModeFor(LinkRelation.of("bar"))).isEqualTo(RenderSingleLinks.AS_SINGLE);
39+
}
40+
41+
@Test // #811
42+
public void registersWildcardedArrayLinksPattern() {
43+
44+
HalConfiguration configuration = new HalConfiguration().withRenderSingleLinksFor("foo*",
45+
RenderSingleLinks.AS_ARRAY);
46+
47+
assertThat(configuration.getSingleLinkRenderModeFor(LinkRelation.of("foo"))).isEqualTo(RenderSingleLinks.AS_ARRAY);
48+
assertThat(configuration.getSingleLinkRenderModeFor(LinkRelation.of("foobar")))
49+
.isEqualTo(RenderSingleLinks.AS_ARRAY);
50+
assertThat(configuration.getSingleLinkRenderModeFor(LinkRelation.of("bar"))).isEqualTo(RenderSingleLinks.AS_SINGLE);
51+
}
52+
53+
@Test // #811
54+
public void registersWildcardedArrayLinksPatternForUri() {
55+
56+
HalConfiguration configuration = new HalConfiguration().withRenderSingleLinksFor("http://somehost/foo/**",
57+
RenderSingleLinks.AS_ARRAY);
58+
59+
assertThat(configuration.getSingleLinkRenderModeFor(LinkRelation.of("http://somehost/foo")))
60+
.isEqualTo(RenderSingleLinks.AS_ARRAY);
61+
assertThat(configuration.getSingleLinkRenderModeFor(LinkRelation.of("http://somehost/foo/bar")))
62+
.isEqualTo(RenderSingleLinks.AS_ARRAY);
63+
assertThat(configuration.getSingleLinkRenderModeFor(LinkRelation.of("http://somehost/foo/bar/foobar")))
64+
.isEqualTo(RenderSingleLinks.AS_ARRAY);
65+
assertThat(configuration.getSingleLinkRenderModeFor(LinkRelation.of("http://somehost/bar")))
66+
.isEqualTo(RenderSingleLinks.AS_SINGLE);
67+
}
68+
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,19 @@ public void handleTemplatedLinksOnDeserialization() throws IOException {
475475
assertThat(deserialized).isEqualTo(original);
476476
}
477477

478+
@Test // #811
479+
public void rendersSpecificRelWithSingleLinkAsArrayIfConfigured() throws Exception {
480+
481+
mapper.setHandlerInstantiator(new HalHandlerInstantiator(new AnnotationRelProvider(), null, null,
482+
new HalConfiguration().withRenderSingleLinksFor("foo", RenderSingleLinks.AS_ARRAY)));
483+
484+
ResourceSupport resource = new ResourceSupport();
485+
resource.add(new Link("/some-href", "foo"));
486+
487+
assertThat(mapper.writeValueAsString(resource)) //
488+
.isEqualTo("{\"_links\":{\"foo\":[{\"href\":\"/some-href\"}]}}");
489+
}
490+
478491
private static void verifyResolvedTitle(String resourceBundleKey) throws Exception {
479492

480493
LocaleContextHolder.setLocale(Locale.US);

0 commit comments

Comments
 (0)