From 6d0034f15f292922d9d10812dd73503acea84e28 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Thu, 2 Mar 2023 17:20:09 +0100 Subject: [PATCH] Support for query methods returning a Slice. Built on the the just introduced RepresentationModelAssembler implementations based on Slice in Spring HATEOAS and Spring Data Commons we now support returning a SlicedModel from the controller backing search resources ultimately triggering repository query methods. The introduction triggered the refactoring to introduce RepresentationModelAssemblers (RMA) to remove the need for controllers inheriting from AbstractRepositoryController to access RepresentationModel assembly functionality. RMA acts as a facade for both Paged-/SlicedResourceAssembler as well as PersistentEntityResourceAssembler. Fixes #2235. --- ...tyReferenceControllerIntegrationTests.java | 10 +- ...itorySearchControllerIntegrationTests.java | 49 +++++- .../data/rest/webmvc/jpa/JpaWebTests.java | 2 - .../AbstractRepositoryRestController.java | 119 --------------- .../webmvc/BasePathAwareHandlerMapping.java | 12 +- .../data/rest/webmvc/ControllerUtils.java | 6 + .../rest/webmvc/PersistentEntityResource.java | 1 - .../rest/webmvc/RepositoryController.java | 28 ++-- .../webmvc/RepositoryEntityController.java | 46 +++--- ...RepositoryPropertyReferenceController.java | 73 ++++----- .../webmvc/RepositoryRestHandlerMapping.java | 2 +- .../webmvc/RepositorySearchController.java | 119 +++++++-------- .../webmvc/RepresentationModelAssemblers.java | 142 ++++++++++++++++++ .../webmvc/RestControllerConfiguration.java | 26 ++-- .../rest/webmvc/RootResourceInformation.java | 18 ++- ...rverHttpRequestMethodArgumentResolver.java | 29 ---- ...tityResourceAssemblerArgumentResolver.java | 3 +- .../RepositoryRestMvcConfiguration.java | 85 ++++++----- ...tationModelAssemblersArgumentResolver.java | 78 ++++++++++ .../data/rest/webmvc/package-info.java | 2 + .../RepositoryEntityControllerTest.java | 8 +- ...yPropertyReferenceControllerUnitTests.java | 4 +- ...RepositoryRestHandlerMappingUnitTests.java | 2 +- ...ryRestMvConfigurationIntegrationTests.java | 1 + src/main/asciidoc/paging-and-sorting.adoc | 5 +- 25 files changed, 482 insertions(+), 388 deletions(-) delete mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/AbstractRepositoryRestController.java create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepresentationModelAssemblers.java delete mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ServerHttpRequestMethodArgumentResolver.java create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepresentationModelAssemblersArgumentResolver.java create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/package-info.java diff --git a/spring-data-rest-tests/spring-data-rest-tests-jpa/src/test/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceControllerIntegrationTests.java b/spring-data-rest-tests/spring-data-rest-tests-jpa/src/test/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceControllerIntegrationTests.java index 6df0c4fcf..03a09eea1 100755 --- a/spring-data-rest-tests/spring-data-rest-tests-jpa/src/test/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceControllerIntegrationTests.java +++ b/spring-data-rest-tests/spring-data-rest-tests-jpa/src/test/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceControllerIntegrationTests.java @@ -16,10 +16,13 @@ package org.springframework.data.rest.webmvc; import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.rest.tests.AbstractControllerIntegrationTests; import org.springframework.data.rest.webmvc.jpa.Book; @@ -33,6 +36,7 @@ import org.springframework.transaction.annotation.Transactional; /** * @author Oliver Gierke */ +@ExtendWith(MockitoExtension.class) @ContextConfiguration(classes = JpaRepositoryConfig.class) @Transactional class RepositoryPropertyReferenceControllerIntegrationTests extends AbstractControllerIntegrationTests { @@ -41,13 +45,11 @@ class RepositoryPropertyReferenceControllerIntegrationTests extends AbstractCont @Autowired TestDataPopulator populator; @Autowired BookRepository books; - PersistentEntityResourceAssembler assembler; + @Mock(answer = Answers.RETURNS_MOCKS) RepresentationModelAssemblers assembler; RootResourceInformation information; @BeforeEach void setUp() { - - this.assembler = mock(PersistentEntityResourceAssembler.class); this.information = getResourceInformation(Book.class); this.populator.populateRepositories(); } diff --git a/spring-data-rest-tests/spring-data-rest-tests-jpa/src/test/java/org/springframework/data/rest/webmvc/RepositorySearchControllerIntegrationTests.java b/spring-data-rest-tests/spring-data-rest-tests-jpa/src/test/java/org/springframework/data/rest/webmvc/RepositorySearchControllerIntegrationTests.java index ad9243d22..9e21daa5c 100755 --- a/spring-data-rest-tests/spring-data-rest-tests-jpa/src/test/java/org/springframework/data/rest/webmvc/RepositorySearchControllerIntegrationTests.java +++ b/spring-data-rest-tests/spring-data-rest-tests-jpa/src/test/java/org/springframework/data/rest/webmvc/RepositorySearchControllerIntegrationTests.java @@ -16,11 +16,17 @@ 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.tests.TestMvcClient.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Answers; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.data.rest.core.mapping.ResourceMetadata; @@ -35,6 +41,7 @@ import org.springframework.data.rest.webmvc.jpa.JpaRepositoryConfig; import org.springframework.data.rest.webmvc.jpa.Person; import org.springframework.data.rest.webmvc.jpa.TestDataPopulator; import org.springframework.data.rest.webmvc.support.DefaultedPageable; +import org.springframework.data.web.PagedResourcesAssembler; import org.springframework.hateoas.CollectionModel; import org.springframework.hateoas.PagedModel; import org.springframework.hateoas.RepresentationModel; @@ -61,11 +68,16 @@ class RepositorySearchControllerIntegrationTests extends AbstractControllerInteg @Autowired TestDataPopulator loader; @Autowired RepositorySearchController controller; - @Autowired PersistentEntityResourceAssembler assembler; + @Autowired PagedResourcesAssembler pagedResourcesAssembler; + @Autowired PersistentEntityResourceAssembler entityResourceAssembler; + + RepresentationModelAssemblers assembler; @BeforeEach void setUp() { loader.populateRepositories(); + + this.assembler = mock(RepresentationModelAssemblers.class, Answers.RETURNS_SMART_NULLS); } @Test @@ -76,7 +88,8 @@ class RepositorySearchControllerIntegrationTests extends AbstractControllerInteg ResourceTester tester = ResourceTester.of(resource); tester.assertNumberOfLinks(7); // Self link included - tester.assertHasLinkEndingWith("findFirstPersonByFirstName", "findFirstPersonByFirstName{?firstname,projection}"); + tester.assertHasLinkEndingWith("findFirstPersonByFirstName", + "findFirstPersonByFirstName{?firstname,projection}"); tester.assertHasLinkEndingWith("firstname", "firstname{?firstname,page,size,sort,projection}"); tester.assertHasLinkEndingWith("lastname", "lastname{?lastname,sort,projection}"); tester.assertHasLinkEndingWith("findByCreatedUsingISO8601Date", @@ -107,8 +120,19 @@ class RepositorySearchControllerIntegrationTests extends AbstractControllerInteg MultiValueMap parameters = new LinkedMultiValueMap(1); parameters.add("firstname", "John"); + doAnswer(new Answer>() { + + @Override + public CollectionModel answer(InvocationOnMock invocation) throws Throwable { + + var page = (Page) invocation.getArgument(0); + + return pagedResourcesAssembler.toModel(page, entityResourceAssembler); + } + }).when(assembler).toCollectionModel(any(), any()); + ResponseEntity response = controller.executeSearch(resourceInformation, parameters, "firstname", PAGEABLE, - Sort.unsorted(), assembler, new HttpHeaders()); + Sort.unsorted(), new HttpHeaders(), assembler); ResourceTester tester = ResourceTester.of(response.getBody()); PagedModel pagedResources = tester.assertIsPage(); @@ -173,13 +197,22 @@ class RepositorySearchControllerIntegrationTests extends AbstractControllerInteg @Test // DATAREST-502 void interpretsUriAsReferenceToRelatedEntity() { - MultiValueMap parameters = new LinkedMultiValueMap(1); + var parameters = new LinkedMultiValueMap(1); parameters.add("author", "/author/1"); - RootResourceInformation resourceInformation = getResourceInformation(Book.class); + var resourceInformation = getResourceInformation(Book.class); - ResponseEntity result = controller.executeSearch(resourceInformation, parameters, "findByAuthorsContains", - PAGEABLE, Sort.unsorted(), assembler, new HttpHeaders()); + when(assembler.toCollectionModel(any(), any())) + .thenAnswer(new Answer>() { + + @Override + public CollectionModel answer(InvocationOnMock invocation) throws Throwable { + return CollectionModel.of(invocation.getArgument(0)); + } + }); + + var result = controller.executeSearch(resourceInformation, parameters, "findByAuthorsContains", PAGEABLE, + Sort.unsorted(), new HttpHeaders(), assembler); assertThat(result.getBody()).isInstanceOf(CollectionModel.class); } @@ -199,7 +232,7 @@ class RepositorySearchControllerIntegrationTests extends AbstractControllerInteg parameters.add("lastname", "Thornton"); ResponseEntity entity = controller.executeSearch(getResourceInformation(Person.class), parameters, - "findCreatedDateByLastName", PAGEABLE, Sort.unsorted(), assembler, new HttpHeaders()); + "findCreatedDateByLastName", PAGEABLE, Sort.unsorted(), new HttpHeaders(), assembler); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(entity.getHeaders()).isEmpty(); diff --git a/spring-data-rest-tests/spring-data-rest-tests-jpa/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java b/spring-data-rest-tests/spring-data-rest-tests-jpa/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java index acd919fbe..36a9934d6 100755 --- a/spring-data-rest-tests/spring-data-rest-tests-jpa/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java +++ b/spring-data-rest-tests/spring-data-rest-tests-jpa/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java @@ -53,7 +53,6 @@ import org.springframework.hateoas.server.RepresentationModelProcessor; import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.web.servlet.result.MockMvcResultHandlers; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -556,7 +555,6 @@ public class JpaWebTests extends CommonWebTests { // Assert results returned as specified client.follow(findBySortedLink.expand(Arrays.asList("title", "desc"))).// - andDo(MockMvcResultHandlers.print()).// andExpect(jsonPath("$._embedded.books[0].title").value("Spring Data (Second Edition)")).// andExpect(jsonPath("$._embedded.books[1].title").value("Spring Data")).// andExpect(client.hasLinkWithRel(IanaLinkRelations.SELF)); diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/AbstractRepositoryRestController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/AbstractRepositoryRestController.java deleted file mode 100644 index c18183ca0..000000000 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/AbstractRepositoryRestController.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright 2012-2023 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 - * - * https://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; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; - -import org.springframework.data.auditing.AuditableBeanWrapperFactory; -import org.springframework.data.domain.Page; -import org.springframework.data.rest.core.mapping.ResourceMetadata; -import org.springframework.data.web.PagedResourcesAssembler; -import org.springframework.hateoas.CollectionModel; -import org.springframework.hateoas.EntityModel; -import org.springframework.hateoas.IanaLinkRelations; -import org.springframework.hateoas.Link; -import org.springframework.hateoas.LinkRelation; -import org.springframework.hateoas.PagedModel; -import org.springframework.hateoas.server.core.EmbeddedWrappers; -import org.springframework.util.Assert; -import org.springframework.web.servlet.support.ServletUriComponentsBuilder; - -/** - * @author Jon Brisbin - * @author Oliver Gierke - * @author Thibaud Lepretre - */ -@SuppressWarnings({ "rawtypes" }) -class AbstractRepositoryRestController { - - private static final EmbeddedWrappers WRAPPERS = new EmbeddedWrappers(false); - - private final PagedResourcesAssembler pagedResourcesAssembler; - - /** - * Creates a new {@link AbstractRepositoryRestController} for the given {@link PagedResourcesAssembler} and - * {@link AuditableBeanWrapperFactory}. - * - * @param pagedResourcesAssembler must not be {@literal null}. - */ - public AbstractRepositoryRestController(PagedResourcesAssembler pagedResourcesAssembler) { - - Assert.notNull(pagedResourcesAssembler, "PagedResourcesAssembler must not be null"); - - this.pagedResourcesAssembler = pagedResourcesAssembler; - } - - protected Link resourceLink(RootResourceInformation resourceLink, EntityModel resource) { - - ResourceMetadata repoMapping = resourceLink.getResourceMetadata(); - - Link selfLink = resource.getRequiredLink(IanaLinkRelations.SELF); - LinkRelation rel = repoMapping.getItemResourceRel(); - - return Link.of(selfLink.getHref(), rel); - } - - @SuppressWarnings({ "unchecked" }) - protected CollectionModel toCollectionModel(Iterable source, PersistentEntityResourceAssembler assembler, - Class domainType, Optional baseLink) { - - if (source instanceof Page) { - Page page = (Page) source; - return entitiesToResources(page, assembler, domainType, baseLink); - } else if (source instanceof Iterable) { - return entitiesToResources((Iterable) source, assembler, domainType); - } else { - return CollectionModel.empty(); - } - } - - protected CollectionModel entitiesToResources(Page page, PersistentEntityResourceAssembler assembler, - Class domainType, Optional baseLink) { - - if (page.getContent().isEmpty()) { - return baseLink.> map(it -> pagedResourcesAssembler.toEmptyModel(page, domainType, it))// - .orElseGet(() -> pagedResourcesAssembler.toEmptyModel(page, domainType)); - } - - return baseLink.map(it -> pagedResourcesAssembler.toModel(page, assembler, it))// - .orElseGet(() -> pagedResourcesAssembler.toModel(page, assembler)); - } - - protected CollectionModel entitiesToResources(Iterable entities, - PersistentEntityResourceAssembler assembler, Class domainType) { - - if (!entities.iterator().hasNext()) { - - List content = Arrays. asList(WRAPPERS.emptyCollectionOf(domainType)); - return CollectionModel.of(content, getDefaultSelfLink()); - } - - List> resources = new ArrayList>(); - - for (Object obj : entities) { - resources.add(obj == null ? null : assembler.toModel(obj)); - } - - return CollectionModel.of(resources, getDefaultSelfLink()); - } - - protected Link getDefaultSelfLink() { - return Link.of(ServletUriComponentsBuilder.fromCurrentRequest().build().toUriString()); - } -} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/BasePathAwareHandlerMapping.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/BasePathAwareHandlerMapping.java index 66b5d9fcd..2e6e3ef7b 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/BasePathAwareHandlerMapping.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/BasePathAwareHandlerMapping.java @@ -16,6 +16,7 @@ package org.springframework.data.rest.webmvc; import static org.springframework.core.annotation.AnnotatedElementUtils.*; + import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequestWrapper; @@ -27,9 +28,6 @@ import java.util.Enumeration; import java.util.List; import java.util.Set; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletRequestWrapper; - import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.util.ProxyUtils; @@ -115,7 +113,7 @@ public class BasePathAwareHandlerMapping extends RequestMappingHandlerMapping { String[] customPrefixes = getBasePathedPrefixes(handlerType); Builder builder = info.mutate(); - if ((customPrefixes.length != 0) || StringUtils.hasText(baseUri)) { + if (customPrefixes.length != 0 || StringUtils.hasText(baseUri)) { builder = builder.paths(resolveEmbeddedValuesInPatterns(customPrefixes)); } @@ -219,8 +217,7 @@ public class BasePathAwareHandlerMapping extends RequestMappingHandlerMapping { @Override public String getHeader(String name) { - return HttpHeaders.ACCEPT.equalsIgnoreCase(name) && (acceptMediaTypes != null // - ) + return HttpHeaders.ACCEPT.equalsIgnoreCase(name) && acceptMediaTypes != null ? StringUtils.collectionToCommaDelimitedString(acceptMediaTypes) // : super.getHeader(name); } @@ -228,8 +225,7 @@ public class BasePathAwareHandlerMapping extends RequestMappingHandlerMapping { @Override public Enumeration getHeaders(String name) { - return HttpHeaders.ACCEPT.equalsIgnoreCase(name) && (acceptMediaTypes != null // - ) + return HttpHeaders.ACCEPT.equalsIgnoreCase(name) && acceptMediaTypes != null ? Collections.enumeration(acceptMediaTypeStrings) // : super.getHeaders(name); } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ControllerUtils.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ControllerUtils.java index f38329f8a..5e04add5a 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ControllerUtils.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ControllerUtils.java @@ -17,11 +17,13 @@ package org.springframework.data.rest.webmvc; import java.util.Optional; +import org.springframework.hateoas.Link; import org.springframework.hateoas.RepresentationModel; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.util.Assert; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; /** * @author Oliver Gierke @@ -80,4 +82,8 @@ public class ControllerUtils { public static ResponseEntity> toEmptyResponse(HttpStatus status, HttpHeaders headers) { return toResponseEntity(status, headers, Optional.empty()); } + + static Link getDefaultSelfLink() { + return Link.of(ServletUriComponentsBuilder.fromCurrentRequest().build().toUriString()); + } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResource.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResource.java index 027050979..f4b911aac 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResource.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResource.java @@ -58,7 +58,6 @@ public class PersistentEntityResource extends EntityModel { * @param links must not be {@literal null}. * @param embeddeds can be {@literal null}. */ - @SuppressWarnings("deprecation") private PersistentEntityResource(PersistentEntity entity, Object content, Iterable links, Iterable embeddeds, boolean isNew, boolean nested) { diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryController.java index f9473dbea..6dc26153a 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryController.java @@ -17,11 +17,9 @@ package org.springframework.data.rest.webmvc; import java.util.Collections; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.repository.support.Repositories; import org.springframework.data.rest.core.mapping.ResourceMappings; -import org.springframework.data.rest.core.mapping.ResourceMetadata; -import org.springframework.data.web.PagedResourcesAssembler; +import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks; import org.springframework.hateoas.server.EntityLinks; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; @@ -39,29 +37,24 @@ import org.springframework.web.bind.annotation.RequestMethod; * @author Oliver Gierke */ @RepositoryRestController -public class RepositoryController extends AbstractRepositoryRestController { +public class RepositoryController { // extends AbstractRepositoryRestController { private final Repositories repositories; - private final EntityLinks entityLinks; + private final RepositoryEntityLinks entityLinks; private final ResourceMappings mappings; /** - * Creates a new {@link RepositoryController} for the given {@link PagedResourcesAssembler}, {@link Repositories}, - * {@link EntityLinks} and {@link ResourceMappings}. + * Creates a new {@link RepositoryController} for the given {@link Repositories}, {@link EntityLinks} and + * {@link ResourceMappings}. * - * @param assembler must not be {@literal null}. * @param repositories must not be {@literal null}. * @param entityLinks must not be {@literal null}. * @param mappings must not be {@literal null}. */ - @Autowired - public RepositoryController(PagedResourcesAssembler assembler, Repositories repositories, - EntityLinks entityLinks, ResourceMappings mappings) { - - super(assembler); + public RepositoryController(Repositories repositories, RepositoryEntityLinks entityLinks, ResourceMappings mappings) { Assert.notNull(repositories, "Repositories must not be null"); - Assert.notNull(entityLinks, "EntityLinks must not be null"); + Assert.notNull(entityLinks, "RepositoryEntityLinks must not be null"); Assert.notNull(mappings, "ResourceMappings must not be null"); this.repositories = repositories; @@ -78,7 +71,7 @@ public class RepositoryController extends AbstractRepositoryRestController { @RequestMapping(value = { "/", "" }, method = RequestMethod.OPTIONS) public HttpEntity optionsForRepositories() { - HttpHeaders headers = new HttpHeaders(); + var headers = new HttpHeaders(); headers.setAllow(Collections.singleton(HttpMethod.GET)); return new ResponseEntity(headers, HttpStatus.OK); @@ -103,11 +96,12 @@ public class RepositoryController extends AbstractRepositoryRestController { @RequestMapping(value = { "/", "" }, method = RequestMethod.GET) public HttpEntity listRepositories() { - RepositoryLinksResource resource = new RepositoryLinksResource(); + var resource = new RepositoryLinksResource(); for (Class domainType : repositories) { - ResourceMetadata metadata = mappings.getMetadataFor(domainType); + var metadata = mappings.getMetadataFor(domainType); + if (metadata.isExported()) { resource.add(entityLinks.linkToCollectionResource(domainType)); } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java index e7df909ab..09f78e24e 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java @@ -30,7 +30,6 @@ import org.springframework.data.auditing.AuditableBeanWrapperFactory; import org.springframework.data.domain.Sort; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.querydsl.binding.QuerydslPredicate; -import org.springframework.data.repository.support.Repositories; import org.springframework.data.repository.support.RepositoryInvoker; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.core.event.AfterCreateEvent; @@ -48,7 +47,6 @@ import org.springframework.data.rest.webmvc.support.DefaultedPageable; import org.springframework.data.rest.webmvc.support.ETag; import org.springframework.data.rest.webmvc.support.ETagDoesntMatchException; import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks; -import org.springframework.data.web.PagedResourcesAssembler; import org.springframework.hateoas.CollectionModel; import org.springframework.hateoas.EntityModel; import org.springframework.hateoas.Link; @@ -75,7 +73,8 @@ import org.springframework.web.bind.annotation.ResponseBody; * @author Jeroen Reijn */ @RepositoryRestController -class RepositoryEntityController extends AbstractRepositoryRestController implements ApplicationEventPublisherAware { +class RepositoryEntityController + /*extends AbstractRepositoryRestController*/ implements ApplicationEventPublisherAware { private static final String BASE_MAPPING = "/{repository}"; private static final List ACCEPT_PATCH_HEADERS = Arrays.asList(// @@ -94,23 +93,18 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem private ApplicationEventPublisher publisher; /** - * Creates a new {@link RepositoryEntityController} for the given {@link Repositories}, - * {@link RepositoryRestConfiguration}, {@link RepositoryEntityLinks}, {@link PagedResourcesAssembler}, - * {@link ConversionService} and {@link AuditableBeanWrapperFactory}. + * Creates a new {@link RepositoryEntityController} for the given {@link RepresentationModelAssemblers}, + * {@link RepositoryRestConfiguration}, {@link RepositoryEntityLinks}, {@link ConversionService} and + * {@link AuditableBeanWrapperFactory}. * - * @param repositories must not be {@literal null}. * @param config must not be {@literal null}. * @param entityLinks must not be {@literal null}. - * @param assembler must not be {@literal null}. - * @param auditableBeanWrapperFactory must not be {@literal null}. + * @param headersPreparer must not be {@literal null}. */ @Autowired - public RepositoryEntityController(Repositories repositories, RepositoryRestConfiguration config, - RepositoryEntityLinks entityLinks, PagedResourcesAssembler assembler, + public RepositoryEntityController(RepositoryRestConfiguration config, RepositoryEntityLinks entityLinks, HttpHeadersPreparer headersPreparer) { - super(assembler); - this.entityLinks = entityLinks; this.config = config; this.headersPreparer = headersPreparer; @@ -160,7 +154,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem throw new ResourceNotFoundException(); } - Links links = Links.of(getDefaultSelfLink()) // + Links links = Links.of(ControllerUtils.getDefaultSelfLink()) // .and(getCollectionResourceLinks(resourceInformation, pageable)); HttpHeaders headers = new HttpHeaders(); @@ -183,7 +177,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem @ResponseBody @RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET) public CollectionModel getCollectionResource(@QuerydslPredicate RootResourceInformation resourceInformation, - DefaultedPageable pageable, Sort sort, PersistentEntityResourceAssembler assembler) + DefaultedPageable pageable, Sort sort, RepresentationModelAssemblers assemblers) throws ResourceNotFoundException, HttpRequestMethodNotSupportedException { resourceInformation.verifySupportedMethod(HttpMethod.GET, ResourceType.COLLECTION); @@ -196,9 +190,9 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem Iterable results = invoker.invokeFindAll(pageable.getPageable()); ResourceMetadata metadata = resourceInformation.getResourceMetadata(); - Optional baseLink = Optional.of(getDefaultSelfLink()); - return toCollectionModel(results, assembler, metadata.getDomainType(), baseLink) + return assemblers.toCollectionModel(results, metadata.getDomainType()) // , + // ControllerUtils.getDefaultSelfLink()) .add(getCollectionResourceLinks(resourceInformation, pageable)); } @@ -219,15 +213,16 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem @ResponseBody @RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET, produces = { "application/x-spring-data-compact+json", "text/uri-list" }) - public CollectionModel getCollectionResourceCompact(@QuerydslPredicate RootResourceInformation resourceinformation, - DefaultedPageable pageable, Sort sort, PersistentEntityResourceAssembler assembler) + public CollectionModel getCollectionResourceCompact( + @QuerydslPredicate RootResourceInformation resourceinformation, + DefaultedPageable pageable, Sort sort, RepresentationModelAssemblers assemblers) throws ResourceNotFoundException, HttpRequestMethodNotSupportedException { - CollectionModel resources = getCollectionResource(resourceinformation, pageable, sort, assembler); + CollectionModel resources = getCollectionResource(resourceinformation, pageable, sort, assemblers); Links links = resources.getContent().stream() // .map(PersistentEntityResource.class::cast) // - .map(it -> resourceLink(resourceinformation, it)) // + .map(resourceinformation::resourceLink) // .reduce(resources.getLinks(), Links::and, Links::and); CollectionModel model = resources instanceof PagedModel // @@ -289,7 +284,8 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem * @since 2.2 */ @RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.HEAD) - public ResponseEntity headForItemResource(RootResourceInformation resourceInformation, @BackendId Serializable id, + public ResponseEntity headForItemResource(RootResourceInformation resourceInformation, + @BackendId Serializable id, PersistentEntityResourceAssembler assembler) throws HttpRequestMethodNotSupportedException { return getItemResource(resourceInformation, id).map(it -> { @@ -314,7 +310,8 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem */ @RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.GET) public ResponseEntity> getItemResource(RootResourceInformation resourceInformation, - @BackendId Serializable id, final PersistentEntityResourceAssembler assembler, @RequestHeader HttpHeaders headers) + @BackendId Serializable id, final PersistentEntityResourceAssembler assembler, + @RequestHeader HttpHeaders headers) throws HttpRequestMethodNotSupportedException { return getItemResource(resourceInformation, id).map(it -> { @@ -355,7 +352,8 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem Object objectToSave = payload.getContent(); eTag.verify(resourceInformation.getPersistentEntity(), objectToSave); - return payload.isNew() ? createAndReturn(objectToSave, invoker, assembler, config.returnBodyOnCreate(acceptHeader)) + return payload.isNew() + ? createAndReturn(objectToSave, invoker, assembler, config.returnBodyOnCreate(acceptHeader)) : saveAndReturn(objectToSave, invoker, PUT, assembler, config.returnBodyOnUpdate(acceptHeader)); } 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 062d85894..c27399c73 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 @@ -28,7 +28,6 @@ import java.util.Map.Entry; import java.util.Optional; import java.util.function.Function; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.core.CollectionFactory; @@ -37,16 +36,12 @@ import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.repository.support.Repositories; -import org.springframework.data.repository.support.RepositoryInvoker; import org.springframework.data.repository.support.RepositoryInvokerFactory; import org.springframework.data.rest.core.event.AfterLinkDeleteEvent; import org.springframework.data.rest.core.event.AfterLinkSaveEvent; import org.springframework.data.rest.core.event.BeforeLinkDeleteEvent; import org.springframework.data.rest.core.event.BeforeLinkSaveEvent; -import org.springframework.data.rest.core.mapping.PropertyAwareResourceMapping; -import org.springframework.data.rest.core.mapping.ResourceMetadata; import org.springframework.data.rest.webmvc.support.BackendId; -import org.springframework.data.web.PagedResourcesAssembler; import org.springframework.hateoas.CollectionModel; import org.springframework.hateoas.EntityModel; import org.springframework.hateoas.IanaLinkRelations; @@ -78,7 +73,7 @@ import com.fasterxml.jackson.annotation.JsonAnyGetter; */ @RepositoryRestController @SuppressWarnings({ "unchecked" }) -class RepositoryPropertyReferenceController extends AbstractRepositoryRestController +class RepositoryPropertyReferenceController /*extends AbstractRepositoryRestController*/ implements ApplicationEventPublisherAware { private static final String BASE_MAPPING = "/{repository}/{id}/{property}"; @@ -89,11 +84,8 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro private ApplicationEventPublisher publisher; - @Autowired public RepositoryPropertyReferenceController(Repositories repositories, - RepositoryInvokerFactory repositoryInvokerFactory, PagedResourcesAssembler assembler) { - - super(assembler); + RepositoryInvokerFactory repositoryInvokerFactory) { this.repositories = repositories; this.repositoryInvokerFactory = repositoryInvokerFactory; @@ -107,26 +99,27 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro @RequestMapping(value = BASE_MAPPING, method = GET) public ResponseEntity> followPropertyReference(final RootResourceInformation repoRequest, @BackendId Serializable id, final @PathVariable String property, - final PersistentEntityResourceAssembler assembler) throws Exception { + RepresentationModelAssemblers assemblers) throws Exception { - HttpHeaders headers = new HttpHeaders(); + var headers = new HttpHeaders(); Function> handler = prop -> prop.mapValue(it -> { if (prop.property.isCollectionLike()) { - return toCollectionModel((Iterable) it, assembler, prop.propertyType, Optional.empty()); + return assemblers.toCollectionModel((Iterable) it, prop.propertyType); } else if (prop.property.isMap()) { return ((Map) it).entrySet().stream() // - .collect(collectingAndThen(toMap(Map.Entry::getKey, entry -> assembler.toModel(entry.getValue())), + .collect(collectingAndThen( + toMap(Map.Entry::getKey, entry -> assemblers.toModel(entry.getValue())), MapModel::new)); } else { - PersistentEntityResource resource = assembler.toModel(it); - headers.set("Content-Location", resource.getRequiredLink(IanaLinkRelations.SELF).getHref()); + var resource = assemblers.toModel(it); + headers.set(HttpHeaders.CONTENT_LOCATION, resource.getRequiredLink(IanaLinkRelations.SELF).getHref()); return resource; } @@ -150,7 +143,7 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro } publisher.publishEvent(new BeforeLinkDeleteEvent(prop.accessor.getBean(), prop.propertyValue)); - Object result = repoRequest.getInvoker().invokeSave(prop.accessor.getBean()); + var result = repoRequest.getInvoker().invokeSave(prop.accessor.getBean()); publisher.publishEvent(new AfterLinkDeleteEvent(result, prop.propertyValue)); return (RepresentationModel) null; @@ -165,9 +158,9 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro @RequestMapping(value = BASE_MAPPING + "/{propertyId}", method = GET) public ResponseEntity> followPropertyReference(RootResourceInformation repoRequest, @BackendId Serializable id, @PathVariable String property, @PathVariable String propertyId, - PersistentEntityResourceAssembler assembler) throws Exception { + RepresentationModelAssemblers assemblers) throws Exception { - HttpHeaders headers = new HttpHeaders(); + var headers = new HttpHeaders(); Function> handler = prop -> prop.mapValue(it -> { @@ -178,7 +171,7 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro IdentifierAccessor accessor1 = prop.entity.getIdentifierAccessor(obj); if (propertyId.equals(accessor1.getIdentifier().toString())) { - PersistentEntityResource resource1 = assembler.toModel(obj); + var resource1 = assemblers.toModel(obj); headers.set("Content-Location", resource1.getRequiredLink(IanaLinkRelations.SELF).getHref()); return resource1; } @@ -191,7 +184,7 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro IdentifierAccessor accessor2 = prop.entity.getIdentifierAccessor(entry.getValue()); if (propertyId.equals(accessor2.getIdentifier().toString())) { - PersistentEntityResource resource2 = assembler.toModel(entry.getValue()); + var resource2 = assemblers.toModel(entry.getValue()); headers.set("Content-Location", resource2.getRequiredLink(IanaLinkRelations.SELF).getHref()); return resource2; } @@ -212,14 +205,15 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro @RequestMapping(value = BASE_MAPPING, method = GET, produces = TEXT_URI_LIST_VALUE) public ResponseEntity> followPropertyReferenceCompact(RootResourceInformation repoRequest, @BackendId Serializable id, @PathVariable String property, @RequestHeader HttpHeaders requestHeaders, - PersistentEntityResourceAssembler assembler) throws Exception { + RepresentationModelAssemblers assemblers) + throws Exception { Function> handler = prop -> prop.mapValue(it -> { if (prop.property.isCollectionLike()) { Links links = ((Collection) it).stream() // - .map(assembler::getExpandedSelfLink) // + .map(assemblers::getExpandedSelfLink) // .collect(Links.collector()); return new RepresentationModel<>(links.toList()); @@ -228,7 +222,7 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro throw new UnsupportedMediaTypeStatusException("Cannot produce compact representation of map property"); } - return new RepresentationModel<>(assembler.getExpandedSelfLink(it)); + return new RepresentationModel<>(assemblers.getExpandedSelfLink(it)); }).orElse(new RepresentationModel<>()); @@ -246,8 +240,8 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro @RequestBody(required = false) CollectionModel incoming, @BackendId Serializable id, @PathVariable String property) throws Exception { - CollectionModel source = incoming == null ? CollectionModel.empty() : incoming; - RepositoryInvoker invoker = resourceInformation.getInvoker(); + var source = incoming == null ? CollectionModel.empty() : incoming; + var invoker = resourceInformation.getInvoker(); Function> handler = prop -> { @@ -298,7 +292,7 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro } publisher.publishEvent(new BeforeLinkSaveEvent(prop.accessor.getBean(), prop.propertyValue)); - Object result = invoker.invokeSave(prop.accessor.getBean()); + var result = invoker.invokeSave(prop.accessor.getBean()); publisher.publishEvent(new AfterLinkSaveEvent(result, prop.propertyValue)); return null; @@ -351,7 +345,7 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro } publisher.publishEvent(new BeforeLinkDeleteEvent(prop.accessor.getBean(), it)); - Object result = repoRequest.getInvoker().invokeSave(prop.accessor.getBean()); + var result = repoRequest.getInvoker().invokeSave(prop.accessor.getBean()); publisher.publishEvent(new AfterLinkDeleteEvent(result, it)); return (RepresentationModel) null; @@ -365,10 +359,9 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro private Object loadPropertyValue(Class type, Link link) { - String href = link.expand().getHref(); - String id = href.substring(href.lastIndexOf('/') + 1); - - RepositoryInvoker invoker = repositoryInvokerFactory.getInvokerFor(type); + var href = link.expand().getHref(); + var id = href.substring(href.lastIndexOf('/') + 1); + var invoker = repositoryInvokerFactory.getInvokerFor(type); return invoker.invokeFindById(id).orElse(null); } @@ -377,24 +370,24 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro Serializable id, String propertyPath, Function> handler, HttpMethod method) throws Exception { - ResourceMetadata metadata = resourceInformation.getResourceMetadata(); - PropertyAwareResourceMapping mapping = metadata.getProperty(propertyPath); + var metadata = resourceInformation.getResourceMetadata(); + var mapping = metadata.getProperty(propertyPath); if (mapping == null || !mapping.isExported()) { throw new ResourceNotFoundException(); } - PersistentProperty property = mapping.getProperty(); + var property = mapping.getProperty(); resourceInformation.verifySupportedMethod(method, property); - RepositoryInvoker invoker = resourceInformation.getInvoker(); - Optional domainObj = invoker.invokeFindById(id); + var invoker = resourceInformation.getInvoker(); + var domainObj = invoker.invokeFindById(id); domainObj.orElseThrow(() -> new ResourceNotFoundException()); return domainObj.map(it -> { - PersistentPropertyAccessor accessor = property.getOwner().getPropertyAccessor(it); + var accessor = property.getOwner().getPropertyAccessor(it); return handler.apply(new ReferencedProperty(property, accessor.getProperty(property), accessor)); }); } @@ -432,8 +425,8 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro } /** - * Custom {@link RepresentationModel} to be used with maps as {@link EntityModel} doesn't properly unwrap {@link Map}s - * due to some limitation in Jackson. + * Custom {@link RepresentationModel} to be used with maps as {@link EntityModel} doesn't properly unwrap + * {@link Map}s due to some limitation in Jackson. * * @author Oliver Drotbohm * @see https://github.com/FasterXML/jackson-databind/issues/171 diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMapping.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMapping.java index 9faef3c89..b3730a70b 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMapping.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMapping.java @@ -407,7 +407,7 @@ public class RepositoryRestHandlerMapping extends BasePathAwareHandlerMapping { ? DEFAULT_ALLOWED_METHODS : HttpMethods.of(Streamable.of(methods) .map(RequestMethod::name) - .map(HttpMethod::resolve) + .map(HttpMethod::valueOf) .toList()); } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java index 4a1df6939..552974f19 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java @@ -23,25 +23,18 @@ import java.util.Collections; import java.util.List; import java.util.Optional; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.MethodParameter; import org.springframework.data.domain.Sort; -import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.repository.query.Param; import org.springframework.data.repository.support.RepositoryInvoker; -import org.springframework.data.rest.core.mapping.MethodResourceMapping; import org.springframework.data.rest.core.mapping.ResourceMappings; -import org.springframework.data.rest.core.mapping.ResourceMetadata; import org.springframework.data.rest.core.mapping.SearchResourceMappings; import org.springframework.data.rest.webmvc.support.DefaultedPageable; import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks; -import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; import org.springframework.data.web.PagedResourcesAssembler; import org.springframework.hateoas.CollectionModel; import org.springframework.hateoas.EntityModel; import org.springframework.hateoas.Link; -import org.springframework.hateoas.Links; import org.springframework.hateoas.RepresentationModel; import org.springframework.hateoas.server.EntityLinks; import org.springframework.hateoas.server.core.AnnotationAttribute; @@ -69,28 +62,26 @@ import org.springframework.web.bind.annotation.ResponseBody; * @author Oliver Gierke */ @RepositoryRestController -class RepositorySearchController extends AbstractRepositoryRestController { +class RepositorySearchController { private static final String SEARCH = "/search"; private static final String BASE_MAPPING = "/{repository}" + SEARCH; private final RepositoryEntityLinks entityLinks; private final ResourceMappings mappings; + private ResourceStatus resourceStatus; /** * Creates a new {@link RepositorySearchController} using the given {@link PagedResourcesAssembler}, * {@link EntityLinks} and {@link ResourceMappings}. * - * @param assembler must not be {@literal null}. * @param entityLinks must not be {@literal null}. * @param mappings must not be {@literal null}. + * @param headersPreparer must not be {@literal null}. */ - @Autowired - public RepositorySearchController(PagedResourcesAssembler assembler, RepositoryEntityLinks entityLinks, - ResourceMappings mappings, HttpHeadersPreparer headersPreparer) { - - super(assembler); + public RepositorySearchController(RepositoryEntityLinks entityLinks, ResourceMappings mappings, + HttpHeadersPreparer headersPreparer) { Assert.notNull(entityLinks, "EntityLinks must not be null"); Assert.notNull(mappings, "ResourceMappings must not be null"); @@ -112,7 +103,7 @@ class RepositorySearchController extends AbstractRepositoryRestController { verifySearchesExposed(resourceInformation); - HttpHeaders headers = new HttpHeaders(); + var headers = new HttpHeaders(); headers.setAllow(Collections.singleton(HttpMethod.GET)); return ResponseEntity.ok().headers(headers).build(); @@ -145,7 +136,7 @@ class RepositorySearchController extends AbstractRepositoryRestController { verifySearchesExposed(resourceInformation); - Links queryMethodLinks = entityLinks.linksToSearchResources(resourceInformation.getDomainType()); + var queryMethodLinks = entityLinks.linksToSearchResources(resourceInformation.getDomainType()); if (queryMethodLinks.isEmpty()) { throw new ResourceNotFoundException(); @@ -153,7 +144,7 @@ class RepositorySearchController extends AbstractRepositoryRestController { return new RepositorySearchesResource(resourceInformation.getDomainType()) // .add(queryMethodLinks) // - .add(getDefaultSelfLink()); + .add(ControllerUtils.getDefaultSelfLink()); } /** @@ -171,18 +162,18 @@ class RepositorySearchController extends AbstractRepositoryRestController { @ResponseBody @RequestMapping(value = BASE_MAPPING + "/{search}", method = RequestMethod.GET) public ResponseEntity executeSearch(RootResourceInformation resourceInformation, - @RequestParam MultiValueMap parameters, @PathVariable String search, DefaultedPageable pageable, - Sort sort, PersistentEntityResourceAssembler assembler, @RequestHeader HttpHeaders headers) { + @RequestParam MultiValueMap parameters, @PathVariable String search, + DefaultedPageable pageable, + Sort sort, @RequestHeader HttpHeaders headers, RepresentationModelAssemblers assemblers) { - Method method = checkExecutability(resourceInformation, search); - Optional result = executeQueryMethod(resourceInformation.getInvoker(), parameters, method, pageable, sort, - assembler); + var method = checkExecutability(resourceInformation, search); + var result = executeQueryMethod(resourceInformation.getInvoker(), parameters, method, pageable, sort); - SearchResourceMappings searchMappings = resourceInformation.getSearchMappings(); - MethodResourceMapping methodMapping = searchMappings.getExportedMethodMappingForPath(search); - Class domainType = methodMapping.getReturnedDomainType(); + var searchMappings = resourceInformation.getSearchMappings(); + var methodMapping = searchMappings.getExportedMethodMappingForPath(search); + var domainType = methodMapping.getReturnedDomainType(); - return toModel(result, assembler, domainType, Optional.empty(), headers, resourceInformation); + return toModel(result, domainType, headers, resourceInformation, assemblers); } /** @@ -195,18 +186,18 @@ class RepositorySearchController extends AbstractRepositoryRestController { * @param baseLink can be {@literal null}. * @return */ - protected ResponseEntity toModel(Optional source, final PersistentEntityResourceAssembler assembler, - Class domainType, Optional baseLink, HttpHeaders headers, RootResourceInformation information) { + protected ResponseEntity toModel(Optional source, Class domainType, + HttpHeaders headers, RootResourceInformation information, RepresentationModelAssemblers assemblers) { return source.map(it -> { - if (it instanceof Iterable) { - return ResponseEntity.ok(toCollectionModel((Iterable) it, assembler, domainType, baseLink)); + if (it instanceof Iterable iterable) { + return ResponseEntity.ok(assemblers.toCollectionModel(iterable, domainType)); } else if (ClassUtils.isPrimitiveOrWrapper(it.getClass())) { return ResponseEntity.ok(it); } - PersistentEntity entity = information.getPersistentEntity(); + var entity = information.getPersistentEntity(); // Returned value is not of the aggregates type - probably some projection if (!entity.getType().isInstance(it)) { @@ -214,7 +205,7 @@ class RepositorySearchController extends AbstractRepositoryRestController { } return resourceStatus.getStatusAndHeaders(headers, it, entity).toResponseEntity(// - () -> assembler.toFullResource(it)); + () -> assemblers.toFullResource(it)); }).orElseThrow(() -> new ResourceNotFoundException()); } @@ -223,12 +214,12 @@ class RepositorySearchController extends AbstractRepositoryRestController { * Executes a query method and exposes the results in compact form. * * @param resourceInformation + * @param headers * @param parameters * @param repository * @param search * @param pageable * @param sort - * @param assembler * @return */ @ResponseBody @@ -237,31 +228,26 @@ class RepositorySearchController extends AbstractRepositoryRestController { public RepresentationModel executeSearchCompact(RootResourceInformation resourceInformation, @RequestHeader HttpHeaders headers, @RequestParam MultiValueMap parameters, @PathVariable String repository, @PathVariable String search, DefaultedPageable pageable, Sort sort, - PersistentEntityResourceAssembler assembler) { + RepresentationModelAssemblers assemblers) { - Method method = checkExecutability(resourceInformation, search); - Optional result = executeQueryMethod(resourceInformation.getInvoker(), parameters, method, pageable, sort, - assembler); - ResourceMetadata metadata = resourceInformation.getResourceMetadata(); - ResponseEntity entity = toModel(result, assembler, metadata.getDomainType(), Optional.empty(), headers, - resourceInformation); - Object resource = entity.getBody(); + var method = checkExecutability(resourceInformation, search); + var result = executeQueryMethod(resourceInformation.getInvoker(), parameters, method, pageable, sort); + var metadata = resourceInformation.getResourceMetadata(); + var entity = toModel(result, metadata.getDomainType(), headers, resourceInformation, assemblers); + var resource = entity.getBody(); - List links = new ArrayList(); + var links = new ArrayList(); - if (resource instanceof CollectionModel && ((CollectionModel) resource).getContent() != null) { + if (resource instanceof CollectionModel model && model.getContent() != null) { - for (Object obj : ((CollectionModel) resource).getContent()) { - if (null != obj && obj instanceof EntityModel) { - EntityModel res = (EntityModel) obj; - links.add(resourceLink(resourceInformation, res)); + for (Object obj : model.getContent()) { + if (null != obj && obj instanceof EntityModel res) { + links.add(resourceInformation.resourceLink(res)); } } - } else if (resource instanceof EntityModel) { - - EntityModel res = (EntityModel) resource; - links.add(resourceLink(resourceInformation, res)); + } else if (resource instanceof EntityModel res) { + links.add(resourceInformation.resourceLink(res)); } return CollectionModel.empty(links); @@ -280,7 +266,7 @@ class RepositorySearchController extends AbstractRepositoryRestController { checkExecutability(information, search); - HttpHeaders headers = new HttpHeaders(); + var headers = new HttpHeaders(); headers.setAllow(Collections.singleton(HttpMethod.GET)); return new ResponseEntity(headers, HttpStatus.OK); @@ -311,9 +297,8 @@ class RepositorySearchController extends AbstractRepositoryRestController { */ private Method checkExecutability(RootResourceInformation resourceInformation, String searchName) { - SearchResourceMappings searchMapping = verifySearchesExposed(resourceInformation); - - Method method = searchMapping.getMappedMethod(searchName); + var searchMapping = verifySearchesExposed(resourceInformation); + var method = searchMapping.getMappedMethod(searchName); if (method == null) { throw new ResourceNotFoundException(); @@ -330,23 +315,21 @@ class RepositorySearchController extends AbstractRepositoryRestController { * @return */ private Optional executeQueryMethod(final RepositoryInvoker invoker, - @RequestParam MultiValueMap parameters, Method method, DefaultedPageable pageable, Sort sort, - PersistentEntityResourceAssembler assembler) { + @RequestParam MultiValueMap parameters, Method method, DefaultedPageable pageable, + Sort sort) { - MultiValueMap result = new LinkedMultiValueMap(parameters); - MethodParameters methodParameters = new MethodParameters(method, new AnnotationAttribute(Param.class)); - List parameterList = methodParameters.getParameters(); - List> parameterTypeInformations = ClassTypeInformation.from(method.getDeclaringClass()) - .getParameterTypes(method); + var result = new LinkedMultiValueMap(parameters); + var methodParameters = new MethodParameters(method, new AnnotationAttribute(Param.class)); + var parameterList = methodParameters.getParameters(); + var parameterTypeInformations = TypeInformation.of(method.getDeclaringClass()).getParameterTypes(method); parameters.entrySet().forEach(entry -> methodParameters.getParameter(entry.getKey()).ifPresent(parameter -> { - int parameterIndex = parameterList.indexOf(parameter); - TypeInformation domainType = parameterTypeInformations.get(parameterIndex).getActualType(); - - ResourceMetadata metadata = mappings.getMetadataFor(domainType.getType()); + var parameterIndex = parameterList.indexOf(parameter); + var domainType = parameterTypeInformations.get(parameterIndex).getActualType(); + var metadata = mappings.getMetadataFor(domainType.getType()); if (metadata != null && metadata.isExported()) { result.put(parameter.getParameterName(), prepareUris(entry.getValue())); @@ -363,7 +346,7 @@ class RepositorySearchController extends AbstractRepositoryRestController { */ private static SearchResourceMappings verifySearchesExposed(RootResourceInformation resourceInformation) { - SearchResourceMappings resourceMappings = resourceInformation.getSearchMappings(); + var resourceMappings = resourceInformation.getSearchMappings(); if (!resourceMappings.isExported()) { throw new ResourceNotFoundException(); @@ -385,7 +368,7 @@ class RepositorySearchController extends AbstractRepositoryRestController { return Collections.emptyList(); } - List result = new ArrayList(source.size()); + var result = new ArrayList(source.size()); for (Object element : source) { diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepresentationModelAssemblers.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepresentationModelAssemblers.java new file mode 100644 index 000000000..f609c0ab5 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepresentationModelAssemblers.java @@ -0,0 +1,142 @@ +/* + * Copyright 2023 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 + * + * https://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; + +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Slice; +import org.springframework.data.web.PagedResourcesAssembler; +import org.springframework.data.web.SlicedResourcesAssembler; +import org.springframework.hateoas.CollectionModel; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.server.core.EmbeddedWrappers; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * A wrapper for a variety of {@link RepresentationModelAssemblers} to avoid having to depend on all of them from our + * controllers. + * + * @author Oliver Drotbohm + * @since 4.1 + * @soundtrack The Intersphere - Down (Wanderer, https://www.youtube.com/watch?v=3RIdTFJvDxg) + */ +public class RepresentationModelAssemblers { + + private static final EmbeddedWrappers WRAPPERS = new EmbeddedWrappers(false); + + private final PagedResourcesAssembler pagedResourcesAssembler; + private final SlicedResourcesAssembler slicedResourcesAssembler; + private final PersistentEntityResourceAssembler persistentEntityResourceAssembler; + + /** + * Creates a new {@link RepresentationModelAssemblers} from the given {@link PagedResourcesAssembler}, + * {@link SlicedResourcesAssembler} and {@link PersistentEntityResourceAssembler}. + * + * @param pagedResourcesAssembler must not be {@literal null}. + * @param slicedResourcesAssembler must not be {@literal null}. + * @param persistentEntityResourceAssembler must not be {@literal null}. + */ + public RepresentationModelAssemblers(PagedResourcesAssembler pagedResourcesAssembler, + SlicedResourcesAssembler slicedResourcesAssembler, + PersistentEntityResourceAssembler persistentEntityResourceAssembler) { + + Assert.notNull(pagedResourcesAssembler, "PagedResourcesAssembler must not be null"); + Assert.notNull(slicedResourcesAssembler, "SlicedResourcesAssembler must not be null"); + Assert.notNull(persistentEntityResourceAssembler, "PersistentEntityResourceAssembler must not be null"); + + this.pagedResourcesAssembler = pagedResourcesAssembler; + this.slicedResourcesAssembler = slicedResourcesAssembler; + this.persistentEntityResourceAssembler = persistentEntityResourceAssembler; + } + + /** + * Creates a new {@link CollectionModel} for the given source {@link Iterable} and domain type for forward into the + * model if the {@link Iterable} is empty. + * + * @param source must not be {@literal null}. + * @param domainType must not be {@literal null}. + * @return will never be {@literal null}. + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + CollectionModel toCollectionModel(@Nullable Iterable source, Class domainType) { + + Assert.notNull(source, "Source Iterable must not be null!"); + Assert.notNull(domainType, "Domain type must not be null!"); + + if (source instanceof Page page) { + return entitiesToResources(page, domainType); + } else if (source instanceof Slice slice) { + return entitiesToResources(slice, domainType); + } else if (source instanceof Iterable) { + return entitiesToResources((Iterable) source, domainType); + } else { + return CollectionModel.empty(domainType); + } + } + + /** + * @param instance must not be {@literal null}. + * @return will never be {@literal null}. + * @see PersistentEntityResourceAssembler#toFullResource(Object) + */ + PersistentEntityResource toFullResource(Object instance) { + return persistentEntityResourceAssembler.toFullResource(instance); + } + + /** + * @param instance must not be {@literal null}. + * @return will never be {@literal null}. + * @see PersistentEntityResourceAssembler#toModel(Object) + */ + PersistentEntityResource toModel(Object instance) { + return persistentEntityResourceAssembler.toModel(instance); + } + + /** + * @param instance must not be {@literal null}. + * @return will never be {@literal null}. + * @see PersistentEntityResourceAssembler#getExpandedSelfLink(Object) + */ + Link getExpandedSelfLink(Object instance) { + return persistentEntityResourceAssembler.getExpandedSelfLink(instance); + } + + private CollectionModel entitiesToResources(Page page, Class domainType) { + + return page.isEmpty() + ? pagedResourcesAssembler.toEmptyModel(page, domainType) + : pagedResourcesAssembler.toModel(page, persistentEntityResourceAssembler); + } + + private CollectionModel entitiesToResources(Slice slice, Class domainType) { + + return slice.isEmpty() + ? slicedResourcesAssembler.toEmptyModel(slice, domainType) // + : slicedResourcesAssembler.toModel(slice, persistentEntityResourceAssembler); + + } + + private CollectionModel entitiesToResources(Iterable entities, Class domainType) { + + var selfLink = ControllerUtils.getDefaultSelfLink(); + + return !entities.iterator().hasNext() + ? CollectionModel.of(List.of(WRAPPERS.emptyCollectionOf(domainType)), selfLink) + : persistentEntityResourceAssembler.toCollectionModel(entities).add(selfLink); + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RestControllerConfiguration.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RestControllerConfiguration.java index a4d0d726e..087d44126 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RestControllerConfiguration.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RestControllerConfiguration.java @@ -25,8 +25,6 @@ import org.springframework.data.rest.webmvc.alps.AlpsController; import org.springframework.data.rest.webmvc.json.JsonSchema; import org.springframework.data.rest.webmvc.json.PersistentEntityToJsonSchemaConverter; import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks; -import org.springframework.data.web.PagedResourcesAssembler; -import org.springframework.hateoas.server.EntityLinks; /** * Configuration class registering required {@link org.springframework.stereotype.Component components} that declare @@ -40,17 +38,16 @@ public class RestControllerConfiguration { private final RepositoryRestConfiguration restConfiguration; private final RepositoryResourceMappings resourceMappings; - private final PagedResourcesAssembler resourcesAssembler; private final Repositories repositories; + private final RepositoryEntityLinks entityLinks; RestControllerConfiguration(RepositoryRestConfiguration restConfiguration, - RepositoryResourceMappings resourceMappings, PagedResourcesAssembler resourcesAssembler, - Repositories repositories) { + RepositoryResourceMappings resourceMappings, Repositories repositories, RepositoryEntityLinks entityLinks) { this.restConfiguration = restConfiguration; this.resourceMappings = resourceMappings; - this.resourcesAssembler = resourcesAssembler; this.repositories = repositories; + this.entityLinks = entityLinks; } /** @@ -61,8 +58,8 @@ public class RestControllerConfiguration { * @return never {@literal null}. */ @Bean - RepositoryController repositoryController(EntityLinks entityLinks) { - return new RepositoryController(resourcesAssembler, repositories, entityLinks, resourceMappings); + RepositoryController repositoryController() { + return new RepositoryController(repositories, entityLinks, resourceMappings); } /** @@ -74,10 +71,8 @@ public class RestControllerConfiguration { * @return never {@literal null}. */ @Bean - RepositoryEntityController repositoryEntityController(RepositoryEntityLinks entityLinks, - HttpHeadersPreparer headersPreparer) { - return new RepositoryEntityController(repositories, restConfiguration, entityLinks, resourcesAssembler, - headersPreparer); + RepositoryEntityController repositoryEntityController(HttpHeadersPreparer headersPreparer) { + return new RepositoryEntityController(restConfiguration, entityLinks, headersPreparer); } /** @@ -89,7 +84,7 @@ public class RestControllerConfiguration { @Bean RepositoryPropertyReferenceController repositoryPropertyReferenceController( RepositoryInvokerFactory repositoryInvokerFactory) { - return new RepositoryPropertyReferenceController(repositories, repositoryInvokerFactory, resourcesAssembler); + return new RepositoryPropertyReferenceController(repositories, repositoryInvokerFactory); } /** @@ -101,9 +96,8 @@ public class RestControllerConfiguration { * @return never {@literal null}. */ @Bean - RepositorySearchController repositorySearchController(RepositoryEntityLinks entityLinks, - HttpHeadersPreparer headersPreparer) { - return new RepositorySearchController(resourcesAssembler, entityLinks, resourceMappings, headersPreparer); + RepositorySearchController repositorySearchController(HttpHeadersPreparer headersPreparer) { + return new RepositorySearchController(entityLinks, resourceMappings, headersPreparer); } /** diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RootResourceInformation.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RootResourceInformation.java index 43e462ee4..c902958d5 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RootResourceInformation.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RootResourceInformation.java @@ -26,6 +26,9 @@ import org.springframework.data.rest.core.mapping.ResourceMetadata; import org.springframework.data.rest.core.mapping.ResourceType; import org.springframework.data.rest.core.mapping.SearchResourceMappings; import org.springframework.data.rest.core.mapping.SupportedHttpMethods; +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.IanaLinkRelations; +import org.springframework.hateoas.Link; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; import org.springframework.web.HttpRequestMethodNotSupportedException; @@ -42,7 +45,8 @@ public class RootResourceInformation { private final RepositoryInvoker invoker; private final PersistentEntity persistentEntity; - public RootResourceInformation(ResourceMetadata metadata, PersistentEntity entity, RepositoryInvoker invoker) { + public RootResourceInformation(ResourceMetadata metadata, PersistentEntity entity, + RepositoryInvoker invoker) { this.resourceMetadata = metadata; @@ -88,7 +92,7 @@ public class RootResourceInformation { * @param resourceType must not be {@literal null}. * @throws ResourceNotFoundException if the repository is not exported at all. * @throws HttpRequestMethodNotSupportedException if the {@link ResourceType} does not support the given - * {@link HttpMethod}. Will contain all supported methods as indicators for clients. + * {@link HttpMethod}. Will contain all supported methods as indicators for clients. */ public void verifySupportedMethod(HttpMethod httpMethod, ResourceType resourceType) throws HttpRequestMethodNotSupportedException, ResourceNotFoundException { @@ -115,7 +119,7 @@ public class RootResourceInformation { * @param property must not be {@literal null}. * @throws ResourceNotFoundException if the repository is not exported at all. * @throws HttpRequestMethodNotSupportedException if the {@link PersistentProperty} does not support the given - * {@link HttpMethod}. Will contain all supported methods as indicators for clients. + * {@link HttpMethod}. Will contain all supported methods as indicators for clients. */ public void verifySupportedMethod(HttpMethod httpMethod, PersistentProperty property) throws HttpRequestMethodNotSupportedException { @@ -144,6 +148,14 @@ public class RootResourceInformation { } } + public Link resourceLink(EntityModel resource) { + + var repoMapping = getResourceMetadata(); + var selfLink = resource.getRequiredLink(IanaLinkRelations.SELF); + + return Link.of(selfLink.getHref(), repoMapping.getItemResourceRel()); + } + private static void reject(HttpMethod method, HttpMethods supported) throws HttpRequestMethodNotSupportedException { Set stringMethods = supported.butWithout(method) // diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ServerHttpRequestMethodArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ServerHttpRequestMethodArgumentResolver.java deleted file mode 100644 index f6d84ee10..000000000 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ServerHttpRequestMethodArgumentResolver.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.springframework.data.rest.webmvc; - -import jakarta.servlet.http.HttpServletRequest; - -import org.springframework.core.MethodParameter; -import org.springframework.http.server.ServletServerHttpRequest; -import org.springframework.util.ClassUtils; -import org.springframework.web.bind.support.WebDataBinderFactory; -import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.method.support.HandlerMethodArgumentResolver; -import org.springframework.web.method.support.ModelAndViewContainer; - -/** - * @author Jon Brisbin - */ -public class ServerHttpRequestMethodArgumentResolver implements HandlerMethodArgumentResolver { - - @Override - public boolean supportsParameter(MethodParameter parameter) { - return ClassUtils.isAssignable(parameter.getParameterType(), ServletServerHttpRequest.class); - } - - @Override - public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, - NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { - return new ServletServerHttpRequest((HttpServletRequest) webRequest.getNativeRequest()); - } - -} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/PersistentEntityResourceAssemblerArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/PersistentEntityResourceAssemblerArgumentResolver.java index dd31f5316..8848bd274 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/PersistentEntityResourceAssemblerArgumentResolver.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/PersistentEntityResourceAssemblerArgumentResolver.java @@ -65,7 +65,8 @@ public class PersistentEntityResourceAssemblerArgumentResolver implements Handle } @Override - public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + public PersistentEntityResourceAssembler resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { String projectionParameter = webRequest.getParameter(projectionDefinitions.getParameterName()); 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 d80b9bfd9..4b977dfe4 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 @@ -15,8 +15,6 @@ */ package org.springframework.data.rest.webmvc.config; -import jakarta.servlet.http.HttpServletRequest; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -94,6 +92,8 @@ import org.springframework.data.util.Lazy; import org.springframework.data.util.StreamUtils; import org.springframework.data.web.HateoasPageableHandlerMethodArgumentResolver; import org.springframework.data.web.HateoasSortHandlerMethodArgumentResolver; +import org.springframework.data.web.PagedResourcesAssembler; +import org.springframework.data.web.SlicedResourcesAssembler; import org.springframework.data.web.config.EnableSpringDataWebSupport; import org.springframework.data.web.config.HateoasAwareSpringDataWebConfiguration; import org.springframework.data.web.config.SpringDataJacksonConfiguration; @@ -117,7 +117,6 @@ import org.springframework.hateoas.server.mvc.TypeConstrainedMappingJackson2Http import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; -import org.springframework.http.server.ServerHttpRequest; import org.springframework.plugin.core.PluginRegistry; import org.springframework.util.ClassUtils; import org.springframework.util.StringValueResolver; @@ -191,12 +190,12 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon private final Lazy backendIdHandlerMethodArgumentResolver; private final Lazy associationLinks; private final Lazy enumTranslator; - private final Lazy serverHttpRequestMethodArgumentResolver; private final Lazy eTagArgumentResolver; private final Lazy repositoryInvokerFactory; private final Lazy repositoryRestConfiguration; private final Lazy pageableResolver; private final Lazy sortResolver; + private final Lazy persistentEntityResourceAssemblerArgumentResolver; private ClassLoader beanClassLoader; private StringValueResolver stringValueResolver; @@ -257,8 +256,6 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon .of(() -> context.getBean(BackendIdHandlerMethodArgumentResolver.class)); this.associationLinks = Lazy.of(() -> context.getBean(Associations.class)); this.enumTranslator = Lazy.of(() -> context.getBean(EnumTranslator.class)); - this.serverHttpRequestMethodArgumentResolver = Lazy - .of(() -> context.getBean(ServerHttpRequestMethodArgumentResolver.class)); this.eTagArgumentResolver = Lazy.of(() -> context.getBean(ETagArgumentResolver.class)); this.repositoryInvokerFactory = Lazy.of(() -> new UnwrappingRepositoryInvokerFactory( @@ -274,6 +271,8 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon this.repositoryRestConfiguration = Lazy.of(() -> context.getBean(RepositoryRestConfiguration.class)); this.pageableResolver = Lazy.of(() -> context.getBean(HateoasPageableHandlerMethodArgumentResolver.class)); this.sortResolver = Lazy.of(() -> context.getBean(HateoasSortHandlerMethodArgumentResolver.class)); + this.persistentEntityResourceAssemblerArgumentResolver = Lazy + .of(() -> context.getBean(PersistentEntityResourceAssemblerArgumentResolver.class)); // Resolution via ResolvableType needed to make the wildcard assignment work @@ -405,16 +404,6 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon return new AnnotatedEventHandlerInvoker(); } - /** - * Turns an {@link HttpServletRequest} into a {@link ServerHttpRequest}. - * - * @return - */ - @Bean - public ServerHttpRequestMethodArgumentResolver serverHttpRequestMethodArgumentResolver() { - return new ServerHttpRequestMethodArgumentResolver(); - } - /** * A convenience resolver that pulls together all the information needed to service a request. * @@ -448,7 +437,8 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon @Bean public BackendIdHandlerMethodArgumentResolver backendIdHandlerMethodArgumentResolver( PluginRegistry> backendIdConverterRegistry, - ResourceMetadataHandlerMethodArgumentResolver resourceMetadataHandlerMethodArgumentResolver, BaseUri baseUri) { + ResourceMetadataHandlerMethodArgumentResolver resourceMetadataHandlerMethodArgumentResolver, + BaseUri baseUri) { return new BackendIdHandlerMethodArgumentResolver(backendIdConverterRegistry, resourceMetadataHandlerMethodArgumentResolver, baseUri); @@ -466,7 +456,8 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon * @return */ @Bean - public RepositoryEntityLinks entityLinks(ObjectFactory pageableResolver, // + public RepositoryEntityLinks entityLinks( + ObjectFactory pageableResolver, // Repositories repositories, // RepositoryResourceMappings resourceMappings, // PluginRegistry> defaultMessageConverters, - RootResourceInformationHandlerMethodArgumentResolver repoRequestArgumentResolver, Associations associationLinks, + RootResourceInformationHandlerMethodArgumentResolver repoRequestArgumentResolver, + Associations associationLinks, BackendIdHandlerMethodArgumentResolver backendIdHandlerMethodArgumentResolver, PersistentEntities entities) { @@ -564,7 +556,8 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon // @Bean - public TypeConstrainedMappingJackson2HttpMessageConverter halJacksonHttpMessageConverter(LinkCollector linkCollector, + public TypeConstrainedMappingJackson2HttpMessageConverter halJacksonHttpMessageConverter( + LinkCollector linkCollector, RepositoryRestConfiguration repositoryRestConfiguration) { ArrayList mediaTypes = new ArrayList<>(); @@ -593,7 +586,8 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon * @since 3.5 */ @Bean - TypeConstrainedMappingJackson2HttpMessageConverter halFormsJacksonHttpMessageConverter(LinkCollector linkCollector) { + TypeConstrainedMappingJackson2HttpMessageConverter halFormsJacksonHttpMessageConverter( + LinkCollector linkCollector) { LinkRelationProvider defaultedRelProvider = this.relProvider.getIfUnique(EvoInflectorLinkRelationProvider::new); HalFormsConfiguration configuration = new HalFormsConfiguration( @@ -644,8 +638,8 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon } /** - * Special {@link org.springframework.web.servlet.HandlerAdapter} that only recognizes handler methods defined in the - * provided controller classes. + * Special {@link org.springframework.web.servlet.HandlerAdapter} that only recognizes handler methods defined in + * the provided controller classes. * * @return */ @@ -655,6 +649,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon @Qualifier("defaultMessageConverters") List> defaultMessageConverters, AlpsJsonHttpMessageConverter alpsJsonHttpMessageConverter, SelfLinkProvider selfLinkProvider, PersistentEntityResourceHandlerMethodArgumentResolver persistentEntityArgumentResolver, + PersistentEntityResourceAssemblerArgumentResolver persistentEntityResourceAssemblerArgumentResolver, RootResourceInformationHandlerMethodArgumentResolver repoRequestArgumentResolver, RepositoryRestConfiguration repositoryRestConfiguration) { @@ -664,7 +659,8 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon initializer.setValidator(validator.getIfUnique()); RepositoryRestHandlerAdapter handlerAdapter = new RepositoryRestHandlerAdapter(defaultMethodArgumentResolvers( - selfLinkProvider, persistentEntityArgumentResolver, repoRequestArgumentResolver)); + selfLinkProvider, persistentEntityArgumentResolver, persistentEntityResourceAssemblerArgumentResolver, + repoRequestArgumentResolver)); handlerAdapter.setWebBindingInitializer(initializer); handlerAdapter.setMessageConverters(defaultMessageConverters); @@ -734,7 +730,8 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon EmbeddedResourcesAssembler assembler = new EmbeddedResourcesAssembler(persistentEntities.get(), associationLinks.get(), excerptProjector.get()); - LookupObjectSerializer lookupObjectSerializer = new LookupObjectSerializer(PluginRegistry.of(getEntityLookups())); + LookupObjectSerializer lookupObjectSerializer = new LookupObjectSerializer( + PluginRegistry.of(getEntityLookups())); return new PersistentEntityJackson2Module(associationLinks.get(), persistentEntities.get(), new UriToEntityConverter(persistentEntities.get(), repositoryInvokerFactory.get(), repositories.get()), @@ -746,7 +743,8 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon Associations associationLinks) { return configurerDelegate.get() - .customizeLinkCollector(new DefaultLinkCollector(persistentEntities, selfLinkProvider, associationLinks)); + .customizeLinkCollector( + new DefaultLinkCollector(persistentEntities, selfLinkProvider, associationLinks)); } @Bean @@ -763,7 +761,8 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon ExceptionHandlerExceptionResolver er = new ExceptionHandlerExceptionResolver(); er.setCustomArgumentResolvers(defaultMethodArgumentResolvers(selfLinkProvider.get(), - persistentEntityArgumentResolver.get(), repoRequestArgumentResolver.get())); + persistentEntityArgumentResolver.get(), persistentEntityResourceAssemblerArgumentResolver.get(), + repoRequestArgumentResolver.get())); er.setMessageConverters(defaultMessageConverters.get()); configurerDelegate.get().configureExceptionHandlerExceptionResolver(er); @@ -898,12 +897,9 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon protected List defaultMethodArgumentResolvers(SelfLinkProvider selfLinkProvider, PersistentEntityResourceHandlerMethodArgumentResolver persistentEntityArgumentResolver, + PersistentEntityResourceAssemblerArgumentResolver persistentEntityResourceAssemblerArgumentResolver, RootResourceInformationHandlerMethodArgumentResolver repoRequestArgumentResolver) { - SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory(); - projectionFactory.setBeanFactory(applicationContext); - projectionFactory.setBeanClassLoader(beanClassLoader); - JacksonMappingAwareSortTranslator sortTranslator = new JacksonMappingAwareSortTranslator(objectMapper(), repositories.get(), DomainClassResolver.of(repositories.get(), resourceMappings.get(), baseUri.get()), persistentEntities.get(), associationLinks.get()); @@ -912,17 +908,38 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon new MappingAwareDefaultedPageableArgumentResolver(sortTranslator, pageableResolver.get()), // new MappingAwarePageableArgumentResolver(sortTranslator, pageableResolver.get()), // new MappingAwareSortArgumentResolver(sortTranslator, this.sortResolver.get()), // - serverHttpRequestMethodArgumentResolver.get(), // repoRequestArgumentResolver, // persistentEntityArgumentResolver, // resourceMetadataHandlerMethodArgumentResolver.get(), // HttpMethodHandlerMethodArgumentResolver.INSTANCE, // - new PersistentEntityResourceAssemblerArgumentResolver(persistentEntities.get(), selfLinkProvider, - repositoryRestConfiguration.get().getProjectionConfiguration(), projectionFactory, associationLinks.get()), // + persistentEntityResourceAssemblerArgumentResolver, // + applicationContext.getBean(RepresentationModelAssemblersArgumentResolver.class), backendIdHandlerMethodArgumentResolver.get(), // eTagArgumentResolver.get()); } + @Bean + RepresentationModelAssemblersArgumentResolver representationModelAssemblersArgumentResolver( + PagedResourcesAssembler pagedResourcesAssembler, + SlicedResourcesAssembler slicedResourcesAssembler, + PersistentEntityResourceAssemblerArgumentResolver delegate) { + + return new RepresentationModelAssemblersArgumentResolver(pagedResourcesAssembler, slicedResourcesAssembler, + delegate); + } + + @Bean + PersistentEntityResourceAssemblerArgumentResolver persistentEntityResourceAssemblerArgumentResolver() { + + SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory(); + projectionFactory.setBeanFactory(applicationContext); + projectionFactory.setBeanClassLoader(beanClassLoader); + + return new PersistentEntityResourceAssemblerArgumentResolver(persistentEntities.get(), selfLinkProvider.get(), + repositoryRestConfiguration.get().getProjectionConfiguration(), projectionFactory, + associationLinks.get()); + } + protected ObjectMapper basicObjectMapper() { ObjectMapper mapper = this.objectMapper.getIfAvailable(); diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepresentationModelAssemblersArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepresentationModelAssemblersArgumentResolver.java new file mode 100644 index 000000000..f39549acd --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepresentationModelAssemblersArgumentResolver.java @@ -0,0 +1,78 @@ +/* + * Copyright 2023 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 + * + * https://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 org.springframework.core.MethodParameter; +import org.springframework.data.rest.webmvc.RepresentationModelAssemblers; +import org.springframework.data.web.PagedResourcesAssembler; +import org.springframework.data.web.SlicedResourcesAssembler; +import org.springframework.util.Assert; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +/** + * {@link HandlerMethodArgumentResolver} to provide {@link RepresentationModelAssemblers} + * + * @author Oliver Drotbohm + * @since 4.1 + * @soundtrack The Intersphere - Down (Wanderer, https://www.youtube.com/watch?v=3RIdTFJvDxg) + */ +public class RepresentationModelAssemblersArgumentResolver implements HandlerMethodArgumentResolver { + + private final PagedResourcesAssembler pagedResourcesAssembler; + private final SlicedResourcesAssembler slicedResourcesAssembler; + private final PersistentEntityResourceAssemblerArgumentResolver delegate; + + /** + * Creates a new {@link RepresentationModelAssemblersArgumentResolver} for the given + * {@link PagedResourcesAssembler}, {@link SlicedResourcesAssembler}, and + * {@link PersistentEntityResourceAssemblerArgumentResolver}. + * + * @param pagedResourcesAssembler must not be {@literal null}. + * @param slicedResourcesAssembler must not be {@literal null}. + * @param delegate must not be {@literal null}. + */ + RepresentationModelAssemblersArgumentResolver(PagedResourcesAssembler pagedResourcesAssembler, + SlicedResourcesAssembler slicedResourcesAssembler, + PersistentEntityResourceAssemblerArgumentResolver delegate) { + + Assert.notNull(pagedResourcesAssembler, "PagedResourcesAssembler must not be null!"); + Assert.notNull(slicedResourcesAssembler, "SlicedResourcesAssembler must not be null!"); + Assert.notNull(delegate, "PersistentEntityResourceAssemblerArgumentResolver must not be null"); + + this.pagedResourcesAssembler = pagedResourcesAssembler; + this.slicedResourcesAssembler = slicedResourcesAssembler; + this.delegate = delegate; + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return RepresentationModelAssemblers.class.isAssignableFrom(parameter.getParameterType()); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + + var persistentEntityResourceAssembler = delegate.resolveArgument(parameter, mavContainer, webRequest, + binderFactory); + + return new RepresentationModelAssemblers(pagedResourcesAssembler, slicedResourcesAssembler, + persistentEntityResourceAssembler); + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/package-info.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/package-info.java new file mode 100644 index 000000000..221dc507a --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/package-info.java @@ -0,0 +1,2 @@ +@org.springframework.lang.NonNullApi +package org.springframework.data.rest.webmvc; diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryEntityControllerTest.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryEntityControllerTest.java index 7c6fac37a..ab1decc47 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryEntityControllerTest.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryEntityControllerTest.java @@ -27,14 +27,12 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentEntity; import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext; import org.springframework.data.mapping.context.PersistentEntities; -import org.springframework.data.repository.support.Repositories; import org.springframework.data.repository.support.RepositoryInvoker; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.core.mapping.PersistentEntitiesResourceMappings; import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.data.rest.core.mapping.ResourceMetadata; import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks; -import org.springframework.data.web.PagedResourcesAssembler; /** * Unit tests for {@link RepositoryEntityController} @@ -44,12 +42,10 @@ import org.springframework.data.web.PagedResourcesAssembler; @ExtendWith(MockitoExtension.class) class RepositoryEntityControllerTest { - @Mock Repositories repositories; @Mock RepositoryRestConfiguration restConfiguration; @Mock RepositoryEntityLinks repositoryEntityLinks; @Mock HttpHeadersPreparer httpHeadersPreparer; @Mock RepositoryInvoker invoker; - @Mock PagedResourcesAssembler assembler; KeyValueMappingContext mappingContext = new KeyValueMappingContext<>(); @@ -69,8 +65,8 @@ class RepositoryEntityControllerTest { .thenReturn(RepositoryPropertyReferenceControllerUnitTests.AllSupportedHttpMethods.INSTANCE); RootResourceInformation information = new RootResourceInformation(metadata, entity, invoker); - RepositoryEntityController repositoryEntityController = new RepositoryEntityController(repositories, - restConfiguration, repositoryEntityLinks, assembler, httpHeadersPreparer); + RepositoryEntityController repositoryEntityController = new RepositoryEntityController( + restConfiguration, repositoryEntityLinks, httpHeadersPreparer); assertThatExceptionOfType(ResourceNotFoundException.class) // .isThrownBy(() -> repositoryEntityController.getItemResource(information, "1", null, null)); 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 ece3d6104..c2d9c1f94 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 @@ -43,7 +43,6 @@ import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.data.rest.core.mapping.ResourceMetadata; import org.springframework.data.rest.core.mapping.ResourceType; import org.springframework.data.rest.core.mapping.SupportedHttpMethods; -import org.springframework.data.web.PagedResourcesAssembler; import org.springframework.hateoas.CollectionModel; import org.springframework.hateoas.Link; import org.springframework.http.HttpMethod; @@ -57,7 +56,6 @@ import org.springframework.http.HttpMethod; class RepositoryPropertyReferenceControllerUnitTests { @Mock Repositories repositories; - @Mock PagedResourcesAssembler assembler; @Mock RepositoryInvokerFactory invokerFactory; @Mock RepositoryInvoker invoker; @Mock ApplicationEventPublisher publisher; @@ -75,7 +73,7 @@ class RepositoryPropertyReferenceControllerUnitTests { when(metadata.getSupportedHttpMethods()).thenReturn(AllSupportedHttpMethods.INSTANCE); RepositoryPropertyReferenceController controller = new RepositoryPropertyReferenceController(repositories, - invokerFactory, assembler); + invokerFactory); controller.setApplicationEventPublisher(publisher); doReturn(invoker).when(invokerFactory).getInvokerFor(Reference.class); diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMappingUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMappingUnitTests.java index 1625cf737..41755476e 100755 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMappingUnitTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMappingUnitTests.java @@ -95,7 +95,7 @@ class RepositoryRestHandlerMappingUnitTests { mockRequest = new MockHttpServletRequest(); listEntitiesMethod = RepositoryEntityController.class.getMethod("getCollectionResource", - RootResourceInformation.class, DefaultedPageable.class, Sort.class, PersistentEntityResourceAssembler.class); + RootResourceInformation.class, DefaultedPageable.class, Sort.class, RepresentationModelAssemblers.class); rootHandlerMethod = RepositoryController.class.getMethod("listRepositories"); } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvConfigurationIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvConfigurationIntegrationTests.java index 7071758da..0ea239d84 100755 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvConfigurationIntegrationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvConfigurationIntegrationTests.java @@ -90,6 +90,7 @@ class RepositoryRestMvConfigurationIntegrationTests { @AfterAll public static void tearDown() { + if (context != null) { context.close(); } diff --git a/src/main/asciidoc/paging-and-sorting.adoc b/src/main/asciidoc/paging-and-sorting.adoc index 660020e70..9f3525871 100644 --- a/src/main/asciidoc/paging-and-sorting.adoc +++ b/src/main/asciidoc/paging-and-sorting.adoc @@ -18,7 +18,7 @@ http://localhost:8080/people/?size=5 The preceding example sets the page size to 5. -To use paging in your own query methods, you need to change the method signature to accept an additional `Pageable` parameter and return a `Page` rather than a `List`. For example, the following query method is exported to `/people/search/nameStartsWith` and supports paging: +To use paging in your own query methods, you need to change the method signature to accept an additional `Pageable` parameter and return a `Page` or `Slice` rather than a `List`. For example, the following query method is exported to `/people/search/nameStartsWith` and supports paging: ==== [source,java] @@ -28,7 +28,7 @@ public Page findByNameStartsWith(@Param("name") String name, Pageable p); ---- ==== -The Spring Data REST exporter recognizes the returned `Page` and gives you the results in the body of the response, just as it would with a non-paged response, but additional links are added to the resource to represent the previous and next pages of data. +The Spring Data REST exporter recognizes the returned `Page`/`Slice` and gives you the results in the body of the response, just as it would with a non-paged response, but additional links are added to the resource to represent the previous and next pages of data. [[paging-and-sorting.paging.prev-and-next-links]] === Previous and Next Links @@ -68,7 +68,6 @@ curl localhost:8080/people?size=5 } } ---- - At the top, we see `_links`: <1> The `self` link serves up the whole collection with some options.