DATAREST-1304 - EntityLookupConfiguration is now used in PUT for creation.

Previously the backend identifier derived from the URI was set as identifier on the object to be created in a PUT request. However, in case a custom entity lookup is used, this is not the identifier but an arbitrary property pointed to through user defined code, mostly a method reference.

We now use the newly introduced MethodInvocationRecorder API in Spring Data Commons to be able to obtain the property that is supposed to be used and set that to the value calculated. This requires the entity type for which the custom lookup is configured to be non-final as the invocation recording is build on top of proxies.

Related tickets: DATACMNS-1449.
This commit is contained in:
Oliver Drotbohm
2018-12-20 16:25:35 +01:00
parent 6d0acfd997
commit dc10166679
13 changed files with 167 additions and 67 deletions

View File

@@ -15,6 +15,9 @@
*/
package org.springframework.data.rest.webmvc.config;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.io.IOException;
import java.io.Serializable;
import java.util.List;
@@ -25,8 +28,12 @@ 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.IdentifierAccessor;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.data.rest.core.support.EntityLookup;
import org.springframework.data.rest.webmvc.IncomingRequest;
import org.springframework.data.rest.webmvc.PersistentEntityResource;
import org.springframework.data.rest.webmvc.PersistentEntityResource.Builder;
@@ -40,7 +47,7 @@ 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.plugin.core.PluginRegistry;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
@@ -56,42 +63,19 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
* @author Jon Brisbin
* @author Oliver Gierke
*/
@RequiredArgsConstructor
public class PersistentEntityResourceHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
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 resourceInformationResolver;
private final BackendIdHandlerMethodArgumentResolver idResolver;
private final DomainObjectReader reader;
private final List<HttpMessageConverter<?>> messageConverters;
private final @NonNull List<HttpMessageConverter<?>> messageConverters;
private final @NonNull RootResourceInformationHandlerMethodArgumentResolver resourceInformationResolver;
private final @NonNull BackendIdHandlerMethodArgumentResolver idResolver;
private final @NonNull DomainObjectReader reader;
private final @NonNull PluginRegistry<EntityLookup<?>, Class<?>> lookups;
private final ConversionService conversionService = new DefaultConversionService();
/**
* Creates a new {@link PersistentEntityResourceHandlerMethodArgumentResolver} for the given
* {@link HttpMessageConverter}s and {@link RootResourceInformationHandlerMethodArgumentResolver}..
*
* @param messageConverters 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 resourceInformationResolver,
BackendIdHandlerMethodArgumentResolver idResolver, DomainObjectReader reader) {
Assert.notEmpty(messageConverters, "MessageConverters must not be null or 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.resourceInformationResolver = resourceInformationResolver;
this.idResolver = idResolver;
this.reader = reader;
}
/*
* (non-Javadoc)
* @see org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter(org.springframework.core.MethodParameter)
@@ -129,31 +113,45 @@ public class PersistentEntityResourceHandlerMethodArgumentResolver implements Ha
Optional<Serializable> id = Optional
.ofNullable(idResolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory));
Optional<Object> objectToUpdate = id.flatMap(it -> resourceInformation.getInvoker().invokeFindById(it));
Object newObject = read(resourceInformation, incoming, converter, objectToUpdate);
Object obj = read(resourceInformation, incoming, converter, objectToUpdate);
if (obj == null) {
throw new HttpMessageNotReadableException(String.format(ERROR_MESSAGE, domainType));
if (newObject == null) {
throw new HttpMessageNotReadableException(String.format(ERROR_MESSAGE, domainType), request);
}
PersistentEntity<?, ?> entity = resourceInformation.getPersistentEntity();
if (!id.isPresent()) {
return toResource(newObject, entity, false);
}
PersistentPropertyAccessor<Object> accessor = entity.getPropertyAccessor(newObject);
PersistentProperty<?> idProperty = entity.getRequiredIdProperty();
boolean forUpdate = objectToUpdate.isPresent();
Optional<Object> entityIdentifier = objectToUpdate.map(it -> entity.getIdentifierAccessor(it).getIdentifier());
entityIdentifier.ifPresent(it -> entity.getPropertyAccessor(obj).setProperty(entity.getRequiredIdProperty(),
entityIdentifier.orElse(null)));
// Transfer identifier from existing object
objectToUpdate.map(entity::getIdentifierAccessor) //
.map(IdentifierAccessor::getIdentifier) //
.ifPresent(it -> accessor.setProperty(idProperty, it));
id.ifPresent(it -> {
ConvertingPropertyAccessor accessor = new ConvertingPropertyAccessor(entity.getPropertyAccessor(obj),
conversionService);
accessor.setProperty(entity.getRequiredIdProperty(), it);
});
if (!forUpdate) {
Builder build = PersistentEntityResource.build(obj, entity);
return forUpdate ? build.build() : build.forCreation();
// Find property to map URI derived value from
PersistentProperty<?> propertyToSet = lookups.getPluginFor(domainType) //
.flatMap(EntityLookup::getLookupProperty) //
.<PersistentProperty<?>> map(entity::getPersistentProperty) //
.orElseGet(() -> idProperty);
// Transfer onto new object
new ConvertingPropertyAccessor(accessor, conversionService) //
.setProperty(propertyToSet, id.get());
}
return toResource(accessor.getBean(), entity, forUpdate);
}
throw new HttpMessageNotReadableException(String.format(NO_CONVERTER_FOUND, domainType, contentType));
throw new HttpMessageNotReadableException(String.format(NO_CONVERTER_FOUND, domainType, contentType), request);
}
/**
@@ -205,7 +203,8 @@ public class PersistentEntityResourceHandlerMethodArgumentResolver implements Ha
throw (HttpMessageNotReadableException) o_O;
}
throw new HttpMessageNotReadableException(String.format(ERROR_MESSAGE, existingObject.getClass()), o_O);
throw new HttpMessageNotReadableException(String.format(ERROR_MESSAGE, existingObject.getClass()), o_O,
request.getServerHttpRequest());
}
}
@@ -219,7 +218,8 @@ public class PersistentEntityResourceHandlerMethodArgumentResolver implements Ha
return handler.applyPut((ObjectNode) jsonNode, existingObject);
} catch (Exception o_O) {
throw new HttpMessageNotReadableException(String.format(ERROR_MESSAGE, existingObject.getClass()), o_O);
throw new HttpMessageNotReadableException(String.format(ERROR_MESSAGE, existingObject.getClass()), o_O,
request.getServerHttpRequest());
}
}
@@ -229,7 +229,14 @@ public class PersistentEntityResourceHandlerMethodArgumentResolver implements Ha
try {
return converter.read(information.getDomainType(), request.getServerHttpRequest());
} catch (IOException o_O) {
throw new HttpMessageNotReadableException(String.format(ERROR_MESSAGE, information.getDomainType()), o_O);
throw new HttpMessageNotReadableException(String.format(ERROR_MESSAGE, information.getDomainType()), o_O,
request.getServerHttpRequest());
}
}
private PersistentEntityResource toResource(Object bean, PersistentEntity<?, ?> entity, boolean forUpdate) {
Builder build = PersistentEntityResource.build(bean, entity);
return forUpdate ? build.build() : build.forCreation();
}
}

View File

@@ -389,9 +389,11 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
@Bean
public PersistentEntityResourceHandlerMethodArgumentResolver persistentEntityArgumentResolver() {
PluginRegistry<EntityLookup<?>, Class<?>> lookups = PluginRegistry.of(getEntityLookups());
return new PersistentEntityResourceHandlerMethodArgumentResolver(defaultMessageConverters(),
repoRequestArgumentResolver(), backendIdHandlerMethodArgumentResolver(),
new DomainObjectReader(persistentEntities(), associationLinks()));
new DomainObjectReader(persistentEntities(), associationLinks()), lookups);
}
/**

View File

@@ -17,7 +17,6 @@ package org.springframework.data.rest.webmvc.util;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import org.springframework.hateoas.server.core.AnnotationMappingDiscoverer;
import org.springframework.util.Assert;
@@ -51,14 +50,9 @@ public abstract class UriUtils {
String mapping = DISCOVERER.getMapping(method);
Map<String, String> variables = new org.springframework.web.util.UriTemplate(mapping).match(lookupPath);
String value = variables.get(variable);
if (value != null) {
return value;
}
return null;
return new org.springframework.web.util.UriTemplate(mapping) //
.match(lookupPath) //
.get(variable);
}
/**

View File

@@ -20,6 +20,7 @@ import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.util.Arrays;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
@@ -31,6 +32,7 @@ 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.core.support.EntityLookup;
import org.springframework.data.rest.webmvc.PersistentEntityResource;
import org.springframework.data.rest.webmvc.RootResourceInformation;
import org.springframework.data.rest.webmvc.json.DomainObjectReader;
@@ -39,6 +41,7 @@ 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.plugin.core.PluginRegistry;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
@@ -75,7 +78,8 @@ public class PersistentEntityResourceHandlerMethodArgumentResolverUnitTests {
public void returnsAggregateInstanceWithIdentifierPopulatedForPutRequests() throws Exception {
PersistentEntityResourceHandlerMethodArgumentResolver argumentResolver = new PersistentEntityResourceHandlerMethodArgumentResolver(
Arrays.<HttpMessageConverter<?>> asList(converter), rootResourceResolver, backendIdResolver, reader);
Arrays.<HttpMessageConverter<?>> asList(converter), rootResourceResolver, backendIdResolver, reader,
PluginRegistry.empty());
HttpServletRequest request = new MockHttpServletRequest("PUT", "/foo/4711");
@@ -89,6 +93,32 @@ public class PersistentEntityResourceHandlerMethodArgumentResolverUnitTests {
});
}
@Test // DATAREST-1304
@SuppressWarnings("unchecked")
public void setsLookupPropertyForEntitiesWithCustomLookup() throws Exception {
EntityLookup<?> lookup = mock(EntityLookup.class);
doReturn(Optional.of("name")).when(lookup).getLookupProperty();
doReturn(true).when(lookup).supports(Foo.class);
PersistentEntityResourceHandlerMethodArgumentResolver argumentResolver = new PersistentEntityResourceHandlerMethodArgumentResolver(
Arrays.<HttpMessageConverter<?>> asList(converter), rootResourceResolver, backendIdResolver, reader,
PluginRegistry.of(Arrays.asList(lookup)));
HttpServletRequest request = new MockHttpServletRequest("PUT", "/foo/someName");
doReturn(new Foo()).when(converter).read(Mockito.any(Class.class), Mockito.any(HttpInputMessage.class));
mockInvocationOfResolver(backendIdResolver, "someName");
Object result = argumentResolver.resolveArgument(null, null, new ServletWebRequest(request), null);
assertThat(result).isInstanceOfSatisfying(PersistentEntityResource.class, it -> {
assertThat(it.getContent()).isInstanceOfSatisfying(Foo.class, foo -> {
assertThat(foo.name).isEqualTo("someName");
});
});
}
private void setupRootResourceInfoFor(Class<?> type) throws Exception {
RootResourceInformation information = mock(RootResourceInformation.class);
@@ -113,5 +143,6 @@ public class PersistentEntityResourceHandlerMethodArgumentResolverUnitTests {
static class Foo {
@Id Long id;
String name;
}
}

View File

@@ -49,9 +49,9 @@ import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module.
import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module.NestedEntitySerializer;
import org.springframework.data.rest.webmvc.mapping.Associations;
import org.springframework.data.rest.webmvc.support.ExcerptProjector;
import org.springframework.hateoas.UriTemplate;
import org.springframework.hateoas.server.EntityLinks;
import org.springframework.hateoas.server.RepresentationModelProcessor;
import org.springframework.hateoas.UriTemplate;
import org.springframework.hateoas.server.mvc.RepresentationModelProcessorInvoker;
import org.springframework.plugin.core.PluginRegistry;
@@ -92,7 +92,8 @@ public class PersistentEntityJackson2ModuleUnitTests {
this.persistentEntities = new PersistentEntities(Arrays.asList(mappingContext));
RepresentationModelProcessorInvoker invoker = new RepresentationModelProcessorInvoker(Collections.<RepresentationModelProcessor<?>> emptyList());
RepresentationModelProcessorInvoker invoker = new RepresentationModelProcessorInvoker(
Collections.<RepresentationModelProcessor<?>> emptyList());
NestedEntitySerializer nestedEntitySerializer = new NestedEntitySerializer(persistentEntities,
new EmbeddedResourcesAssembler(persistentEntities, associations, mock(ExcerptProjector.class)), invoker);
@@ -201,6 +202,11 @@ public class PersistentEntityJackson2ModuleUnitTests {
public Optional<Home> lookupEntity(Object id) {
return Optional.of(new Home());
}
@Override
public Optional<String> getLookupProperty() {
return Optional.empty();
}
}
@Getter