Added support for paging and sorting.
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user