diff --git a/spring-data-rest-webmvc/pom.xml b/spring-data-rest-webmvc/pom.xml
index 131f9b826..2d8008880 100644
--- a/spring-data-rest-webmvc/pom.xml
+++ b/spring-data-rest-webmvc/pom.xml
@@ -92,7 +92,15 @@
${jackson}
true
-
+
+
+
+
+ com.github.fge
+ json-patch
+ 1.7
+
+
diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/IncomingRequest.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/IncomingRequest.java
new file mode 100644
index 000000000..9aa801cae
--- /dev/null
+++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/IncomingRequest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 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;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.util.Assert;
+
+/**
+ * Value object to wrap a {@link ServerHttpRequest} to provide a slightly more abstract API to find out about the
+ * request method.
+ *
+ * @author Oliver Gierke
+ */
+public class IncomingRequest {
+
+ private final ServerHttpRequest request;
+ private final MediaType contentType;
+
+ /**
+ * Creates a new {@link IncomingRequest} from {@link ServerHttpRequest}.
+ *
+ * @param request must not be {@literal null}.
+ */
+ public IncomingRequest(ServerHttpRequest request) {
+
+ Assert.notNull(request, "ServerHttpRequest must not be null!");
+
+ this.request = request;
+ this.contentType = request.getHeaders().getContentType();
+ }
+
+ /**
+ * Returns whether the request is a PATCH request.
+ *
+ * @return
+ */
+ public boolean isPatchRequest() {
+ return request.getMethod().equals(HttpMethod.PATCH);
+ }
+
+ /**
+ * Returns whether the request is a PATCH request with a payload of type {@link RestMediaTypes#JSON_PATCH_JSON}.
+ *
+ * @return
+ */
+ public boolean isJsonPatchRequest() {
+ return isPatchRequest() && RestMediaTypes.JSON_PATCH_JSON.equals(contentType);
+ }
+
+ /**
+ * Returns whether the request is a PATCH request with a payload of type {@link RestMediaTypes#MERGE_PATCH_JSON}.
+ *
+ * @return
+ */
+ public boolean isJsonMergePatchRequest() {
+ return isPatchRequest() && RestMediaTypes.MERGE_PATCH_JSON.equals(contentType);
+ }
+
+ /**
+ * Returns the body of the request.
+ *
+ * @return will never be {@literal null}.
+ * @throws IOException
+ */
+ public InputStream getBody() throws IOException {
+ return request.getBody();
+ }
+
+ /**
+ * Returns the underlying {@link ServerHttpRequest}.
+ *
+ * @return will never be {@literal null}.
+ */
+ public ServerHttpRequest getServerHttpRequest() {
+ return request;
+ }
+}
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 0c3c61310..bc26c573f 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
@@ -15,7 +15,6 @@
*/
package org.springframework.data.rest.webmvc;
-import static org.springframework.data.rest.core.support.DomainObjectMerger.NullHandlingPolicy.*;
import static org.springframework.http.HttpMethod.*;
import java.io.Serializable;
@@ -41,8 +40,6 @@ import org.springframework.data.rest.core.event.BeforeSaveEvent;
import org.springframework.data.rest.core.invoke.RepositoryInvoker;
import org.springframework.data.rest.core.mapping.ResourceMetadata;
import org.springframework.data.rest.core.mapping.SearchResourceMappings;
-import org.springframework.data.rest.core.support.DomainObjectMerger;
-import org.springframework.data.rest.core.support.DomainObjectMerger.NullHandlingPolicy;
import org.springframework.data.rest.webmvc.support.BackendId;
import org.springframework.data.rest.webmvc.support.DefaultedPageable;
import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks;
@@ -75,21 +72,19 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
private final RepositoryEntityLinks entityLinks;
private final RepositoryRestConfiguration config;
private final ConversionService conversionService;
- private final DomainObjectMerger domainObjectMerger;
private ApplicationEventPublisher publisher;
@Autowired
public RepositoryEntityController(Repositories repositories, RepositoryRestConfiguration config,
RepositoryEntityLinks entityLinks, PagedResourcesAssembler assembler,
- @Qualifier("defaultConversionService") ConversionService conversionService, DomainObjectMerger domainObjectMerger) {
+ @Qualifier("defaultConversionService") ConversionService conversionService) {
super(assembler);
this.entityLinks = entityLinks;
this.config = config;
this.conversionService = conversionService;
- this.domainObjectMerger = domainObjectMerger;
}
/*
@@ -305,18 +300,15 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
resourceInformation.verifySupportedMethod(HttpMethod.PUT, ResourceType.ITEM);
- Object domainObject = conversionService.convert(id, resourceInformation.getDomainType());
+ // Force ID on unmarshalled object
+ BeanWrapper incomingWrapper = BeanWrapper.create(payload.getContent(), conversionService);
+ incomingWrapper.setProperty(payload.getPersistentEntity().getIdProperty(), id);
+
RepositoryInvoker invoker = resourceInformation.getInvoker();
+ Object objectToSave = incomingWrapper.getBean();
- if (domainObject == null) {
-
- BeanWrapper incomingWrapper = BeanWrapper.create(payload.getContent(), conversionService);
- incomingWrapper.setProperty(payload.getPersistentEntity().getIdProperty(), id);
-
- return createAndReturn(incomingWrapper.getBean(), invoker, assembler);
- }
-
- return mergeAndReturn(payload.getContent(), domainObject, invoker, PUT, assembler);
+ return invoker.invokeFindOne(id) == null ? createAndReturn(objectToSave, invoker, assembler) : saveAndReturn(
+ objectToSave, invoker, PUT, assembler);
}
/**
@@ -336,13 +328,11 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
resourceInformation.verifySupportedMethod(HttpMethod.PATCH, ResourceType.ITEM);
- Object domainObject = conversionService.convert(id, resourceInformation.getDomainType());
-
- if (domainObject == null) {
+ if (resourceInformation.getInvoker().invokeFindOne(id) == null) {
throw new ResourceNotFoundException();
}
- return mergeAndReturn(payload.getContent(), domainObject, resourceInformation.getInvoker(), PATCH, assembler);
+ return saveAndReturn(payload.getContent(), resourceInformation.getInvoker(), PATCH, assembler);
}
/**
@@ -379,11 +369,8 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
* @param httpMethod
* @return
*/
- private ResponseEntity mergeAndReturn(Object incoming, Object domainObject,
- RepositoryInvoker invoker, HttpMethod httpMethod, PersistentEntityResourceAssembler assembler) {
-
- NullHandlingPolicy nullPolicy = httpMethod.equals(PATCH) ? IGNORE_NULLS : APPLY_NULLS;
- domainObjectMerger.merge(incoming, domainObject, nullPolicy);
+ private ResponseEntity saveAndReturn(Object domainObject, RepositoryInvoker invoker,
+ HttpMethod httpMethod, PersistentEntityResourceAssembler assembler) {
publisher.publishEvent(new BeforeSaveEvent(domainObject));
Object obj = invoker.invokeSave(domainObject);
diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySchemaController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySchemaController.java
index 369cf7c84..d0644ceed 100644
--- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySchemaController.java
+++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySchemaController.java
@@ -60,7 +60,7 @@ class RepositorySchemaController {
@RequestMapping(value = BASE_MAPPING + "/schema", method = GET, produces = { "application/schema+json" })
public HttpEntity schema(RootResourceInformation resourceInformation) {
- JsonSchema schema = jsonSchemaConverter.convert(resourceInformation.getPersistentEntity().getType());
+ JsonSchema schema = jsonSchemaConverter.convert(resourceInformation.getDomainType());
return new ResponseEntity(schema, HttpStatus.OK);
}
}
diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RestMediaTypes.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RestMediaTypes.java
new file mode 100644
index 000000000..7afe1fc16
--- /dev/null
+++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RestMediaTypes.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 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;
+
+import org.springframework.hateoas.MediaTypes;
+import org.springframework.http.MediaType;
+
+/**
+ * Constants to refer to supported media types.
+ *
+ * @author Oliver Gierke
+ */
+public class RestMediaTypes {
+
+ public static final MediaType HAL_JSON = MediaTypes.HAL_JSON;
+
+ public static final MediaType JSON_PATCH_JSON = MediaType.valueOf("application/json-patch+json");
+ public static final MediaType MERGE_PATCH_JSON = MediaType.valueOf("application/merge-patch+json");
+
+ public static final MediaType SCHEMA_JSON = MediaType.valueOf("application/schema+json");
+
+ public static final MediaType SPRING_DATA_VERBOSE_JSON = MediaType.valueOf("application/x-spring-data-verbose+json");
+ public static final MediaType SPRING_DATA_COMPACT_JSON = MediaType.valueOf("application/x-spring-data-compact+json");
+}
diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/JsonPatchHandler.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/JsonPatchHandler.java
new file mode 100644
index 000000000..c79603fe0
--- /dev/null
+++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/JsonPatchHandler.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 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.config;
+
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.util.List;
+
+import org.springframework.data.rest.webmvc.IncomingRequest;
+import org.springframework.data.rest.webmvc.json.DomainObjectReader;
+import org.springframework.util.Assert;
+import org.springframework.util.ReflectionUtils;
+
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.NullNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.type.CollectionType;
+import com.github.fge.jackson.jsonpointer.JsonPointer;
+import com.github.fge.jsonpatch.JsonPatchOperation;
+import com.github.fge.jsonpatch.RemoveOperation;
+import com.github.fge.jsonpatch.ReplaceOperation;
+
+/**
+ * Component to apply JSON Patch and JSON Merge Patch payloads to existing domain objects. The implementation uses the
+ * JSON Patch library by Francis Galiegue but applies a customization to remove operations. As the patched node set
+ * needs to be applied to the existing domain objects (see {@link DomainObjectReader} for that) we turn remove calls
+ * into
+ *
+ * @author Oliver Gierke
+ * @see http://tools.ietf.org/html/rfc6902
+ * @see http://tools.ietf.org/html/draft-ietf-appsawg-json-merge-patch-02
+ */
+class JsonPatchHandler {
+
+ private static final Field PATH_FIELD;
+
+ static {
+
+ Field field = ReflectionUtils.findField(JsonPatchOperation.class, "path");
+ ReflectionUtils.makeAccessible(field);
+
+ PATH_FIELD = field;
+ }
+
+ private final ObjectMapper mapper;
+ private final ObjectMapper sourceMapper;
+ private final DomainObjectReader reader;
+
+ /**
+ * Creates a new {@link JsonPatchHandler} with the given {@link ObjectMapper} and {@link DomainObjectReader}.
+ *
+ * @param mapper must not be {@literal null}.
+ * @param reader must not be {@literal null}.
+ */
+ public JsonPatchHandler(ObjectMapper mapper, DomainObjectReader reader) {
+
+ Assert.notNull(mapper, "ObjectMapper must not be null!");
+ Assert.notNull(reader, "DomainObjectReader must not be null!");
+
+ this.mapper = mapper;
+ this.reader = reader;
+
+ this.sourceMapper = mapper.copy();
+ this.sourceMapper.setSerializationInclusion(Include.NON_NULL);
+ }
+
+ /**
+ * Applies the body of the given {@link IncomingRequest} as patch on the given target object.
+ *
+ * @param request must not be {@literal null}.
+ * @param target must not be {@literal null}.
+ * @return
+ * @throws Exception
+ */
+ public T apply(IncomingRequest request, T target) throws Exception {
+
+ Assert.notNull(request, "Request must not be null!");
+ Assert.isTrue(request.isPatchRequest(), "Cannot handle non-PATCH request!");
+ Assert.notNull(target, "Target must not be null!");
+
+ if (request.isJsonPatchRequest()) {
+ return applyPatch(request.getBody(), target);
+ } else {
+ return applyMergePatch(request.getBody(), target);
+ }
+ }
+
+ T applyPatch(InputStream source, T target) throws Exception {
+
+ CollectionType listOfOperationsType = mapper.getTypeFactory().constructCollectionType(List.class,
+ JsonPatchOperation.class);
+ List readValue = mapper.readValue(source, listOfOperationsType);
+
+ JsonNode existingAsNode = mapper.readTree(sourceMapper.writeValueAsBytes(target));
+ JsonNode patchedNode = existingAsNode;
+
+ for (JsonPatchOperation operation : readValue) {
+
+ if (operation instanceof RemoveOperation) {
+
+ // Replace remove operation with replace operation and a value of null.
+ JsonPointer path = (JsonPointer) ReflectionUtils.getField(PATH_FIELD, operation);
+ patchedNode = new ReplaceOperation(path, NullNode.getInstance()).apply(patchedNode);
+
+ } else {
+ patchedNode = operation.apply(patchedNode);
+ }
+ }
+
+ return reader.merge((ObjectNode) patchedNode, target, mapper);
+ }
+
+ T applyMergePatch(InputStream source, T existingObject) throws Exception {
+ return reader.read(source, existingObject, mapper);
+ }
+}
diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/PersistentEntityResourceHandlerMethodArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/PersistentEntityResourceHandlerMethodArgumentResolver.java
index 8f0411812..395cc7afc 100644
--- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/PersistentEntityResourceHandlerMethodArgumentResolver.java
+++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/PersistentEntityResourceHandlerMethodArgumentResolver.java
@@ -15,16 +15,25 @@
*/
package org.springframework.data.rest.webmvc.config;
+import java.io.IOException;
+import java.io.Serializable;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.springframework.core.MethodParameter;
+import org.springframework.data.rest.core.invoke.RepositoryInvoker;
+import org.springframework.data.rest.webmvc.IncomingRequest;
import org.springframework.data.rest.webmvc.PersistentEntityResource;
+import org.springframework.data.rest.webmvc.ResourceNotFoundException;
import org.springframework.data.rest.webmvc.RootResourceInformation;
+import org.springframework.data.rest.webmvc.json.DomainObjectReader;
+import org.springframework.data.rest.webmvc.support.BackendIdHandlerMethodArgumentResolver;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.util.Assert;
import org.springframework.web.bind.support.WebDataBinderFactory;
@@ -32,6 +41,8 @@ import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
/**
* Custom {@link HandlerMethodArgumentResolver} to create {@link PersistentEntityResource} instances.
*
@@ -40,10 +51,13 @@ import org.springframework.web.method.support.ModelAndViewContainer;
*/
public class PersistentEntityResourceHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
- private static final String ERROR_MESSAGE = "Could not read an object of type %s from the request! Converter %s returned null!";
+ private static final String ERROR_MESSAGE = "Could not read an object of type %s from the request!";
private static final String NO_CONVERTER_FOUND = "No suitable HttpMessageConverter found to read request body into object of type %s from request with content type of %s!";
- private final RootResourceInformationHandlerMethodArgumentResolver repoRequestResolver;
+ private final RootResourceInformationHandlerMethodArgumentResolver resourceInformationResolver;
+
+ private final BackendIdHandlerMethodArgumentResolver idResolver;
+ private final DomainObjectReader reader;
private final List> messageConverters;
/**
@@ -51,17 +65,24 @@ public class PersistentEntityResourceHandlerMethodArgumentResolver implements Ha
* {@link HttpMessageConverter}s and {@link RootResourceInformationHandlerMethodArgumentResolver}..
*
* @param messageConverters must not be {@literal null}.
- * @param repositoryRequestResolver must not be {@literal null}.
+ * @param resourceInformationResolver must not be {@literal null}.
+ * @param idResolver must not be {@literal null}.
+ * @param reader must not be {@literal null}.
*/
public PersistentEntityResourceHandlerMethodArgumentResolver(List> messageConverters,
- RootResourceInformationHandlerMethodArgumentResolver repositoryRequestResolver) {
+ RootResourceInformationHandlerMethodArgumentResolver resourceInformationResolver,
+ BackendIdHandlerMethodArgumentResolver idResolver, DomainObjectReader reader) {
Assert.notEmpty(messageConverters, "MessageConverters must not be null or empty!");
- Assert
- .notNull(repositoryRequestResolver, "RootResourceInformationHandlerMethodArgumentResolver must not be empty!");
+ Assert.notNull(resourceInformationResolver,
+ "RootResourceInformationHandlerMethodArgumentResolver must not be empty!");
+ Assert.notNull(idResolver, "BackendIdHandlerMethodArgumentResolver must not be null!");
+ Assert.notNull(reader, "DomainObjectReader must not be null!");
this.messageConverters = messageConverters;
- this.repoRequestResolver = repositoryRequestResolver;
+ this.resourceInformationResolver = resourceInformationResolver;
+ this.idResolver = idResolver;
+ this.reader = reader;
}
/*
@@ -82,13 +103,14 @@ public class PersistentEntityResourceHandlerMethodArgumentResolver implements Ha
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
- RootResourceInformation resourceInformation = repoRequestResolver.resolveArgument(parameter, mavContainer,
+ RootResourceInformation resourceInformation = resourceInformationResolver.resolveArgument(parameter, mavContainer,
webRequest, binderFactory);
HttpServletRequest nativeRequest = webRequest.getNativeRequest(HttpServletRequest.class);
ServletServerHttpRequest request = new ServletServerHttpRequest(nativeRequest);
+ IncomingRequest incoming = new IncomingRequest(request);
- Class> domainType = resourceInformation.getPersistentEntity().getType();
+ Class> domainType = resourceInformation.getDomainType();
MediaType contentType = request.getHeaders().getContentType();
for (HttpMessageConverter converter : messageConverters) {
@@ -97,10 +119,11 @@ public class PersistentEntityResourceHandlerMethodArgumentResolver implements Ha
continue;
}
- Object obj = converter.read(domainType, request);
+ Serializable id = idResolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
+ Object obj = read(resourceInformation, incoming, converter, id);
if (obj == null) {
- throw new HttpMessageNotReadableException(String.format(ERROR_MESSAGE, domainType, converter));
+ throw new HttpMessageNotReadableException(String.format(ERROR_MESSAGE, domainType));
}
return PersistentEntityResource.build(obj, resourceInformation.getPersistentEntity()).build();
@@ -108,4 +131,61 @@ public class PersistentEntityResourceHandlerMethodArgumentResolver implements Ha
throw new HttpMessageNotReadableException(String.format(NO_CONVERTER_FOUND, domainType, contentType));
}
+
+ /**
+ * Reads the given {@link ServerHttpRequest} into an object of the type of the given {@link RootResourceInformation},
+ * potentially applying the content to an object of the given id.
+ *
+ * @param information
+ * @param request
+ * @param converter
+ * @param id
+ * @return
+ * @throws IOException
+ */
+ private Object read(RootResourceInformation information, IncomingRequest request,
+ HttpMessageConverter converter, Serializable id) {
+
+ if (request.isPatchRequest() && converter instanceof MappingJackson2HttpMessageConverter) {
+
+ if (id == null) {
+ new ResourceNotFoundException();
+ }
+
+ RepositoryInvoker invoker = information.getInvoker();
+ Object existingObject = invoker.invokeFindOne(id);
+
+ if (existingObject == null) {
+ throw new ResourceNotFoundException();
+ }
+
+ ObjectMapper mapper = ((MappingJackson2HttpMessageConverter) converter).getObjectMapper();
+ Object result = readPatch(request, mapper, existingObject);
+
+ return result;
+ }
+
+ return read(request, converter, information);
+ }
+
+ private Object readPatch(IncomingRequest request, ObjectMapper mapper, Object existingObject) {
+
+ try {
+ JsonPatchHandler handler = new JsonPatchHandler(mapper, reader);
+ return handler.apply(request, existingObject);
+ } catch (Exception e) {
+ throw new HttpMessageNotReadableException(String.format(ERROR_MESSAGE, existingObject.getClass()));
+ }
+ }
+
+ private Object read(IncomingRequest request, HttpMessageConverter converter,
+ RootResourceInformation information) {
+
+ try {
+ return converter.read(information.getDomainType(), request.getServerHttpRequest());
+ } catch (IOException e) {
+ throw new HttpMessageNotReadableException(String.format(ERROR_MESSAGE, information.getDomainType()));
+ }
+ }
+
}
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 fb14f888f..ae0d39e33 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
@@ -66,10 +66,12 @@ import org.springframework.data.rest.webmvc.BaseUri;
import org.springframework.data.rest.webmvc.RepositoryRestController;
import org.springframework.data.rest.webmvc.RepositoryRestHandlerAdapter;
import org.springframework.data.rest.webmvc.RepositoryRestHandlerMapping;
+import org.springframework.data.rest.webmvc.RestMediaTypes;
import org.springframework.data.rest.webmvc.ServerHttpRequestMethodArgumentResolver;
import org.springframework.data.rest.webmvc.convert.StringToDistanceConverter;
import org.springframework.data.rest.webmvc.convert.StringToPointConverter;
import org.springframework.data.rest.webmvc.convert.UriListHttpMessageConverter;
+import org.springframework.data.rest.webmvc.json.DomainObjectReader;
import org.springframework.data.rest.webmvc.json.Jackson2DatatypeHelper;
import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module;
import org.springframework.data.rest.webmvc.json.PersistentEntityToJsonSchemaConverter;
@@ -312,7 +314,8 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
List> messageConverters = defaultMessageConverters();
configureHttpMessageConverters(messageConverters);
- return new PersistentEntityResourceHandlerMethodArgumentResolver(messageConverters, repoRequestArgumentResolver());
+ return new PersistentEntityResourceHandlerMethodArgumentResolver(messageConverters, repoRequestArgumentResolver(),
+ backendIdHandlerMethodArgumentResolver(), new DomainObjectReader(persistentEntities(), resourceMappings()));
}
/**
@@ -360,9 +363,9 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
public MappingJackson2HttpMessageConverter jacksonHttpMessageConverter() {
List mediaTypes = new ArrayList();
- mediaTypes.addAll(Arrays.asList(MediaType.valueOf("application/schema+json"),
- MediaType.valueOf("application/x-spring-data-verbose+json"),
- MediaType.valueOf("application/x-spring-data-compact+json")));
+ mediaTypes.addAll(Arrays.asList(RestMediaTypes.SCHEMA_JSON, //
+ RestMediaTypes.JSON_PATCH_JSON, RestMediaTypes.MERGE_PATCH_JSON, //
+ RestMediaTypes.SPRING_DATA_VERBOSE_JSON, RestMediaTypes.SPRING_DATA_COMPACT_JSON));
// Configure this mapper to be used if HAL is not the default media type
if (!config().useHalAsDefaultJsonMediaType()) {
diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/ResourceMetadataHandlerMethodArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/ResourceMetadataHandlerMethodArgumentResolver.java
index bd0b91847..c85b9a138 100644
--- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/ResourceMetadataHandlerMethodArgumentResolver.java
+++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/ResourceMetadataHandlerMethodArgumentResolver.java
@@ -81,7 +81,7 @@ public class ResourceMetadataHandlerMethodArgumentResolver implements HandlerMet
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
String lookupPath = baseUri.getRepositoryLookupPath(webRequest);
- String repositoryKey = UriUtils.findMappingVariable("repository", parameter, lookupPath);
+ String repositoryKey = UriUtils.findMappingVariable("repository", parameter.getMethod(), lookupPath);
if (!hasText(repositoryKey)) {
return null;
diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/DomainObjectReader.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/DomainObjectReader.java
new file mode 100644
index 000000000..3fa3e7556
--- /dev/null
+++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/DomainObjectReader.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright 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.io.InputStream;
+import java.util.Iterator;
+import java.util.Map.Entry;
+
+import org.springframework.data.mapping.PersistentEntity;
+import org.springframework.data.mapping.PersistentProperty;
+import org.springframework.data.mapping.context.PersistentEntities;
+import org.springframework.data.mapping.model.BeanWrapper;
+import org.springframework.data.rest.core.mapping.ResourceMappings;
+import org.springframework.data.rest.webmvc.mapping.AssociationLinks;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.util.Assert;
+
+import com.fasterxml.jackson.databind.BeanDescription;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
+import com.fasterxml.jackson.databind.introspect.BasicClassIntrospector;
+import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
+import com.fasterxml.jackson.databind.introspect.ClassIntrospector;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+/**
+ * Component to apply an {@link ObjectNode} to an existing domain object. This is effectively a best-effort workaround
+ * for Jacksons inability to apply a (partial) JSON document to an existing object in a deeply nestes way. We manually
+ * detect nested objects, lookup the original value and apply the merge recursively.
+ *
+ * @author Oliver Gierke
+ * @since 2.2
+ */
+public class DomainObjectReader {
+
+ private final PersistentEntities entities;
+ private final AssociationLinks associationLinks;
+ private final ClassIntrospector introspector;
+
+ /**
+ * Creates a new {@link DomainObjectReader} using the given {@link PersistentEntities} and {@link ResourceMappings}.
+ *
+ * @param entities must not be {@literal null}.
+ * @param mappings must not be {@literal null}.
+ */
+ public DomainObjectReader(PersistentEntities entities, ResourceMappings mappings) {
+
+ Assert.notNull(entities, "PersistentEntites must not be null!");
+ Assert.notNull(mappings, "ResourceMappings must not be null!");
+
+ this.entities = entities;
+ this.associationLinks = new AssociationLinks(mappings);
+ this.introspector = new BasicClassIntrospector();
+ }
+
+ /**
+ * Reads the given input stream into an {@link ObjectNode} and applies that to the given existing instance.
+ *
+ * @param request must not be {@literal null}.
+ * @param target must not be {@literal null}.
+ * @param mapper must not be {@literal null}.
+ * @return
+ */
+ public T read(InputStream source, T target, ObjectMapper mapper) {
+
+ Assert.notNull(target, "Target object must not be null!");
+ Assert.notNull(source, "InputStream must not be null!");
+ Assert.notNull(mapper, "ObjectMapper must not be null!");
+
+ try {
+ return merge((ObjectNode) mapper.readTree(source), target, mapper);
+ } catch (Exception e) {
+ throw new HttpMessageNotReadableException("Could not read payload!", e);
+ }
+ }
+
+ /**
+ * Merges the given {@link ObjectNode} onto the given object.
+ *
+ * @param root must not be {@literal null}.
+ * @param existingObject
+ * @param mapper
+ * @return
+ * @throws Exception
+ */
+ public T merge(ObjectNode root, T existingObject, ObjectMapper mapper) throws Exception {
+
+ Assert.notNull(root, "Root ObjectNode must not be null!");
+ Assert.notNull(existingObject, "Existing object instance must not be null!");
+ Assert.notNull(mapper, "ObjectMapper must not be null!");
+
+ for (Iterator> i = root.fields(); i.hasNext();) {
+
+ Entry entry = i.next();
+ JsonNode child = entry.getValue();
+
+ if (child.isArray()) {
+ // We ignore arrays so they get instantiated fresh every time
+ } else if (child.isObject()) {
+
+ PersistentProperty> property = findProperty(existingObject, entry.getKey(), mapper);
+
+ if (property == null || associationLinks.isLinkableAssociation(property)) {
+ continue;
+ }
+
+ BeanWrapper wrapper = BeanWrapper.create(existingObject, null);
+ Object nested = wrapper.getProperty(property);
+
+ if (nested != null) {
+
+ // Only remove the JsonNode if the object already exists. Otherwise it will be instantiated when the parent
+ // gets deserialized.
+
+ i.remove();
+ merge((ObjectNode) child, nested, mapper);
+ }
+ }
+ }
+
+ ObjectReader jsonReader = mapper.readerForUpdating(existingObject);
+ jsonReader.readValue(root);
+
+ return existingObject;
+ }
+
+ /**
+ * Finds the {@link PersistentProperty} for the JSON field of the given name on the given object.
+ *
+ * @param object must not be {@literal null}.
+ * @param fieldName must not be {@literal null} or empty.
+ * @param mapper must not be {@literal null}.
+ * @return the {@link PersistentProperty} for the JSON field of the given name on the given object or {@literal null}
+ * if either the given source object is no persistent entity or the property cannot be found.
+ */
+ private PersistentProperty> findProperty(Object object, String fieldName, ObjectMapper mapper) {
+
+ PersistentEntity, ?> entity = entities.getPersistentEntity(object.getClass());
+
+ if (entity == null) {
+ return null;
+ }
+
+ BeanDescription description = introspector.forDeserialization(mapper.getDeserializationConfig(),
+ mapper.constructType(object.getClass()), mapper.getDeserializationConfig());
+
+ for (BeanPropertyDefinition definition : description.findProperties()) {
+ if (definition.getName().equals(fieldName)) {
+ return entity.getPersistentProperty(definition.getInternalName());
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/BackendIdHandlerMethodArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/BackendIdHandlerMethodArgumentResolver.java
index 595cbc319..47370c13d 100644
--- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/BackendIdHandlerMethodArgumentResolver.java
+++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/BackendIdHandlerMethodArgumentResolver.java
@@ -77,12 +77,12 @@ public class BackendIdHandlerMethodArgumentResolver implements HandlerMethodArgu
* @see org.springframework.web.method.support.HandlerMethodArgumentResolver#resolveArgument(org.springframework.core.MethodParameter, org.springframework.web.method.support.ModelAndViewContainer, org.springframework.web.context.request.NativeWebRequest, org.springframework.web.bind.support.WebDataBinderFactory)
*/
@Override
- public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
+ public Serializable resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest request, WebDataBinderFactory binderFactory) throws Exception {
Class> parameterType = parameter.getParameterType();
- if (!parameterType.equals(Serializable.class)) {
+ if (parameter.getMethodAnnotation(BackendId.class) != null && !parameterType.equals(Serializable.class)) {
throw new IllegalArgumentException(String.format(
"Method parameter for @%s must be of type %s! Got %s for method %s.", BackendId.class.getSimpleName(),
Serializable.class.getSimpleName(), parameterType.getSimpleName(), parameter.getMethod()));
@@ -97,6 +97,7 @@ public class BackendIdHandlerMethodArgumentResolver implements HandlerMethodArgu
BackendIdConverter pluginFor = idConverters.getPluginFor(metadata.getDomainType(), DefaultIdConverter.INSTANCE);
String lookupPath = baseUri.getRepositoryLookupPath(request);
- return pluginFor.fromRequestId(UriUtils.findMappingVariable("id", parameter, lookupPath), metadata.getDomainType());
+ return pluginFor.fromRequestId(UriUtils.findMappingVariable("id", parameter.getMethod(), lookupPath),
+ metadata.getDomainType());
}
}
diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/util/UriUtils.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/util/UriUtils.java
index d62740ea4..cbd679e25 100644
--- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/util/UriUtils.java
+++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/util/UriUtils.java
@@ -15,9 +15,9 @@
*/
package org.springframework.data.rest.webmvc.util;
+import java.lang.reflect.Method;
import java.util.Map;
-import org.springframework.core.MethodParameter;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -38,12 +38,12 @@ public abstract class UriUtils {
* @param request
* @return
*/
- public static String findMappingVariable(String variable, MethodParameter parameter, String lookupPath) {
+ public static String findMappingVariable(String variable, Method method, String lookupPath) {
Assert.hasText(variable, "Variable name must not be null or empty!");
- Assert.notNull(parameter, "Method parameter must not be null!");
+ Assert.notNull(method, "Method must not be null!");
- RequestMapping annotation = parameter.getMethodAnnotation(RequestMapping.class);
+ RequestMapping annotation = method.getAnnotation(RequestMapping.class);
for (String mapping : annotation.value()) {
diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/config/JsonPatchHandlerUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/config/JsonPatchHandlerUnitTests.java
new file mode 100644
index 000000000..0d5012f5e
--- /dev/null
+++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/config/JsonPatchHandlerUnitTests.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 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.config;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+import static org.springframework.data.rest.webmvc.util.TestUtils.*;
+
+import java.util.Arrays;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.springframework.data.mapping.context.PersistentEntities;
+import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
+import org.springframework.data.rest.core.mapping.ResourceMappings;
+import org.springframework.data.rest.webmvc.json.DomainObjectReader;
+import org.springframework.data.rest.webmvc.mongodb.Address;
+import org.springframework.data.rest.webmvc.mongodb.User;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Unit tests for {@link JsonPatchHandler}.
+ *
+ * @author Oliver Gierke
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class JsonPatchHandlerUnitTests {
+
+ JsonPatchHandler handler;
+ User user;
+
+ @Mock ResourceMappings mappings;
+
+ @Before
+ public void setUp() {
+
+ MongoMappingContext context = new MongoMappingContext();
+ context.getPersistentEntity(User.class);
+
+ PersistentEntities entities = new PersistentEntities(Arrays.asList(context));
+
+ this.handler = new JsonPatchHandler(new ObjectMapper(), new DomainObjectReader(entities, mappings));
+
+ Address address = new Address();
+ address.street = "Foo";
+ address.zipCode = "Bar";
+
+ this.user = new User();
+ this.user.firstname = "Oliver";
+ this.user.lastname = "Gierke";
+ this.user.address = address;
+ }
+
+ /**
+ * @see DATAREST-348
+ */
+ @Test
+ public void appliesRemoveOperationCorrectly() throws Exception {
+
+ String input = "[{ \"op\": \"replace\", \"path\": \"/address/zipCode\", \"value\": \"ZIP\" },"
+ + "{ \"op\": \"remove\", \"path\": \"/lastname\" }]";
+
+ User result = handler.applyPatch(asStream(input), user);
+
+ assertThat(result.lastname, is(nullValue()));
+ assertThat(result.address.zipCode, is("ZIP"));
+ }
+
+ /**
+ * @see DATAREST-348
+ */
+ @Test
+ public void appliesMergePatchCorrectly() throws Exception {
+
+ String input = "{ \"address\" : { \"zipCode\" : \"ZIP\"}, \"lastname\" : null }";
+
+ User result = handler.applyMergePatch(asStream(input), user);
+
+ assertThat(result.lastname, is(nullValue()));
+ assertThat(result.address.zipCode, is("ZIP"));
+ }
+}
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 5e0463a70..d7ff222b7 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
@@ -215,13 +215,18 @@ public class JpaWebTests extends AbstractWebIntegrationTests {
Link bilboLink = assertHasLinkWithRel("self", bilbo);
- assertThat((String) JsonPath.read(bilbo.getContentAsString(), "$.firstName"), equalTo("Bilbo"));
- assertThat((String) JsonPath.read(bilbo.getContentAsString(), "$.lastName"), equalTo("Baggins"));
+ assertThat((String) JsonPath.read(bilbo.getContentAsString(), "$.firstName"), is("Bilbo"));
+ assertThat((String) JsonPath.read(bilbo.getContentAsString(), "$.lastName"), is("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"));
+ assertThat((String) JsonPath.read(frodo.getContentAsString(), "$.firstName"), is("Frodo"));
+ assertThat((String) JsonPath.read(frodo.getContentAsString(), "$.lastName"), is("Baggins"));
+
+ frodo = patchAndGet(bilboLink, "{ \"firstName\" : null }", MediaType.APPLICATION_JSON);
+
+ assertThat((String) JsonPath.read(frodo.getContentAsString(), "$.firstName"), is(nullValue()));
+ assertThat((String) JsonPath.read(frodo.getContentAsString(), "$.lastName"), is("Baggins"));
}
/**
diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/MongoWebTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/MongoWebTests.java
index e692e66e1..e20b6c81e 100644
--- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/MongoWebTests.java
+++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/MongoWebTests.java
@@ -26,10 +26,14 @@ import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.rest.webmvc.AbstractWebIntegrationTests;
+import org.springframework.data.rest.webmvc.RestMediaTypes;
import org.springframework.hateoas.Link;
+import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.context.ContextConfiguration;
+import com.jayway.jsonpath.JsonPath;
+
/**
* Integration tests for MongoDB repositories.
*
@@ -114,4 +118,33 @@ public class MongoWebTests extends AbstractWebIntegrationTests {
MockHttpServletResponse response = request(countByTypeLink.expand("Twitter"));
assertThat(response.getContentAsString(), is("1"));
}
+
+ @Test
+ public void testname() throws Exception {
+
+ Link usersLink = discoverUnique("users");
+ Link userLink = assertHasContentLinkWithRel("self", request(usersLink));
+
+ MockHttpServletResponse response = patchAndGet(userLink,
+ "{\"lastname\" : null, \"address\" : { \"zipCode\" : \"ZIP\"}}", MediaType.APPLICATION_JSON);
+
+ assertThat(JsonPath.read(response.getContentAsString(), "$.lastname"), is(nullValue()));
+ assertThat(JsonPath.read(response.getContentAsString(), "$.address.zipCode"), is((Object) "ZIP"));
+ }
+
+ @Test
+ public void testname2() throws Exception {
+
+ Link usersLink = discoverUnique("users");
+ Link userLink = assertHasContentLinkWithRel("self", request(usersLink));
+
+ MockHttpServletResponse response = patchAndGet(userLink,
+ "[{ \"op\": \"replace\", \"path\": \"/address/zipCode\", \"value\": \"ZIP\" },"
+ // + "{ \"op\": \"replace\", \"path\": \"/lastname\", \"value\": null }]", //
+ + "{ \"op\": \"remove\", \"path\": \"/lastname\" }]", //
+ RestMediaTypes.JSON_PATCH_JSON);
+
+ assertThat(JsonPath.read(response.getContentAsString(), "$.lastname"), is(nullValue()));
+ assertThat(JsonPath.read(response.getContentAsString(), "$.address.zipCode"), is((Object) "ZIP"));
+ }
}
diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/util/TestUtils.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/util/TestUtils.java
index 745d9e0e5..4e75d962c 100644
--- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/util/TestUtils.java
+++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/util/TestUtils.java
@@ -15,10 +15,14 @@
*/
package org.springframework.data.rest.webmvc.util;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.charset.Charset;
import java.util.Scanner;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.rest.webmvc.jpa.JpaWebTests;
+import org.springframework.util.Assert;
/**
* Test helper methods.
@@ -46,4 +50,15 @@ public class TestUtils {
return builder.toString();
}
+
+ /**
+ * Returns the given {@link String} as {@link InputStream}.
+ *
+ * @param source must not be {@literal null}.
+ * @return
+ */
+ public static InputStream asStream(String source) {
+ Assert.notNull(source, "Source string must not be null!");
+ return new ByteArrayInputStream(source.getBytes(Charset.forName("UTF-8")));
+ }
}