From 3b18df355f7647ea2b98c3b79c403b1f08181d65 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Sun, 1 Dec 2019 21:47:44 +0100 Subject: [PATCH] #1158 - Register custom serializers to handle Map based EntityModel instances. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We now register a custom serializer to massage EntityModel instances into dedicated types that – in case of a Map being the content of the entity model – wrap the model into a type that applies the necessary Jackson tweaks to properly unwrap a Map. --- .../mediatype/hal/Jackson2HalModule.java | 74 +++++++++++++++++++ .../hal/Jackson2HalIntegrationTest.java | 28 ++++++- 2 files changed, 98 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/Jackson2HalModule.java b/src/main/java/org/springframework/hateoas/mediatype/hal/Jackson2HalModule.java index 49fdc21d..7300b346 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/Jackson2HalModule.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/Jackson2HalModule.java @@ -40,12 +40,14 @@ import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonUnwrapped; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonStreamContext; import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.core.Version; import com.fasterxml.jackson.databind.*; @@ -63,6 +65,7 @@ import com.fasterxml.jackson.databind.ser.ContainerSerializer; import com.fasterxml.jackson.databind.ser.ContextualSerializer; import com.fasterxml.jackson.databind.ser.std.MapSerializer; import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; import com.fasterxml.jackson.databind.type.TypeFactory; /** @@ -85,6 +88,8 @@ public class Jackson2HalModule extends SimpleModule { setMixInAnnotation(Link.class, LinkMixin.class); setMixInAnnotation(RepresentationModel.class, RepresentationModelMixin.class); setMixInAnnotation(CollectionModel.class, CollectionModelMixin.class); + + addSerializer(EntityModel.class, new EntityModelSerializer()); } /** @@ -99,6 +104,75 @@ public class Jackson2HalModule extends SimpleModule { return LinkMixin.class.equals(mapper.findMixInClassFor(Link.class)); } + /** + * Custom serializer for {@link EntityModel} to make sure we get {@link Map} instances properly unwrapped. The + * serializer wraps the model instance into a dedicated classes that apply the proper Jackson configuration that's + * needed to achieve this and serializes those. + * + * @author Oliver Drotbohm + * @see https://github.com/FasterXML/jackson-databind/issues/171 + */ + @SuppressWarnings("rawtypes") + static class EntityModelSerializer extends StdSerializer { + + private static final long serialVersionUID = -5933309398043585183L; + + public EntityModelSerializer() { + super(EntityModel.class, false); + } + + /* + * (non-Javadoc) + * @see com.fasterxml.jackson.databind.ser.std.StdSerializer#serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) + */ + @Override + @SuppressWarnings("null") + public void serialize(EntityModel value, JsonGenerator gen, SerializerProvider provider) throws IOException { + + Object content = value.getContent(); + + RepresentationModel wrapped = Map.class.isInstance(content) // + ? new MapModel(value) // + : new NonMapModel(value); + + provider.defaultSerializeValue(wrapped, gen); + } + + static class MapModel extends RepresentationModel { + + private final @Nullable Map map; + + public MapModel(EntityModel model) { + + super(model.getLinks().toList()); + + this.map = (Map) model.getContent(); + } + + @Nullable + @JsonAnyGetter + public Map getContent() { + return map; + } + } + + static class NonMapModel extends RepresentationModel { + + private final EntityModel model; + + public NonMapModel(EntityModel model) { + super(model.getLinks().toList()); + this.model = model; + } + + @Nullable + @JsonUnwrapped + public Object getContent() { + return model.getContent(); + } + } + } + /** * Custom {@link JsonSerializer} to render Link instances in HAL compatible JSON. * diff --git a/src/test/java/org/springframework/hateoas/mediatype/hal/Jackson2HalIntegrationTest.java b/src/test/java/org/springframework/hateoas/mediatype/hal/Jackson2HalIntegrationTest.java index 6951b961..cdcae4a2 100755 --- a/src/test/java/org/springframework/hateoas/mediatype/hal/Jackson2HalIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/mediatype/hal/Jackson2HalIntegrationTest.java @@ -22,8 +22,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -43,6 +45,8 @@ import org.springframework.hateoas.server.core.EmbeddedWrappers; import org.springframework.lang.Nullable; import com.fasterxml.jackson.databind.ObjectMapper; +import com.jayway.jsonpath.DocumentContext; +import com.jayway.jsonpath.JsonPath; /** * Integration tests for Jackson 2 HAL integration. @@ -88,8 +92,8 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg DefaultLinkRelationProvider.INSTANCE); mapper.registerModule(new Jackson2HalModule()); - mapper.setHandlerInstantiator( - new HalHandlerInstantiator(provider, CurieProvider.NONE, MessageResolver.DEFAULTS_ONLY, new HalConfiguration())); + mapper.setHandlerInstantiator(new HalHandlerInstantiator(provider, CurieProvider.NONE, + MessageResolver.DEFAULTS_ONLY, new HalConfiguration())); } /** @@ -480,8 +484,9 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg AnnotationLinkRelationProvider provider = new AnnotationLinkRelationProvider(); - mapper.setHandlerInstantiator(new HalHandlerInstantiator(provider, CurieProvider.NONE, MessageResolver.DEFAULTS_ONLY, - new HalConfiguration().withRenderSingleLinksFor("foo", RenderSingleLinks.AS_ARRAY))); + mapper + .setHandlerInstantiator(new HalHandlerInstantiator(provider, CurieProvider.NONE, MessageResolver.DEFAULTS_ONLY, + new HalConfiguration().withRenderSingleLinksFor("foo", RenderSingleLinks.AS_ARRAY))); RepresentationModel resource = new RepresentationModel<>(); resource.add(new Link("/some-href", "foo")); @@ -511,6 +516,21 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg }).doesNotThrowAnyException(); } + @Test // #1157 + void rendersMapContentCorrectly() throws Exception { + + Map map = new HashMap<>(); + map.put("key", "value"); + map.put("anotherKey", "anotherValue"); + + EntityModel model = new EntityModel<>(map, new Link("foo", IanaLinkRelations.SELF)); + + DocumentContext context = JsonPath.parse(mapper.writeValueAsString(model)); + + assertThat(context.read("$.key", String.class)).isEqualTo("value"); + assertThat(context.read("$.anotherKey", String.class)).isEqualTo("anotherValue"); + } + private void verifyResolvedTitle(String resourceBundleKey) throws Exception { LocaleContextHolder.setLocale(Locale.US);