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
|
||||
.settings
|
||||
*.log
|
||||
vf.gf.*
|
||||
vf.gf.*
|
||||
out/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user