#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:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user