From 70586c9f22e108d772db81f79caa3523985e24fc Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Thu, 27 Jun 2019 15:46:50 +0200 Subject: [PATCH] #832 - Tweaked setup of HalFormsConfiguration and HalConfiguration. HalFormsConfiguration now delegates to a HalConfiguration and the configuration will favor a user registered instance of the former over an instance of the latter or even fall back to default instances in case none of that is defined in the ApplicationContext. This allows seamless pickup of HalConfiguration for HAL FORMS but also explicit opt-out if not desired. --- .../hal/forms/HalFormsConfiguration.java | 48 +++---------- .../forms/HalFormsMediaTypeConfiguration.java | 9 ++- .../hal/forms/Jackson2HalFormsModule.java | 11 +-- .../forms/HalFormsWebMvcIntegrationTest.java | 68 +++++++++++++++++++ 4 files changed, 92 insertions(+), 44 deletions(-) diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsConfiguration.java b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsConfiguration.java index 818369b4..195f3ecc 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsConfiguration.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 the original author or authors. + * Copyright 2017-2019 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. @@ -15,52 +15,26 @@ */ package org.springframework.hateoas.mediatype.hal.forms; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.experimental.Wither; +import lombok.RequiredArgsConstructor; -import org.springframework.hateoas.Link; import org.springframework.hateoas.mediatype.hal.HalConfiguration; /** + * HAL-FORMS specific configuration extension of {@link HalConfiguration}. + * * @author Greg Turnquist + * @author Oliver Drotbohm */ -@NoArgsConstructor -@AllArgsConstructor(access = AccessLevel.PRIVATE) -class HalFormsConfiguration { +@RequiredArgsConstructor +public class HalFormsConfiguration { - private @Wither @Getter RenderSingleLinks renderSingleLinks = RenderSingleLinks.AS_SINGLE; - - 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 - } + private final @Getter HalConfiguration halConfiguration; /** - * Translate a {@link HalFormsConfiguration} into a {@link HalConfiguration}. - * - * @return + * Creates a new {@link HalFormsConfiguration} backed by a default {@link HalConfiguration}. */ - public HalConfiguration toHalConfiguration() { - - if (this.getRenderSingleLinks() == RenderSingleLinks.AS_SINGLE) { - return new HalConfiguration().withRenderSingleLinks(HalConfiguration.RenderSingleLinks.AS_SINGLE); - } - - if (this.getRenderSingleLinks() == RenderSingleLinks.AS_ARRAY) { - return new HalConfiguration().withRenderSingleLinks(HalConfiguration.RenderSingleLinks.AS_ARRAY); - } - - throw new IllegalStateException("Don't know how to translate " + this); + public HalFormsConfiguration() { + this.halConfiguration = new HalConfiguration(); } } 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 7247fad2..dc468a28 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 @@ -27,6 +27,7 @@ import org.springframework.hateoas.client.LinkDiscoverer; import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; import org.springframework.hateoas.config.HypermediaMappingInformation; import org.springframework.hateoas.mediatype.hal.CurieProvider; +import org.springframework.hateoas.mediatype.hal.HalConfiguration; import org.springframework.hateoas.server.core.DelegatingLinkRelationProvider; import org.springframework.http.MediaType; @@ -46,6 +47,7 @@ class HalFormsMediaTypeConfiguration implements HypermediaMappingInformation { private final DelegatingLinkRelationProvider relProvider; private final ObjectProvider curieProvider; private final ObjectProvider halFormsConfiguration; + private final ObjectProvider halConfiguration; private final MessageSourceAccessor messageSourceAccessor; @Bean @@ -69,13 +71,14 @@ class HalFormsMediaTypeConfiguration implements HypermediaMappingInformation { @Override public ObjectMapper configureObjectMapper(ObjectMapper mapper) { + HalFormsConfiguration configuration = halFormsConfiguration + .getIfAvailable(() -> new HalFormsConfiguration(halConfiguration.getIfAvailable(HalConfiguration::new))); + mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); mapper.registerModule(new Jackson2HalFormsModule()); mapper.setHandlerInstantiator(new Jackson2HalFormsModule.HalFormsHandlerInstantiator(relProvider, - curieProvider.getIfAvailable(() -> CurieProvider.NONE), messageSourceAccessor, true, - halFormsConfiguration.getIfAvailable(HalFormsConfiguration::new))); + curieProvider.getIfAvailable(() -> CurieProvider.NONE), messageSourceAccessor, true, configuration)); 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 f2cb3709..fe24e41c 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 @@ -82,6 +82,7 @@ class Jackson2HalFormsModule extends SimpleModule { super("hal-forms-module", new Version(1, 0, 0, null, "org.springframework.hateoas", "spring-hateoas")); setMixInAnnotation(Link.class, LinkMixin.class); + setMixInAnnotation(Links.class, LinksMixin.class); setMixInAnnotation(RepresentationModel.class, RepresentationModelMixin.class); setMixInAnnotation(EntityModel.class, EntityModelMixin.class); setMixInAnnotation(CollectionModel.class, CollectionModelMixin.class); @@ -89,6 +90,9 @@ class Jackson2HalFormsModule extends SimpleModule { setMixInAnnotation(MediaType.class, MediaTypeMixin.class); } + @JsonSerialize(using = HalLinkListSerializer.class) + abstract class LinksMixin {} + @JsonSerialize(using = HalFormsResourceSerializer.class) abstract class EntityModelMixin extends EntityModel {} @@ -156,17 +160,16 @@ class Jackson2HalFormsModule extends SimpleModule { private final Map, Object> serializers = new HashMap<>(); public HalFormsHandlerInstantiator(LinkRelationProvider resolver, CurieProvider curieProvider, - MessageSourceAccessor accessor, boolean enforceEmbeddedCollections, - HalFormsConfiguration halFormsConfiguration) { + MessageSourceAccessor accessor, boolean enforceEmbeddedCollections, HalFormsConfiguration configuration) { - super(resolver, curieProvider, accessor, enforceEmbeddedCollections, halFormsConfiguration.toHalConfiguration()); + super(resolver, curieProvider, accessor, enforceEmbeddedCollections, configuration.getHalConfiguration()); EmbeddedMapper mapper = new EmbeddedMapper(resolver, curieProvider, enforceEmbeddedCollections); this.serializers.put(HalFormsResourceSerializer.class, new HalFormsResourceSerializer(accessor)); this.serializers.put(HalFormsResourcesSerializer.class, new HalFormsResourcesSerializer(accessor, mapper)); this.serializers.put(HalLinkListSerializer.class, - new HalLinkListSerializer(curieProvider, mapper, accessor, halFormsConfiguration.toHalConfiguration())); + new HalLinkListSerializer(curieProvider, mapper, accessor, configuration.getHalConfiguration())); } public HalFormsHandlerInstantiator(LinkRelationProvider relProvider, CurieProvider curieProvider, diff --git a/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsWebMvcIntegrationTest.java b/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsWebMvcIntegrationTest.java index 3f2b2bd7..d0769050 100644 --- a/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsWebMvcIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsWebMvcIntegrationTest.java @@ -15,8 +15,10 @@ */ package org.springframework.hateoas.mediatype.hal.forms; +import static org.assertj.core.api.Assertions.*; import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.collection.IsCollectionWithSize.*; +import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*; @@ -25,22 +27,30 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; +import org.springframework.hateoas.Links; import org.springframework.hateoas.MediaTypes; import org.springframework.hateoas.config.EnableHypermediaSupport; import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; +import org.springframework.hateoas.mediatype.hal.HalConfiguration; +import org.springframework.hateoas.mediatype.hal.Jackson2HalModule.HalLinkListSerializer; import org.springframework.hateoas.support.MappingUtils; import org.springframework.hateoas.support.WebMvcEmployeeController; import org.springframework.http.HttpHeaders; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.servlet.MockMvc; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; + /** * @author Greg Turnquist */ @@ -120,6 +130,36 @@ class HalFormsWebMvcIntegrationTest { .andExpect(header().stringValues(HttpHeaders.LOCATION, "http://localhost/employees/2")); } + @Test // #832 + public void usesRegisteredHalFormsConfiguration() { + assertInstanceUsed(WithHalFormsConfiguration.class, WithHalFormsConfiguration.CONFIG); + } + + @Test // #832 + public void usesRegisteredHalConfiguration() { + assertInstanceUsed(WithHalConfiguration.class, WithHalConfiguration.CONFIG); + } + + private static void assertInstanceUsed(Class configurationClass, HalConfiguration configuration) { + + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(configurationClass)) { + + HalFormsMediaTypeConfiguration mediaTypeConfiguration = context.getBean(HalFormsMediaTypeConfiguration.class); + ObjectMapper mapper = mediaTypeConfiguration.configureObjectMapper(new ObjectMapper()); + + assertThatCode(() -> { + + JsonSerializer serializer = mapper.getSerializerProviderInstance() // + .findValueSerializer(Links.class); + + assertThat(serializer).isInstanceOfSatisfying(HalLinkListSerializer.class, it -> { + assertThat(ReflectionTestUtils.getField(serializer, "halConfiguration")).isSameAs(configuration); + }); + + }).doesNotThrowAnyException(); + } + } + @Configuration @EnableWebMvc @EnableHypermediaSupport(type = { HypermediaType.HAL_FORMS }) @@ -130,4 +170,32 @@ class HalFormsWebMvcIntegrationTest { return new WebMvcEmployeeController(); } } + + @Configuration + @EnableHypermediaSupport(type = HypermediaType.HAL_FORMS) + static class WithHalFormsConfiguration { + + static final HalConfiguration CONFIG = new HalConfiguration(); + + @Bean + public HalFormsConfiguration halFormsConfiguration() { + + HalFormsConfiguration config = mock(HalFormsConfiguration.class); + when(config.getHalConfiguration()).thenReturn(CONFIG); + + return config; + } + } + + @Configuration + @EnableHypermediaSupport(type = HypermediaType.HAL_FORMS) + static class WithHalConfiguration { + + static final HalConfiguration CONFIG = new HalConfiguration(); + + @Bean + public HalConfiguration halConfiguration() { + return CONFIG; + } + } }