From 8504a8837c81a615bfb5b174f21cd4f37b095693 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Wed, 15 Oct 2014 13:30:53 +0200 Subject: [PATCH] DATAREST-384 - Fixed affordances and execution of sortable search resources. Search resources are now considered sortable if they contain a Sort parameter. This is now reflected in MethodResourceMapping.isSortableResource(). Building on top of that, the RepositorySearchController now appends the sort template variable to links generated when listing search resources. It also now accepts resolved Sort instances to forward them to the query method execution. The controller now also uses DefaultedPageable so that request missing pagination information use the defaults configured for the PageableHandlerMethodArgumentResolver. --- .../core/mapping/MethodResourceMapping.java | 7 +++++ .../RepositoryMethodResourceMapping.java | 17 ++++++++++- ...ositoryMethodResourceMappingUnitTests.java | 20 +++++++++++++ .../webmvc/RepositorySearchController.java | 28 +++++++++++++------ .../webmvc/support/DefaultedPageable.java | 2 +- ...itorySearchControllerIntegrationTests.java | 4 ++- .../data/rest/webmvc/jpa/BookRepository.java | 6 ++++ .../data/rest/webmvc/jpa/JpaWebTests.java | 24 ++++++++++++++++ .../rest/webmvc/jpa/TestDataPopulator.java | 2 +- 9 files changed, 98 insertions(+), 12 deletions(-) diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/MethodResourceMapping.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/MethodResourceMapping.java index fb8a66964..5504ea9d8 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/MethodResourceMapping.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/MethodResourceMapping.java @@ -37,4 +37,11 @@ public interface MethodResourceMapping extends ResourceMapping { * @return */ ParametersMetadata getParametersMetadata(); + + /** + * Returns whether the resource is sortable. + * + * @return + */ + boolean isSortableResource(); } diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMapping.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMapping.java index c5b1e1340..2bf7288c5 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMapping.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMapping.java @@ -24,6 +24,7 @@ import java.util.List; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.data.repository.query.Param; import org.springframework.data.rest.core.Path; import org.springframework.data.rest.core.annotation.RestResource; @@ -46,6 +47,7 @@ class RepositoryMethodResourceMapping implements MethodResourceMapping { private final Path path; private final Method method; private final boolean paging; + private final boolean sorting; private final List parameterMetadata; @@ -69,7 +71,11 @@ class RepositoryMethodResourceMapping implements MethodResourceMapping { annotation.path()); this.method = method; this.parameterMetadata = discoverParameterMetadata(method, resourceRel.concat(".").concat(rel)); - this.paging = Arrays.asList(method.getParameterTypes()).contains(Pageable.class); + + List> parameterTypes = Arrays.asList(method.getParameterTypes()); + + this.paging = parameterTypes.contains(Pageable.class); + this.sorting = parameterTypes.contains(Sort.class); } private static final List discoverParameterMetadata(Method method, String baseRel) { @@ -139,6 +145,15 @@ class RepositoryMethodResourceMapping implements MethodResourceMapping { return paging; } + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.MethodResourceMapping#isSortableResource() + */ + @Override + public boolean isSortableResource() { + return sorting; + } + /* * (non-Javadoc) * @see org.springframework.data.rest.core.mapping.ResourceMapping#getDescription() diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMappingUnitTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMappingUnitTests.java index 32535b028..407d99662 100644 --- a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMappingUnitTests.java +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMappingUnitTests.java @@ -23,6 +23,7 @@ import java.lang.reflect.Method; import org.junit.Test; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; @@ -104,6 +105,23 @@ public class RepositoryMethodResourceMappingUnitTests { assertThat(mapping.getRel(), is("findByEmailAddress")); } + /** + * @see DATAREST-384 + */ + @Test + public void considersResourceSortableIfSortParameterIsPresent() throws Exception { + + Method method = PersonRepository.class.getMethod("findByEmailAddress", String.class, Sort.class); + RepositoryMethodResourceMapping mapping = new RepositoryMethodResourceMapping(method, resourceMapping); + + assertThat(mapping.isSortableResource(), is(true)); + + method = PersonRepository.class.getMethod("findByEmailAddress", String.class, Pageable.class); + mapping = new RepositoryMethodResourceMapping(method, resourceMapping); + + assertThat(mapping.isSortableResource(), is(false)); + } + static class Person {} interface PersonRepository extends Repository { @@ -118,5 +136,7 @@ public class RepositoryMethodResourceMappingUnitTests { @RestResource(path = "fooPaged") Page findByEmailAddress(String email, Pageable pageable); + + Page findByEmailAddress(String email, Sort pageable); } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java index 520242a0d..9b8196af0 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java @@ -24,12 +24,14 @@ import java.util.List; import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.data.rest.core.invoke.RepositoryInvoker; import org.springframework.data.rest.core.mapping.MethodResourceMapping; import org.springframework.data.rest.core.mapping.ParameterMetadata; import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.data.rest.core.mapping.SearchResourceMappings; +import org.springframework.data.rest.webmvc.support.DefaultedPageable; +import org.springframework.data.web.HateoasSortHandlerMethodArgumentResolver; import org.springframework.data.web.PagedResourcesAssembler; import org.springframework.hateoas.EntityLinks; import org.springframework.hateoas.Link; @@ -41,6 +43,7 @@ import org.springframework.hateoas.Resources; import org.springframework.hateoas.TemplateVariable; import org.springframework.hateoas.TemplateVariable.VariableType; import org.springframework.hateoas.TemplateVariables; +import org.springframework.hateoas.UriTemplate; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -53,6 +56,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.context.request.WebRequest; +import org.springframework.web.util.UriComponentsBuilder; /** * Controller to lookup and execute searches on a given repository. @@ -69,6 +73,7 @@ class RepositorySearchController extends AbstractRepositoryRestController { private final EntityLinks entityLinks; private final ResourceMappings mappings; private final PagedResourcesAssembler assembler; + private final HateoasSortHandlerMethodArgumentResolver sortResolver; /** * Creates a new {@link RepositorySearchController} using the given {@link PagedResourcesAssembler}, @@ -80,16 +85,18 @@ class RepositorySearchController extends AbstractRepositoryRestController { */ @Autowired public RepositorySearchController(PagedResourcesAssembler assembler, EntityLinks entityLinks, - ResourceMappings mappings) { + ResourceMappings mappings, HateoasSortHandlerMethodArgumentResolver sortResolver) { super(assembler); Assert.notNull(entityLinks, "EntityLinks must not be null!"); Assert.notNull(mappings, "ResourceMappings must not be null!"); + Assert.notNull(sortResolver, "HateoasSortHandlerMethodArgumentResolver must not be null!"); this.entityLinks = entityLinks; this.mappings = mappings; this.assembler = assembler; + this.sortResolver = sortResolver; } /** @@ -162,10 +169,10 @@ class RepositorySearchController extends AbstractRepositoryRestController { @ResponseBody @RequestMapping(value = BASE_MAPPING + "/{search}", method = RequestMethod.GET) public ResponseEntity executeSearch(RootResourceInformation resourceInformation, WebRequest request, - @PathVariable String search, Pageable pageable, PersistentEntityResourceAssembler assembler) { + @PathVariable String search, DefaultedPageable pageable, Sort sort, PersistentEntityResourceAssembler assembler) { Method method = checkExecutability(resourceInformation, search); - Object resources = executeQueryMethod(resourceInformation.getInvoker(), request, method, pageable, assembler); + Object resources = executeQueryMethod(resourceInformation.getInvoker(), request, method, pageable, sort, assembler); return new ResponseEntity(resources, HttpStatus.OK); } @@ -183,11 +190,11 @@ class RepositorySearchController extends AbstractRepositoryRestController { @RequestMapping(value = BASE_MAPPING + "/{search}", method = RequestMethod.GET, // produces = { "application/x-spring-data-compact+json" }) public ResourceSupport executeSearchCompact(RootResourceInformation resourceInformation, WebRequest request, - @PathVariable String repository, @PathVariable String search, Pageable pageable, + @PathVariable String repository, @PathVariable String search, DefaultedPageable pageable, Sort sort, PersistentEntityResourceAssembler assembler) { Method method = checkExecutability(resourceInformation, search); - Object resource = executeQueryMethod(resourceInformation.getInvoker(), request, method, pageable, assembler); + Object resource = executeQueryMethod(resourceInformation.getInvoker(), request, method, pageable, sort, assembler); List links = new ArrayList(); @@ -272,10 +279,10 @@ class RepositorySearchController extends AbstractRepositoryRestController { * @return */ private Object executeQueryMethod(final RepositoryInvoker invoker, WebRequest request, Method method, - Pageable pageable, PersistentEntityResourceAssembler assembler) { + DefaultedPageable pageable, Sort sort, PersistentEntityResourceAssembler assembler) { Map parameters = request.getParameterMap(); - Object result = invoker.invokeQueryMethod(method, parameters, pageable, null); + Object result = invoker.invokeQueryMethod(method, parameters, pageable.getPageable(), sort); if (ClassUtils.isPrimitiveOrWrapper(method.getReturnType())) { return result; @@ -315,6 +322,11 @@ class RepositorySearchController extends AbstractRepositoryRestController { if (mapping.isPagingResource()) { link = assembler.appendPaginationParameterTemplates(link); + } else if (mapping.isSortableResource()) { + + TemplateVariables sortVariable = sortResolver.getSortTemplateVariables(null, UriComponentsBuilder + .fromUriString(link.expand().getHref()).build()); + link = new Link(new UriTemplate(link.getHref()).with(sortVariable), link.getRel()); } links.add(link); diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/DefaultedPageable.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/DefaultedPageable.java index 8d6b41d2f..de4558e29 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/DefaultedPageable.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/DefaultedPageable.java @@ -33,7 +33,7 @@ public class DefaultedPageable { * @param pageable can be {@literal null}. * @param isDefault */ - DefaultedPageable(Pageable pageable, boolean isDefault) { + public DefaultedPageable(Pageable pageable, boolean isDefault) { this.pageable = pageable; this.isDefault = isDefault; diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositorySearchControllerIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositorySearchControllerIntegrationTests.java index a0cba6d11..96f3e4178 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositorySearchControllerIntegrationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositorySearchControllerIntegrationTests.java @@ -22,6 +22,7 @@ import static org.springframework.data.rest.webmvc.WebTestUtils.*; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; import org.springframework.data.rest.core.mapping.ResourceMetadata; import org.springframework.data.rest.webmvc.ResourceTester.HasSelfLink; import org.springframework.data.rest.webmvc.jpa.Address; @@ -30,6 +31,7 @@ import org.springframework.data.rest.webmvc.jpa.CreditCard; 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.hateoas.PagedResources; import org.springframework.hateoas.ResourceSupport; import org.springframework.http.HttpEntity; @@ -88,7 +90,7 @@ public class RepositorySearchControllerIntegrationTests extends AbstractControll RootResourceInformation resourceInformation = getResourceInformation(Person.class); ResponseEntity response = controller.executeSearch(resourceInformation, getRequest(parameters), - "firstname", null, assembler); + "firstname", new DefaultedPageable(new PageRequest(0, 10), true), null, assembler); ResourceTester tester = ResourceTester.of(response.getBody()); PagedResources pagedResources = tester.assertIsPage(); diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/BookRepository.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/BookRepository.java index 716ea0ff0..96ad97cd8 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/BookRepository.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/BookRepository.java @@ -15,8 +15,12 @@ */ package org.springframework.data.rest.webmvc.jpa; +import java.util.List; + +import org.springframework.data.domain.Sort; import org.springframework.data.repository.CrudRepository; import org.springframework.data.rest.core.annotation.RepositoryRestResource; +import org.springframework.data.rest.core.annotation.RestResource; /** * @author Oliver Gierke @@ -24,4 +28,6 @@ import org.springframework.data.rest.core.annotation.RepositoryRestResource; @RepositoryRestResource(excerptProjection = BookExcerpt.class) public interface BookRepository extends CrudRepository { + @RestResource(rel = "find-by-sorted") + List findBy(Sort sort); } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java index 8abb0f6b6..193d8c897 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java @@ -557,6 +557,30 @@ public class JpaWebTests extends AbstractWebIntegrationTests { andExpect(status().isNotFound()); } + /** + * @see DATAREST-384 + */ + @Test + public void execturesSearchThatTakesASort() throws Exception { + + Link booksLink = discoverUnique("books"); + Link searchLink = discoverUnique(booksLink, "search"); + Link findBySortedLink = discoverUnique(searchLink, "find-by-sorted"); + + // Assert sort options advertised + assertThat(findBySortedLink.isTemplated(), is(true)); + assertThat(findBySortedLink.getVariableNames(), contains("sort")); + + // Assert results returned as specified + follow(findBySortedLink.expand("title,desc")).// + andExpect(jsonPath("$._embedded.books[0].title").value("Spring Data (Second Edition)")).// + andExpect(jsonPath("$._embedded.books[1].title").value("Spring Data")); + + follow(findBySortedLink.expand("title,asc")).// + andExpect(jsonPath("$._embedded.books[0].title").value("Spring Data")).// + andExpect(jsonPath("$._embedded.books[1].title").value("Spring Data (Second Edition)")); + } + /** * Asserts the {@link Person} resource the given link points to contains siblings with the given names. * diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/TestDataPopulator.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/TestDataPopulator.java index 15750d0da..39bd404f0 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/TestDataPopulator.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/TestDataPopulator.java @@ -37,7 +37,7 @@ public class TestDataPopulator { Iterable authors = authorRepository.save(Arrays.asList(ollie, mark, michael, david, john, thomas)); books.save(new Book("1449323952", "Spring Data", authors)); - books.save(new Book("1449323953", "Spring Data (SecondEdition)", authors)); + books.save(new Book("1449323953", "Spring Data (Second Edition)", authors)); } private void populateOrders() {