DATAREST-1050 - PUT for create now makes sure the resource id is set on the instance to create.

So far, we handed the plain instance deserialized from the request body to the repository to persist it. That caused issues in PUT for create scenarios where the URI contains the identifier to be used for the aggregate to create and identifier generation being used in the backend. In that case the identifier submitted was never considered and subsequent requests would've created new instances, effectively breaking the idempotent nature of PUT.

We now make sure the backend identifier derived from the resource is set on the aggregate instance about to be created, so that backend can either accept that situation (new entity + manually defined identifier) or reject it (in case it insists on identifier generation).
This commit is contained in:
Oliver Gierke
2017-04-11 08:26:41 +02:00
parent 4354d35c9c
commit 2b63e4bcdb
2 changed files with 129 additions and 2 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2014 the original author or authors.
* Copyright 2012-2017 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.
@@ -22,7 +22,10 @@ import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.data.repository.support.RepositoryInvoker;
import org.springframework.data.rest.webmvc.IncomingRequest;
import org.springframework.data.rest.webmvc.PersistentEntityResource;
@@ -59,10 +62,10 @@ public class PersistentEntityResourceHandlerMethodArgumentResolver implements Ha
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 resourceInformationResolver;
private final BackendIdHandlerMethodArgumentResolver idResolver;
private final DomainObjectReader reader;
private final List<HttpMessageConverter<?>> messageConverters;
private final ConversionService conversionService = new DefaultConversionService();
/**
* Creates a new {@link PersistentEntityResourceHandlerMethodArgumentResolver} for the given
@@ -143,6 +146,11 @@ public class PersistentEntityResourceHandlerMethodArgumentResolver implements Ha
if (entityIdentifier != null) {
entity.getPropertyAccessor(obj).setProperty(entity.getIdProperty(), entityIdentifier);
} else if (id != null) {
ConvertingPropertyAccessor accessor = new ConvertingPropertyAccessor(entity.getPropertyAccessor(obj),
conversionService);
accessor.setProperty(entity.getIdProperty(), id);
}
Builder build = PersistentEntityResource.build(obj, entity);

View File

@@ -0,0 +1,119 @@
/*
* Copyright 2017 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.mockito.Mockito.*;
import java.util.Arrays;
import javax.servlet.http.HttpServletRequest;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.core.MethodParameter;
import org.springframework.data.annotation.Id;
import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentEntity;
import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext;
import org.springframework.data.repository.support.RepositoryInvoker;
import org.springframework.data.rest.webmvc.PersistentEntityResource;
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.HttpInputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* Unit tests for {@link PersistentEntityResourceHandlerMethodArgumentResolver}.
*
* @author Oliver Gierke
*/
public class PersistentEntityResourceHandlerMethodArgumentResolverUnitTests {
HttpMessageConverter<?> converter;
RootResourceInformationHandlerMethodArgumentResolver rootResourceResolver;
BackendIdHandlerMethodArgumentResolver backendIdResolver;
DomainObjectReader reader;
@Before
public void setUp() throws Exception {
this.converter = mock(HttpMessageConverter.class);
when(this.converter.canRead(Mockito.any(Class.class), Mockito.any(MediaType.class))).thenReturn(true);
this.rootResourceResolver = mock(RootResourceInformationHandlerMethodArgumentResolver.class);
setupRootResourceInfoFor(Foo.class);
this.backendIdResolver = mock(BackendIdHandlerMethodArgumentResolver.class);
this.reader = mock(DomainObjectReader.class);
}
@Test // DATAREST-1050
@SuppressWarnings("unchecked")
public void returnsAggregateInstanceWithIdentifierPopulatedForPutRequests() throws Exception {
PersistentEntityResourceHandlerMethodArgumentResolver argumentResolver = new PersistentEntityResourceHandlerMethodArgumentResolver(
Arrays.<HttpMessageConverter<?>> asList(converter), rootResourceResolver, backendIdResolver, reader);
HttpServletRequest request = new MockHttpServletRequest("PUT", "/foo/4711");
doReturn(new Foo()).when(converter).read(Mockito.any(Class.class), Mockito.any(HttpInputMessage.class));
mockInvocationOfResolver(backendIdResolver, "4711");
Object result = argumentResolver.resolveArgument(null, null, new ServletWebRequest(request), null);
assertThat(result, is(instanceOf(PersistentEntityResource.class)));
Object content = ((PersistentEntityResource) result).getContent();
assertThat(content, is(instanceOf(Foo.class)));
assertThat(((Foo) content).id, is(4711L));
}
private void setupRootResourceInfoFor(Class<?> type) throws Exception {
RootResourceInformation information = mock(RootResourceInformation.class);
doReturn(type).when(information).getDomainType();
mockInvocationOfResolver(rootResourceResolver, information);
KeyValueMappingContext context = new KeyValueMappingContext();
KeyValuePersistentEntity<?> entity = context.getPersistentEntity(Foo.class);
doReturn(entity).when(information).getPersistentEntity();
doReturn(mock(RepositoryInvoker.class)).when(information).getInvoker();
}
private static void mockInvocationOfResolver(HandlerMethodArgumentResolver resolver, Object result) throws Exception {
doReturn(result).when(resolver).resolveArgument(Mockito.any(MethodParameter.class),
Mockito.any(ModelAndViewContainer.class), Mockito.any(NativeWebRequest.class),
Mockito.any(WebDataBinderFactory.class));
}
static class Foo {
@Id Long id;
}
}