Skip to content

Add ability to customize ObjectMapper for HAL types. #1383

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.hateoas</groupId>
<artifactId>spring-hateoas</artifactId>
<version>1.2.0-SNAPSHOT</version>
<version>1.2.0-HATEOAS-1382-SNAPSHOT</version>

<name>Spring HATEOAS</name>
<url>https://github.com/spring-projects/spring-hateoas</url>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@
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;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;

import com.fasterxml.jackson.databind.ObjectMapper;

/**
* HAL specific configuration.
*
Expand All @@ -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}
Expand All @@ -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<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;
}

/**
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -145,7 +152,7 @@ private HalConfiguration withSingleLinksPerPattern(Map<String, RenderSingleLinks

return this.singleLinksPerPattern == singleLinksPerPattern ? this
: new HalConfiguration(this.renderSingleLinks, singleLinksPerPattern, this.applyPropertyNamingStrategy,
this.enforceEmbeddedCollections);
this.enforceEmbeddedCollections, this.objectMapperCustomizer);
}

/**
Expand All @@ -159,7 +166,7 @@ public HalConfiguration withApplyPropertyNamingStrategy(boolean applyPropertyNam

return this.applyPropertyNamingStrategy == applyPropertyNamingStrategy ? this
: new HalConfiguration(this.renderSingleLinks, this.singleLinksPerPattern, applyPropertyNamingStrategy,
this.enforceEmbeddedCollections);
this.enforceEmbeddedCollections, this.objectMapperCustomizer);
}

/**
Expand All @@ -173,7 +180,14 @@ public HalConfiguration withEnforceEmbeddedCollections(boolean enforceEmbeddedCo

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() {
Expand All @@ -188,6 +202,10 @@ public boolean isEnforceEmbeddedCollections() {
return this.enforceEmbeddedCollections;
}

public Consumer<ObjectMapper> getObjectMapperCustomizer() {
return this.objectMapperCustomizer;
}

/**
* Configuration option how to render single links of a given {@link LinkRelation}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,14 @@ public List<MediaType> 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
}
}
Original file line number Diff line number Diff line change
@@ -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"
} ]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name" : "Frodo Baggins",
"role" : "ring bearer",
"_links" : {
"self" : {
"href" : "http://localhost/employees/0"
},
"employees" : {
"href" : "http://localhost/employees"
}
}
}