DATAREST-95 - Add support for PATCH to partially update entities.

Added general support for HTTP PATCH to partially update entities and amend property resources.
This commit is contained in:
Greg Turnquist
2014-01-14 13:23:47 -06:00
committed by Oliver Gierke
parent c662065ef2
commit d65179ccc8
6 changed files with 100 additions and 10 deletions

3
.gitignore vendored
View File

@@ -8,4 +8,5 @@ target/
.project
.settings
*.log
vf.gf.*
vf.gf.*
out/

View File

@@ -61,7 +61,7 @@ public class DomainObjectMerger {
* @param from can be {@literal null}.
* @param target can be {@literal null}.
*/
public void merge(Object from, Object target) {
public void merge(Object from, Object target, final MergeNullPolicy nullPolicy) {
if (null == from || null == target) {
return;
@@ -88,7 +88,9 @@ public class DomainObjectMerger {
}
if (!ObjectUtils.nullSafeEquals(sourceValue, targetValue)) {
targetWrapper.setProperty(persistentProperty, sourceValue);
if (nullPolicy == MergeNullPolicy.APPLY_NULLS || sourceValue != null) {
targetWrapper.setProperty(persistentProperty, sourceValue);
}
}
}
});
@@ -110,4 +112,15 @@ public class DomainObjectMerger {
}
});
}
/**
* A switch on whether or not to ignore nulls.
* NOTE: This could have been a simple boolean flag but the enumerated value clearly
* denotes which version is being used.
*/
public static enum MergeNullPolicy {
APPLY_NULLS, IGNORE_NULLS;
}
}

View File

@@ -57,7 +57,7 @@ public class DomainObjectMergerTests {
Person existingDomainObject = new Person("Frodo", "Baggins");
DomainObjectMerger merger = new DomainObjectMerger(repositories, conversionService);
merger.merge(incoming, existingDomainObject);
merger.merge(incoming, existingDomainObject, DomainObjectMerger.MergeNullPolicy.APPLY_NULLS);
assertThat(existingDomainObject.getFirstName(), equalTo(incoming.getFirstName()));
assertThat(existingDomainObject.getLastName(), equalTo(incoming.getLastName()));
@@ -76,7 +76,7 @@ public class DomainObjectMergerTests {
Person existingDomainObject = new Person("Frodo", "Baggins");
DomainObjectMerger merger = new DomainObjectMerger(repositories, conversionService);
merger.merge(incoming, existingDomainObject);
merger.merge(incoming, existingDomainObject, DomainObjectMerger.MergeNullPolicy.APPLY_NULLS);
assertThat(existingDomainObject.getFirstName(), equalTo(incoming.getFirstName()));
assertThat(existingDomainObject.getLastName(), equalTo(incoming.getLastName()));

View File

@@ -17,11 +17,15 @@ package org.springframework.data.rest.webmvc;
import static org.springframework.data.rest.webmvc.ControllerUtils.*;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationEventPublisher;
@@ -57,10 +61,7 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.*;
/**
* @author Jon Brisbin
@@ -238,7 +239,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
return createNewEntity(resourceInformation, incoming);
}
domainObjectMerger.merge(incoming.getContent(), domainObj);
domainObjectMerger.merge(incoming.getContent(), domainObj, DomainObjectMerger.MergeNullPolicy.APPLY_NULLS);
publisher.publishEvent(new BeforeSaveEvent(incoming.getContent()));
Object obj = invoker.invokeSave(domainObj);
@@ -255,6 +256,36 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
}
}
@RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.PATCH, consumes = { "application/json" })
public ResponseEntity<? extends ResourceSupport> patchEntity(
RepositoryRestRequest request, PersistentEntityResource<Object> incoming,
@PathVariable String id) {
RepositoryInvoker invoker = request.getRepositoryInvoker();
if (null == invoker || !invoker.exposesSave() || !invoker.exposesFindOne()) {
return new ResponseEntity<Resource<?>>(HttpStatus.METHOD_NOT_ALLOWED);
}
Object domainObj = converter.convert(id, STRING_TYPE,
TypeDescriptor.valueOf(request.getPersistentEntity().getType()));
if (null == domainObj) {
return new ResponseEntity<Resource<?>>(HttpStatus.NOT_FOUND);
}
domainObjectMerger.merge(incoming.getContent(), domainObj, DomainObjectMerger.MergeNullPolicy.IGNORE_NULLS);
publisher.publishEvent(new BeforeSaveEvent(domainObj));
Object obj = invoker.invokeSave(domainObj);
publisher.publishEvent(new AfterSaveEvent(domainObj));
if (config.isReturnBodyOnUpdate()) {
return ControllerUtils.toResponseEntity(HttpStatus.OK, null, perAssembler.toResource(obj));
} else {
return ControllerUtils.toEmptyResponse(HttpStatus.NO_CONTENT);
}
}
@RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.DELETE)
public ResponseEntity<?> deleteEntity(final RootResourceInformation resourceInformation, @PathVariable final String id)
throws ResourceNotFoundException, HttpRequestMethodNotSupportedException {

View File

@@ -37,6 +37,7 @@ import org.springframework.hateoas.Link;
import org.springframework.hateoas.LinkDiscoverer;
import org.springframework.hateoas.LinkDiscoverers;
import org.springframework.hateoas.MediaTypes;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.context.ContextConfiguration;
@@ -47,6 +48,7 @@ import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@@ -172,6 +174,18 @@ public abstract class AbstractWebIntegrationTests {
return request(response.getHeader("Location"));
}
protected MockHttpServletResponse patchAndGet(Link link, Object payload, MediaType mediaType) throws Exception {
String href = link.isTemplated() ? link.expand().getHref() : link.getHref();
MockHttpServletResponse response = mvc.perform(MockMvcRequestBuilders.request(HttpMethod.PATCH, href).//
content(payload.toString()).contentType(mediaType)).//
andExpect(status().isNoContent()).//
andReturn().getResponse();
return request(href);
}
protected MockHttpServletResponse deleteAndGet(Link link, MediaType mediaType) throws Exception {
String href = link.isTemplated() ? link.expand().getHref() : link.getHref();

View File

@@ -27,6 +27,7 @@ import java.util.List;
import java.util.Map;
import java.util.Scanner;
import com.jayway.jsonpath.JsonPath;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
@@ -34,9 +35,11 @@ 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.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@@ -176,6 +179,34 @@ public class JpaWebTests extends AbstractWebIntegrationTests {
).andExpect(status().isCreated());
}
/**
* @see DATAREST-95
*/
@Test
public void createThenPatch() throws Exception {
MockHttpServletResponse bilbo = postAndGet(new Link("/people/"),//
"{ \"firstName\" : \"Bilbo\", \"lastName\" : \"Baggins\" }",//
MediaType.APPLICATION_JSON);
Link bilboLink = assertHasLinkWithRel("self", bilbo);
assertThat((String) JsonPath.read(bilbo.getContentAsString(), "$.firstName"),
equalTo("Bilbo"));
assertThat((String) JsonPath.read(bilbo.getContentAsString(), "$.lastName"),
equalTo("Baggins"));
MockHttpServletResponse frodo = patchAndGet(bilboLink,//
"{ \"firstName\" : \"Frodo\" }",//
MediaType.APPLICATION_JSON);
assertThat((String) JsonPath.read(frodo.getContentAsString(), "$.firstName"),
equalTo("Frodo"));
assertThat((String) JsonPath.read(frodo.getContentAsString(), "$.lastName"),
equalTo("Baggins"));
}
@Test
public void listsSiblingsWithContentCorrectly() throws Exception {