#1158 - Register custom serializers to handle Map based EntityModel instances.

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.
This commit is contained in:
Oliver Drotbohm
2019-12-01 21:47:44 +01:00
parent eab124718b
commit 3b18df355f
2 changed files with 98 additions and 4 deletions

View File

@@ -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<EntityModel> {
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<MapModel> {
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<NonMapModel> {
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.
*

View File

@@ -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<String, Object> 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);