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:
committed by
Oliver Gierke
parent
c662065ef2
commit
d65179ccc8
3
.gitignore
vendored
3
.gitignore
vendored
@@ -8,4 +8,5 @@ target/
|
|||||||
.project
|
.project
|
||||||
.settings
|
.settings
|
||||||
*.log
|
*.log
|
||||||
vf.gf.*
|
vf.gf.*
|
||||||
|
out/
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ public class DomainObjectMerger {
|
|||||||
* @param from can be {@literal null}.
|
* @param from can be {@literal null}.
|
||||||
* @param target 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) {
|
if (null == from || null == target) {
|
||||||
return;
|
return;
|
||||||
@@ -88,7 +88,9 @@ public class DomainObjectMerger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!ObjectUtils.nullSafeEquals(sourceValue, targetValue)) {
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ public class DomainObjectMergerTests {
|
|||||||
Person existingDomainObject = new Person("Frodo", "Baggins");
|
Person existingDomainObject = new Person("Frodo", "Baggins");
|
||||||
|
|
||||||
DomainObjectMerger merger = new DomainObjectMerger(repositories, conversionService);
|
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.getFirstName(), equalTo(incoming.getFirstName()));
|
||||||
assertThat(existingDomainObject.getLastName(), equalTo(incoming.getLastName()));
|
assertThat(existingDomainObject.getLastName(), equalTo(incoming.getLastName()));
|
||||||
@@ -76,7 +76,7 @@ public class DomainObjectMergerTests {
|
|||||||
Person existingDomainObject = new Person("Frodo", "Baggins");
|
Person existingDomainObject = new Person("Frodo", "Baggins");
|
||||||
|
|
||||||
DomainObjectMerger merger = new DomainObjectMerger(repositories, conversionService);
|
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.getFirstName(), equalTo(incoming.getFirstName()));
|
||||||
assertThat(existingDomainObject.getLastName(), equalTo(incoming.getLastName()));
|
assertThat(existingDomainObject.getLastName(), equalTo(incoming.getLastName()));
|
||||||
|
|||||||
@@ -17,11 +17,15 @@ package org.springframework.data.rest.webmvc;
|
|||||||
|
|
||||||
import static org.springframework.data.rest.webmvc.ControllerUtils.*;
|
import static org.springframework.data.rest.webmvc.ControllerUtils.*;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
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.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.context.ApplicationEventPublisher;
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
@@ -57,10 +61,7 @@ import org.springframework.http.HttpMethod;
|
|||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMethod;
|
|
||||||
import org.springframework.web.bind.annotation.ResponseBody;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Jon Brisbin
|
* @author Jon Brisbin
|
||||||
@@ -238,7 +239,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
|
|||||||
return createNewEntity(resourceInformation, incoming);
|
return createNewEntity(resourceInformation, incoming);
|
||||||
}
|
}
|
||||||
|
|
||||||
domainObjectMerger.merge(incoming.getContent(), domainObj);
|
domainObjectMerger.merge(incoming.getContent(), domainObj, DomainObjectMerger.MergeNullPolicy.APPLY_NULLS);
|
||||||
|
|
||||||
publisher.publishEvent(new BeforeSaveEvent(incoming.getContent()));
|
publisher.publishEvent(new BeforeSaveEvent(incoming.getContent()));
|
||||||
Object obj = invoker.invokeSave(domainObj);
|
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)
|
@RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.DELETE)
|
||||||
public ResponseEntity<?> deleteEntity(final RootResourceInformation resourceInformation, @PathVariable final String id)
|
public ResponseEntity<?> deleteEntity(final RootResourceInformation resourceInformation, @PathVariable final String id)
|
||||||
throws ResourceNotFoundException, HttpRequestMethodNotSupportedException {
|
throws ResourceNotFoundException, HttpRequestMethodNotSupportedException {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import org.springframework.hateoas.Link;
|
|||||||
import org.springframework.hateoas.LinkDiscoverer;
|
import org.springframework.hateoas.LinkDiscoverer;
|
||||||
import org.springframework.hateoas.LinkDiscoverers;
|
import org.springframework.hateoas.LinkDiscoverers;
|
||||||
import org.springframework.hateoas.MediaTypes;
|
import org.springframework.hateoas.MediaTypes;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.mock.web.MockHttpServletResponse;
|
import org.springframework.mock.web.MockHttpServletResponse;
|
||||||
import org.springframework.test.context.ContextConfiguration;
|
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.ResultActions;
|
||||||
import org.springframework.test.web.servlet.ResultMatcher;
|
import org.springframework.test.web.servlet.ResultMatcher;
|
||||||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
|
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.test.web.servlet.setup.MockMvcBuilders;
|
||||||
import org.springframework.util.LinkedMultiValueMap;
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
import org.springframework.util.MultiValueMap;
|
import org.springframework.util.MultiValueMap;
|
||||||
@@ -172,6 +174,18 @@ public abstract class AbstractWebIntegrationTests {
|
|||||||
return request(response.getHeader("Location"));
|
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 {
|
protected MockHttpServletResponse deleteAndGet(Link link, MediaType mediaType) throws Exception {
|
||||||
|
|
||||||
String href = link.isTemplated() ? link.expand().getHref() : link.getHref();
|
String href = link.isTemplated() ? link.expand().getHref() : link.getHref();
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Scanner;
|
import java.util.Scanner;
|
||||||
|
|
||||||
|
import com.jayway.jsonpath.JsonPath;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
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.core.mapping.ResourceMappings;
|
||||||
import org.springframework.data.rest.webmvc.AbstractWebIntegrationTests;
|
import org.springframework.data.rest.webmvc.AbstractWebIntegrationTests;
|
||||||
import org.springframework.hateoas.Link;
|
import org.springframework.hateoas.Link;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.mock.web.MockHttpServletResponse;
|
import org.springframework.mock.web.MockHttpServletResponse;
|
||||||
import org.springframework.test.context.ContextConfiguration;
|
import org.springframework.test.context.ContextConfiguration;
|
||||||
|
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.util.LinkedMultiValueMap;
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
import org.springframework.util.MultiValueMap;
|
import org.springframework.util.MultiValueMap;
|
||||||
@@ -176,6 +179,34 @@ public class JpaWebTests extends AbstractWebIntegrationTests {
|
|||||||
).andExpect(status().isCreated());
|
).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
|
@Test
|
||||||
public void listsSiblingsWithContentCorrectly() throws Exception {
|
public void listsSiblingsWithContentCorrectly() throws Exception {
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user