diff --git a/pom.xml b/pom.xml index 16a8956d9..27193cbff 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.hateoas spring-hateoas - 1.2.0-SNAPSHOT + 1.2.0-HATEOAS-1382-SNAPSHOT Spring HATEOAS https://github.com/spring-projects/spring-hateoas 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 a30ae94ae..47b72dc98 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/HalConfiguration.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/HalConfiguration.java @@ -18,6 +18,7 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; +import java.util.function.Consumer; import org.springframework.hateoas.Link; import org.springframework.hateoas.LinkRelation; @@ -25,6 +26,8 @@ import org.springframework.util.Assert; import org.springframework.util.PathMatcher; +import com.fasterxml.jackson.databind.ObjectMapper; + /** * HAL specific configuration. * @@ -41,6 +44,7 @@ public class HalConfiguration { */ private final RenderSingleLinks renderSingleLinks; private final Map singleLinksPerPattern; + private final Consumer objectMapperCustomizer; /** * Configures whether the Jackson property naming strategy is applied to link relations and within {@code _embedded} @@ -63,15 +67,18 @@ public HalConfiguration() { this.singleLinksPerPattern = new LinkedHashMap<>(); this.applyPropertyNamingStrategy = true; this.enforceEmbeddedCollections = true; + this.objectMapperCustomizer = objectMapper -> {}; // Default to no action. } private HalConfiguration(RenderSingleLinks renderSingleLinks, Map singleLinksPerPattern, - boolean applyPropertyNamingStrategy, boolean enforceEmbeddedCollections) { + boolean applyPropertyNamingStrategy, boolean enforceEmbeddedCollections, + Consumer objectMapperCustomizer) { this.renderSingleLinks = renderSingleLinks; this.singleLinksPerPattern = singleLinksPerPattern; this.applyPropertyNamingStrategy = applyPropertyNamingStrategy; this.enforceEmbeddedCollections = enforceEmbeddedCollections; + this.objectMapperCustomizer = objectMapperCustomizer; } /** @@ -132,7 +139,7 @@ public HalConfiguration withRenderSingleLinks(RenderSingleLinks renderSingleLink return this.renderSingleLinks == renderSingleLinks ? this : new HalConfiguration(renderSingleLinks, this.singleLinksPerPattern, this.applyPropertyNamingStrategy, - this.enforceEmbeddedCollections); + this.enforceEmbeddedCollections, this.objectMapperCustomizer); } /** @@ -145,7 +152,7 @@ private HalConfiguration withSingleLinksPerPattern(Map objectMapperCustomizer) { + + return this.objectMapperCustomizer == objectMapperCustomizer ? this + : new HalConfiguration(this.renderSingleLinks, this.singleLinksPerPattern, this.applyPropertyNamingStrategy, + this.enforceEmbeddedCollections, objectMapperCustomizer); } public RenderSingleLinks getRenderSingleLinks() { @@ -188,6 +202,10 @@ public boolean isEnforceEmbeddedCollections() { return this.enforceEmbeddedCollections; } + public Consumer getObjectMapperCustomizer() { + return this.objectMapperCustomizer; + } + /** * Configuration option how to render single links of a given {@link LinkRelation}. * 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 f2228e2a8..be9cd853f 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/HalMediaTypeConfiguration.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/HalMediaTypeConfiguration.java @@ -79,11 +79,14 @@ public List getMediaTypes() { @Override public ObjectMapper configureObjectMapper(ObjectMapper mapper) { + HalConfiguration halConfiguration = this.halConfiguration.getIfAvailable(HalConfiguration::new); + mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); mapper.registerModule(new Jackson2HalModule()); mapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, - curieProvider.getIfAvailable(() -> CurieProvider.NONE), resolver, - halConfiguration.getIfAvailable(HalConfiguration::new), beanFactory)); + curieProvider.getIfAvailable(() -> CurieProvider.NONE), resolver, halConfiguration, beanFactory)); + + halConfiguration.getObjectMapperCustomizer().accept(mapper); return mapper; } 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 f0e0b5610..9c50741f8 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 @@ -82,6 +82,8 @@ public ObjectMapper configureObjectMapper(ObjectMapper mapper) { mapper.setHandlerInstantiator(new Jackson2HalFormsModule.HalFormsHandlerInstantiator(relProvider, curieProvider.getIfAvailable(() -> CurieProvider.NONE), resolver, configuration, beanFactory)); + configuration.getHalConfiguration().getObjectMapperCustomizer().accept(mapper); + return mapper; } diff --git a/src/test/java/org/springframework/hateoas/mediatype/hal/HalObjectMapperCustomizerTest.java b/src/test/java/org/springframework/hateoas/mediatype/hal/HalObjectMapperCustomizerTest.java new file mode 100644 index 000000000..1aaa4ebc3 --- /dev/null +++ b/src/test/java/org/springframework/hateoas/mediatype/hal/HalObjectMapperCustomizerTest.java @@ -0,0 +1,68 @@ +package org.springframework.hateoas.mediatype.hal; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*; + +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.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.hateoas.MappingTestUtils; +import org.springframework.hateoas.config.EnableHypermediaSupport; +import org.springframework.hateoas.support.WebMvcEmployeeController; +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.web.servlet.MockMvc; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import com.fasterxml.jackson.databind.SerializationFeature; + +/** + * @author Greg Turnquist + */ +@ExtendWith(SpringExtension.class) +@WebAppConfiguration +@ContextConfiguration +public class HalObjectMapperCustomizerTest { + + @Autowired WebApplicationContext context; + + MockMvc mockMvc; + + MappingTestUtils.ContextualMapper mapper = MappingTestUtils.createMapper(getClass()); + + @BeforeEach + void setUp() { + + this.mockMvc = webAppContextSetup(this.context).build(); + WebMvcEmployeeController.reset(); + } + + @Test // #1382 + void objectMapperCustomizerShouldBeApplied() throws Exception { + + String actualHalJson = this.mockMvc.perform(get("/employees/0")).andReturn().getResponse().getContentAsString(); + String expectedHalJson = this.mapper.readFile("hal-custom.json"); + + assertThat(actualHalJson).isEqualTo(expectedHalJson); + } + + @Configuration + @EnableWebMvc + @EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL) + @Import(WebMvcEmployeeController.class) + static class TestConfig { + + @Bean + HalConfiguration halConfiguration() { + return new HalConfiguration() + .withObjectMapperCustomizer(objectMapper -> objectMapper.configure(SerializationFeature.INDENT_OUTPUT, true)); + } + } +} diff --git a/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsObjectMapperCustomizerTest.java b/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsObjectMapperCustomizerTest.java new file mode 100644 index 000000000..fe6aca1e0 --- /dev/null +++ b/src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsObjectMapperCustomizerTest.java @@ -0,0 +1,70 @@ +package org.springframework.hateoas.mediatype.hal.forms; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*; + +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.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.hateoas.MappingTestUtils; +import org.springframework.hateoas.config.EnableHypermediaSupport; +import org.springframework.hateoas.mediatype.hal.HalConfiguration; +import org.springframework.hateoas.support.WebMvcEmployeeController; +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.web.servlet.MockMvc; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import com.fasterxml.jackson.databind.SerializationFeature; + +/** + * @author Greg Turnquist + */ +@ExtendWith(SpringExtension.class) +@WebAppConfiguration +@ContextConfiguration +public class HalFormsObjectMapperCustomizerTest { + + @Autowired WebApplicationContext context; + + MockMvc mockMvc; + + MappingTestUtils.ContextualMapper mapper = MappingTestUtils.createMapper(getClass()); + + @BeforeEach + void setUp() { + + this.mockMvc = webAppContextSetup(this.context).build(); + WebMvcEmployeeController.reset(); + } + + @Test // #1382 + void objectMapperCustomizerShouldBeApplied() throws Exception { + + String actualHalFormsJson = this.mockMvc.perform(get("/employees/0")).andReturn().getResponse() + .getContentAsString(); + String expectedHalFormsJson = this.mapper.readFile("hal-forms-custom.json"); + + assertThat(actualHalFormsJson).isEqualTo(expectedHalFormsJson); + } + + @Configuration + @EnableWebMvc + @EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL_FORMS) + @Import(WebMvcEmployeeController.class) + static class TestConfig { + + @Bean + HalConfiguration halConfiguration() { + return new HalConfiguration() + .withObjectMapperCustomizer(objectMapper -> objectMapper.configure(SerializationFeature.INDENT_OUTPUT, true)); + } + } +} diff --git a/src/test/resources/org/springframework/hateoas/mediatype/hal/forms/hal-forms-custom.json b/src/test/resources/org/springframework/hateoas/mediatype/hal/forms/hal-forms-custom.json new file mode 100644 index 000000000..2a107b262 --- /dev/null +++ b/src/test/resources/org/springframework/hateoas/mediatype/hal/forms/hal-forms-custom.json @@ -0,0 +1,31 @@ +{ + "name" : "Frodo Baggins", + "role" : "ring bearer", + "_links" : { + "self" : { + "href" : "http://localhost/employees/0" + }, + "employees" : { + "href" : "http://localhost/employees" + } + }, + "_templates" : { + "default" : { + "method" : "put", + "properties" : [ { + "name" : "name", + "required" : true + }, { + "name" : "role" + } ] + }, + "partiallyUpdateEmployee" : { + "method" : "patch", + "properties" : [ { + "name" : "name" + }, { + "name" : "role" + } ] + } + } +} \ No newline at end of file diff --git a/src/test/resources/org/springframework/hateoas/mediatype/hal/hal-custom.json b/src/test/resources/org/springframework/hateoas/mediatype/hal/hal-custom.json new file mode 100644 index 000000000..7dc8251d9 --- /dev/null +++ b/src/test/resources/org/springframework/hateoas/mediatype/hal/hal-custom.json @@ -0,0 +1,12 @@ +{ + "name" : "Frodo Baggins", + "role" : "ring bearer", + "_links" : { + "self" : { + "href" : "http://localhost/employees/0" + }, + "employees" : { + "href" : "http://localhost/employees" + } + } +} \ No newline at end of file