Added support for paging and sorting.

This commit is contained in:
Jon Brisbin
2012-06-18 15:31:23 -05:00
parent 3e2a79d329
commit 079661d19e
6 changed files with 317 additions and 31 deletions

View File

@@ -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<Sort.Order> 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();
}
}

View File

@@ -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<Sort.Order> orders = new ArrayList<Sort.Order>();
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);
}
}

View File

@@ -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 extends Serializable> V stringToSerializable(String s, Class<V> targetType) {
if (ClassUtils.isAssignable(targetType, String.class)) {

View File

@@ -52,6 +52,7 @@ public class RepositoryRestMvcConfiguration extends WebMvcConfigurerAdapter {
@Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new ServerHttpRequestMethodArgumentResolver());
argumentResolvers.add(new PagingAndSortingMethodArgumentResolver());
}
@Override public void addInterceptors(InterceptorRegistry registry) {

View File

@@ -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 <jbrisbin@vmware.com>
*/
@@ -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:

View File

@@ -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 <jbrisbin@vmware.com>
*/
@RestResource(path = "people", rel = "peeps")
public interface PersonRepository extends CrudRepository<Person, Long> {
public interface PersonRepository extends PagingAndSortingRepository<Person, Long> {
@RestResource(path = "name", rel = "names")
public List<Person> findByName(@Param("name") String name);
@RestResource(path = "nameStartsWith", rel = "nameStartsWith")
public Page findByNameStartsWith(@Param("name") String name, Pageable p);
}