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) { + + } + } +}