Skip to content

Commit df0d332

Browse files
committed
#288 - Opt to render single links as an array.
Provide the means to render a single link entry as a JSON Array. Original pull-request: #295 Related issues: #291
1 parent 4807ae0 commit df0d332

File tree

6 files changed

+134
-56
lines changed

6 files changed

+134
-56
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2017 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;
17+
18+
/**
19+
* Whether to render a single {@link Link} as either a single entry or a collection.
20+
*/
21+
public enum RenderSingleLinks {
22+
23+
/**
24+
* A single {@link Link} is rendered as a JSON object.
25+
*/
26+
AS_SINGLE,
27+
28+
/**
29+
* A single {@link Link} is rendered as a JSON Array.
30+
*/
31+
AS_ARRAY
32+
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.springframework.context.annotation.Import;
2626
import org.springframework.hateoas.EntityLinks;
2727
import org.springframework.hateoas.LinkDiscoverer;
28+
import org.springframework.hateoas.RenderSingleLinks;
2829

2930
/**
3031
* Activates hypermedia support in the {@link ApplicationContext}. Will register infrastructure beans available for
@@ -58,7 +59,7 @@
5859
*
5960
* @author Oliver Gierke
6061
*/
61-
static enum HypermediaType {
62+
enum HypermediaType {
6263

6364
/**
6465
* HAL - Hypermedia Application Language.
@@ -68,4 +69,6 @@ static enum HypermediaType {
6869
*/
6970
HAL;
7071
}
72+
73+
RenderSingleLinks renderSingleLinks() default RenderSingleLinks.AS_SINGLE;
7174
}

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

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import org.springframework.hateoas.LinkDiscoverer;
4747
import org.springframework.hateoas.LinkDiscoverers;
4848
import org.springframework.hateoas.RelProvider;
49+
import org.springframework.hateoas.RenderSingleLinks;
4950
import org.springframework.hateoas.ResourceSupport;
5051
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
5152
import org.springframework.hateoas.core.AnnotationRelProvider;
@@ -101,6 +102,7 @@ public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionR
101102

102103
Map<String, Object> attributes = metadata.getAnnotationAttributes(EnableHypermediaSupport.class.getName());
103104
Collection<HypermediaType> types = Arrays.asList((HypermediaType[]) attributes.get("type"));
105+
RenderSingleLinks renderSingleLinks = (RenderSingleLinks) attributes.get("renderSingleLinks");
104106

105107
for (HypermediaType type : types) {
106108

@@ -123,6 +125,7 @@ public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionR
123125
registerSourcedBeanDefinition(customizerBeanDefinition, metadata, registry);
124126

125127
BeanDefinitionBuilder builder = rootBeanDefinition(Jackson2ModuleRegisteringBeanPostProcessor.class);
128+
builder.addPropertyValue("renderSingleLinks", renderSingleLinks);
126129
registerSourcedBeanDefinition(builder, metadata, registry);
127130
}
128131
}
@@ -224,6 +227,7 @@ private static String registerSourcedBeanDefinition(BeanDefinitionBuilder builde
224227
static class Jackson2ModuleRegisteringBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware {
225228

226229
private AutowireCapableBeanFactory beanFactory;
230+
private RenderSingleLinks renderSingleLinks;
227231

228232
/*
229233
* (non-Javadoc)
@@ -248,13 +252,13 @@ public Object postProcessBeforeInitialization(Object bean, String beanName) thro
248252
if (bean instanceof RequestMappingHandlerAdapter) {
249253

250254
RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) bean;
251-
adapter.setMessageConverters(potentiallyRegisterModule(adapter.getMessageConverters()));
255+
adapter.setMessageConverters(potentiallyRegisterModule(adapter.getMessageConverters(), renderSingleLinks));
252256
}
253257

254258
if (bean instanceof RestTemplate) {
255259

256260
RestTemplate template = (RestTemplate) bean;
257-
template.setMessageConverters(potentiallyRegisterModule(template.getMessageConverters()));
261+
template.setMessageConverters(potentiallyRegisterModule(template.getMessageConverters(), renderSingleLinks));
258262
}
259263

260264
return bean;
@@ -269,7 +273,8 @@ public Object postProcessAfterInitialization(Object bean, String beanName) throw
269273
return bean;
270274
}
271275

272-
private List<HttpMessageConverter<?>> potentiallyRegisterModule(List<HttpMessageConverter<?>> converters) {
276+
private List<HttpMessageConverter<?>> potentiallyRegisterModule(List<HttpMessageConverter<?>> converters,
277+
RenderSingleLinks renderSingleLinks) {
273278

274279
for (HttpMessageConverter<?> converter : converters) {
275280
if (converter instanceof MappingJackson2HttpMessageConverter) {
@@ -289,7 +294,7 @@ private List<HttpMessageConverter<?>> potentiallyRegisterModule(List<HttpMessage
289294

290295
halObjectMapper.registerModule(new Jackson2HalModule());
291296
halObjectMapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, curieProvider,
292-
linkRelationMessageSource, beanFactory));
297+
linkRelationMessageSource, beanFactory, renderSingleLinks));
293298

294299
MappingJackson2HttpMessageConverter halConverter = new TypeConstrainedMappingJackson2HttpMessageConverter(
295300
ResourceSupport.class);
@@ -310,6 +315,10 @@ private static CurieProvider getCurieProvider(BeanFactory factory) {
310315
return null;
311316
}
312317
}
318+
319+
public void setRenderSingleLinks(RenderSingleLinks renderSingleLinks) {
320+
this.renderSingleLinks = renderSingleLinks;
321+
}
313322
}
314323

315324
/**

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

Lines changed: 38 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.springframework.hateoas.Link;
3333
import org.springframework.hateoas.Links;
3434
import org.springframework.hateoas.RelProvider;
35+
import org.springframework.hateoas.RenderSingleLinks;
3536
import org.springframework.hateoas.Resource;
3637
import org.springframework.hateoas.ResourceSupport;
3738
import org.springframework.hateoas.Resources;
@@ -123,20 +124,22 @@ public static class HalLinkListSerializer extends ContainerSerializer<List<Link>
123124
private final CurieProvider curieProvider;
124125
private final EmbeddedMapper mapper;
125126
private final MessageSourceAccessor accessor;
127+
private final RenderSingleLinks renderSingleLinks;
126128

127-
public HalLinkListSerializer(CurieProvider curieProvider, EmbeddedMapper mapper, MessageSourceAccessor accessor) {
128-
this(null, curieProvider, mapper, accessor);
129+
public HalLinkListSerializer(CurieProvider curieProvider, EmbeddedMapper mapper, MessageSourceAccessor accessor, RenderSingleLinks renderSingleLinks) {
130+
this(null, curieProvider, mapper, accessor, renderSingleLinks);
129131
}
130132

131133
public HalLinkListSerializer(BeanProperty property, CurieProvider curieProvider, EmbeddedMapper mapper,
132-
MessageSourceAccessor accessor) {
134+
MessageSourceAccessor accessor, RenderSingleLinks renderSingleLinks) {
133135

134136
super(TypeFactory.defaultInstance().constructType(List.class));
135137

136138
this.property = property;
137139
this.curieProvider = curieProvider;
138140
this.mapper = mapper;
139141
this.accessor = accessor;
142+
this.renderSingleLinks = renderSingleLinks;
140143
}
141144

142145
/*
@@ -198,7 +201,7 @@ public void serialize(List<Link> value, JsonGenerator jgen, SerializerProvider p
198201
JavaType mapType = typeFactory.constructMapType(HashMap.class, keyType, valueType);
199202

200203
MapSerializer serializer = MapSerializer.construct(new String[] {}, mapType, true, null,
201-
provider.findKeySerializer(keyType, null), new OptionalListJackson2Serializer(property), null);
204+
provider.findKeySerializer(keyType, null), new OptionalListJackson2Serializer(property, renderSingleLinks), null);
202205

203206
serializer.serialize(sortedLinks, jgen, provider);
204207
}
@@ -246,7 +249,7 @@ private String getTitle(String localRel) {
246249
@Override
247250
public JsonSerializer<?> createContextual(SerializerProvider provider, BeanProperty property)
248251
throws JsonMappingException {
249-
return new HalLinkListSerializer(property, curieProvider, mapper, accessor);
252+
return new HalLinkListSerializer(property, curieProvider, mapper, accessor, renderSingleLinks);
250253
}
251254

252255
/*
@@ -267,15 +270,7 @@ public JsonSerializer<?> getContentSerializer() {
267270
return null;
268271
}
269272

270-
/*
271-
* (non-Javadoc)
272-
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#isEmpty(java.lang.Object)
273-
*/
274-
public boolean isEmpty(List<Link> value) {
275-
return isEmpty(null, value);
276-
}
277-
278-
/*
273+
/*
279274
* (non-Javadoc)
280275
* @see com.fasterxml.jackson.databind.JsonSerializer#isEmpty(com.fasterxml.jackson.databind.SerializerProvider, java.lang.Object)
281276
*/
@@ -368,10 +363,6 @@ public JsonSerializer<?> getContentSerializer() {
368363
return null;
369364
}
370365

371-
public boolean isEmpty(Collection<?> value) {
372-
return isEmpty(null, value);
373-
}
374-
375366
public boolean isEmpty(SerializerProvider provider, Collection<?> value) {
376367
return value.isEmpty();
377368
}
@@ -401,22 +392,28 @@ public static class OptionalListJackson2Serializer extends ContainerSerializer<O
401392

402393
private final BeanProperty property;
403394
private final Map<Class<?>, JsonSerializer<Object>> serializers;
395+
private final RenderSingleLinks renderSingleLinks;
404396

405397
public OptionalListJackson2Serializer() {
406-
this(null);
398+
this(null, RenderSingleLinks.AS_SINGLE);
399+
}
400+
401+
public OptionalListJackson2Serializer(BeanProperty property) {
402+
this(property, RenderSingleLinks.AS_SINGLE);
407403
}
408404

409405
/**
410406
* Creates a new {@link OptionalListJackson2Serializer} using the given {@link BeanProperty}.
411407
*
412408
* @param property
413409
*/
414-
public OptionalListJackson2Serializer(BeanProperty property) {
410+
public OptionalListJackson2Serializer(BeanProperty property, RenderSingleLinks renderSingleLinks) {
415411

416412
super(TypeFactory.defaultInstance().constructType(List.class));
417413

418414
this.property = property;
419415
this.serializers = new HashMap<Class<?>, JsonSerializer<Object>>();
416+
this.renderSingleLinks = renderSingleLinks;
420417
}
421418

422419
/*
@@ -434,15 +431,15 @@ public ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) {
434431
*/
435432
@Override
436433
public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider)
437-
throws IOException, JsonGenerationException {
434+
throws IOException {
438435

439436
List<?> list = (List<?>) value;
440437

441438
if (list.isEmpty()) {
442439
return;
443440
}
444441

445-
if (list.size() == 1) {
442+
if (list.size() == 1 && this.renderSingleLinks == RenderSingleLinks.AS_SINGLE) {
446443
serializeContents(list.iterator(), jgen, provider);
447444
return;
448445
}
@@ -506,15 +503,7 @@ public boolean hasSingleElement(Object arg0) {
506503
return false;
507504
}
508505

509-
/*
510-
* (non-Javadoc)
511-
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#isEmpty(java.lang.Object)
512-
*/
513-
public boolean isEmpty(Object value) {
514-
return isEmpty(null, value);
515-
}
516-
517-
/*
506+
/*
518507
* (non-Javadoc)
519508
* @see com.fasterxml.jackson.databind.JsonSerializer#isEmpty(com.fasterxml.jackson.databind.SerializerProvider, java.lang.Object)
520509
*/
@@ -577,7 +566,7 @@ public List<Link> deserialize(JsonParser jp, DeserializationContext ctxt)
577566
while (!JsonToken.END_OBJECT.equals(jp.nextToken())) {
578567

579568
if (!JsonToken.FIELD_NAME.equals(jp.getCurrentToken())) {
580-
throw new JsonParseException("Expected relation name", jp.getCurrentLocation());
569+
throw new JsonParseException(jp, "Expected relation name", jp.getCurrentLocation());
581570
}
582571

583572
// save the relation in case the link does not contain it
@@ -653,7 +642,7 @@ public List<Object> deserialize(JsonParser jp, DeserializationContext ctxt)
653642
while (!JsonToken.END_OBJECT.equals(jp.nextToken())) {
654643

655644
if (!JsonToken.FIELD_NAME.equals(jp.getCurrentToken())) {
656-
throw new JsonParseException("Expected relation name", jp.getCurrentLocation());
645+
throw new JsonParseException(jp, "Expected relation name", jp.getCurrentLocation());
657646
}
658647

659648
if (JsonToken.START_ARRAY.equals(jp.nextToken())) {
@@ -702,8 +691,16 @@ public static class HalHandlerInstantiator extends HandlerInstantiator {
702691
* @param beanFactory can be {@literal null}
703692
*/
704693
public HalHandlerInstantiator(RelProvider provider, CurieProvider curieProvider, MessageSourceAccessor accessor,
705-
AutowireCapableBeanFactory beanFactory) {
706-
this(provider, curieProvider, accessor, true, beanFactory);
694+
AutowireCapableBeanFactory beanFactory, RenderSingleLinks renderSingleLinks) {
695+
this(provider, curieProvider, accessor, true, beanFactory, renderSingleLinks);
696+
}
697+
698+
public HalHandlerInstantiator(RelProvider provider, CurieProvider curieProvider, MessageSourceAccessor messageSourceAccessor, AutowireCapableBeanFactory beanFactory) {
699+
this(provider, curieProvider, messageSourceAccessor, beanFactory, RenderSingleLinks.AS_SINGLE);
700+
}
701+
702+
public HalHandlerInstantiator(RelProvider provider, CurieProvider curieProvider, MessageSourceAccessor messageSourceAccessor) {
703+
this(provider, curieProvider, messageSourceAccessor, RenderSingleLinks.AS_SINGLE);
707704
}
708705

709706
/**
@@ -716,8 +713,8 @@ public HalHandlerInstantiator(RelProvider provider, CurieProvider curieProvider,
716713
* @param messageSourceAccessor can be {@literal null}.
717714
*/
718715
public HalHandlerInstantiator(RelProvider provider, CurieProvider curieProvider,
719-
MessageSourceAccessor messageSourceAccessor) {
720-
this(provider, curieProvider, messageSourceAccessor, true);
716+
MessageSourceAccessor messageSourceAccessor, RenderSingleLinks renderSingleLinks) {
717+
this(provider, curieProvider, messageSourceAccessor, true, renderSingleLinks);
721718
}
722719

723720
/**
@@ -732,12 +729,12 @@ public HalHandlerInstantiator(RelProvider provider, CurieProvider curieProvider,
732729
* @param enforceEmbeddedCollections
733730
*/
734731
public HalHandlerInstantiator(RelProvider provider, CurieProvider curieProvider, MessageSourceAccessor accessor,
735-
boolean enforceEmbeddedCollections) {
736-
this(provider, curieProvider, accessor, enforceEmbeddedCollections, null);
732+
boolean enforceEmbeddedCollections, RenderSingleLinks renderSingleLinks) {
733+
this(provider, curieProvider, accessor, enforceEmbeddedCollections, null, renderSingleLinks);
737734
}
738735

739736
private HalHandlerInstantiator(RelProvider provider, CurieProvider curieProvider, MessageSourceAccessor accessor,
740-
boolean enforceEmbeddedCollections, AutowireCapableBeanFactory delegate) {
737+
boolean enforceEmbeddedCollections, AutowireCapableBeanFactory delegate, RenderSingleLinks renderSingleLinks) {
741738

742739
Assert.notNull(provider, "RelProvider must not be null!");
743740

@@ -746,7 +743,7 @@ private HalHandlerInstantiator(RelProvider provider, CurieProvider curieProvider
746743
this.delegate = delegate;
747744

748745
this.serializers.put(HalResourcesSerializer.class, new HalResourcesSerializer(mapper));
749-
this.serializers.put(HalLinkListSerializer.class, new HalLinkListSerializer(curieProvider, mapper, accessor));
746+
this.serializers.put(HalLinkListSerializer.class, new HalLinkListSerializer(curieProvider, mapper, accessor, renderSingleLinks));
750747
}
751748

752749
/*
@@ -820,14 +817,6 @@ public TrueOnlyBooleanSerializer() {
820817
}
821818

822819
/*
823-
* (non-Javadoc)
824-
* @see com.fasterxml.jackson.databind.JsonSerializer#isEmpty(java.lang.Object)
825-
*/
826-
public boolean isEmpty(Boolean value) {
827-
return isEmpty(null, value);
828-
}
829-
830-
/*
831820
* (non-Javadoc)
832821
* @see com.fasterxml.jackson.databind.JsonSerializer#isEmpty(com.fasterxml.jackson.databind.SerializerProvider, java.lang.Object)
833822
*/

0 commit comments

Comments
 (0)