Support for query methods returning a Slice.
Built on the the just introduced RepresentationModelAssembler implementations based on Slice in Spring HATEOAS and Spring Data Commons we now support returning a SlicedModel from the controller backing search resources ultimately triggering repository query methods. The introduction triggered the refactoring to introduce RepresentationModelAssemblers (RMA) to remove the need for controllers inheriting from AbstractRepositoryController to access RepresentationModel assembly functionality. RMA acts as a facade for both Paged-/SlicedResourceAssembler as well as PersistentEntityResourceAssembler. Fixes #2235.
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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<Object> 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<String, Object> parameters = new LinkedMultiValueMap<String, Object>(1);
|
||||
parameters.add("firstname", "John");
|
||||
|
||||
doAnswer(new Answer<CollectionModel<?>>() {
|
||||
|
||||
@Override
|
||||
public CollectionModel<?> answer(InvocationOnMock invocation) throws Throwable {
|
||||
|
||||
var page = (Page<Object>) 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<Object> pagedResources = tester.assertIsPage();
|
||||
@@ -173,13 +197,22 @@ class RepositorySearchControllerIntegrationTests extends AbstractControllerInteg
|
||||
@Test // DATAREST-502
|
||||
void interpretsUriAsReferenceToRelatedEntity() {
|
||||
|
||||
MultiValueMap<String, Object> parameters = new LinkedMultiValueMap<String, Object>(1);
|
||||
var parameters = new LinkedMultiValueMap<String, Object>(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<CollectionModel<?>>() {
|
||||
|
||||
@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();
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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<Object> pagedResourcesAssembler;
|
||||
|
||||
/**
|
||||
* Creates a new {@link AbstractRepositoryRestController} for the given {@link PagedResourcesAssembler} and
|
||||
* {@link AuditableBeanWrapperFactory}.
|
||||
*
|
||||
* @param pagedResourcesAssembler must not be {@literal null}.
|
||||
*/
|
||||
public AbstractRepositoryRestController(PagedResourcesAssembler<Object> 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<Link> baseLink) {
|
||||
|
||||
if (source instanceof Page) {
|
||||
Page<Object> page = (Page<Object>) source;
|
||||
return entitiesToResources(page, assembler, domainType, baseLink);
|
||||
} else if (source instanceof Iterable) {
|
||||
return entitiesToResources((Iterable<Object>) source, assembler, domainType);
|
||||
} else {
|
||||
return CollectionModel.empty();
|
||||
}
|
||||
}
|
||||
|
||||
protected CollectionModel<?> entitiesToResources(Page<Object> page, PersistentEntityResourceAssembler assembler,
|
||||
Class<?> domainType, Optional<Link> baseLink) {
|
||||
|
||||
if (page.getContent().isEmpty()) {
|
||||
return baseLink.<PagedModel<?>> 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<Object> entities,
|
||||
PersistentEntityResourceAssembler assembler, Class<?> domainType) {
|
||||
|
||||
if (!entities.iterator().hasNext()) {
|
||||
|
||||
List<Object> content = Arrays.<Object> asList(WRAPPERS.emptyCollectionOf(domainType));
|
||||
return CollectionModel.of(content, getDefaultSelfLink());
|
||||
}
|
||||
|
||||
List<EntityModel<Object>> resources = new ArrayList<EntityModel<Object>>();
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -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<String> getHeaders(String name) {
|
||||
|
||||
return HttpHeaders.ACCEPT.equalsIgnoreCase(name) && (acceptMediaTypes != null //
|
||||
)
|
||||
return HttpHeaders.ACCEPT.equalsIgnoreCase(name) && acceptMediaTypes != null
|
||||
? Collections.enumeration(acceptMediaTypeStrings) //
|
||||
: super.getHeaders(name);
|
||||
}
|
||||
|
||||
@@ -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<RepresentationModel<?>> toEmptyResponse(HttpStatus status, HttpHeaders headers) {
|
||||
return toResponseEntity(status, headers, Optional.empty());
|
||||
}
|
||||
|
||||
static Link getDefaultSelfLink() {
|
||||
return Link.of(ServletUriComponentsBuilder.fromCurrentRequest().build().toUriString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,6 @@ public class PersistentEntityResource extends EntityModel<Object> {
|
||||
* @param links must not be {@literal null}.
|
||||
* @param embeddeds can be {@literal null}.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
private PersistentEntityResource(PersistentEntity<?, ?> entity, Object content, Iterable<Link> links,
|
||||
Iterable<EmbeddedWrapper> embeddeds, boolean isNew, boolean nested) {
|
||||
|
||||
|
||||
@@ -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<Object> 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<Object>(headers, HttpStatus.OK);
|
||||
@@ -103,11 +96,12 @@ public class RepositoryController extends AbstractRepositoryRestController {
|
||||
@RequestMapping(value = { "/", "" }, method = RequestMethod.GET)
|
||||
public HttpEntity<RepositoryLinksResource> 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));
|
||||
}
|
||||
|
||||
@@ -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<String> 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<Object> 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<Link> 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<EntityModel<?>> 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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Object> 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<RepresentationModel<?>> 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<ReferencedProperty, RepresentationModel<?>> handler = prop -> prop.mapValue(it -> {
|
||||
|
||||
if (prop.property.isCollectionLike()) {
|
||||
|
||||
return toCollectionModel((Iterable<?>) it, assembler, prop.propertyType, Optional.empty());
|
||||
return assemblers.toCollectionModel((Iterable<Object>) it, prop.propertyType);
|
||||
|
||||
} else if (prop.property.isMap()) {
|
||||
|
||||
return ((Map<Object, Object>) 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<RepresentationModel<?>> 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<ReferencedProperty, RepresentationModel<?>> 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<RepresentationModel<?>> followPropertyReferenceCompact(RootResourceInformation repoRequest,
|
||||
@BackendId Serializable id, @PathVariable String property, @RequestHeader HttpHeaders requestHeaders,
|
||||
PersistentEntityResourceAssembler assembler) throws Exception {
|
||||
RepresentationModelAssemblers assemblers)
|
||||
throws Exception {
|
||||
|
||||
Function<ReferencedProperty, RepresentationModel<?>> 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<Object> incoming, @BackendId Serializable id,
|
||||
@PathVariable String property) throws Exception {
|
||||
|
||||
CollectionModel<Object> source = incoming == null ? CollectionModel.empty() : incoming;
|
||||
RepositoryInvoker invoker = resourceInformation.getInvoker();
|
||||
var source = incoming == null ? CollectionModel.empty() : incoming;
|
||||
var invoker = resourceInformation.getInvoker();
|
||||
|
||||
Function<ReferencedProperty, RepresentationModel<?>> 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<ReferencedProperty, RepresentationModel<?>> 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<Object> 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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Object> 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<String, Object> parameters, @PathVariable String search, DefaultedPageable pageable,
|
||||
Sort sort, PersistentEntityResourceAssembler assembler, @RequestHeader HttpHeaders headers) {
|
||||
@RequestParam MultiValueMap<String, Object> parameters, @PathVariable String search,
|
||||
DefaultedPageable pageable,
|
||||
Sort sort, @RequestHeader HttpHeaders headers, RepresentationModelAssemblers assemblers) {
|
||||
|
||||
Method method = checkExecutability(resourceInformation, search);
|
||||
Optional<Object> 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<Object> source, final PersistentEntityResourceAssembler assembler,
|
||||
Class<?> domainType, Optional<Link> baseLink, HttpHeaders headers, RootResourceInformation information) {
|
||||
protected ResponseEntity<?> toModel(Optional<Object> 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<String, Object> parameters,
|
||||
@PathVariable String repository, @PathVariable String search, DefaultedPageable pageable, Sort sort,
|
||||
PersistentEntityResourceAssembler assembler) {
|
||||
RepresentationModelAssemblers assemblers) {
|
||||
|
||||
Method method = checkExecutability(resourceInformation, search);
|
||||
Optional<Object> 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<Link> links = new ArrayList<Link>();
|
||||
var links = new ArrayList<Link>();
|
||||
|
||||
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<Object>(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<Object> executeQueryMethod(final RepositoryInvoker invoker,
|
||||
@RequestParam MultiValueMap<String, Object> parameters, Method method, DefaultedPageable pageable, Sort sort,
|
||||
PersistentEntityResourceAssembler assembler) {
|
||||
@RequestParam MultiValueMap<String, Object> parameters, Method method, DefaultedPageable pageable,
|
||||
Sort sort) {
|
||||
|
||||
MultiValueMap<String, Object> result = new LinkedMultiValueMap<String, Object>(parameters);
|
||||
MethodParameters methodParameters = new MethodParameters(method, new AnnotationAttribute(Param.class));
|
||||
List<MethodParameter> parameterList = methodParameters.getParameters();
|
||||
List<TypeInformation<?>> parameterTypeInformations = ClassTypeInformation.from(method.getDeclaringClass())
|
||||
.getParameterTypes(method);
|
||||
var result = new LinkedMultiValueMap<String, Object>(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<Object> result = new ArrayList<Object>(source.size());
|
||||
var result = new ArrayList<Object>(source.size());
|
||||
|
||||
for (Object element : source) {
|
||||
|
||||
|
||||
@@ -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<Object> pagedResourcesAssembler;
|
||||
private final SlicedResourcesAssembler<Object> 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<Object> pagedResourcesAssembler,
|
||||
SlicedResourcesAssembler<Object> 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<Object>) 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<Object> page, Class<?> domainType) {
|
||||
|
||||
return page.isEmpty()
|
||||
? pagedResourcesAssembler.toEmptyModel(page, domainType)
|
||||
: pagedResourcesAssembler.toModel(page, persistentEntityResourceAssembler);
|
||||
}
|
||||
|
||||
private CollectionModel<?> entitiesToResources(Slice<Object> slice, Class<?> domainType) {
|
||||
|
||||
return slice.isEmpty()
|
||||
? slicedResourcesAssembler.toEmptyModel(slice, domainType) //
|
||||
: slicedResourcesAssembler.toModel(slice, persistentEntityResourceAssembler);
|
||||
|
||||
}
|
||||
|
||||
private CollectionModel<?> entitiesToResources(Iterable<Object> entities, Class<?> domainType) {
|
||||
|
||||
var selfLink = ControllerUtils.getDefaultSelfLink();
|
||||
|
||||
return !entities.iterator().hasNext()
|
||||
? CollectionModel.of(List.of(WRAPPERS.emptyCollectionOf(domainType)), selfLink)
|
||||
: persistentEntityResourceAssembler.toCollectionModel(entities).add(selfLink);
|
||||
}
|
||||
}
|
||||
@@ -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<Object> resourcesAssembler;
|
||||
private final Repositories repositories;
|
||||
private final RepositoryEntityLinks entityLinks;
|
||||
|
||||
RestControllerConfiguration(RepositoryRestConfiguration restConfiguration,
|
||||
RepositoryResourceMappings resourceMappings, PagedResourcesAssembler<Object> 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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<String> stringMethods = supported.butWithout(method) //
|
||||
|
||||
@@ -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 <jbrisbin@vmware.com>
|
||||
*/
|
||||
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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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> backendIdHandlerMethodArgumentResolver;
|
||||
private final Lazy<Associations> associationLinks;
|
||||
private final Lazy<EnumTranslator> enumTranslator;
|
||||
private final Lazy<ServerHttpRequestMethodArgumentResolver> serverHttpRequestMethodArgumentResolver;
|
||||
private final Lazy<ETagArgumentResolver> eTagArgumentResolver;
|
||||
private final Lazy<RepositoryInvokerFactory> repositoryInvokerFactory;
|
||||
private final Lazy<RepositoryRestConfiguration> repositoryRestConfiguration;
|
||||
private final Lazy<HateoasPageableHandlerMethodArgumentResolver> pageableResolver;
|
||||
private final Lazy<HateoasSortHandlerMethodArgumentResolver> sortResolver;
|
||||
private final Lazy<PersistentEntityResourceAssemblerArgumentResolver> 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<BackendIdConverter, Class<?>> 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<HateoasPageableHandlerMethodArgumentResolver> pageableResolver, //
|
||||
public RepositoryEntityLinks entityLinks(
|
||||
ObjectFactory<HateoasPageableHandlerMethodArgumentResolver> pageableResolver, //
|
||||
Repositories repositories, //
|
||||
RepositoryResourceMappings resourceMappings, //
|
||||
PluginRegistry<BackendIdConverter, //
|
||||
@@ -490,7 +481,8 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
|
||||
@Bean
|
||||
public PersistentEntityResourceHandlerMethodArgumentResolver persistentEntityArgumentResolver(
|
||||
@Qualifier("defaultMessageConverters") List<HttpMessageConverter<?>> 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<MediaType> 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<HttpMessageConverter<?>> 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<HandlerMethodArgumentResolver> 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<Object> pagedResourcesAssembler,
|
||||
SlicedResourcesAssembler<Object> 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();
|
||||
|
||||
@@ -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<Object> pagedResourcesAssembler;
|
||||
private final SlicedResourcesAssembler<Object> 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<Object> pagedResourcesAssembler,
|
||||
SlicedResourcesAssembler<Object> 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
@org.springframework.lang.NonNullApi
|
||||
package org.springframework.data.rest.webmvc;
|
||||
@@ -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<Object> 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));
|
||||
|
||||
@@ -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<Object> 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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ class RepositoryRestMvConfigurationIntegrationTests {
|
||||
|
||||
@AfterAll
|
||||
public static void tearDown() {
|
||||
|
||||
if (context != null) {
|
||||
context.close();
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user