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.