diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java index 21eb819f7..70766124f 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java @@ -19,13 +19,10 @@ import static org.springframework.data.rest.webmvc.RestMediaTypes.*; import static org.springframework.web.bind.annotation.RequestMethod.*; +import java.io.InputStream; import java.io.Serializable; -import java.util.Arrays; -import java.util.Collection; -import java.util.Iterator; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; -import java.util.Optional; import java.util.function.Function; import org.springframework.context.ApplicationEventPublisher; @@ -42,6 +39,7 @@ import org.springframework.data.rest.core.event.BeforeLinkDeleteEvent; import org.springframework.data.rest.core.event.BeforeLinkSaveEvent; import org.springframework.data.rest.webmvc.support.BackendId; +import org.springframework.data.rest.webmvc.util.InputStreamHttpInputMessage; import org.springframework.hateoas.CollectionModel; import org.springframework.hateoas.EntityModel; import org.springframework.hateoas.IanaLinkRelations; @@ -54,6 +52,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -248,6 +247,10 @@ public ResponseEntity> createPropertyReference( Class propertyType = prop.property.getType(); if (prop.property.isCollectionLike()) { + if (source.getLinks().isEmpty()) { + throw new HttpMessageNotReadableException("No links provided", + InputStreamHttpInputMessage.of(InputStream.nullInputStream())); + } Collection collection = AUGMENTING_METHODS.contains(requestMethod) // ? (Collection) prop.propertyValue // @@ -261,6 +264,10 @@ public ResponseEntity> createPropertyReference( prop.accessor.setProperty(prop.property, collection); } else if (prop.property.isMap()) { + if (source.getLinks().isEmpty()) { + throw new HttpMessageNotReadableException("No links provided", + InputStreamHttpInputMessage.of(InputStream.nullInputStream())); + } Map map = AUGMENTING_METHODS.contains(requestMethod) // ? (Map) prop.propertyValue // @@ -283,8 +290,9 @@ public ResponseEntity> createPropertyReference( } if (!source.getLinks().hasSingleLink()) { - throw new IllegalArgumentException( - "Must send only 1 link to update a property reference that isn't a List or a Map."); + throw new HttpMessageNotReadableException( + "Must send only 1 link to update a property reference that isn't a List or a Map.", + InputStreamHttpInputMessage.of(InputStream.nullInputStream())); } prop.accessor.setProperty(prop.property, diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceControllerUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceControllerUnitTests.java index c2d9c1f94..491b343e5 100755 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceControllerUnitTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceControllerUnitTests.java @@ -15,13 +15,12 @@ */ package org.springframework.data.rest.webmvc; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; +import java.util.*; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -46,6 +45,7 @@ import org.springframework.hateoas.CollectionModel; import org.springframework.hateoas.Link; import org.springframework.http.HttpMethod; +import org.springframework.http.converter.HttpMessageNotReadableException; /** * Unit tests for {@link RepositoryPropertyReferenceController}. @@ -90,11 +90,105 @@ void usesRepositoryInvokerToLookupRelatedInstance() throws Exception { verify(invoker).invokeFindById("some-id"); } - @RestResource + @Test // DATAREST-2495 + void rejectsEmptyLinksForAssociationUpdate() throws Exception { + + KeyValuePersistentEntity entity = mappingContext.getRequiredPersistentEntity(Sample.class); + + ResourceMappings mappings = new PersistentEntitiesResourceMappings( + new PersistentEntities(Collections.singleton(mappingContext))); + ResourceMetadata metadata = spy(mappings.getMetadataFor(Sample.class)); + when(metadata.getSupportedHttpMethods()).thenReturn(AllSupportedHttpMethods.INSTANCE); + + RepositoryPropertyReferenceController controller = new RepositoryPropertyReferenceController(repositories, + invokerFactory); + controller.setApplicationEventPublisher(publisher); + + doReturn(Optional.of(new Sample())).when(invoker).invokeFindById(4711); + + RootResourceInformation information = new RootResourceInformation(metadata, entity, invoker); + + // Do we need integration test to verify HTTP response code? + Throwable thrown = catchThrowable( + () -> controller.createPropertyReference(information, HttpMethod.POST, null, 4711, "references")); + assertThat(thrown).isInstanceOf(HttpMessageNotReadableException.class); + + verify(invoker, never()).invokeFindById("some-id"); + } + + @Test // GH-2495 + void rejectsMultipleLinksForSingleValuedAssociation() throws Exception { + + KeyValuePersistentEntity entity = mappingContext.getRequiredPersistentEntity(SingleSample.class); + + ResourceMappings mappings = new PersistentEntitiesResourceMappings( + new PersistentEntities(Collections.singleton(mappingContext))); + ResourceMetadata metadata = spy(mappings.getMetadataFor(SingleSample.class)); + when(metadata.getSupportedHttpMethods()).thenReturn(AllSupportedHttpMethods.INSTANCE); + + RepositoryPropertyReferenceController controller = new RepositoryPropertyReferenceController(repositories, + invokerFactory); + controller.setApplicationEventPublisher(publisher); + + doReturn(Optional.of(new SingleSample())).when(invoker).invokeFindById(4711); + + RootResourceInformation information = new RootResourceInformation(metadata, entity, invoker); + CollectionModel request = CollectionModel + .empty(List.of(Link.of("/reference/some-id"), Link.of("/reference/some-another-id"))); + + // Do we need integration test to verify HTTP response code? + Throwable thrown = catchThrowable( + () -> controller.createPropertyReference(information, HttpMethod.POST, request, 4711, "reference")); + assertThat(thrown).isInstanceOf(HttpMessageNotReadableException.class); + + verify(invokerFactory, never()).getInvokerFor(Reference.class); + verify(invoker, never()).invokeFindById("some-id"); + verify(invoker, never()).invokeFindById("some-another-id"); + } + + @Test // GH-2495 + void rejectsMapLinksForSingleValuedAssociation() throws Exception { + + KeyValuePersistentEntity entity = mappingContext.getRequiredPersistentEntity(MapSample.class); + + ResourceMappings mappings = new PersistentEntitiesResourceMappings( + new PersistentEntities(Collections.singleton(mappingContext))); + ResourceMetadata metadata = spy(mappings.getMetadataFor(MapSample.class)); + when(metadata.getSupportedHttpMethods()).thenReturn(AllSupportedHttpMethods.INSTANCE); + + RepositoryPropertyReferenceController controller = new RepositoryPropertyReferenceController(repositories, + invokerFactory); + controller.setApplicationEventPublisher(publisher); + + doReturn(Optional.of(new MapSample())).when(invoker).invokeFindById(4711); + + RootResourceInformation information = new RootResourceInformation(metadata, entity, invoker); + + // Do we need integration test to verify HTTP response code? + Throwable thrown = catchThrowable( + () -> controller.createPropertyReference(information, HttpMethod.POST, null, 4711, "reference")); + assertThat(thrown).isInstanceOf(HttpMessageNotReadableException.class); + + verify(invokerFactory, never()).getInvokerFor(Reference.class); + verify(invoker, never()).invokeFindById("some-id"); + } + + + @RestResource static class Sample { @org.springframework.data.annotation.Reference List references = new ArrayList(); } + @RestResource + static class SingleSample { + @org.springframework.data.annotation.Reference Reference reference; + } + + @RestResource + static class MapSample { + @org.springframework.data.annotation.Reference Map reference; + } + @RestResource static class Reference {}