From 079661d19e4b233fa3c51e6757b563f96bbfc7e6 Mon Sep 17 00:00:00 2001 From: Jon Brisbin Date: Mon, 18 Jun 2012 15:31:23 -0500 Subject: [PATCH] Added support for paging and sorting. --- .../data/rest/webmvc/PagingAndSorting.java | 66 ++++++++ ...agingAndSortingMethodArgumentResolver.java | 117 ++++++++++++++ .../rest/webmvc/RepositoryRestController.java | 146 ++++++++++++++---- .../RepositoryRestMvcConfiguration.java | 1 + .../spec/RepositoryRestControllerSpec.groovy | 10 +- .../rest/test/webmvc/PersonRepository.java | 8 +- 6 files changed, 317 insertions(+), 31 deletions(-) create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PagingAndSorting.java create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PagingAndSortingMethodArgumentResolver.java diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PagingAndSorting.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PagingAndSorting.java new file mode 100644 index 000000000..690064bcd --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PagingAndSorting.java @@ -0,0 +1,66 @@ +package org.springframework.data.rest.webmvc; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Iterator; + +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * @author Jon Brisbin + */ +public class PagingAndSorting implements Pageable { + + final String pageParameter; + final String limitParameter; + final String orderParameter; + private final PageRequest pageRequest; + + public PagingAndSorting(String pageParameter, + String limitParameter, + String orderParameter, + PageRequest pageRequest) { + this.pageParameter = pageParameter; + this.limitParameter = limitParameter; + this.orderParameter = orderParameter; + this.pageRequest = pageRequest; + } + + public PagingAndSorting addSortParameters(UriComponentsBuilder urib) { + Sort sort = pageRequest.getSort(); + if (null != sort) { + Iterator iter = sort.iterator(); + while (iter.hasNext()) { + Sort.Order order = iter.next(); + urib.queryParam(orderParameter, order.getProperty()); + try { + urib.queryParam(URLEncoder.encode(order.getProperty() + ".dir", "ISO-8859-1"), + order.getDirection().toString().toLowerCase()); + } catch (UnsupportedEncodingException ignored) { + // this should never happen + } + } + } + return this; + } + + @Override public int getPageNumber() { + return pageRequest.getPageNumber(); + } + + @Override public int getPageSize() { + return pageRequest.getPageSize(); + } + + @Override public int getOffset() { + return pageRequest.getOffset(); + } + + @Override public Sort getSort() { + return pageRequest.getSort(); + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PagingAndSortingMethodArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PagingAndSortingMethodArgumentResolver.java new file mode 100644 index 000000000..e3c91b412 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PagingAndSortingMethodArgumentResolver.java @@ -0,0 +1,117 @@ +package org.springframework.data.rest.webmvc; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.List; +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.lang.ClassUtils; +import org.springframework.core.MethodParameter; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefaults; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +/** + * @author Jon Brisbin + */ +public class PagingAndSortingMethodArgumentResolver implements HandlerMethodArgumentResolver { + + private static final int DEFAULT_PAGE = 1; // We're 1-based, not 0-based + private static final int DEFAULT_LIMIT = 1000; + + private String pageParameter = "page"; + private String limitParameter = "limit"; + private String sortParameter = "sort"; + + public String getPageParameter() { + return pageParameter; + } + + public PagingAndSortingMethodArgumentResolver setPageParameter(String pageParameter) { + this.pageParameter = pageParameter; + return this; + } + + public String getLimitParameter() { + return limitParameter; + } + + public PagingAndSortingMethodArgumentResolver setLimitParameter(String limitParameter) { + this.limitParameter = limitParameter; + return this; + } + + public String getSortParameter() { + return sortParameter; + } + + public PagingAndSortingMethodArgumentResolver setSortParameter(String sortParameter) { + this.sortParameter = sortParameter; + return this; + } + + @Override public boolean supportsParameter(MethodParameter parameter) { + return ClassUtils.isAssignable(parameter.getParameterType(), PagingAndSorting.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) throws Exception { + HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); + + PageRequest pr = null; + for (Annotation annotation : parameter.getParameterAnnotations()) { + if (annotation instanceof PageableDefaults) { + PageableDefaults defaults = (PageableDefaults) annotation; + pr = new PageRequest(defaults.pageNumber(), defaults.value()); + break; + } + } + if (null == pr) { + int page = DEFAULT_PAGE; + String sPage = request.getParameter(pageParameter); + if (StringUtils.hasText(sPage)) { + try { + page = Integer.parseInt(sPage); + } catch (NumberFormatException ignored) {} + } + int limit = DEFAULT_LIMIT; + String sLimit = request.getParameter(limitParameter); + if (StringUtils.hasText(sLimit)) { + try { + limit = Integer.parseInt(sLimit); + } catch (NumberFormatException ignored) {} + } + + Sort sort = null; + List orders = new ArrayList(); + String[] orderValues = request.getParameterValues(sortParameter); + if (null != orderValues) { + for (String orderParam : orderValues) { + String sortDir = request.getParameter(orderParam + ".dir"); + Sort.Direction dir = (null != sortDir ? Sort.Direction.valueOf(sortDir.toUpperCase()) : Sort.Direction.ASC); + orders.add(new Sort.Order(dir, orderParam)); + } + if (!orders.isEmpty()) { + sort = new Sort(orders); + } + } + + if (null != sort) { + pr = new PageRequest(page - 1, limit, sort); + } else { + pr = new PageRequest(page - 1, limit); + } + } + + return new PagingAndSorting(pageParameter, limitParameter, sortParameter, pr); + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestController.java index fa74ab05f..17e94acec 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestController.java @@ -30,9 +30,11 @@ import org.springframework.context.ApplicationContextAware; import org.springframework.core.convert.ConversionService; import org.springframework.dao.DataRetrievalFailureException; import org.springframework.dao.OptimisticLockingFailureException; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.Repository; import org.springframework.data.rest.core.Handler; import org.springframework.data.rest.core.Link; @@ -262,14 +264,22 @@ public class RepositoryRestController "application/json" } ) - public ModelAndView listEntities(UriComponentsBuilder uriBuilder, + public ModelAndView listEntities(PagingAndSorting pageSort, + UriComponentsBuilder uriBuilder, @PathVariable String repository) { URI baseUri = uriBuilder.build().toUri(); RepositoryMetadata repoMeta = repositoryMetadataFor(repository); Links links = new Links(); - Iterator iter = repoMeta.repository().findAll().iterator(); + Page page = null; + Iterator iter; + if (repoMeta.repository() instanceof PagingAndSortingRepository) { + page = ((PagingAndSortingRepository) repoMeta.repository()).findAll(pageSort); + iter = page.iterator(); + } else { + iter = repoMeta.repository().findAll().iterator(); + } while (iter.hasNext()) { Object o = iter.next(); Serializable id = (Serializable) repoMeta.entityMetadata().idAttribute().get(o); @@ -280,6 +290,35 @@ public class RepositoryRestController buildUri(baseUri, repository, "search"))); Map model = new HashMap(); + // Add paging links + if (null != page) { + HttpHeaders headers = new HttpHeaders(); + headers.set("x-springdata-meta-total-count", String.valueOf(page.getTotalElements())); + headers.set("x-springdata-meta-current-page", String.valueOf(page.getNumber() + 1)); + headers.set("x-springdata-meta-total-pages", String.valueOf(page.getTotalPages())); + model.put(HEADERS, headers); + maybeAddPrevNextLink( + buildUri(baseUri, repository), + repoMeta, + pageSort, + page, + !page.isFirstPage() && page.hasPreviousPage(), + page.getNumber(), + "prev", + links.getLinks() + ); + maybeAddPrevNextLink( + buildUri(baseUri, repository), + repoMeta, + pageSort, + page, + !page.isLastPage() && page.hasNextPage(), + page.getNumber() + 2, + "next", + links.getLinks() + ); + } + model.put(STATUS, HttpStatus.OK); model.put(RESOURCE, links); return new ModelAndView(viewName("list_entities"), model); @@ -331,10 +370,12 @@ public class RepositoryRestController } ) public ModelAndView query(WebRequest request, + PagingAndSorting pageSort, UriComponentsBuilder uriBuilder, @PathVariable String repository, @PathVariable String query) { URI baseUri = uriBuilder.build().toUri(); + Page page = null; RepositoryMetadata repoMeta = repositoryMetadataFor(repository); Repository repo = repoMeta.repository(); @@ -351,8 +392,10 @@ public class RepositoryRestController paramVals[i] = queryVal; } else if (Pageable.class.isAssignableFrom(paramTypes[i])) { // Handle paging + paramVals[i] = pageSort; } else if (Sort.class.isAssignableFrom(paramTypes[i])) { // Handle sorting + paramVals[i] = (null != pageSort ? pageSort.getSort() : null); } else if (conversionService.canConvert(String.class, paramTypes[i])) { // There's a converter from String -> param type paramVals[i] = conversionService.convert(queryVal, paramTypes[i]); @@ -368,35 +411,64 @@ public class RepositoryRestController try { Object result = queryMethod.method().invoke(repo, paramVals); + Iterator iter; if (result instanceof Collection) { - Collection coll = new ArrayList(); - for (Object o : (Collection) result) { - RepositoryMetadata elemRepoMeta = repositoryMetadataFor(o.getClass()); - if (null != elemRepoMeta) { - String id = elemRepoMeta.entityMetadata().idAttribute().get(o).toString(); - String rel = elemRepoMeta.rel() + "." + elemRepoMeta.entityMetadata().type().getSimpleName(); - URI path = buildUri(baseUri, repository, id); - coll.add(new SimpleLink(rel, path)); - } else { - coll.add(o); - } - } - - model.put(RESOURCE, coll); - model.put(STATUS, HttpStatus.OK); + iter = ((Collection) result).iterator(); + } else if (result instanceof Page) { + page = (Page) result; + iter = page.iterator(); } else { - RepositoryMetadata elemRepoMeta = repositoryMetadataFor(result.getClass()); - if (null != elemRepoMeta) { - String id = elemRepoMeta.entityMetadata().idAttribute().get(result).toString(); - String rel = elemRepoMeta.rel() + "." + elemRepoMeta.entityMetadata().type().getSimpleName(); - URI path = buildUri(baseUri, repository, id); - Link link = new SimpleLink(rel, path); - model.put(RESOURCE, link); - } else { - model.put(RESOURCE, result); - } - model.put(STATUS, HttpStatus.OK); + List l = new ArrayList(); + l.add(result); + iter = l.iterator(); } + + List resultList = new ArrayList(); + while (iter.hasNext()) { + Object obj = iter.next(); + RepositoryMetadata elemRepoMeta = repositoryMetadataFor(obj.getClass()); + if (null != elemRepoMeta) { + String id = elemRepoMeta.entityMetadata().idAttribute().get(obj).toString(); + String rel = elemRepoMeta.rel() + "." + elemRepoMeta.entityMetadata().type().getSimpleName() + "." + id; + URI path = buildUri(baseUri, repository, id); + resultList.add(new SimpleLink(rel, path)); + } else { + resultList.add(obj); + } + } + + // Add paging links + if (null != page) { + HttpHeaders headers = new HttpHeaders(); + headers.set("x-springdata-meta-total-count", String.valueOf(page.getTotalElements())); + headers.set("x-springdata-meta-current-page", String.valueOf(page.getNumber() + 1)); + headers.set("x-springdata-meta-total-pages", String.valueOf(page.getTotalPages())); + model.put(HEADERS, headers); + maybeAddPrevNextLink( + buildUri(baseUri, repository, "search", query), + repoMeta, + pageSort, + page, + !page.isFirstPage() && page.hasPreviousPage(), + page.getNumber(), + "prev", + resultList + ); + maybeAddPrevNextLink( + buildUri(baseUri, repository, "search", query), + repoMeta, + pageSort, + page, + !page.isLastPage() && page.hasNextPage(), + page.getNumber() + 2, + "next", + resultList + ); + } + + model.put(RESOURCE, resultList); + model.put(STATUS, HttpStatus.OK); + } catch (IllegalAccessException e) { throw new DataRetrievalFailureException(e.getMessage(), e); } catch (InvocationTargetException e) { @@ -1063,6 +1135,24 @@ public class RepositoryRestController return selfUri; } + @SuppressWarnings({"unchecked"}) + private void maybeAddPrevNextLink(URI resourceUri, + RepositoryMetadata repoMeta, + PagingAndSorting pageSort, + Page page, + boolean addIf, + int nextPage, + String rel, + List links) { + if (null != page && addIf) { + UriComponentsBuilder urib = UriComponentsBuilder.fromUri(resourceUri); + urib.queryParam(pageSort.pageParameter, nextPage); // PageRequest is 0-based, so it's already (page - 1) + urib.queryParam(pageSort.limitParameter, page.getSize()); + pageSort.addSortParameters(urib); + links.add(new SimpleLink(repoMeta.rel() + "." + rel, urib.build().toUri())); + } + } + @SuppressWarnings({"unchecked"}) private V stringToSerializable(String s, Class targetType) { if (ClassUtils.isAssignable(targetType, String.class)) { diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestMvcConfiguration.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestMvcConfiguration.java index e87891c56..ecafe05a3 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestMvcConfiguration.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestMvcConfiguration.java @@ -52,6 +52,7 @@ public class RepositoryRestMvcConfiguration extends WebMvcConfigurerAdapter { @Override public void addArgumentResolvers(List argumentResolvers) { argumentResolvers.add(new ServerHttpRequestMethodArgumentResolver()); + argumentResolvers.add(new PagingAndSortingMethodArgumentResolver()); } @Override public void addInterceptors(InterceptorRegistry registry) { diff --git a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/RepositoryRestControllerSpec.groovy b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/RepositoryRestControllerSpec.groovy index 275534c8e..0a94856bf 100644 --- a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/RepositoryRestControllerSpec.groovy +++ b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/RepositoryRestControllerSpec.groovy @@ -1,12 +1,13 @@ package org.springframework.data.rest.webmvc.spec -import javax.persistence.EntityManagerFactory import org.codehaus.jackson.map.ObjectMapper import org.codehaus.jackson.map.ser.CustomSerializerFactory import org.springframework.context.support.ClassPathXmlApplicationContext +import org.springframework.data.domain.PageRequest import org.springframework.data.rest.core.SimpleLink import org.springframework.data.rest.core.util.FluentBeanSerializer import org.springframework.data.rest.test.webmvc.Address +import org.springframework.data.rest.webmvc.PagingAndSorting import org.springframework.data.rest.webmvc.RepositoryRestController import org.springframework.data.rest.webmvc.RepositoryRestMvcConfiguration import org.springframework.http.HttpStatus @@ -22,6 +23,8 @@ import org.springframework.web.util.UriComponentsBuilder import spock.lang.Shared import spock.lang.Specification +import javax.persistence.EntityManagerFactory + /** * @author Jon Brisbin */ @@ -34,6 +37,8 @@ class RepositoryRestControllerSpec extends Specification { @Shared RepositoryRestController controller @Shared + PagingAndSorting pageSort + @Shared EntityManagerFactory emf MockHttpServletRequest createRequest(String method, String path) { @@ -62,6 +67,7 @@ class RepositoryRestControllerSpec extends Specification { emf = webAppCtx.getBean(EntityManagerFactory) controller = webAppCtx.getBean(RepositoryRestController) + pageSort = new PagingAndSorting("page", "limit", "sort", new PageRequest(0, 1000)) uriBuilder = UriComponentsBuilder.fromUriString("http://localhost:8080/data") def customSerializerFactory = new CustomSerializerFactory() @@ -118,7 +124,7 @@ class RepositoryRestControllerSpec extends Specification { when: "listing available entities" mv.model.clear() - mv = controller.listEntities(uriBuilder, "people") + mv = controller.listEntities(pageSort, uriBuilder, "people") def peopleLinks = mv.model.resource?.links then: diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/PersonRepository.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/PersonRepository.java index 50c3fce80..3c7561c5e 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/PersonRepository.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/PersonRepository.java @@ -2,7 +2,10 @@ package org.springframework.data.rest.test.webmvc; import java.util.List; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; import org.springframework.data.rest.repository.annotation.RestResource; @@ -10,9 +13,12 @@ import org.springframework.data.rest.repository.annotation.RestResource; * @author Jon Brisbin */ @RestResource(path = "people", rel = "peeps") -public interface PersonRepository extends CrudRepository { +public interface PersonRepository extends PagingAndSortingRepository { @RestResource(path = "name", rel = "names") public List findByName(@Param("name") String name); + @RestResource(path = "nameStartsWith", rel = "nameStartsWith") + public Page findByNameStartsWith(@Param("name") String name, Pageable p); + }