From 5502bfdf8572a4cf8114939de4385565a14b8c87 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Wed, 11 Dec 2019 22:10:13 +0100 Subject: [PATCH] #1159 - HalConfiguration now exposes whether to enforce arrays in embedded clauses. The flag to control this had been directly configured on the HalHandlerInstantiator which wasn't accessible to users in the first place. Refined the setup and forwarding of the bean factory into HalHandlerInstantiator so that non-Spring-HATEOAS Jackson serializer lookups are routed through the Spring container potentially picking up prototype bean definitions. --- .../mediatype/hal/HalConfiguration.java | 7 ++++++ .../hal/HalMediaTypeConfiguration.java | 22 +++++----------- .../mediatype/hal/Jackson2HalModule.java | 25 +++---------------- .../forms/HalFormsMediaTypeConfiguration.java | 4 ++- .../hal/forms/Jackson2HalFormsModule.java | 14 +++++------ .../HalFormsMessageConverterUnitTest.java | 3 ++- .../Jackson2HalFormsIntegrationTest.java | 6 +++-- 7 files changed, 33 insertions(+), 48 deletions(-) diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/HalConfiguration.java b/src/main/java/org/springframework/hateoas/mediatype/hal/HalConfiguration.java index b56a088d..94cf923c 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/HalConfiguration.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/HalConfiguration.java @@ -54,6 +54,12 @@ public class HalConfiguration { */ private final @Wither @Getter boolean applyPropertyNamingStrategy; + /** + * Configures whether to always use collections for embeddeds, even if there's only one entry for a link relation. + * Defaults to {@literal true}. + */ + private final @Wither @Getter boolean enforceEmbeddedCollections; + /** * Creates a new default {@link HalConfiguration} rendering single links as immediate sub-document. */ @@ -62,6 +68,7 @@ public class HalConfiguration { this.renderSingleLinks = RenderSingleLinks.AS_SINGLE; this.singleLinksPerPattern = new LinkedHashMap<>(); this.applyPropertyNamingStrategy = true; + this.enforceEmbeddedCollections = true; } /** diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/HalMediaTypeConfiguration.java b/src/main/java/org/springframework/hateoas/mediatype/hal/HalMediaTypeConfiguration.java index 7accce1f..0df2affd 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/HalMediaTypeConfiguration.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/HalMediaTypeConfiguration.java @@ -15,10 +15,13 @@ */ package org.springframework.hateoas.mediatype.hal; +import lombok.RequiredArgsConstructor; + import java.util.List; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.hateoas.client.LinkDiscoverer; @@ -38,27 +41,14 @@ import com.fasterxml.jackson.databind.ObjectMapper; * @author Oliver Drotbohm */ @Configuration +@RequiredArgsConstructor public class HalMediaTypeConfiguration implements HypermediaMappingInformation { private final LinkRelationProvider relProvider; private final ObjectProvider curieProvider; private final ObjectProvider halConfiguration; private final @Qualifier("messageResolver") MessageResolver resolver; - - /** - * @param relProvider - * @param curieProvider - * @param halConfiguration - * @param resolver - */ - public HalMediaTypeConfiguration(LinkRelationProvider relProvider, ObjectProvider curieProvider, - ObjectProvider halConfiguration, MessageResolver resolver) { - - this.relProvider = relProvider; - this.curieProvider = curieProvider; - this.halConfiguration = halConfiguration; - this.resolver = resolver; - } + private final AutowireCapableBeanFactory beanFactory; @Bean LinkDiscoverer halLinkDisocoverer() { @@ -85,7 +75,7 @@ public class HalMediaTypeConfiguration implements HypermediaMappingInformation { mapper.registerModule(new Jackson2HalModule()); mapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, curieProvider.getIfAvailable(() -> CurieProvider.NONE), resolver, - halConfiguration.getIfAvailable(HalConfiguration::new))); + halConfiguration.getIfAvailable(HalConfiguration::new), beanFactory)); return mapper; } diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/Jackson2HalModule.java b/src/main/java/org/springframework/hateoas/mediatype/hal/Jackson2HalModule.java index 4ebcd490..b914bfa1 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/Jackson2HalModule.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/Jackson2HalModule.java @@ -52,7 +52,6 @@ import com.fasterxml.jackson.annotation.JsonUnwrapped; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonStreamContext; import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.core.Version; import com.fasterxml.jackson.databind.*; @@ -809,33 +808,17 @@ public class Jackson2HalModule extends SimpleModule { */ public HalHandlerInstantiator(LinkRelationProvider provider, CurieProvider curieProvider, MessageResolver resolver, HalConfiguration halConfiguration) { - this(provider, curieProvider, resolver, true, halConfiguration); + this(provider, curieProvider, resolver, halConfiguration, null); } - /** - * Creates a new {@link HalHandlerInstantiator} using the given {@link LinkRelationProvider}, {@link CurieProvider} - * and {@link MessageResolver} and whether to enforce embedded collections. Registers a prepared - * {@link HalResourcesSerializer} and {@link HalLinkListSerializer} falling back to instantiation expecting a - * default constructor. - * - * @param provider must not be {@literal null}. - * @param curieProvider can be {@literal null} - * @param resolver must not be {@literal null}.. - * @param enforceEmbeddedCollections - */ public HalHandlerInstantiator(LinkRelationProvider provider, CurieProvider curieProvider, MessageResolver resolver, - boolean enforceEmbeddedCollections, HalConfiguration halConfiguration) { - this(provider, curieProvider, resolver, enforceEmbeddedCollections, null, halConfiguration); - } - - private HalHandlerInstantiator(LinkRelationProvider provider, CurieProvider curieProvider, MessageResolver resolver, - boolean enforceEmbeddedCollections, @Nullable AutowireCapableBeanFactory delegate, - HalConfiguration halConfiguration) { + HalConfiguration halConfiguration, @Nullable AutowireCapableBeanFactory delegate) { Assert.notNull(provider, "RelProvider must not be null!"); Assert.notNull(curieProvider, "CurieProvider must not be null!"); - EmbeddedMapper mapper = new EmbeddedMapper(provider, curieProvider, enforceEmbeddedCollections); + EmbeddedMapper mapper = new EmbeddedMapper(provider, curieProvider, + halConfiguration.isEnforceEmbeddedCollections()); this.delegate = delegate; diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsMediaTypeConfiguration.java b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsMediaTypeConfiguration.java index 7aa41bc6..a5802a0d 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsMediaTypeConfiguration.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsMediaTypeConfiguration.java @@ -20,6 +20,7 @@ import lombok.RequiredArgsConstructor; import java.util.List; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.hateoas.client.LinkDiscoverer; @@ -49,6 +50,7 @@ class HalFormsMediaTypeConfiguration implements HypermediaMappingInformation { private final ObjectProvider halFormsConfiguration; private final ObjectProvider halConfiguration; private final MessageResolver resolver; + private final AbstractAutowireCapableBeanFactory beanFactory; @Bean LinkDiscoverer halFormsLinkDiscoverer() { @@ -77,7 +79,7 @@ class HalFormsMediaTypeConfiguration implements HypermediaMappingInformation { mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); mapper.registerModule(new Jackson2HalFormsModule()); mapper.setHandlerInstantiator(new Jackson2HalFormsModule.HalFormsHandlerInstantiator(relProvider, - curieProvider.getIfAvailable(() -> CurieProvider.NONE), resolver, true, configuration)); + curieProvider.getIfAvailable(() -> CurieProvider.NONE), resolver, configuration, beanFactory)); return mapper; } diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsModule.java b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsModule.java index 848c93e9..c860e403 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsModule.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsModule.java @@ -164,13 +164,14 @@ public class Jackson2HalFormsModule extends SimpleModule { private final Map, Object> serializers = new HashMap<>(); public HalFormsHandlerInstantiator(LinkRelationProvider resolver, CurieProvider curieProvider, - MessageResolver accessor, boolean enforceEmbeddedCollections, HalFormsConfiguration configuration) { + MessageResolver accessor, HalFormsConfiguration configuration, AutowireCapableBeanFactory beanFactory) { - super(resolver, curieProvider, accessor, enforceEmbeddedCollections, configuration.getHalConfiguration()); + super(resolver, curieProvider, accessor, configuration.getHalConfiguration(), beanFactory); - EmbeddedMapper mapper = new EmbeddedMapper(resolver, curieProvider, enforceEmbeddedCollections); - HalFormsTemplateBuilder builder = new HalFormsTemplateBuilder(configuration, accessor); HalConfiguration halConfiguration = configuration.getHalConfiguration(); + EmbeddedMapper mapper = new EmbeddedMapper(resolver, curieProvider, + halConfiguration.isEnforceEmbeddedCollections()); + HalFormsTemplateBuilder builder = new HalFormsTemplateBuilder(configuration, accessor); this.serializers.put(HalFormsRepresentationModelSerializer.class, new HalFormsRepresentationModelSerializer(builder)); @@ -182,10 +183,9 @@ public class Jackson2HalFormsModule extends SimpleModule { } public HalFormsHandlerInstantiator(LinkRelationProvider relProvider, CurieProvider curieProvider, - MessageResolver resolver, boolean enforceEmbeddedCollections, AutowireCapableBeanFactory beanFactory) { + MessageResolver resolver, AutowireCapableBeanFactory beanFactory) { - this(relProvider, curieProvider, resolver, enforceEmbeddedCollections, - beanFactory.getBean(HalFormsConfiguration.class)); + this(relProvider, curieProvider, resolver, beanFactory.getBean(HalFormsConfiguration.class), beanFactory); } @Nullable diff --git a/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsMessageConverterUnitTest.java b/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsMessageConverterUnitTest.java index 5011b8da..e5d10ad1 100644 --- a/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsMessageConverterUnitTest.java +++ b/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsMessageConverterUnitTest.java @@ -24,6 +24,7 @@ import java.io.OutputStream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.core.io.ClassPathResource; import org.springframework.hateoas.Link; import org.springframework.hateoas.MediaTypes; @@ -56,7 +57,7 @@ class HalFormsMessageConverterUnitTest { this.mapper.registerModule(new Jackson2HalFormsModule()); this.mapper.setHandlerInstantiator( new Jackson2HalFormsModule.HalFormsHandlerInstantiator(new AnnotationLinkRelationProvider(), CurieProvider.NONE, - MessageResolver.DEFAULTS_ONLY, true, new HalFormsConfiguration())); + MessageResolver.DEFAULTS_ONLY, new HalFormsConfiguration(), new DefaultListableBeanFactory())); TypeConstrainedMappingJackson2HttpMessageConverter converter = new TypeConstrainedMappingJackson2HttpMessageConverter( RepresentationModel.class); diff --git a/src/test/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsIntegrationTest.java b/src/test/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsIntegrationTest.java index b3be199c..d1a6493c 100644 --- a/src/test/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/mediatype/hal/forms/Jackson2HalFormsIntegrationTest.java @@ -35,6 +35,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.MessageSource; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.context.support.StaticMessageSource; @@ -90,7 +91,8 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra mapper.registerModule(new Jackson2HalFormsModule()); mapper.setHandlerInstantiator(new HalFormsHandlerInstantiator( // - provider, CurieProvider.NONE, MessageResolver.DEFAULTS_ONLY, true, new HalFormsConfiguration())); + provider, CurieProvider.NONE, MessageResolver.DEFAULTS_ONLY, new HalFormsConfiguration(), + new DefaultListableBeanFactory())); mapper.configure(SerializationFeature.INDENT_OUTPUT, true); } @@ -595,7 +597,7 @@ class Jackson2HalFormsIntegrationTest extends AbstractJackson2MarshallingIntegra mapper.registerModule(new Jackson2HalFormsModule()); mapper.setHandlerInstantiator(new HalFormsHandlerInstantiator(new AnnotationLinkRelationProvider(), provider, - MessageResolver.of(messageSource), true, new HalFormsConfiguration())); + MessageResolver.of(messageSource), new HalFormsConfiguration(), new DefaultListableBeanFactory())); mapper.configure(SerializationFeature.INDENT_OUTPUT, true); mapper.setSerializationInclusion(Include.NON_NULL);