From 5eee30127dfdea90024dd72aa1de2bd4d6ef7521 Mon Sep 17 00:00:00 2001 From: Greg Turnquist Date: Mon, 22 May 2017 14:48:59 -0500 Subject: [PATCH] #288 - Introduced HalConfiguration to allow customizations of rendering options. We now expose HalConfiguration to be defined as Spring bean in user configuration to control certain aspects of the HAL rendering. Initially we allow to control whether links shall always be rendered as collection. If no user-provided bean is available, we register a default instance. Original pull request: #295 Related issues: #291 --- .../hateoas/RenderSingleLinks.java | 32 +++++++ .../config/EnableHypermediaSupport.java | 3 +- ...ermediaSupportBeanDefinitionRegistrar.java | 23 ++++- .../hateoas/hal/HalConfiguration.java | 35 ++++++++ .../hateoas/hal/Jackson2HalModule.java | 87 ++++++++----------- ...nableHypermediaSupportIntegrationTest.java | 57 +++++++++++- .../hal/Jackson2HalIntegrationTest.java | 14 ++- 7 files changed, 197 insertions(+), 54 deletions(-) create mode 100644 src/main/java/org/springframework/hateoas/RenderSingleLinks.java create mode 100644 src/main/java/org/springframework/hateoas/hal/HalConfiguration.java diff --git a/src/main/java/org/springframework/hateoas/RenderSingleLinks.java b/src/main/java/org/springframework/hateoas/RenderSingleLinks.java new file mode 100644 index 00000000..a369e68f --- /dev/null +++ b/src/main/java/org/springframework/hateoas/RenderSingleLinks.java @@ -0,0 +1,32 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.hateoas; + +/** + * Whether to render a single {@link Link} as either a single entry or a collection. + */ +public enum RenderSingleLinks { + + /** + * A single {@link Link} is rendered as a JSON object. + */ + AS_SINGLE, + + /** + * A single {@link Link} is rendered as a JSON Array. + */ + AS_ARRAY +} diff --git a/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java b/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java index d5feb4a0..3a64f190 100644 --- a/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java +++ b/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java @@ -25,6 +25,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Import; import org.springframework.hateoas.EntityLinks; import org.springframework.hateoas.LinkDiscoverer; +import org.springframework.hateoas.RenderSingleLinks; /** * Activates hypermedia support in the {@link ApplicationContext}. Will register infrastructure beans available for @@ -58,7 +59,7 @@ public @interface EnableHypermediaSupport { * * @author Oliver Gierke */ - static enum HypermediaType { + enum HypermediaType { /** * HAL - Hypermedia Application Language. diff --git a/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java b/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java index 50e22134..6fb1ff4d 100644 --- a/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java +++ b/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java @@ -46,6 +46,7 @@ import org.springframework.hateoas.EntityLinks; import org.springframework.hateoas.LinkDiscoverer; import org.springframework.hateoas.LinkDiscoverers; import org.springframework.hateoas.RelProvider; +import org.springframework.hateoas.RenderSingleLinks; import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; import org.springframework.hateoas.core.AnnotationRelProvider; @@ -53,6 +54,7 @@ import org.springframework.hateoas.core.DefaultRelProvider; import org.springframework.hateoas.core.DelegatingRelProvider; import org.springframework.hateoas.core.EvoInflectorRelProvider; import org.springframework.hateoas.hal.CurieProvider; +import org.springframework.hateoas.hal.HalConfiguration; import org.springframework.hateoas.hal.HalLinkDiscoverer; import org.springframework.hateoas.hal.Jackson2HalModule; import org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter; @@ -76,7 +78,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; * * @author Oliver Gierke */ -class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { +class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar, BeanFactoryAware { private static final String DELEGATING_REL_PROVIDER_BEAN_NAME = "_relProvider"; private static final String LINK_DISCOVERER_REGISTRY_BEAN_NAME = "_linkDiscovererRegistry"; @@ -89,8 +91,10 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe private static final boolean EVO_PRESENT = ClassUtils.isPresent("org.atteo.evo.inflector.English", null); private final ImportBeanDefinitionRegistrar linkBuilderBeanDefinitionRegistrar = new LinkBuilderBeanDefinitionRegistrar(); + + private BeanFactory beanFactory; - /* + /* * (non-Javadoc) * @see org.springframework.context.annotation.ImportBeanDefinitionRegistrar#registerBeanDefinitions(org.springframework.core.type.AnnotationMetadata, org.springframework.beans.factory.support.BeanDefinitionRegistry) */ @@ -125,6 +129,16 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe BeanDefinitionBuilder builder = rootBeanDefinition(Jackson2ModuleRegisteringBeanPostProcessor.class); registerSourcedBeanDefinition(builder, metadata, registry); } + + try { + this.beanFactory.getBean(HalConfiguration.class); + } catch (BeansException e) { + + // If no HalConfiguration bean, create a default one. + BeanDefinitionBuilder defaultHalConfiguration = rootBeanDefinition(HalConfiguration.class); + defaultHalConfiguration.addPropertyValue("renderSingleLinks", RenderSingleLinks.AS_SINGLE); + registerSourcedBeanDefinition(defaultHalConfiguration, metadata, registry); + } } if (!types.isEmpty()) { @@ -143,6 +157,11 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe registerRelProviderPluginRegistryAndDelegate(registry); } + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } + /** * Registers bean definitions for a {@link PluginRegistry} to capture {@link RelProvider} instances. Wraps the * registry into a {@link DelegatingRelProvider} bean definition backed by the registry. diff --git a/src/main/java/org/springframework/hateoas/hal/HalConfiguration.java b/src/main/java/org/springframework/hateoas/hal/HalConfiguration.java new file mode 100644 index 00000000..837832db --- /dev/null +++ b/src/main/java/org/springframework/hateoas/hal/HalConfiguration.java @@ -0,0 +1,35 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.hateoas.hal; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Wither; + +import org.springframework.hateoas.HypermediaConfiguration; +import org.springframework.hateoas.RenderSingleLinks; + +/** + * @author Greg Turnquist + */ +@AllArgsConstructor +@NoArgsConstructor +public class HalConfiguration implements HypermediaConfiguration { + + private @Wither @Getter @Setter RenderSingleLinks renderSingleLinks; +} diff --git a/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java b/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java index 729933c1..071864e6 100644 --- a/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java +++ b/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java @@ -32,6 +32,7 @@ import org.springframework.context.support.MessageSourceAccessor; import org.springframework.hateoas.Link; import org.springframework.hateoas.Links; import org.springframework.hateoas.RelProvider; +import org.springframework.hateoas.RenderSingleLinks; import org.springframework.hateoas.Resource; import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.Resources; @@ -123,13 +124,14 @@ public class Jackson2HalModule extends SimpleModule { private final CurieProvider curieProvider; private final EmbeddedMapper mapper; private final MessageSourceAccessor accessor; + private final HalConfiguration halConfiguration; - public HalLinkListSerializer(CurieProvider curieProvider, EmbeddedMapper mapper, MessageSourceAccessor accessor) { - this(null, curieProvider, mapper, accessor); + public HalLinkListSerializer(CurieProvider curieProvider, EmbeddedMapper mapper, MessageSourceAccessor accessor, HalConfiguration halConfiguration) { + this(null, curieProvider, mapper, accessor, halConfiguration); } public HalLinkListSerializer(BeanProperty property, CurieProvider curieProvider, EmbeddedMapper mapper, - MessageSourceAccessor accessor) { + MessageSourceAccessor accessor, HalConfiguration halConfiguration) { super(TypeFactory.defaultInstance().constructType(List.class)); @@ -137,6 +139,7 @@ public class Jackson2HalModule extends SimpleModule { this.curieProvider = curieProvider; this.mapper = mapper; this.accessor = accessor; + this.halConfiguration = halConfiguration; } /* @@ -198,7 +201,7 @@ public class Jackson2HalModule extends SimpleModule { JavaType mapType = typeFactory.constructMapType(HashMap.class, keyType, valueType); MapSerializer serializer = MapSerializer.construct(new String[] {}, mapType, true, null, - provider.findKeySerializer(keyType, null), new OptionalListJackson2Serializer(property), null); + provider.findKeySerializer(keyType, null), new OptionalListJackson2Serializer(property, halConfiguration), null); serializer.serialize(sortedLinks, jgen, provider); } @@ -246,7 +249,7 @@ public class Jackson2HalModule extends SimpleModule { @Override public JsonSerializer createContextual(SerializerProvider provider, BeanProperty property) throws JsonMappingException { - return new HalLinkListSerializer(property, curieProvider, mapper, accessor); + return new HalLinkListSerializer(property, curieProvider, mapper, accessor, halConfiguration); } /* @@ -267,15 +270,7 @@ public class Jackson2HalModule extends SimpleModule { return null; } - /* - * (non-Javadoc) - * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#isEmpty(java.lang.Object) - */ - public boolean isEmpty(List value) { - return isEmpty(null, value); - } - - /* + /* * (non-Javadoc) * @see com.fasterxml.jackson.databind.JsonSerializer#isEmpty(com.fasterxml.jackson.databind.SerializerProvider, java.lang.Object) */ @@ -368,10 +363,6 @@ public class Jackson2HalModule extends SimpleModule { return null; } - public boolean isEmpty(Collection value) { - return isEmpty(null, value); - } - public boolean isEmpty(SerializerProvider provider, Collection value) { return value.isEmpty(); } @@ -401,9 +392,14 @@ public class Jackson2HalModule extends SimpleModule { private final BeanProperty property; private final Map, JsonSerializer> serializers; + private final HalConfiguration halConfiguration; public OptionalListJackson2Serializer() { - this(null); + this(null, new HalConfiguration().withRenderSingleLinks(RenderSingleLinks.AS_SINGLE)); + } + + public OptionalListJackson2Serializer(BeanProperty property) { + this(property, new HalConfiguration().withRenderSingleLinks(RenderSingleLinks.AS_SINGLE)); } /** @@ -411,12 +407,13 @@ public class Jackson2HalModule extends SimpleModule { * * @param property */ - public OptionalListJackson2Serializer(BeanProperty property) { + public OptionalListJackson2Serializer(BeanProperty property, HalConfiguration halConfiguration) { super(TypeFactory.defaultInstance().constructType(List.class)); this.property = property; this.serializers = new HashMap, JsonSerializer>(); + this.halConfiguration = halConfiguration; } /* @@ -434,7 +431,7 @@ public class Jackson2HalModule extends SimpleModule { */ @Override public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) - throws IOException, JsonGenerationException { + throws IOException { List list = (List) value; @@ -442,7 +439,7 @@ public class Jackson2HalModule extends SimpleModule { return; } - if (list.size() == 1) { + if (list.size() == 1 && this.halConfiguration.getRenderSingleLinks() == RenderSingleLinks.AS_SINGLE) { serializeContents(list.iterator(), jgen, provider); return; } @@ -506,15 +503,7 @@ public class Jackson2HalModule extends SimpleModule { return false; } - /* - * (non-Javadoc) - * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#isEmpty(java.lang.Object) - */ - public boolean isEmpty(Object value) { - return isEmpty(null, value); - } - - /* + /* * (non-Javadoc) * @see com.fasterxml.jackson.databind.JsonSerializer#isEmpty(com.fasterxml.jackson.databind.SerializerProvider, java.lang.Object) */ @@ -577,7 +566,7 @@ public class Jackson2HalModule extends SimpleModule { while (!JsonToken.END_OBJECT.equals(jp.nextToken())) { if (!JsonToken.FIELD_NAME.equals(jp.getCurrentToken())) { - throw new JsonParseException("Expected relation name", jp.getCurrentLocation()); + throw new JsonParseException(jp, "Expected relation name", jp.getCurrentLocation()); } // save the relation in case the link does not contain it @@ -653,7 +642,7 @@ public class Jackson2HalModule extends SimpleModule { while (!JsonToken.END_OBJECT.equals(jp.nextToken())) { if (!JsonToken.FIELD_NAME.equals(jp.getCurrentToken())) { - throw new JsonParseException("Expected relation name", jp.getCurrentLocation()); + throw new JsonParseException(jp, "Expected relation name", jp.getCurrentLocation()); } if (JsonToken.START_ARRAY.equals(jp.nextToken())) { @@ -702,8 +691,16 @@ public class Jackson2HalModule extends SimpleModule { * @param beanFactory can be {@literal null} */ public HalHandlerInstantiator(RelProvider provider, CurieProvider curieProvider, MessageSourceAccessor accessor, - AutowireCapableBeanFactory beanFactory) { - this(provider, curieProvider, accessor, true, beanFactory); + AutowireCapableBeanFactory beanFactory, HalConfiguration halConfiguration) { + this(provider, curieProvider, accessor, true, beanFactory, halConfiguration); + } + + public HalHandlerInstantiator(RelProvider provider, CurieProvider curieProvider, MessageSourceAccessor messageSourceAccessor, AutowireCapableBeanFactory beanFactory) { + this(provider, curieProvider, messageSourceAccessor, beanFactory, beanFactory.getBean(HalConfiguration.class)); + } + + public HalHandlerInstantiator(RelProvider provider, CurieProvider curieProvider, MessageSourceAccessor messageSourceAccessor) { + this(provider, curieProvider, messageSourceAccessor, new HalConfiguration().withRenderSingleLinks(RenderSingleLinks.AS_SINGLE)); } /** @@ -716,8 +713,8 @@ public class Jackson2HalModule extends SimpleModule { * @param messageSourceAccessor can be {@literal null}. */ public HalHandlerInstantiator(RelProvider provider, CurieProvider curieProvider, - MessageSourceAccessor messageSourceAccessor) { - this(provider, curieProvider, messageSourceAccessor, true); + MessageSourceAccessor messageSourceAccessor, HalConfiguration halConfiguration) { + this(provider, curieProvider, messageSourceAccessor, true, halConfiguration); } /** @@ -732,12 +729,12 @@ public class Jackson2HalModule extends SimpleModule { * @param enforceEmbeddedCollections */ public HalHandlerInstantiator(RelProvider provider, CurieProvider curieProvider, MessageSourceAccessor accessor, - boolean enforceEmbeddedCollections) { - this(provider, curieProvider, accessor, enforceEmbeddedCollections, null); + boolean enforceEmbeddedCollections, HalConfiguration halConfiguration) { + this(provider, curieProvider, accessor, enforceEmbeddedCollections, null, halConfiguration); } private HalHandlerInstantiator(RelProvider provider, CurieProvider curieProvider, MessageSourceAccessor accessor, - boolean enforceEmbeddedCollections, AutowireCapableBeanFactory delegate) { + boolean enforceEmbeddedCollections, AutowireCapableBeanFactory delegate, HalConfiguration halConfiguration) { Assert.notNull(provider, "RelProvider must not be null!"); @@ -746,7 +743,7 @@ public class Jackson2HalModule extends SimpleModule { this.delegate = delegate; this.serializers.put(HalResourcesSerializer.class, new HalResourcesSerializer(mapper)); - this.serializers.put(HalLinkListSerializer.class, new HalLinkListSerializer(curieProvider, mapper, accessor)); + this.serializers.put(HalLinkListSerializer.class, new HalLinkListSerializer(curieProvider, mapper, accessor, halConfiguration)); } /* @@ -820,14 +817,6 @@ public class Jackson2HalModule extends SimpleModule { } /* - * (non-Javadoc) - * @see com.fasterxml.jackson.databind.JsonSerializer#isEmpty(java.lang.Object) - */ - public boolean isEmpty(Boolean value) { - return isEmpty(null, value); - } - - /* * (non-Javadoc) * @see com.fasterxml.jackson.databind.JsonSerializer#isEmpty(com.fasterxml.jackson.databind.SerializerProvider, java.lang.Object) */ diff --git a/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java b/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java index 3d08d80b..0beec7a4 100644 --- a/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java @@ -32,14 +32,18 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.hateoas.EntityLinks; +import org.springframework.hateoas.Link; import org.springframework.hateoas.LinkDiscoverer; import org.springframework.hateoas.LinkDiscoverers; import org.springframework.hateoas.MediaTypes; import org.springframework.hateoas.RelProvider; +import org.springframework.hateoas.RenderSingleLinks; +import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; import org.springframework.hateoas.config.HypermediaSupportBeanDefinitionRegistrar.Jackson2ModuleRegisteringBeanPostProcessor; import org.springframework.hateoas.core.DelegatingEntityLinks; import org.springframework.hateoas.core.DelegatingRelProvider; +import org.springframework.hateoas.hal.HalConfiguration; import org.springframework.hateoas.hal.HalLinkDiscoverer; import org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; @@ -52,6 +56,7 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolverCompo import org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; @@ -94,7 +99,8 @@ public class EnableHypermediaSupportIntegrationTest { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(HalConfig.class); - Jackson2ModuleRegisteringBeanPostProcessor postProcessor = new HypermediaSupportBeanDefinitionRegistrar.Jackson2ModuleRegisteringBeanPostProcessor(); + Jackson2ModuleRegisteringBeanPostProcessor postProcessor = + new HypermediaSupportBeanDefinitionRegistrar.Jackson2ModuleRegisteringBeanPostProcessor(); postProcessor.setBeanFactory(context.getAutowireCapableBeanFactory()); RequestMappingHandlerAdapter adapter = context.getBean(RequestMappingHandlerAdapter.class); @@ -149,6 +155,39 @@ public class EnableHypermediaSupportIntegrationTest { context.close(); } + @Test + public void verifyDefaultHalConfigurationRendersSingleItemAsSingleItem() throws JsonProcessingException { + + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(HalConfig.class); + + ObjectMapper mapper = context.getBean("_halObjectMapper", ObjectMapper.class); + + ResourceSupport resourceSupport = new ResourceSupport(); + resourceSupport.add(new Link("localhost").withSelfRel()); + + assertThat(mapper.writeValueAsString(resourceSupport), + is("{\"_links\":{\"self\":{\"href\":\"localhost\"}}}")); + + context.close(); + } + + + @Test + public void verifyRenderSingleLinkAsArrayViaOverridingBean() throws JsonProcessingException { + + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(RenderLinkAsSingleLinksConfig.class); + + ObjectMapper mapper = context.getBean("_halObjectMapper", ObjectMapper.class); + + ResourceSupport resourceSupport = new ResourceSupport(); + resourceSupport.add(new Link("localhost").withSelfRel()); + + assertThat(mapper.writeValueAsString(resourceSupport), + is("{\"_links\":{\"self\":[{\"href\":\"localhost\"}]}}")); + + context.close(); + } + private static void assertEntityLinksSetUp(ApplicationContext context) { Map discoverers = context.getBeansOfType(EntityLinks.class); @@ -227,4 +266,20 @@ public class EnableHypermediaSupportIntegrationTest { static class DelegateConfig { } + + @Configuration + @EnableHypermediaSupport(type = HypermediaType.HAL) + static class RenderLinkAsSingleLinksConfig { + + @Bean + HalConfiguration halConfiguration() { + return new HalConfiguration().withRenderSingleLinks(RenderSingleLinks.AS_ARRAY); + } + + @Bean + public RequestMappingHandlerAdapter rmh() { + return new RequestMappingHandlerAdapter(); + } + + } } diff --git a/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java b/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java index 25a4faa0..e5e18bd2 100644 --- a/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java @@ -37,6 +37,7 @@ import org.springframework.hateoas.Link; import org.springframework.hateoas.Links; import org.springframework.hateoas.PagedResources; import org.springframework.hateoas.PagedResources.PageMetadata; +import org.springframework.hateoas.RenderSingleLinks; import org.springframework.hateoas.Resource; import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.Resources; @@ -86,7 +87,7 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg public void setUpModule() { mapper.registerModule(new Jackson2HalModule()); - mapper.setHandlerInstantiator(new HalHandlerInstantiator(new AnnotationRelProvider(), null, null)); + mapper.setHandlerInstantiator(new HalHandlerInstantiator(new AnnotationRelProvider(), null, null, new HalConfiguration().withRenderSingleLinks(RenderSingleLinks.AS_SINGLE))); } /** @@ -411,6 +412,17 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg verifyResolvedTitle("_links.foobar.title"); } + @Test + public void rendersSingleLinkAsArrayWhenConfigured() throws Exception { + + mapper.setHandlerInstantiator(new HalHandlerInstantiator(new AnnotationRelProvider(), null, null, new HalConfiguration().withRenderSingleLinks(RenderSingleLinks.AS_ARRAY))); + + ResourceSupport resourceSupport = new ResourceSupport(); + resourceSupport.add(new Link("localhost").withSelfRel()); + + assertThat(write(resourceSupport), is("{\"_links\":{\"self\":[{\"href\":\"localhost\"}]}}")); + } + private static void verifyResolvedTitle(String resourceBundleKey) throws Exception { LocaleContextHolder.setLocale(Locale.US);