#1354 - Change in implementation strategy for Maps in EntityModel.

We now use a custom serializer for EntityModel.getContent() as we need that to still return the Map content for programmatic clients. The serializer skips maps entirely and programmatically unwraps all other content.

A simple Converter (@JsonSerialize(convert = …)) to filter Maps unfortunately didn't do the trick as that causes a a delegating serializer to be registered for the property and @JsonUnwrapped stops working as it's designed to work with bean serializers only.

Adapted test cases accordingly.

Original pull request: #1353.
This commit is contained in:
Oliver Drotbohm
2020-08-11 23:27:53 +02:00
parent 657406034f
commit f3b9738b1e
2 changed files with 49 additions and 26 deletions

View File

@@ -15,20 +15,24 @@
*/
package org.springframework.hateoas;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.fasterxml.jackson.databind.util.NameTransformer;
/**
* A simple {@link EntityModel} wrapping a domain object and adding links to it.
@@ -117,10 +121,11 @@ public class EntityModel<T> extends RepresentationModel<EntityModel<T>> {
*
* @return the content
*/
@JsonUnwrapped
@Nullable
@JsonUnwrapped
@JsonSerialize(using = MapSuppressingUnwrappingSerializer.class)
public T getContent() {
return !Map.class.isInstance(content) ? content : null;
return content;
}
// Hacks to allow deserialization into an EntityModel<Map<String, Object>>
@@ -128,7 +133,7 @@ public class EntityModel<T> extends RepresentationModel<EntityModel<T>> {
@Nullable
@JsonAnyGetter
@SuppressWarnings("unchecked")
public Map<String, Object> getMapContent() {
private Map<String, Object> getMapContent() {
return Map.class.isInstance(content) ? (Map<String, Object>) content : null;
}
@@ -190,4 +195,36 @@ public class EntityModel<T> extends RepresentationModel<EntityModel<T>> {
result += content == null ? 0 : 17 * content.hashCode();
return result;
}
private static class MapSuppressingUnwrappingSerializer extends StdSerializer<Object> {
public MapSuppressingUnwrappingSerializer() {
super(Object.class);
}
/*
* (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
public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
if (value == null || Map.class.isInstance(value)) {
return;
}
provider.findValueSerializer(value.getClass()) //
.unwrappingSerializer(NameTransformer.NOP) //
.serialize(value, gen, provider);
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.JsonSerializer#isUnwrappingSerializer()
*/
@Override
public boolean isUnwrappingSerializer() {
return true;
}
}
}

View File

@@ -26,7 +26,6 @@ import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
@@ -60,8 +59,10 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.Option;
/**
* Integration tests for Jackson 2 HAL integration.
@@ -565,7 +566,7 @@ class Jackson2HalIntegrationTest {
.forEach(it -> assertThat(it).containsKey("someSample"));
}
@Test // #1157
@Test // #1157, #1352
void rendersMapContentCorrectly() throws Exception {
Map<String, Object> map = new HashMap<>();
@@ -574,10 +575,12 @@ class Jackson2HalIntegrationTest {
EntityModel<?> model = EntityModel.of(map, Link.of("foo", IanaLinkRelations.SELF));
DocumentContext context = JsonPath.parse(mapper.writeValueAsString(model));
DocumentContext context = JsonPath.parse(mapper.writeValueAsString(model),
Configuration.defaultConfiguration().addOptions(Option.SUPPRESS_EXCEPTIONS));
assertThat(context.read("$.key", String.class)).isEqualTo("value");
assertThat(context.read("$.anotherKey", String.class)).isEqualTo("anotherValue");
assertThat(context.read("$.content", Object.class)).isNull();
}
@Test // #1157
@@ -592,8 +595,7 @@ class Jackson2HalIntegrationTest {
EntityModel<Map<String, Object>> result = mapper.readValue(source, modelType);
assertThat(result.getContent()).isNull();
assertThat(result.getMapContent()).containsEntry("key", "value");
assertThat(result.getContent()).containsEntry("key", "value");
}
@Test // #1157
@@ -608,22 +610,6 @@ class Jackson2HalIntegrationTest {
assertThat(result.getContent().name).isEqualTo("Dave");
}
@Test // #1352
void rendersMapWithoutDuplicateEntries() throws JsonProcessingException {
Map<String, String> map = new TreeMap<>();
map.put("key", "value");
map.put("key2", "value2");
EntityModel<Map<String, String>> entityModel = EntityModel.of(map);
entityModel.add(Link.of("http://example.com"));
String serialized = mapper.writeValueAsString(entityModel);
assertThat(serialized)
.isEqualTo("{\"_links\":{\"self\":{\"href\":\"http://example.com\"}},\"key\":\"value\",\"key2\":\"value2\"}");
}
@Relation(collectionRelation = "someSample")
static class SomeSample {
@JsonProperty String name;