From dc1016667941f4c59fb0bd385d1e6e58c24c1f1e Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Thu, 20 Dec 2018 16:25:35 +0100 Subject: [PATCH] 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. --- .../config/EntityLookupConfiguration.java | 8 ++ .../core/mapping/ExposureConfiguration.java | 18 ++- .../data/rest/core/support/EntityLookup.java | 8 ++ ...toryRestConfigurationIntegrationTests.java | 23 ++++ .../data/rest/core/domain/Profile.java | 2 + .../rest/core/domain/ProfileRepository.java | 5 +- .../data/rest/tests/shop/Product.java | 2 + .../rest/tests/shop/ShopConfiguration.java | 6 +- ...ResourceHandlerMethodArgumentResolver.java | 103 ++++++++++-------- .../RepositoryRestMvcConfiguration.java | 4 +- .../data/rest/webmvc/util/UriUtils.java | 12 +- ...andlerMethodArgumentResolverUnitTests.java | 33 +++++- ...rsistentEntityJackson2ModuleUnitTests.java | 10 +- 13 files changed, 167 insertions(+), 67 deletions(-) diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/EntityLookupConfiguration.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/EntityLookupConfiguration.java index 8481cb6c8..63e21d85e 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/EntityLookupConfiguration.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/EntityLookupConfiguration.java @@ -15,6 +15,7 @@ */ package org.springframework.data.rest.core.config; +import lombok.Getter; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.Value; @@ -30,6 +31,7 @@ import org.springframework.data.repository.core.support.AbstractRepositoryMetada import org.springframework.data.repository.support.Repositories; import org.springframework.data.rest.core.config.EntityLookupRegistrar.LookupRegistrar.Lookup; import org.springframework.data.rest.core.support.EntityLookup; +import org.springframework.data.util.MethodInvocationRecorder; import org.springframework.data.util.StreamUtils; import org.springframework.util.Assert; @@ -169,6 +171,7 @@ class EntityLookupConfiguration implements EntityLookupRegistrar { private final LookupInformation> lookupInfo; private final Repository repository; private final Class domainType; + private final @Getter Optional lookupProperty; /** * Creates a new {@link RepositoriesEntityLookup} for the given {@link Repositories} and {@link LookupInformation}. @@ -192,6 +195,11 @@ class EntityLookupConfiguration implements EntityLookupRegistrar { this.repository = (Repository) repositories.getRepositoryFor(information.getDomainType())// .orElseThrow(() -> new IllegalStateException( "No repository found for type " + information.getDomainType().getName() + "!")); + + this.lookupProperty = Optional.of(domainType) // + .flatMap(it -> MethodInvocationRecorder.forProxyOf(it) // + .record(lookupInfo.identifierMapping::convert) // + .getPropertyPath()); } /* diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ExposureConfiguration.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ExposureConfiguration.java index f49a76dcb..da7fe1f77 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ExposureConfiguration.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ExposureConfiguration.java @@ -125,7 +125,8 @@ public class ExposureConfiguration implements ExposureConfigurer { } /** - * Returns whether PUT is supported for the given {@link ResourceMetadata}. + * Returns whether PUT requests can be used to create new instances for the type backing the given + * {@link ResourceMetadata}. * * @param metadata must not be {@literal null}. * @return @@ -134,7 +135,20 @@ public class ExposureConfiguration implements ExposureConfigurer { Assert.notNull(metadata, "ResourceMetadata must not be null!"); - return creationViaPut.apply(metadata.getDomainType()); + return allowsPutForCreation(metadata.getDomainType()); + } + + /** + * Returns whether PUT requests can be used to create new instances of the given domain type. + * + * @param metadata must not be {@literal null}. + * @return + */ + public boolean allowsPutForCreation(Class domainType) { + + Assert.notNull(domainType, "Domain type must not be null!"); + + return creationViaPut.apply(domainType); } HttpMethods filter(ConfigurableHttpMethods methods, ResourceType type, ResourceMetadata metadata) { diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/EntityLookup.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/EntityLookup.java index 7b79b892e..b6e590350 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/EntityLookup.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/support/EntityLookup.java @@ -55,4 +55,12 @@ public interface EntityLookup extends Plugin> { * @return can be {@literal null}. */ Optional lookupEntity(Object id); + + /** + * Returns the lookup property if available. If {@link Optional#empty()} is returned, we assume the identifier + * property is the one to be used for lookup. + * + * @return will never be {@literal null}. + */ + Optional getLookupProperty(); } diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/RepositoryRestConfigurationIntegrationTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/RepositoryRestConfigurationIntegrationTests.java index 72ff8aed7..4e6dcbbcd 100755 --- a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/RepositoryRestConfigurationIntegrationTests.java +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/RepositoryRestConfigurationIntegrationTests.java @@ -19,9 +19,15 @@ import static org.assertj.core.api.Assertions.*; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.repository.support.Repositories; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.core.config.ResourceMapping; import org.springframework.data.rest.core.domain.ConfiguredPersonRepository; +import org.springframework.data.rest.core.domain.Profile; +import org.springframework.data.rest.core.domain.ProfileRepository; +import org.springframework.data.rest.core.support.EntityLookup; +import org.springframework.plugin.core.PluginRegistry; +import org.springframework.test.annotation.DirtiesContext; /** * Tests to check that {@link ResourceMapping}s are handled correctly. @@ -33,6 +39,7 @@ import org.springframework.data.rest.core.domain.ConfiguredPersonRepository; public class RepositoryRestConfigurationIntegrationTests extends AbstractIntegrationTests { @Autowired RepositoryRestConfiguration config; + @Autowired Repositories repositories; @Test public void shouldProvideResourceMappingForConfiguredRepository() throws Exception { @@ -44,4 +51,20 @@ public class RepositoryRestConfigurationIntegrationTests extends AbstractIntegra assertThat(mapping.getPath()).isEqualTo("people"); assertThat(mapping.isExported()).isFalse(); } + + @Test // DATAREST-1304 + @DirtiesContext + public void exposesLookupPropertyFromLambda() { + + config.withEntityLookup() // + .forRepository(ProfileRepository.class) // + .withIdMapping(Profile::getName) // + .withLookup(ProfileRepository::findByName); + + PluginRegistry, Class> lookups = PluginRegistry.of(config.getEntityLookups(repositories)); + + assertThat(lookups.getPluginFor(Profile.class)).hasValueSatisfying(it -> { + assertThat(it.getLookupProperty()).hasValue("name"); + }); + } } diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/domain/Profile.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/domain/Profile.java index 5198aa62b..4c39fb050 100644 --- a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/domain/Profile.java +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/domain/Profile.java @@ -16,6 +16,7 @@ package org.springframework.data.rest.core.domain; import lombok.Value; +import lombok.experimental.NonFinal; import java.util.UUID; @@ -25,6 +26,7 @@ import org.springframework.data.annotation.Id; * @author Jon Brisbin * @author Oliver Gierke */ +@NonFinal @Value public class Profile { diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/domain/ProfileRepository.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/domain/ProfileRepository.java index 7b78a7cf0..8888ba5ed 100644 --- a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/domain/ProfileRepository.java +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/domain/ProfileRepository.java @@ -25,4 +25,7 @@ import org.springframework.data.repository.CrudRepository; * @author Jon Brisbin * @author Oliver Gierke */ -public interface ProfileRepository extends CrudRepository {} +public interface ProfileRepository extends CrudRepository { + + Profile findByName(String name); +} diff --git a/spring-data-rest-tests/spring-data-rest-tests-shop/src/main/java/org/springframework/data/rest/tests/shop/Product.java b/spring-data-rest-tests/spring-data-rest-tests-shop/src/main/java/org/springframework/data/rest/tests/shop/Product.java index 4c416c34c..5c461a4e5 100644 --- a/spring-data-rest-tests/spring-data-rest-tests-shop/src/main/java/org/springframework/data/rest/tests/shop/Product.java +++ b/spring-data-rest-tests/spring-data-rest-tests-shop/src/main/java/org/springframework/data/rest/tests/shop/Product.java @@ -17,6 +17,7 @@ package org.springframework.data.rest.tests.shop; import lombok.RequiredArgsConstructor; import lombok.Value; +import lombok.experimental.NonFinal; import java.math.BigDecimal; import java.util.UUID; @@ -28,6 +29,7 @@ import org.springframework.data.rest.core.config.Projection; * @author Oliver Gierke * @author Craig Andrews */ +@NonFinal @Value @RequiredArgsConstructor public class Product { diff --git a/spring-data-rest-tests/spring-data-rest-tests-shop/src/test/java/org/springframework/data/rest/tests/shop/ShopConfiguration.java b/spring-data-rest-tests/spring-data-rest-tests-shop/src/test/java/org/springframework/data/rest/tests/shop/ShopConfiguration.java index 2d20b4232..78d5cd502 100644 --- a/spring-data-rest-tests/spring-data-rest-tests-shop/src/test/java/org/springframework/data/rest/tests/shop/ShopConfiguration.java +++ b/spring-data-rest-tests/spring-data-rest-tests-shop/src/test/java/org/springframework/data/rest/tests/shop/ShopConfiguration.java @@ -26,9 +26,9 @@ import org.springframework.data.map.repository.config.EnableMapRepositories; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.tests.shop.Customer.Gender; import org.springframework.data.rest.tests.shop.Product.ProductNameOnlyProjection; -import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurerAdapter; -import org.springframework.hateoas.Link; +import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer; import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.Link; import org.springframework.hateoas.server.RepresentationModelProcessor; /** @@ -96,7 +96,7 @@ public class ShopConfiguration { } @Configuration - static class SpringDataRestConfiguration extends RepositoryRestConfigurerAdapter { + static class SpringDataRestConfiguration implements RepositoryRestConfigurer { /* * (non-Javadoc) 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 6f226ff7a..e713fd8f2 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,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> messageConverters; + private final @NonNull List> messageConverters; + private final @NonNull RootResourceInformationHandlerMethodArgumentResolver resourceInformationResolver; + private final @NonNull BackendIdHandlerMethodArgumentResolver idResolver; + private final @NonNull DomainObjectReader reader; + private final @NonNull PluginRegistry, 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> 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 id = Optional .ofNullable(idResolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory)); Optional 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 accessor = entity.getPropertyAccessor(newObject); + PersistentProperty idProperty = entity.getRequiredIdProperty(); + boolean forUpdate = objectToUpdate.isPresent(); - Optional 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) // + .> 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(); + } } 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 d59304418..6ff2840ba 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 @@ -389,9 +389,11 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon @Bean public PersistentEntityResourceHandlerMethodArgumentResolver persistentEntityArgumentResolver() { + PluginRegistry, Class> lookups = PluginRegistry.of(getEntityLookups()); + return new PersistentEntityResourceHandlerMethodArgumentResolver(defaultMessageConverters(), repoRequestArgumentResolver(), backendIdHandlerMethodArgumentResolver(), - new DomainObjectReader(persistentEntities(), associationLinks())); + new DomainObjectReader(persistentEntities(), associationLinks()), lookups); } /** 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 730904137..876532124 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 @@ -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 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); } /** diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/config/PersistentEntityResourceHandlerMethodArgumentResolverUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/config/PersistentEntityResourceHandlerMethodArgumentResolverUnitTests.java index 784a87a51..10ee5b65e 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/config/PersistentEntityResourceHandlerMethodArgumentResolverUnitTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/config/PersistentEntityResourceHandlerMethodArgumentResolverUnitTests.java @@ -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.> asList(converter), rootResourceResolver, backendIdResolver, reader); + Arrays.> 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.> 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; } } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2ModuleUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2ModuleUnitTests.java index 1d6b378ed..90fb290ef 100755 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2ModuleUnitTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2ModuleUnitTests.java @@ -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.> emptyList()); + RepresentationModelProcessorInvoker invoker = new RepresentationModelProcessorInvoker( + Collections.> emptyList()); NestedEntitySerializer nestedEntitySerializer = new NestedEntitySerializer(persistentEntities, new EmbeddedResourcesAssembler(persistentEntities, associations, mock(ExcerptProjector.class)), invoker); @@ -201,6 +202,11 @@ public class PersistentEntityJackson2ModuleUnitTests { public Optional lookupEntity(Object id) { return Optional.of(new Home()); } + + @Override + public Optional getLookupProperty() { + return Optional.empty(); + } } @Getter