From 44420d862cdcd3bfb766ce65326edc0a8cbc29c1 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Wed, 4 Dec 2013 22:13:25 +0100 Subject: [PATCH] DATAREST-199 - Tweaked unmarshalling of JSON payloads. The BeanWrapper used to set and get values of entities now gets a autowired ConversionService applied. Simplified collection and map creation by using Spring API. Added test case to update an entity with an embedded collection. --- .../RepositoryRestMvcConfiguration.java | 2 +- .../json/PersistentEntityJackson2Module.java | 48 +++++++++---------- .../webmvc/AbstractWebIntegrationTests.java | 4 ++ .../data/rest/webmvc/jpa/JpaWebTests.java | 25 ++++++++++ .../webmvc/json/RepositoryTestsConfig.java | 3 +- .../data/rest/webmvc/jpa/order.json | 7 +++ 6 files changed, 62 insertions(+), 27 deletions(-) create mode 100644 spring-data-rest-webmvc/src/test/resources/org/springframework/data/rest/webmvc/jpa/order.json diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java index c850b7007..0b15b6667 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java @@ -353,7 +353,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon */ @Bean public Module persistentEntityJackson2Module() { - return new PersistentEntityJackson2Module(resourceMappings()); + return new PersistentEntityJackson2Module(resourceMappings(), defaultConversionService()); } /** diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java index 3d7827405..4d56cb7ed 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java @@ -6,17 +6,15 @@ import java.io.IOException; import java.net.URI; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.CollectionFactory; +import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.PersistentEntity; @@ -60,12 +58,18 @@ public class PersistentEntityJackson2Module extends SimpleModule implements Init @Autowired private RepositoryRestConfiguration config; @Autowired private UriDomainClassConverter uriDomainClassConverter; private final ResourceMappings mappings; + private final ConversionService conversionService; - public PersistentEntityJackson2Module(ResourceMappings resourceMappings) { + public PersistentEntityJackson2Module(ResourceMappings resourceMappings, ConversionService conversionService) { super(new Version(1, 1, 0, "BUILD-SNAPSHOT", "org.springframework.data.rest", "jackson-module")); + Assert.notNull(resourceMappings, "ResourceMappings must not be null!"); + Assert.notNull(conversionService, "ConversionService must not be null!"); + this.mappings = resourceMappings; + this.conversionService = conversionService; + addSerializer(new ResourceSerializer()); } @@ -116,11 +120,12 @@ public class PersistentEntityJackson2Module extends SimpleModule implements Init this.persistentEntity = persistentEntity; } - @SuppressWarnings({ "unchecked", "incomplete-switch", "null", "unused" }) + @SuppressWarnings({ "unchecked", "incomplete-switch", "unused" }) @Override public T deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { Object entity = instantiateClass(getValueClass()); - BeanWrapper wrapper = BeanWrapper.create(entity, null); + + BeanWrapper wrapper = BeanWrapper.create(entity, conversionService); ResourceMetadata metadata = mappings.getMappingFor(getValueClass()); @@ -172,34 +177,27 @@ public class PersistentEntityJackson2Module extends SimpleModule implements Init // Try and read the value of this attribute. // The method of doing that varies based on the type of the property. if (persistentProperty.isCollectionLike()) { - Class> ctype = (Class>) persistentProperty.getType(); - Collection c = (Collection) wrapper.getProperty(persistentProperty); - if (null == c || c == Collections.EMPTY_LIST || c == Collections.EMPTY_SET) { - if (Collection.class.isAssignableFrom(ctype)) { - c = new ArrayList(); - } else if (Set.class.isAssignableFrom(ctype)) { - c = new HashSet(); - } - } + + Class> collectionType = (Class>) persistentProperty + .getType(); + Collection collection = CollectionFactory.createCollection(collectionType, 0); if ((tok = jp.nextToken()) == JsonToken.START_ARRAY) { while ((tok = jp.nextToken()) != JsonToken.END_ARRAY) { Object cval = jp.readValueAs(persistentProperty.getComponentType()); - c.add(cval); + collection.add(cval); } - val = c; + val = collection; } else if (tok == JsonToken.VALUE_NULL) { val = null; } else { throw new HttpMessageNotReadableException("Cannot read a JSON " + tok + " as a Collection."); } } else if (persistentProperty.isMap()) { - Class> mtype = (Class>) persistentProperty.getType(); - Map m = (Map) wrapper.getProperty(persistentProperty); - if (null == m || m == Collections.EMPTY_MAP) { - m = new HashMap(); - } + + Class> mapType = (Class>) persistentProperty.getType(); + Map map = CollectionFactory.createMap(mapType, 0); if ((tok = jp.nextToken()) == JsonToken.START_OBJECT) { do { @@ -208,10 +206,10 @@ public class PersistentEntityJackson2Module extends SimpleModule implements Init tok = jp.nextToken(); Object mval = jp.readValueAs(persistentProperty.getMapValueType()); - m.put(name, mval); + map.put(name, mval); } while ((tok = jp.nextToken()) != JsonToken.END_OBJECT); - val = m; + val = map; } else if (tok == JsonToken.VALUE_NULL) { val = null; } else { diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractWebIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractWebIntegrationTests.java index fabc99e24..61a0b3f53 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractWebIntegrationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractWebIntegrationTests.java @@ -133,6 +133,10 @@ public abstract class AbstractWebIntegrationTests { assertThat("Expected not to find link with rel " + rel + " but found " + link + "!", link, is(nullValue())); } + protected void assertHasJsonPathValue(String path, MockHttpServletResponse response) throws Exception { + assertThat(JsonPath.read(response.getContentAsString(), path), is(notNullValue())); + } + protected ResultMatcher hasLinkWithRel(final String rel) { return new ResultMatcher() { diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java index ba89c70f3..35c87a431 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java @@ -20,17 +20,23 @@ import static org.junit.Assert.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import java.nio.charset.Charset; +import java.nio.file.Files; import java.util.Arrays; +import java.util.List; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.ClassPathResource; import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.data.rest.webmvc.AbstractWebIntegrationTests; import org.springframework.hateoas.Link; +import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.context.ContextConfiguration; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; /** * Web integration tests specific to JPA. @@ -120,4 +126,23 @@ public class JpaWebTests extends AbstractWebIntegrationTests { MockHttpServletResponse orders = request(ordersLink); assertHasJsonPathValue("$..lineItems", orders); } + + /** + * @see DATAREST-199 + */ + @Test + public void createsOrderUsingPut() throws Exception { + + mvc.perform(// + put("/orders/{id}", 4711).// + content(readFile("order.json")).contentType(MediaType.APPLICATION_JSON)// + ).andExpect(status().isCreated()); + } + + private String readFile(String name) throws Exception { + + ClassPathResource file = new ClassPathResource(name, getClass()); + List lines = Files.readAllLines(file.getFile().toPath(), Charset.forName("UTF-8")); + return StringUtils.collectionToDelimitedString(lines, System.lineSeparator()); + } } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java index d4225f670..47bfc61b3 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java @@ -71,7 +71,8 @@ public class RepositoryTestsConfig { @Bean public Module persistentEntityModule() { - return new PersistentEntityJackson2Module(new ResourceMappings(config(), repositories())); + return new PersistentEntityJackson2Module(new ResourceMappings(config(), repositories()), + defaultConversionService()); } @Bean diff --git a/spring-data-rest-webmvc/src/test/resources/org/springframework/data/rest/webmvc/jpa/order.json b/spring-data-rest-webmvc/src/test/resources/org/springframework/data/rest/webmvc/jpa/order.json new file mode 100644 index 000000000..11ecab6c2 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/resources/org/springframework/data/rest/webmvc/jpa/order.json @@ -0,0 +1,7 @@ +{ "creator" : { + "href" : "http://localhost:8080/persons/1" + }, + "lineItems" : [ + { "name" : "Java Chip" }, + { "name" : "Chocolate Mocca " } ] +} \ No newline at end of file