diff --git a/src/main/java/org/springframework/hateoas/HypermediaConfiguration.java b/src/main/java/org/springframework/hateoas/HypermediaConfiguration.java new file mode 100644 index 000000000..67b399570 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/HypermediaConfiguration.java @@ -0,0 +1,23 @@ +/* + * 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; + +/** + * @author Greg Turnquist + */ +public interface HypermediaConfiguration { + +} 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 000000000..a369e68f9 --- /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 d5feb4a09..3a64f1906 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.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 @@ * * @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 6268de0f3..d56f577e5 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.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.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 @@ * * @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 @@ public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionR 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 @@ public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionR 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 000000000..837832db0 --- /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 729933c1f..071864e6b 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.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 static class HalLinkListSerializer extends ContainerSerializer 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 HalLinkListSerializer(BeanProperty property, CurieProvider curieProvider, this.curieProvider = curieProvider; this.mapper = mapper; this.accessor = accessor; + this.halConfiguration = halConfiguration; } /* @@ -198,7 +201,7 @@ public void serialize(List value, JsonGenerator jgen, SerializerProvider p 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 @@ private String getTitle(String localRel) { @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 JsonSerializer getContentSerializer() { 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 JsonSerializer getContentSerializer() { 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 static class OptionalListJackson2Serializer extends ContainerSerializer, 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 OptionalListJackson2Serializer() { * * @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 ContainerSerializer _withValueTypeSerializer(TypeSerializer vts) { */ @Override public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) - throws IOException, JsonGenerationException { + throws IOException { List list = (List) value; @@ -442,7 +439,7 @@ public void serialize(Object value, JsonGenerator jgen, SerializerProvider provi 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 boolean hasSingleElement(Object arg0) { 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 List deserialize(JsonParser jp, DeserializationContext ctxt) 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 List deserialize(JsonParser jp, DeserializationContext ctxt) 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 static class HalHandlerInstantiator extends HandlerInstantiator { * @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 HalHandlerInstantiator(RelProvider provider, CurieProvider curieProvider, * @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 HalHandlerInstantiator(RelProvider provider, CurieProvider curieProvider, * @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 @@ private HalHandlerInstantiator(RelProvider provider, CurieProvider curieProvider 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 TrueOnlyBooleanSerializer() { } /* - * (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 4ac60d3aa..250ec50b0 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.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.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; @@ -93,7 +98,8 @@ public void halSetupIsAppliedToAllTransitiveComponentsInRequestMappingHandlerAda 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); @@ -146,6 +152,39 @@ public void configuresDefaultObjectMapperForHalToIgnoreUnknownProperties() { 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); @@ -224,4 +263,20 @@ static class ExtendedHalConfig extends HalConfig { 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 25a4faa09..e5e18bd2f 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.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 void rendersTitleIfMessageSourceResolvesLocalKey() throws Exception { 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);