#1382 - Add ability to customize the ObjectMapper for HAL and HAL-FORMS.

Use HalConfiguration to allow applying customizations to the ObjectMapper.

Original pull request: #1383.
This commit is contained in:
Greg L. Turnquist
2020-10-27 12:59:47 -05:00
committed by Oliver Drotbohm
parent 0643624e7c
commit 89fade21c2
7 changed files with 211 additions and 7 deletions

View File

@@ -18,6 +18,7 @@ package org.springframework.hateoas.mediatype.hal;
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.AntPathMatcher;
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<String, RenderSingleLinks> singleLinksPerPattern;
private final Consumer<ObjectMapper> objectMapperCustomizer;
/**
* Configures whether the Jackson property naming strategy is applied to link relations and within {@code _embedded}
@@ -63,15 +67,18 @@ public class HalConfiguration {
this.singleLinksPerPattern = new LinkedHashMap<>();
this.applyPropertyNamingStrategy = true;
this.enforceEmbeddedCollections = true;
this.objectMapperCustomizer = objectMapper -> {}; // Default to no action.
}
private HalConfiguration(RenderSingleLinks renderSingleLinks, Map<String, RenderSingleLinks> singleLinksPerPattern,
boolean applyPropertyNamingStrategy, boolean enforceEmbeddedCollections) {
boolean applyPropertyNamingStrategy, boolean enforceEmbeddedCollections,
Consumer<ObjectMapper> objectMapperCustomizer) {
this.renderSingleLinks = renderSingleLinks;
this.singleLinksPerPattern = singleLinksPerPattern;
this.applyPropertyNamingStrategy = applyPropertyNamingStrategy;
this.enforceEmbeddedCollections = enforceEmbeddedCollections;
this.objectMapperCustomizer = objectMapperCustomizer;
}
/**
@@ -132,7 +139,7 @@ public class HalConfiguration {
return this.renderSingleLinks == renderSingleLinks ? this
: new HalConfiguration(renderSingleLinks, this.singleLinksPerPattern, this.applyPropertyNamingStrategy,
this.enforceEmbeddedCollections);
this.enforceEmbeddedCollections, this.objectMapperCustomizer);
}
/**
@@ -145,7 +152,7 @@ public class HalConfiguration {
return this.singleLinksPerPattern == singleLinksPerPattern ? this
: new HalConfiguration(this.renderSingleLinks, singleLinksPerPattern, this.applyPropertyNamingStrategy,
this.enforceEmbeddedCollections);
this.enforceEmbeddedCollections, this.objectMapperCustomizer);
}
/**
@@ -159,7 +166,7 @@ public class HalConfiguration {
return this.applyPropertyNamingStrategy == applyPropertyNamingStrategy ? this
: new HalConfiguration(this.renderSingleLinks, this.singleLinksPerPattern, applyPropertyNamingStrategy,
this.enforceEmbeddedCollections);
this.enforceEmbeddedCollections, this.objectMapperCustomizer);
}
/**
@@ -173,7 +180,14 @@ public class HalConfiguration {
return this.enforceEmbeddedCollections == enforceEmbeddedCollections ? this
: new HalConfiguration(this.renderSingleLinks, this.singleLinksPerPattern, this.applyPropertyNamingStrategy,
enforceEmbeddedCollections);
enforceEmbeddedCollections, this.objectMapperCustomizer);
}
public HalConfiguration withObjectMapperCustomizer(Consumer<ObjectMapper> 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 class HalConfiguration {
return this.enforceEmbeddedCollections;
}
public Consumer<ObjectMapper> getObjectMapperCustomizer() {
return this.objectMapperCustomizer;
}
/**
* Configuration option how to render single links of a given {@link LinkRelation}.
*

View File

@@ -79,11 +79,14 @@ public class HalMediaTypeConfiguration implements HypermediaMappingInformation {
@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;
}

View File

@@ -82,6 +82,8 @@ class HalFormsMediaTypeConfiguration implements HypermediaMappingInformation {
mapper.setHandlerInstantiator(new Jackson2HalFormsModule.HalFormsHandlerInstantiator(relProvider,
curieProvider.getIfAvailable(() -> CurieProvider.NONE), resolver, configuration, beanFactory));
configuration.getHalConfiguration().getObjectMapperCustomizer().accept(mapper);
return mapper;
}

View File

@@ -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));
}
}
}

View File

@@ -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));
}
}
}

View File

@@ -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"
} ]
}
}
}

View File

@@ -0,0 +1,12 @@
{
"name" : "Frodo Baggins",
"role" : "ring bearer",
"_links" : {
"self" : {
"href" : "http://localhost/employees/0"
},
"employees" : {
"href" : "http://localhost/employees"
}
}
}