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