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; + } + } }