From e093aead13e745f6ba4717b348fc04a3845ccff1 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Tue, 12 Feb 2013 17:35:15 +0100 Subject: [PATCH] DATACMNS-117 - Added PageableHandlerArgumentResolver. Added PageableHandlerArgumentResolver to supersede the now deprecated PageableArgumentResolver. The latter still stays available for Spring 3.0.x based deployments. Updated reference documentation to mention the newly introduced type as well as possible configuration options. --- src/docbkx/repositories.xml | 29 +- .../data/web/PageableArgumentResolver.java | 2 + .../web/PageableHandlerArgumentResolver.java | 295 ++++++++++++++++++ ...eableHandlerArgumentResolverUnitTests.java | 227 ++++++++++++++ 4 files changed, 547 insertions(+), 6 deletions(-) create mode 100644 src/main/java/org/springframework/data/web/PageableHandlerArgumentResolver.java create mode 100644 src/test/java/org/springframework/data/web/PageableHandlerArgumentResolverUnitTests.java 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) { + + } + } +}