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.
This commit is contained in:
Oliver Gierke
2014-10-15 13:30:53 +02:00
parent 0b5726ac41
commit 8504a8837c
9 changed files with 98 additions and 12 deletions

View File

@@ -37,4 +37,11 @@ public interface MethodResourceMapping extends ResourceMapping {
* @return
*/
ParametersMetadata getParametersMetadata();
/**
* Returns whether the resource is sortable.
*
* @return
*/
boolean isSortableResource();
}

View File

@@ -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> 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<Class<?>> parameterTypes = Arrays.asList(method.getParameterTypes());
this.paging = parameterTypes.contains(Pageable.class);
this.sorting = parameterTypes.contains(Sort.class);
}
private static final List<ParameterMetadata> 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()

View File

@@ -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<Person, Long> {
@@ -118,5 +136,7 @@ public class RepositoryMethodResourceMappingUnitTests {
@RestResource(path = "fooPaged")
Page<Person> findByEmailAddress(String email, Pageable pageable);
Page<Person> findByEmailAddress(String email, Sort pageable);
}
}

View File

@@ -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<Object> 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<Object> 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<Object> 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<Object>(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<Link> links = new ArrayList<Link>();
@@ -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<String, String[]> 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);

View File

@@ -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;

View File

@@ -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<Object> 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<Object> pagedResources = tester.assertIsPage();

View File

@@ -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<Book, Long> {
@RestResource(rel = "find-by-sorted")
List<Book> findBy(Sort sort);
}

View File

@@ -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.
*

View File

@@ -37,7 +37,7 @@ public class TestDataPopulator {
Iterable<Author> 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() {