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..e41be1364 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; @@ -78,6 +77,7 @@ class RepositoryPropertyReferenceController /*extends AbstractRepositoryRestCont private static final String BASE_MAPPING = "/{repository}/{id}/{property}"; private static final Collection AUGMENTING_METHODS = Arrays.asList(HttpMethod.PATCH, HttpMethod.POST); + private static final List SUPPORTED_CONTENT_TYPE = Arrays.asList(MediaType.APPLICATION_JSON, TEXT_URI_LIST); private final Repositories repositories; private final RepositoryInvokerFactory repositoryInvokerFactory; @@ -237,17 +237,35 @@ public ResponseEntity> followPropertyReferenceCompact(Roo consumes = { MediaType.APPLICATION_JSON_VALUE, SPRING_DATA_COMPACT_JSON_VALUE, TEXT_URI_LIST_VALUE }) public ResponseEntity> createPropertyReference( RootResourceInformation resourceInformation, HttpMethod requestMethod, - @RequestBody(required = false) CollectionModel incoming, @BackendId Serializable id, + @RequestBody(required = false) CollectionModel incoming, + @RequestHeader(required = false) HttpHeaders requestHeaders, + @BackendId Serializable id, @PathVariable String property) throws Exception { var source = incoming == null ? CollectionModel.empty() : incoming; var invoker = resourceInformation.getInvoker(); + MediaType contentType = requestHeaders == null ? null : requestHeaders.getContentType(); + Function> handler = prop -> { Class propertyType = prop.property.getType(); if (prop.property.isCollectionLike()) { + /*if(HttpMethod.PATCH.equals(requestMethod) + || HttpMethod.POST.equals(requestMethod) + || HttpMethod.PUT.equals(requestMethod)) { + if(contentType == null + || (!TEXT_URI_LIST.isCompatibleWith(contentType) + && !MediaType.APPLICATION_JSON.isCompatibleWith(contentType)) + ) { + throw new UnsupportedMediaTypeStatusException("Unsuppoted Content Type", SUPPORTED_CONTENT_TYPE); + } + }*/ + 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 +279,20 @@ public ResponseEntity> createPropertyReference( prop.accessor.setProperty(prop.property, collection); } else if (prop.property.isMap()) { + /*if(HttpMethod.PATCH.equals(requestMethod) + || HttpMethod.POST.equals(requestMethod) + || HttpMethod.PUT.equals(requestMethod)) { + if(contentType == null + || (!TEXT_URI_LIST.isCompatibleWith(contentType) + && !MediaType.APPLICATION_JSON.isCompatibleWith(contentType)) + ) { + throw new UnsupportedMediaTypeStatusException("Unsuppoted Content Type", SUPPORTED_CONTENT_TYPE); + } + }*/ + 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 +315,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..9c0171576 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.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import static org.springframework.data.rest.webmvc.RestMediaTypes.TEXT_URI_LIST; -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; @@ -45,7 +44,9 @@ import org.springframework.data.rest.core.mapping.SupportedHttpMethods; import org.springframework.hateoas.CollectionModel; import org.springframework.hateoas.Link; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; +import org.springframework.http.converter.HttpMessageNotReadableException; /** * Unit tests for {@link RepositoryPropertyReferenceController}. @@ -84,17 +85,142 @@ void usesRepositoryInvokerToLookupRelatedInstance() throws Exception { RootResourceInformation information = new RootResourceInformation(metadata, entity, invoker); CollectionModel request = CollectionModel.empty(Link.of("/reference/some-id")); - controller.createPropertyReference(information, HttpMethod.POST, request, 4711, "references"); + controller.createPropertyReference(information, HttpMethod.POST, request, HttpHeaders.EMPTY, 4711, "references"); verify(invokerFactory).getInvokerFor(Reference.class); 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, HttpHeaders.EMPTY, 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, HttpHeaders.EMPTY, 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); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(TEXT_URI_LIST); + + //Do we need integration test to verify HTTP response code? + Throwable thrown = catchThrowable(() -> controller.createPropertyReference(information, HttpMethod.POST, null, headers, 4711, + "reference")); + assertThat(thrown).isInstanceOf(HttpMessageNotReadableException.class); + + verify(invokerFactory, never()).getInvokerFor(Reference.class); + verify(invoker, never()).invokeFindById("some-id"); + } + + /*@Test // GH-2495 + void rejectsInvalidContentTypeForSingleValuedAssociation() 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); + CollectionModel request = CollectionModel.empty(Link.of("/reference/some-id")); + + //Do we need integration test to verify HTTP response code? + Throwable thrown = catchThrowable(() -> controller.createPropertyReference(information, HttpMethod.POST, null, HttpHeaders.EMPTY, 4711, + "references")); + 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 {}