DATAREST-348 - Support for json-patch+json and merge-patch+json media types.
We now support PATCH request with application/json-patch+json [0] and application/merge-patch+json media types. The support is based on some customized usage of the FGE JSON Patch library [2]. As we need to apply the patch to an existing object, we need translate the JSON Patch remove operation into a replace operation setting the value to null so that out entity processing component registers the request for removal. Same applies to requests for merge-patch+json. From an implementation point of view the handling of the media type has moved from the controller into PersistentEntityResourceHandlerMethodArgumentResolver and its delegates, n particular JsonPatchHandler. The application of the changes to the existing domain object is handled in the newly introduced DomainObjectReader. Related ticket: DATAREST-345. [0] http://tools.ietf.org/html/rfc6902 [1] http://tools.ietf.org/html/draft-ietf-appsawg-json-merge-patch [2] https://github.com/fge/json-patch
This commit is contained in:
@@ -92,7 +92,15 @@
|
||||
<version>${jackson}</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- JSON patch -->
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.fge</groupId>
|
||||
<artifactId>json-patch</artifactId>
|
||||
<version>1.7</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Test dependencies -->
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<Object> 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<Object> incomingWrapper = BeanWrapper.create(payload.getContent(), conversionService);
|
||||
incomingWrapper.setProperty(payload.getPersistentEntity().getIdProperty(), id);
|
||||
|
||||
RepositoryInvoker invoker = resourceInformation.getInvoker();
|
||||
Object objectToSave = incomingWrapper.getBean();
|
||||
|
||||
if (domainObject == null) {
|
||||
|
||||
BeanWrapper<Object> 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<ResourceSupport> 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<ResourceSupport> saveAndReturn(Object domainObject, RepositoryInvoker invoker,
|
||||
HttpMethod httpMethod, PersistentEntityResourceAssembler assembler) {
|
||||
|
||||
publisher.publishEvent(new BeforeSaveEvent(domainObject));
|
||||
Object obj = invoker.invokeSave(domainObject);
|
||||
|
||||
@@ -60,7 +60,7 @@ class RepositorySchemaController {
|
||||
@RequestMapping(value = BASE_MAPPING + "/schema", method = GET, produces = { "application/schema+json" })
|
||||
public HttpEntity<JsonSchema> schema(RootResourceInformation resourceInformation) {
|
||||
|
||||
JsonSchema schema = jsonSchemaConverter.convert(resourceInformation.getPersistentEntity().getType());
|
||||
JsonSchema schema = jsonSchemaConverter.convert(resourceInformation.getDomainType());
|
||||
return new ResponseEntity<JsonSchema>(schema, HttpStatus.OK);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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> 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> T applyPatch(InputStream source, T target) throws Exception {
|
||||
|
||||
CollectionType listOfOperationsType = mapper.getTypeFactory().constructCollectionType(List.class,
|
||||
JsonPatchOperation.class);
|
||||
List<JsonPatchOperation> 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> T applyMergePatch(InputStream source, T existingObject) throws Exception {
|
||||
return reader.read(source, existingObject, mapper);
|
||||
}
|
||||
}
|
||||
@@ -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<HttpMessageConverter<?>> 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<HttpMessageConverter<?>> 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<Object> 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<Object> converter,
|
||||
RootResourceInformation information) {
|
||||
|
||||
try {
|
||||
return converter.read(information.getDomainType(), request.getServerHttpRequest());
|
||||
} catch (IOException e) {
|
||||
throw new HttpMessageNotReadableException(String.format(ERROR_MESSAGE, information.getDomainType()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<HttpMessageConverter<?>> 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<MediaType> mediaTypes = new ArrayList<MediaType>();
|
||||
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()) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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> 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> 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<Entry<String, JsonNode>> i = root.fields(); i.hasNext();) {
|
||||
|
||||
Entry<String, JsonNode> 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<T> 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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user