diff --git a/src/docbkx/repositories.xml b/src/docbkx/repositories.xml
index 69b268a5a..b91a467fb 100644
--- a/src/docbkx/repositories.xml
+++ b/src/docbkx/repositories.xml
@@ -1005,19 +1005,36 @@ public class UserController {
The bottom line is that the controller should not have to handle
the functionality of extracting pagination information from the request.
- So Spring includes a PageableArgumentResolver
- that will do the work for you.
+ So Spring Data ships with a
+ PageableHandlerArgumentResolver that will do the
+ work for you. The Spring MVC JavaConfig support exposes a
+ WebMvcConfigurationSupport helper class to
+ customize the configuration as follows:
- <bean class="….web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
+ @Configuration
+public class WebConfig extends WebMvcConfigurationSupport {
+
+ @Override
+ public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
+ converters.add(new PageableHandlerArgumentResolver());
+ }
+}
+
+ If you're stuck with XML configuration you can register the
+ resolver as follows:
+
+ <bean class="….web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
<property name="customArgumentResolvers">
<list>
- <bean class="org.springframework.data.web.PageableArgumentResolver" />
+ <bean class="org.springframework.data.web.PageableHandlerArgumentResolver" />
</list>
</property>
</bean>
- This configuration allows you to simplify controllers down to
- something like this:
+ When using Spring 3.0.x versions use the
+ PageableArgumentResolver instead. Once you've
+ configured the resolver with Spring MVC it allows you to simplify
+ controllers down to something like this:
@Controller
@RequestMapping("/users")
diff --git a/src/main/java/org/springframework/data/web/PageableArgumentResolver.java b/src/main/java/org/springframework/data/web/PageableArgumentResolver.java
index d593d5a58..4a7783cc8 100644
--- a/src/main/java/org/springframework/data/web/PageableArgumentResolver.java
+++ b/src/main/java/org/springframework/data/web/PageableArgumentResolver.java
@@ -42,8 +42,10 @@ import org.springframework.web.context.request.NativeWebRequest;
* methods. Request properties to be parsed can be configured. Default configuration uses request properties beginning
* with {@link #DEFAULT_PREFIX}{@link #DEFAULT_SEPARATOR}.
*
+ * @deprecated use {@link PageableWebHandlerArgumentResolver} instead.
* @author Oliver Gierke
*/
+@Deprecated
public class PageableArgumentResolver implements WebArgumentResolver {
private static final Pageable DEFAULT_PAGE_REQUEST = new PageRequest(0, 10);
diff --git a/src/main/java/org/springframework/data/web/PageableHandlerArgumentResolver.java b/src/main/java/org/springframework/data/web/PageableHandlerArgumentResolver.java
new file mode 100644
index 000000000..38f2ede6a
--- /dev/null
+++ b/src/main/java/org/springframework/data/web/PageableHandlerArgumentResolver.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright 2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.web;
+
+import java.beans.PropertyEditorSupport;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.servlet.ServletRequest;
+
+import org.springframework.beans.PropertyValue;
+import org.springframework.beans.PropertyValues;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.core.MethodParameter;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.Sort.Direction;
+import org.springframework.validation.DataBinder;
+import org.springframework.web.bind.ServletRequestDataBinder;
+import org.springframework.web.bind.ServletRequestParameterPropertyValues;
+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;
+
+/**
+ * Extracts paging information from web requests and thus allows injecting {@link Pageable} instances into controller
+ * methods. Request properties to be parsed can be configured. Default configuration uses request properties beginning
+ * with {@link #DEFAULT_PREFIX}{@link #DEFAULT_SEPARATOR}.
+ *
+ * @since 1.6
+ * @author Oliver Gierke
+ */
+public class PageableHandlerArgumentResolver implements HandlerMethodArgumentResolver {
+
+ private static final Pageable DEFAULT_PAGE_REQUEST = new PageRequest(0, 10);
+ private static final String DEFAULT_PREFIX = "page";
+ private static final String DEFAULT_SEPARATOR = ".";
+
+ private Pageable fallbackPagable = DEFAULT_PAGE_REQUEST;
+ private String prefix = DEFAULT_PREFIX;
+ private String separator = DEFAULT_SEPARATOR;
+
+ /**
+ * Setter to configure a fallback instance of {@link Pageable} that is being used to back missing parameters. Defaults
+ * to {@link #DEFAULT_PAGE_REQUEST}.
+ *
+ * @param fallbackPagable the fallbackPagable to set
+ */
+ public void setFallbackPagable(Pageable fallbackPagable) {
+ this.fallbackPagable = null == fallbackPagable ? DEFAULT_PAGE_REQUEST : fallbackPagable;
+ }
+
+ /**
+ * Setter to configure the prefix of request parameters to be used to retrieve paging information. Defaults to
+ * {@link #DEFAULT_PREFIX}.
+ *
+ * @param prefix the prefix to set
+ */
+ public void setPrefix(String prefix) {
+ this.prefix = null == prefix ? DEFAULT_PREFIX : prefix;
+ }
+
+ /**
+ * Setter to configure the separator between prefix and actual property value. Defaults to {@link #DEFAULT_SEPARATOR}.
+ *
+ * @param separator the separator to set. Will default to {@link #DEFAULT_SEPEARATOR} if set to {@literal null}.
+ */
+ public void setSeparator(String separator) {
+ this.separator = null == separator ? DEFAULT_SEPARATOR : separator;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter(org.springframework.core.MethodParameter)
+ */
+ public boolean supportsParameter(MethodParameter parameter) {
+ return Pageable.class.equals(parameter.getParameterType());
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.web.method.support.HandlerMethodArgumentResolver#resolveArgument(org.springframework.core.MethodParameter, org.springframework.web.method.support.ModelAndViewContainer, org.springframework.web.context.request.NativeWebRequest, org.springframework.web.bind.support.WebDataBinderFactory)
+ */
+ public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer mavContainer,
+ NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
+
+ assertPageableUniqueness(methodParameter);
+
+ Pageable request = getDefaultFromAnnotationOrFallback(methodParameter);
+ ServletRequest servletRequest = (ServletRequest) webRequest.getNativeRequest();
+ PropertyValues propertyValues = new ServletRequestParameterPropertyValues(servletRequest,
+ getPrefix(methodParameter), separator);
+
+ DataBinder binder = new ServletRequestDataBinder(request);
+
+ binder.initDirectFieldAccess();
+ binder.registerCustomEditor(Sort.class, new SortPropertyEditor("sort.dir", propertyValues));
+ binder.bind(propertyValues);
+
+ if (request.getPageNumber() > 0) {
+ request = new PageRequest(request.getPageNumber() - 1, request.getPageSize(), request.getSort());
+ }
+
+ return request;
+ }
+
+ private Pageable getDefaultFromAnnotationOrFallback(MethodParameter methodParameter) {
+
+ // search for PageableDefaults annotation
+ for (Annotation annotation : methodParameter.getParameterAnnotations()) {
+ if (annotation instanceof PageableDefaults) {
+ return getDefaultPageRequestFrom((PageableDefaults) annotation);
+ }
+ }
+
+ // Construct request with fallback request to ensure sensible
+ // default values. Create fresh copy as Spring will manipulate the
+ // instance under the covers
+ return new PageRequest(fallbackPagable.getPageNumber(), fallbackPagable.getPageSize(), fallbackPagable.getSort());
+ }
+
+ private static Pageable getDefaultPageRequestFrom(PageableDefaults defaults) {
+
+ // +1 is because we substract 1 later
+ int defaultPageNumber = defaults.pageNumber() + 1;
+ int defaultPageSize = defaults.value();
+
+ if (defaults.sort().length == 0) {
+ return new PageRequest(defaultPageNumber, defaultPageSize);
+ }
+
+ return new PageRequest(defaultPageNumber, defaultPageSize, defaults.sortDir(), defaults.sort());
+ }
+
+ /**
+ * Resolves the prefix to use to bind properties from. Will prepend a possible {@link Qualifier} if available or
+ * return the configured prefix otherwise.
+ *
+ * @param parameter
+ * @return
+ */
+ private String getPrefix(MethodParameter parameter) {
+
+ for (Annotation annotation : parameter.getParameterAnnotations()) {
+ if (annotation instanceof Qualifier) {
+ return new StringBuilder(((Qualifier) annotation).value()).append("_").append(prefix).toString();
+ }
+ }
+
+ return prefix;
+ }
+
+ /**
+ * Asserts uniqueness of all {@link Pageable} parameters of the method of the given {@link MethodParameter}.
+ *
+ * @param parameter
+ */
+ private void assertPageableUniqueness(MethodParameter parameter) {
+
+ Method method = parameter.getMethod();
+
+ if (containsMoreThanOnePageableParameter(method)) {
+ Annotation[][] annotations = method.getParameterAnnotations();
+ assertQualifiersFor(method.getParameterTypes(), annotations);
+ }
+ }
+
+ /**
+ * Returns whether the given {@link Method} has more than one {@link Pageable} parameter.
+ *
+ * @param method
+ * @return
+ */
+ private boolean containsMoreThanOnePageableParameter(Method method) {
+
+ boolean pageableFound = false;
+
+ for (Class> type : method.getParameterTypes()) {
+
+ if (pageableFound && type.equals(Pageable.class)) {
+ return true;
+ }
+
+ if (type.equals(Pageable.class)) {
+ pageableFound = true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Asserts that every {@link Pageable} parameter of the given parameters carries an {@link Qualifier} annotation to
+ * distinguish them from each other.
+ *
+ * @param parameterTypes
+ * @param annotations
+ */
+ private void assertQualifiersFor(Class>[] parameterTypes, Annotation[][] annotations) {
+
+ Set values = new HashSet();
+
+ for (int i = 0; i < annotations.length; i++) {
+
+ if (Pageable.class.equals(parameterTypes[i])) {
+
+ Qualifier qualifier = findAnnotation(annotations[i]);
+
+ if (null == qualifier) {
+ throw new IllegalStateException(
+ "Ambiguous Pageable arguments in handler method. If you use multiple parameters of type Pageable you need to qualify them with @Qualifier");
+ }
+
+ if (values.contains(qualifier.value())) {
+ throw new IllegalStateException("Values of the user Qualifiers must be unique!");
+ }
+
+ values.add(qualifier.value());
+ }
+ }
+ }
+
+ /**
+ * Returns a {@link Qualifier} annotation from the given array of {@link Annotation}s. Returns {@literal null} if the
+ * array does not contain a {@link Qualifier} annotation.
+ *
+ * @param annotations
+ * @return
+ */
+ private Qualifier findAnnotation(Annotation[] annotations) {
+
+ for (Annotation annotation : annotations) {
+ if (annotation instanceof Qualifier) {
+ return (Qualifier) annotation;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * {@link java.beans.PropertyEditor} to create {@link Sort} instances from textual representations. The implementation
+ * interprets the string as a comma separated list where the first entry is the sort direction ( {@code asc},
+ * {@code desc}) followed by the properties to sort by.
+ *
+ * @author Oliver Gierke
+ */
+ private static class SortPropertyEditor extends PropertyEditorSupport {
+
+ private final String orderProperty;
+ private final PropertyValues values;
+
+ /**
+ * Creates a new {@link SortPropertyEditor}.
+ *
+ * @param orderProperty
+ * @param values
+ */
+ public SortPropertyEditor(String orderProperty, PropertyValues values) {
+
+ this.orderProperty = orderProperty;
+ this.values = values;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.beans.PropertyEditorSupport#setAsText(java.lang.String)
+ */
+ @Override
+ public void setAsText(String text) {
+
+ PropertyValue rawOrder = values.getPropertyValue(orderProperty);
+ Direction order = null == rawOrder ? Direction.ASC : Direction.fromString(rawOrder.getValue().toString());
+
+ setValue(new Sort(order, text));
+ }
+ }
+}
diff --git a/src/test/java/org/springframework/data/web/PageableHandlerArgumentResolverUnitTests.java b/src/test/java/org/springframework/data/web/PageableHandlerArgumentResolverUnitTests.java
new file mode 100644
index 000000000..cf9da3538
--- /dev/null
+++ b/src/test/java/org/springframework/data/web/PageableHandlerArgumentResolverUnitTests.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.web;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+
+import java.lang.reflect.Method;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.core.MethodParameter;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.Sort.Direction;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.web.context.request.NativeWebRequest;
+import org.springframework.web.context.request.ServletWebRequest;
+
+/**
+ * Unit tests for {@link PageableHandlerArgumentResolver}.
+ *
+ * @since 1.6
+ * @author Oliver Gierke
+ */
+public class PageableHandlerArgumentResolverUnitTests {
+
+ Method correctMethod, failedMethod, invalidQualifiers, defaultsMethod, defaultsMethodWithSort,
+ defaultsMethodWithSortAndDirection, otherMethod;
+
+ MockHttpServletRequest request;
+
+ @Before
+ public void setUp() throws SecurityException, NoSuchMethodException {
+
+ correctMethod = SampleController.class.getMethod("correctMethod", Pageable.class, Pageable.class);
+ failedMethod = SampleController.class.getMethod("failedMethod", Pageable.class, Pageable.class);
+ invalidQualifiers = SampleController.class.getMethod("invalidQualifiers", Pageable.class, Pageable.class);
+ otherMethod = SampleController.class.getMethod("otherMethod", String.class);
+
+ defaultsMethod = SampleController.class.getMethod("defaultsMethod", Pageable.class);
+ defaultsMethodWithSort = SampleController.class.getMethod("defaultsMethodWithSort", Pageable.class);
+ defaultsMethodWithSortAndDirection = SampleController.class.getMethod("defaultsMethodWithSortAndDirection",
+ Pageable.class);
+
+ request = new MockHttpServletRequest();
+
+ // Add pagination info for foo table
+ request.addParameter("foo_page.size", "50");
+ request.addParameter("foo_page.sort", "foo");
+ request.addParameter("foo_page.sort.dir", "asc");
+
+ // Add pagination info for bar table
+ request.addParameter("bar_page.size", "60");
+ }
+
+ @Test
+ public void supportsPageableParameter() {
+
+ PageableHandlerArgumentResolver resolver = new PageableHandlerArgumentResolver();
+ resolver.supportsParameter(new MethodParameter(correctMethod, 0));
+ }
+
+ @Test
+ public void doesNotSupportNonPageableParameter() {
+
+ PageableHandlerArgumentResolver resolver = new PageableHandlerArgumentResolver();
+ resolver.supportsParameter(new MethodParameter(otherMethod, 0));
+ }
+
+ @Test
+ public void testname() throws Exception {
+
+ assertSizeForPrefix(50, new Sort(Direction.ASC, "foo"), 0);
+ assertSizeForPrefix(60, null, 1);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void rejectsInvalidlyMappedPageables() throws Exception {
+
+ MethodParameter parameter = new MethodParameter(failedMethod, 0);
+ NativeWebRequest webRequest = new ServletWebRequest(request);
+
+ new PageableHandlerArgumentResolver().resolveArgument(parameter, null, webRequest, null);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void rejectsInvalidQualifiers() throws Exception {
+
+ MethodParameter parameter = new MethodParameter(invalidQualifiers, 0);
+ NativeWebRequest webRequest = new ServletWebRequest(request);
+
+ new PageableHandlerArgumentResolver().resolveArgument(parameter, null, webRequest, null);
+ }
+
+ @Test
+ public void assertDefaults() throws Exception {
+
+ Object argument = setupAndResolve(defaultsMethod);
+
+ assertThat(argument, is(instanceOf(Pageable.class)));
+
+ Pageable pageable = (Pageable) argument;
+ assertThat(pageable.getPageSize(), is(SampleController.DEFAULT_PAGESIZE));
+ assertThat(pageable.getPageNumber(), is(SampleController.DEFAULT_PAGENUMBER));
+ assertThat(pageable.getSort(), is(nullValue()));
+ }
+
+ @Test
+ public void assertOverridesDefaults() throws Exception {
+
+ Integer sizeParam = 5;
+
+ MethodParameter parameter = new MethodParameter(defaultsMethod, 0);
+ MockHttpServletRequest mockRequest = new MockHttpServletRequest();
+
+ mockRequest.addParameter("page.page", sizeParam.toString());
+ NativeWebRequest webRequest = new ServletWebRequest(mockRequest);
+ Object argument = new PageableHandlerArgumentResolver().resolveArgument(parameter, null, webRequest, null);
+
+ assertTrue(argument instanceof Pageable);
+
+ Pageable pageable = (Pageable) argument;
+ assertEquals(SampleController.DEFAULT_PAGESIZE, pageable.getPageSize());
+ assertEquals(sizeParam - 1, pageable.getPageNumber());
+ }
+
+ @Test
+ public void appliesDefaultSort() throws Exception {
+
+ Object argument = setupAndResolve(defaultsMethodWithSort);
+
+ assertThat(argument, is(instanceOf(Pageable.class)));
+
+ Pageable pageable = (Pageable) argument;
+ assertThat(pageable.getPageSize(), is(SampleController.DEFAULT_PAGESIZE));
+ assertThat(pageable.getPageNumber(), is(SampleController.DEFAULT_PAGENUMBER));
+ assertThat(pageable.getSort(), is(new Sort("foo")));
+ }
+
+ @Test
+ public void appliesDefaultSortAndDirection() throws Exception {
+
+ Object argument = setupAndResolve(defaultsMethodWithSortAndDirection);
+
+ assertThat(argument, is(instanceOf(Pageable.class)));
+
+ Pageable pageable = (Pageable) argument;
+ assertThat(pageable.getPageSize(), is(SampleController.DEFAULT_PAGESIZE));
+ assertThat(pageable.getPageNumber(), is(SampleController.DEFAULT_PAGENUMBER));
+ assertThat(pageable.getSort(), is(new Sort(Direction.DESC, "foo")));
+ }
+
+ private void assertSizeForPrefix(int size, Sort sort, int index) throws Exception {
+
+ MethodParameter parameter = new MethodParameter(correctMethod, index);
+ NativeWebRequest webRequest = new ServletWebRequest(request);
+
+ Object argument = new PageableHandlerArgumentResolver().resolveArgument(parameter, null, webRequest, null);
+ assertThat(argument, is(instanceOf(Pageable.class)));
+
+ Pageable pageable = (Pageable) argument;
+ assertThat(pageable.getPageSize(), is(size));
+
+ if (null != sort) {
+ assertThat(pageable.getSort(), is(sort));
+ }
+ }
+
+ private Object setupAndResolve(Method method) throws Exception {
+
+ MethodParameter parameter = new MethodParameter(method, 0);
+ NativeWebRequest webRequest = new ServletWebRequest(new MockHttpServletRequest());
+ return new PageableHandlerArgumentResolver().resolveArgument(parameter, null, webRequest, null);
+ }
+
+ static class SampleController {
+
+ static final int DEFAULT_PAGESIZE = 198;
+ static final int DEFAULT_PAGENUMBER = 42;
+
+ public void defaultsMethod(
+ @PageableDefaults(value = DEFAULT_PAGESIZE, pageNumber = DEFAULT_PAGENUMBER) Pageable pageable) {
+
+ }
+
+ public void defaultsMethodWithSort(
+ @PageableDefaults(value = DEFAULT_PAGESIZE, pageNumber = DEFAULT_PAGENUMBER, sort = "foo") Pageable pageable) {
+
+ }
+
+ public void defaultsMethodWithSortAndDirection(
+ @PageableDefaults(value = DEFAULT_PAGESIZE, pageNumber = DEFAULT_PAGENUMBER, sort = "foo", sortDir = Direction.DESC) Pageable pageable) {
+
+ }
+
+ public void correctMethod(@Qualifier("foo") Pageable first, @Qualifier("bar") Pageable second) {
+
+ }
+
+ public void failedMethod(Pageable first, Pageable second) {
+
+ }
+
+ public void invalidQualifiers(@Qualifier("foo") Pageable first, @Qualifier("foo") Pageable second) {
+
+ }
+
+ public void otherMethod(String foo) {
+
+ }
+ }
+}