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() {