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.
This commit is contained in:
Greg Turnquist
2014-02-03 11:40:47 -06:00
committed by Oliver Gierke
parent 5dd33ba7c0
commit ef1ee10940
8 changed files with 134 additions and 168 deletions

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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<T extends Object> extends StdDeserializer<T> {
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<?, Object> 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<? extends Collection<?>> collectionType = (Class<? extends Collection<?>>) persistentProperty
.getType();
Collection<Object> 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<? extends Map<?, ?>> mapType = (Class<? extends Map<?, ?>>) persistentProperty.getType();
Map<Object, Object> 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<PersistentEntityResource<?>> {
@SuppressWarnings({ "unchecked", "rawtypes" })

View File

@@ -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() {

View File

@@ -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<Link> preparePersonResources(Person primary, Person... persons) throws Exception {
Link peopleLink = discoverUnique("people");
ObjectMapper mapper = new ObjectMapper();
List<Link> links = new ArrayList<Link>();
MockHttpServletResponse primaryResponse = postAndGet(peopleLink, mapper.writeValueAsString(primary),

View File

@@ -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 {

View File

@@ -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;
}

View File

@@ -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 "
}
]
}