From ef1ee10940a17f3bd1e30e39cf054ab95f53f47e Mon Sep 17 00:00:00 2001 From: Greg Turnquist Date: Mon, 3 Feb 2014 11:40:47 -0600 Subject: [PATCH] DATAREST-238 - Changed JSON deserializer to ignore links. Links are a read-only and basically have to be ignored during any inputs. This required removing the deserializer from PersistentEntityJackson2Module. This will cause the object mapping to fail as it doesn't know about the links property in the request body. Instead of requiring that every entity apply the necessary annotation, the mapper is configured to not fail on non-existent attributes. This allowed all the tests to pass while properly handling PUT operations. Original pull request: #130. --- .../webmvc/RepositoryEntityController.java | 11 +- .../RepositoryRestMvcConfiguration.java | 2 + .../json/PersistentEntityJackson2Module.java | 173 ++---------------- .../webmvc/AbstractWebIntegrationTests.java | 11 +- .../data/rest/webmvc/jpa/JpaWebTests.java | 30 ++- .../PersistentEntitySerializationTests.java | 37 ++++ .../webmvc/json/RepositoryTestsConfig.java | 18 ++ .../data/rest/webmvc/jpa/order.json | 20 +- 8 files changed, 134 insertions(+), 168 deletions(-) diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java index 96b73e0b8..83f11ede3 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * Copyright 2013-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -64,6 +64,7 @@ import org.springframework.web.bind.annotation.ResponseBody; /** * @author Jon Brisbin * @author Oliver Gierke + * @author Greg Turnquist */ @RepositoryRestController class RepositoryEntityController extends AbstractRepositoryRestController implements ApplicationEventPublisherAware { @@ -237,10 +238,14 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem Object obj = invoker.invokeSave(domainObj); publisher.publishEvent(new AfterSaveEvent(obj)); + Link selfLink = perAssembler.getSelfLinkFor(obj); + HttpHeaders headers = new HttpHeaders(); + headers.setLocation(URI.create(selfLink.getHref())); + if (config.isReturnBodyOnUpdate()) { - return ControllerUtils.toResponseEntity(HttpStatus.OK, null, perAssembler.toResource(obj)); + return ControllerUtils.toResponseEntity(HttpStatus.OK, headers, perAssembler.toResource(obj)); } else { - return ControllerUtils.toResponseEntity(HttpStatus.NO_CONTENT, null, null); + return ControllerUtils.toResponseEntity(HttpStatus.NO_CONTENT, headers, null); } } 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 bc7d5398c..89782d8b8 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 @@ -82,6 +82,7 @@ import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExc import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; @@ -482,6 +483,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon objectMapper.configure(SerializationFeature.INDENT_OUTPUT, true); // Our special PersistentEntityResource Module objectMapper.registerModule(persistentEntityJackson2Module()); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); Jackson2DatatypeHelper.configureObjectMapper(objectMapper); // Configure custom Modules configureJacksonObjectMapper(objectMapper); 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 e14c79938..93528b113 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 @@ -1,22 +1,30 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.springframework.data.rest.webmvc.json; -import static org.springframework.beans.BeanUtils.*; - import java.io.IOException; -import java.net.URI; import java.util.ArrayList; -import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; 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; import org.springframework.data.mapping.PersistentProperty; @@ -24,7 +32,6 @@ import org.springframework.data.mapping.SimpleAssociationHandler; import org.springframework.data.mapping.SimplePropertyHandler; import org.springframework.data.mapping.model.BeanWrapper; import org.springframework.data.repository.support.Repositories; -import org.springframework.data.rest.core.UriDomainClassConverter; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.core.mapping.ResourceMapping; import org.springframework.data.rest.core.mapping.ResourceMappings; @@ -33,38 +40,31 @@ import org.springframework.data.rest.webmvc.PersistentEntityResource; import org.springframework.data.rest.webmvc.support.RepositoryLinkBuilder; import org.springframework.hateoas.Link; import org.springframework.hateoas.Resource; -import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.util.Assert; import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.core.JsonGenerationException; import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.core.Version; -import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.std.StdSerializer; /** * @author Jon Brisbin + * @author Oliver Gierke + * @author Greg Turnquist */ -public class PersistentEntityJackson2Module extends SimpleModule implements InitializingBean { +public class PersistentEntityJackson2Module extends SimpleModule { private static final long serialVersionUID = -7289265674870906323L; private static final Logger LOG = LoggerFactory.getLogger(PersistentEntityJackson2Module.class); - private static final TypeDescriptor URI_TYPE = TypeDescriptor.valueOf(URI.class); private final ResourceMappings mappings; - private final ConversionService conversionService; @Autowired private Repositories repositories; @Autowired private RepositoryRestConfiguration config; - @Autowired private UriDomainClassConverter uriDomainClassConverter; public PersistentEntityJackson2Module(ResourceMappings resourceMappings, ConversionService conversionService) { @@ -74,7 +74,6 @@ public class PersistentEntityJackson2Module extends SimpleModule implements Init Assert.notNull(conversionService, "ConversionService must not be null!"); this.mappings = resourceMappings; - this.conversionService = conversionService; addSerializer(new ResourceSerializer()); } @@ -101,142 +100,6 @@ public class PersistentEntityJackson2Module extends SimpleModule implements Init return false; } - @SuppressWarnings({ "unchecked", "rawtypes" }) - @Override - public void afterPropertiesSet() throws Exception { - for (Class domainType : repositories) { - PersistentEntity pe = repositories.getPersistentEntity(domainType); - if (null == pe) { - if (LOG.isWarnEnabled()) { - LOG.warn("The domain class {} does not have PersistentEntity metadata.", domainType.getName()); - } - } else { - addDeserializer(domainType, new ResourceDeserializer(pe)); - } - } - } - - private class ResourceDeserializer extends StdDeserializer { - - private static final long serialVersionUID = 8195592798684027681L; - private final PersistentEntity persistentEntity; - - private ResourceDeserializer(final PersistentEntity persistentEntity) { - super(persistentEntity.getType()); - this.persistentEntity = persistentEntity; - } - - @SuppressWarnings({ "unchecked", "incomplete-switch", "unused" }) - @Override - public T deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { - Object entity = instantiateClass(handledType()); - - BeanWrapper wrapper = BeanWrapper.create(entity, conversionService); - ResourceMetadata metadata = mappings.getMappingFor(handledType()); - - for (JsonToken tok = jp.nextToken(); tok != JsonToken.END_OBJECT; tok = jp.nextToken()) { - String name = jp.getCurrentName(); - switch (tok) { - case FIELD_NAME: { - if ("href".equals(name)) { - URI uri = URI.create(jp.nextTextValue()); - TypeDescriptor entityType = TypeDescriptor.forObject(entity); - if (uriDomainClassConverter.matches(URI_TYPE, entityType)) { - entity = uriDomainClassConverter.convert(uri, URI_TYPE, entityType); - } - - continue; - } - - if ("rel".equals(name)) { - // rel is currently ignored - continue; - } - - PersistentProperty persistentProperty = persistentEntity.getPersistentProperty(name); - if (null == persistentProperty) { - continue; - } - - Object val = null; - - if ("links".equals(name)) { - if ((tok = jp.nextToken()) == JsonToken.START_ARRAY) { - while ((tok = jp.nextToken()) != JsonToken.END_ARRAY) { - // Advance past the links - } - } else if (tok == JsonToken.VALUE_NULL) { - // skip null value - } else { - throw new HttpMessageNotReadableException( - "Property 'links' is not of array type. Either eliminate this property from the document or make it an array."); - } - continue; - } - - if (null == persistentProperty) { - // do nothing - continue; - } - - // 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> 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()); - collection.add(cval); - } - - 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> mapType = (Class>) persistentProperty.getType(); - Map map = CollectionFactory.createMap(mapType, 0); - - if ((tok = jp.nextToken()) == JsonToken.START_OBJECT) { - do { - name = jp.getCurrentName(); - // TODO resolve domain object from URI - tok = jp.nextToken(); - Object mval = jp.readValueAs(persistentProperty.getMapValueType()); - - map.put(name, mval); - } while ((tok = jp.nextToken()) != JsonToken.END_OBJECT); - - val = map; - } else if (tok == JsonToken.VALUE_NULL) { - val = null; - } else { - throw new HttpMessageNotReadableException("Cannot read a JSON " + tok + " as a Map."); - } - } else { - if ((tok = jp.nextToken()) != JsonToken.VALUE_NULL) { - val = jp.readValueAs(persistentProperty.getType()); - } - } - - wrapper.setProperty(persistentProperty, val, false); - - break; - } - } - } - - return (T) entity; - } - } - private class ResourceSerializer extends StdSerializer> { @SuppressWarnings({ "unchecked", "rawtypes" }) 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 99756403b..141116e73 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 @@ -159,7 +159,7 @@ public abstract class AbstractWebIntegrationTests { String href = link.isTemplated() ? link.expand().getHref() : link.getHref(); MockHttpServletResponse response = mvc.perform(put(href).content(payload.toString()).contentType(mediaType)).// - andExpect(status().isCreated()).// + andExpect(status().is(both(greaterThanOrEqualTo(200)).and(lessThan(300)))).// andExpect(header().string("Location", is(notNullValue()))).// andReturn().getResponse(); @@ -259,6 +259,15 @@ public abstract class AbstractWebIntegrationTests { return (T) jsonPathResult; } + protected String assertJsonPathEquals(String path, MockHttpServletResponse response, String expected) + throws Exception { + + String jsonQueryResults = assertHasJsonPathValue(path, response); + assertThat(jsonQueryResults, is(expected)); + + return jsonQueryResults; + } + 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 6ec83005a..ea321d8ed 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 @@ -62,6 +62,8 @@ public class JpaWebTests extends AbstractWebIntegrationTests { @Autowired TestDataPopulator loader; @Autowired ResourceMappings mappings; + ObjectMapper mapper = new ObjectMapper(); + /* * (non-Javadoc) * @see org.springframework.data.rest.webmvc.AbstractWebIntegrationTests#setUp() @@ -265,7 +267,7 @@ public class JpaWebTests extends AbstractWebIntegrationTests { Link frodoSiblingLink = links.get(0); - putAndGet(frodoSiblingLink, toUriList(links.get(1), links.get(2), (links.get(3))), TEXT_URI_LIST); + putAndGet(frodoSiblingLink, toUriList(links.get(1), links.get(2), links.get(3)), TEXT_URI_LIST); assertSiblingNames(frodoSiblingLink, "Bilbo", "Merry", "Pippin"); putAndGet(frodoSiblingLink, toUriList(links.get(3)), TEXT_URI_LIST); @@ -305,7 +307,6 @@ public class JpaWebTests extends AbstractWebIntegrationTests { public void propertiesCanHaveNulls() throws Exception { Link peopleLink = discoverUnique("people"); - ObjectMapper mapper = new ObjectMapper(); Person frodo = new Person(); frodo.setFirstName("Frodo"); @@ -319,10 +320,33 @@ public class JpaWebTests extends AbstractWebIntegrationTests { assertNull(JsonPath.read(responseBody, "$.lastName")); } + /** + * @see DATAREST-238 + */ + @Test + public void putShouldWorkDespiteExistingLinks() throws Exception { + + Link peopleLink = discoverUnique("people"); + + Person frodo = new Person("Frodo", "Baggins"); + String frodoString = mapper.writeValueAsString(frodo); + + MockHttpServletResponse createdPerson = postAndGet(peopleLink, frodoString, MediaType.APPLICATION_JSON); + + Link frodoLink = assertHasLinkWithRel("self", createdPerson); + assertJsonPathEquals("$.firstName", createdPerson, "Frodo"); + + String bilboWithFrodosLinks = createdPerson.getContentAsString().replace("Frodo", "Bilbo"); + + MockHttpServletResponse overwrittenResponse = putAndGet(frodoLink, bilboWithFrodosLinks, MediaType.APPLICATION_JSON); + + assertHasLinkWithRel("self", overwrittenResponse); + assertJsonPathEquals("$.firstName", overwrittenResponse, "Bilbo"); + } + private List preparePersonResources(Person primary, Person... persons) throws Exception { Link peopleLink = discoverUnique("people"); - ObjectMapper mapper = new ObjectMapper(); List links = new ArrayList(); MockHttpServletResponse primaryResponse = postAndGet(peopleLink, mapper.writeValueAsString(primary), diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntitySerializationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntitySerializationTests.java index c1ad4929e..856ffe51a 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntitySerializationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntitySerializationTests.java @@ -1,3 +1,18 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.springframework.data.rest.webmvc.json; import static org.hamcrest.MatcherAssert.*; @@ -28,6 +43,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; /** * @author Jon Brisbin + * @author Greg Turnquist */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = RepositoryTestsConfig.class) @@ -57,6 +73,27 @@ public class PersistentEntitySerializationTests { assertThat(p.getSiblings(), is(Collections.EMPTY_LIST)); } + /** + * @see DATAREST-238 + */ + @Test + public void deserializePersonWithLinks() throws IOException { + + String bilbo = "{\n" + " \"_links\" : {\n" + " \"self\" : {\n" + + " \"href\" : \"http://localhost/people/4\"\n" + " },\n" + " \"siblings\" : {\n" + + " \"href\" : \"http://localhost/people/4/siblings\"\n" + " },\n" + " \"father\" : {\n" + + " \"href\" : \"http://localhost/people/4/father\"\n" + " }\n" + " },\n" + + " \"firstName\" : \"Bilbo\",\n" + " \"lastName\" : \"Baggins\",\n" + + " \"created\" : \"2014-01-31T21:07:45.574+0000\"\n" + "}\n"; + + Person p = mapper.readValue(bilbo, Person.class); + assertThat(p.getFirstName(), equalTo("Bilbo")); + assertThat(p.getLastName(), equalTo("Baggins")); + } + + /** + * @see DATAREST-238 + */ @Test public void serializesPersonEntity() throws IOException, InterruptedException { 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 3bfa71a43..44caeb9ec 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 @@ -1,3 +1,18 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.springframework.data.rest.webmvc.json; import java.net.URI; @@ -20,11 +35,13 @@ import org.springframework.hateoas.RelProvider; import org.springframework.hateoas.core.EvoInflectorRelProvider; import org.springframework.hateoas.hal.Jackson2HalModule; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; /** * @author Jon Brisbin + * @author Greg Trunquist */ @Configuration @Import({ JpaRepositoryConfig.class }) @@ -87,6 +104,7 @@ public class RepositoryTestsConfig { mapper.registerModule(new Jackson2HalModule()); mapper.registerModule(persistentEntityModule()); mapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, null)); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); return mapper; } 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 index 11ecab6c2..a3b82d1d2 100644 --- 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 @@ -1,7 +1,15 @@ -{ "creator" : { - "href" : "http://localhost:8080/persons/1" - }, - "lineItems" : [ - { "name" : "Java Chip" }, - { "name" : "Chocolate Mocca " } ] +{ + "_links": { + "self": { + "href": "http://localhost:8080/persons/1" + } + }, + "lineItems": [ + { + "name": "Java Chip" + }, + { + "name": "Chocolate Mocca " + } + ] } \ No newline at end of file