From e11efede4d89bc4be807e6cfd8fa8eb465ab7765 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Tue, 19 Feb 2013 13:09:40 +0100 Subject: [PATCH] DATACMNS-236, DATACMNS-117 - Added (Pageable|Sort)HanderMethodArgumentResolvers. Added HandlerMethodArgumentResolver implementations for Pageable and Sort exposing a new default set of properties (page, size, sort) to resolve pagination and sorting information from the request. To mimic the legacy behavior we expose a (deprecated) LEGACY constant in PageableHandlerMethodArgumentResolver. Clients should move to the new properties structure ASAP. Added unit tests to verify old and new defaulting behavior. Introduced new annotations @SortDefault (with @SortDefaults wrapper annotation) and @PageableDefault (superseding the legacy @PageableDefaults). The new annotations have more speaking attribute names and the HMAR implementations transparently alias the generic value attribute into a more semantic one (size). The HMAR implementations implement Spring HATEOAS' UriComponentsContributor to be able to turn Pageable / Sort instances back into URIs created through the MethodLinkBuilderFactory API in Spring HATEOAS. Extracted common API between legacy PageableArgumentResolver and PageableHandlerMethodArgumentResolver into common helper class. Upgraded to Spring Hateoas 0.5.0.BUILD-SNAPSHOT. --- pom.xml | 2 +- .../data/web/PageableArgumentResolver.java | 95 +--- .../data/web/PageableDefault.java | 73 ++++ .../data/web/PageableDefaults.java | 4 + .../web/PageableHandlerArgumentResolver.java | 295 ------------- ...PageableHandlerMethodArgumentResolver.java | 255 +++++++++++ .../springframework/data/web/SortDefault.java | 78 ++++ .../SortHandlerMethodArgumentResolver.java | 406 ++++++++++++++++++ .../data/web/SpringDataAnnotationUtils.java | 142 ++++++ ...ableHandlerArgumentResolverUnitTests.java} | 113 ++--- .../PageableArgumentResolverUnitTests.java | 5 +- .../data/web/PageableDefaultUnitTest.java | 155 +++++++ ...HandlerMethodArgumentResolverUnitTest.java | 76 ++++ .../data/web/SortDefaultUnitTest.java | 143 ++++++ .../SortHandlerArgumentResolverUnitTests.java | 164 +++++++ .../springframework/data/web/TestUtils.java | 51 +++ 16 files changed, 1619 insertions(+), 438 deletions(-) create mode 100644 src/main/java/org/springframework/data/web/PageableDefault.java delete mode 100644 src/main/java/org/springframework/data/web/PageableHandlerArgumentResolver.java create mode 100644 src/main/java/org/springframework/data/web/PageableHandlerMethodArgumentResolver.java create mode 100644 src/main/java/org/springframework/data/web/SortDefault.java create mode 100644 src/main/java/org/springframework/data/web/SortHandlerMethodArgumentResolver.java create mode 100644 src/main/java/org/springframework/data/web/SpringDataAnnotationUtils.java rename src/test/java/org/springframework/data/web/{PageableHandlerArgumentResolverUnitTests.java => LegacyPageableHandlerArgumentResolverUnitTests.java} (54%) create mode 100644 src/test/java/org/springframework/data/web/PageableDefaultUnitTest.java create mode 100644 src/test/java/org/springframework/data/web/PageableHandlerMethodArgumentResolverUnitTest.java create mode 100644 src/test/java/org/springframework/data/web/SortDefaultUnitTest.java create mode 100644 src/test/java/org/springframework/data/web/SortHandlerArgumentResolverUnitTests.java create mode 100644 src/test/java/org/springframework/data/web/TestUtils.java diff --git a/pom.xml b/pom.xml index bd4312054..1823d5992 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ 1.9.7 2.8.0 - 0.4.0.RELEASE + 0.5.0.BUILD-SNAPSHOT DATACMNS diff --git a/src/main/java/org/springframework/data/web/PageableArgumentResolver.java b/src/main/java/org/springframework/data/web/PageableArgumentResolver.java index 4a7783cc8..041bbdfa0 100644 --- a/src/main/java/org/springframework/data/web/PageableArgumentResolver.java +++ b/src/main/java/org/springframework/data/web/PageableArgumentResolver.java @@ -15,11 +15,10 @@ */ package org.springframework.data.web; +import static org.springframework.data.web.SpringDataAnnotationUtils.*; + 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; @@ -131,7 +130,7 @@ public class PageableArgumentResolver implements WebArgumentResolver { return new PageRequest(fallbackPagable.getPageNumber(), fallbackPagable.getPageSize(), fallbackPagable.getSort()); } - private static Pageable getDefaultPageRequestFrom(PageableDefaults defaults) { + static Pageable getDefaultPageRequestFrom(PageableDefaults defaults) { // +1 is because we substract 1 later int defaultPageNumber = defaults.pageNumber() + 1; @@ -162,94 +161,6 @@ public class PageableArgumentResolver implements WebArgumentResolver { 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}, diff --git a/src/main/java/org/springframework/data/web/PageableDefault.java b/src/main/java/org/springframework/data/web/PageableDefault.java new file mode 100644 index 000000000..288d63289 --- /dev/null +++ b/src/main/java/org/springframework/data/web/PageableDefault.java @@ -0,0 +1,73 @@ +/* + * 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.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.web.SortDefault.SortDefaults; + +/** + * Annotation to set defaults when injecting a {@link org.springframework.data.domain.Pageable} into a controller + * method. Instead of configuring {@link #sort()} and {@link #direction()} you can also use {@link SortDefault} or + * {@link SortDefaults}. + * + * @since 1.6 + * @author Oliver Gierke + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface PageableDefault { + + /** + * Alias for {@link #size()}. Prefer to use the {@link #size()} method as it makes the annotation declaration more + * expressive and you'll probably want to configure the {@link #page()} anyway. + * + * @return + */ + int value() default 10; + + /** + * The default-size the injected {@link org.springframework.data.domain.Pageable} should get if no corresponding + * parameter defined in request (default is 10). + */ + int size() default 10; + + /** + * The default-pagenumber the injected {@link org.springframework.data.domain.Pageable} should get if no corresponding + * parameter defined in request (default is 0). + */ + int page() default 0; + + /** + * The properties to sort by by default. If unset, no sorting will be applied at all. + * + * @return + */ + String[] sort() default {}; + + /** + * The direction to sort by. Defaults to {@link Direction#ASC}. + * + * @return + */ + Direction direction() default Direction.ASC; +} diff --git a/src/main/java/org/springframework/data/web/PageableDefaults.java b/src/main/java/org/springframework/data/web/PageableDefaults.java index ded798036..8981aae1c 100644 --- a/src/main/java/org/springframework/data/web/PageableDefaults.java +++ b/src/main/java/org/springframework/data/web/PageableDefaults.java @@ -15,6 +15,7 @@ */ package org.springframework.data.web; +import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -26,10 +27,13 @@ import org.springframework.data.domain.Sort.Direction; * Annotation to set defaults when injecting a {@link org.springframework.data.domain.Pageable} into a controller * method. * + * @deprecated use {@link PageableDefault} instead. * @author Oliver Gierke */ +@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.PARAMETER) +@Deprecated public @interface PageableDefaults { /** diff --git a/src/main/java/org/springframework/data/web/PageableHandlerArgumentResolver.java b/src/main/java/org/springframework/data/web/PageableHandlerArgumentResolver.java deleted file mode 100644 index 38f2ede6a..000000000 --- a/src/main/java/org/springframework/data/web/PageableHandlerArgumentResolver.java +++ /dev/null @@ -1,295 +0,0 @@ -/* - * 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/main/java/org/springframework/data/web/PageableHandlerMethodArgumentResolver.java b/src/main/java/org/springframework/data/web/PageableHandlerMethodArgumentResolver.java new file mode 100644 index 000000000..40d1e6e01 --- /dev/null +++ b/src/main/java/org/springframework/data/web/PageableHandlerMethodArgumentResolver.java @@ -0,0 +1,255 @@ +/* + * 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.springframework.data.web.SpringDataAnnotationUtils.*; + +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.hateoas.mvc.UriComponentsContributor; +import org.springframework.util.Assert; +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; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * 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 #PAGE_PROPERTY}{@link #DEFAULT_SEPARATOR}. + * + * @since 1.6 + * @author Oliver Gierke + */ +@SuppressWarnings("deprecation") +public class PageableHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver, UriComponentsContributor { + + /** + * A {@link PageableHandlerMethodArgumentResolver} preconfigured to the setup of {@link PageableArgumentResolver}. Use + * that if you need to stick to the former request parameters an 1-indexed behavior. This will be removed in the next + * major version (1.7). So consider migrating to the new way of exposing request parameters. + */ + @Deprecated + public static final PageableHandlerMethodArgumentResolver LEGACY; + + static { + LEGACY = new PageableHandlerMethodArgumentResolver(); + LEGACY.pageProperty = "page.page"; + LEGACY.sizeProperty = "page.size"; + LEGACY.fallbackPageable = new PageRequest(1, 10); + LEGACY.oneIndexedParameters = true; + LEGACY.sortResolver.setLegacyMode(true); + LEGACY.sortResolver.setSortParameter("page.sort"); + } + + private static final Pageable DEFAULT_PAGE_REQUEST = new PageRequest(0, 20); + private static final String DEFAULT_PAGE_PROPERTY = "page"; + private static final String DEFAULT_SIZE_PROPERTY = "size"; + private static final String DEFAULT_PREFIX = ""; + private static final String DEFAULT_QUALIFIER_SEPARATOR = "_"; + + private Pageable fallbackPageable = DEFAULT_PAGE_REQUEST; + private SortHandlerMethodArgumentResolver sortResolver = new SortHandlerMethodArgumentResolver(); + private String pageProperty = DEFAULT_PAGE_PROPERTY; + private String sizeProperty = DEFAULT_SIZE_PROPERTY; + private String prefix = DEFAULT_PREFIX; + private String qualifierSeparator = DEFAULT_QUALIFIER_SEPARATOR; + private boolean oneIndexedParameters = false; + + /** + * Configures the {@link Pageable} to be used as fallback in case no {@link PageableDefault} or + * {@link PageableDefaults} (the latter only supported in legacy mode) can be found at the method parameter to be + * resolved. + *

+ * If you set this to {@literal null}, be aware that you controller methods will get {@literal null} handed into them + * in case no {@link Pageable} data can be found in the request. + * + * @param fallbackPageable the {@link Pageable} to be used as general fallback. + */ + public void setFallbackPageable(Pageable fallbackPageable) { + this.fallbackPageable = fallbackPageable; + } + + /** + * Configures the parameter name to be used to find the page number in the request. Defaults to {@code page}. + * + * @param pageProperty the parameter name to be used, must not be {@literal null} or empty. + */ + public void setPageProperty(String pageProperty) { + + Assert.hasText(pageProperty, "Page parameter name must not be null or empty!"); + this.pageProperty = pageProperty; + } + + /** + * Configures the parameter name to be used to find the page size in the request. Defaults to {@code size}. + * + * @param sizeProperty the parameter name to be used, must not be {@literal null} or empty. + */ + public void setSizeProperty(String sizeProperty) { + + Assert.hasText(sizeProperty, "Size parameter name must not be null or empty!"); + this.sizeProperty = sizeProperty; + } + + /** + * Configures a general prefix to be prepended to the page number and page size parameters. Useful to namespace the + * property names used in case they are clashing with ones used by your application. By default, no prefix is used. + * + * @param prefix the prefix to be used or {@literal null} to reset to the default. + */ + public void setPrefix(String prefix) { + this.prefix = prefix == null ? DEFAULT_PREFIX : prefix; + } + + /** + * The separator to be used between the qualifier and the actual page number and size properties. Defaults to + * {@code _}. So a qualifier of {@code foo} will result in a page number parameter of {@code foo_page}. + * + * @param qualifierSeparator the qualifierSeparator to be used or {@literal null} to reset to the default. + */ + public void setQualifierSeparator(String qualifierSeparator) { + this.qualifierSeparator = qualifierSeparator == null ? DEFAULT_QUALIFIER_SEPARATOR : qualifierSeparator; + } + + /** + * Configure the {@link SortHandlerMethodArgumentResolver} to be used with the + * {@link PageableHandlerMethodArgumentResolver}. + * + * @param sortResolver the {@link SortHandlerMethodArgumentResolver} to be used ot {@literal null} to reset it to the + * default one. + */ + public void setSortResolver(SortHandlerMethodArgumentResolver sortResolver) { + this.sortResolver = sortResolver == null ? new SortHandlerMethodArgumentResolver() : sortResolver; + } + + /** + * Configures whether to expose and assume 1-based page number indexes in the request parameters. Defaults to + * {@literal false}, meaning a page number of 0 in the request equals the first page. If this is set to + * {@literal true}, a page number of 1 in the request will be considered the first page. + * + * @param oneIndexedParameters the oneIndexedParameters to set + */ + public void setOneIndexedParameters(boolean oneIndexedParameters) { + this.oneIndexedParameters = oneIndexedParameters; + } + + /* + * (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.hateoas.mvc.UriComponentsContributor#enhance(org.springframework.web.util.UriComponentsBuilder, org.springframework.core.MethodParameter, java.lang.Object) + */ + public void enhance(UriComponentsBuilder builder, MethodParameter parameter, Object value) { + + if (!(value instanceof Pageable)) { + return; + } + + Pageable pageable = (Pageable) value; + + String pagePropertyName = getParameterNameToUse(pageProperty, parameter); + String propertyToLookup = getParameterNameToUse(sizeProperty, parameter); + + int pageNumber = pageable.getPageNumber(); + + builder.queryParam(pagePropertyName, oneIndexedParameters ? pageNumber + 1 : pageNumber); + builder.queryParam(propertyToLookup, pageable.getPageSize()); + + sortResolver.enhance(builder, parameter, pageable.getSort()); + } + + /* + * (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 Pageable resolveArgument(MethodParameter methodParameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + + assertPageableUniqueness(methodParameter); + + Pageable defaultOrFallback = getDefaultFromAnnotationOrFallback(methodParameter); + + String pageString = webRequest.getParameter(getParameterNameToUse(pageProperty, methodParameter)); + String pageSizeString = webRequest.getParameter(getParameterNameToUse(sizeProperty, methodParameter)); + + int page = StringUtils.hasText(pageString) ? Integer.parseInt(pageString) - (oneIndexedParameters ? 1 : 0) + : defaultOrFallback.getPageNumber(); + int pageSize = StringUtils.hasText(pageSizeString) ? Integer.parseInt(pageSizeString) : defaultOrFallback + .getPageSize(); + + Sort sort = sortResolver.resolveArgument(methodParameter, mavContainer, webRequest, binderFactory); + return new PageRequest(page, pageSize, sort == null ? defaultOrFallback.getSort() : sort); + } + + /** + * Returns the name of the request parameter to find the {@link Pageable} information in. Inspects the given + * {@link MethodParameter} for {@link Qualifier} present and prefixes the given source parameter name with it. + * + * @param source the basic parameter name. + * @param parameter the {@link MethodParameter} potentially qualified. + * @return + */ + private String getParameterNameToUse(String source, MethodParameter parameter) { + + StringBuilder builder = new StringBuilder(prefix); + + if (parameter.hasParameterAnnotation(Qualifier.class)) { + builder.append(parameter.getParameterAnnotation(Qualifier.class).value()); + builder.append(qualifierSeparator); + } + + return builder.append(source).toString(); + } + + private Pageable getDefaultFromAnnotationOrFallback(MethodParameter methodParameter) { + + if (sortResolver.legacyMode && methodParameter.hasParameterAnnotation(PageableDefaults.class)) { + Pageable pageable = PageableArgumentResolver.getDefaultPageRequestFrom(methodParameter + .getParameterAnnotation(PageableDefaults.class)); + return new PageRequest(pageable.getPageNumber() - 1, pageable.getPageSize(), pageable.getSort()); + } + + if (methodParameter.hasParameterAnnotation(PageableDefault.class)) { + return getDefaultPageRequestFrom(methodParameter.getParameterAnnotation(PageableDefault.class)); + } + + return fallbackPageable; + } + + private static Pageable getDefaultPageRequestFrom(PageableDefault defaults) { + + int defaultPageNumber = defaults.page(); + int defaultPageSize = getSpecificPropertyOrDefaultFromValue(defaults, "size"); + + if (defaults.sort().length == 0) { + return new PageRequest(defaultPageNumber, defaultPageSize); + } + + return new PageRequest(defaultPageNumber, defaultPageSize, defaults.direction(), defaults.sort()); + } +} diff --git a/src/main/java/org/springframework/data/web/SortDefault.java b/src/main/java/org/springframework/data/web/SortDefault.java new file mode 100644 index 000000000..8ceeb6bc3 --- /dev/null +++ b/src/main/java/org/springframework/data/web/SortDefault.java @@ -0,0 +1,78 @@ +/* + * 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.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; + +/** + * Annotation to define the default {@link Sort} options to be used when injecting a {@link Sort} instance into a + * controller handler method. + * + * @since 1.6 + * @author Oliver Gierke + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface SortDefault { + + /** + * Alias for {@link #sort()} to make a declaration configuring fields only more concise. + * + * @return + */ + String[] value() default {}; + + /** + * The properties to sort by by default. If unset, no sorting will be applied at all. + * + * @return + */ + String[] sort() default {}; + + /** + * The direction to sort by. Defaults to {@link Direction#ASC}. + * + * @return + */ + Direction direction() default Direction.ASC; + + /** + * Wrapper annotation to allow declaring multiple {@link SortDefault} annotations on a method parameter. + * + * @since 1.6 + * @author Oliver Gierke + */ + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.PARAMETER) + public @interface SortDefaults { + + /** + * The individual {@link SortDefault} declarations to be sorted by. + * + * @return + */ + SortDefault[] value(); + } +} diff --git a/src/main/java/org/springframework/data/web/SortHandlerMethodArgumentResolver.java b/src/main/java/org/springframework/data/web/SortHandlerMethodArgumentResolver.java new file mode 100644 index 000000000..20a29a248 --- /dev/null +++ b/src/main/java/org/springframework/data/web/SortHandlerMethodArgumentResolver.java @@ -0,0 +1,406 @@ +/* + * 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.util.ArrayList; +import java.util.List; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.MethodParameter; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.domain.Sort.Order; +import org.springframework.data.web.SortDefault.SortDefaults; +import org.springframework.hateoas.mvc.UriComponentsContributor; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * {@link HandlerMethodArgumentResolver} to automatically create {@link Sort} instances from request parameters or + * {@link SortDefault} annotations. + * + * @since 1.6 + * @author Oliver Gierke + */ +public class SortHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver, UriComponentsContributor { + + private static final String DEFAULT_PARAMETER = "sort"; + private static final String DEFAULT_PROPERTY_DELIMITER = ","; + private static final String DEFAULT_QUALIFIER_DELIMITER = "_"; + + private static final String SORT_DEFAULTS_NAME = SortDefaults.class.getSimpleName(); + private static final String SORT_DEFAULT_NAME = SortDefault.class.getSimpleName(); + + private String sortParameter = DEFAULT_PARAMETER; + private String propertyDelimiter = DEFAULT_PROPERTY_DELIMITER; + private String qualifierDelimiter = DEFAULT_QUALIFIER_DELIMITER; + + boolean legacyMode = false; + + /** + * Enables legacy mode parsing of the sorting parameter from the incoming request. Uses the sort property configured + * to lookup the fields to sort on and {@code $sortParameter.dir} for the direction. + * + * @param legacyMode whether to enable the legacy mode or not. + */ + @Deprecated + void setLegacyMode(boolean legacyMode) { + this.legacyMode = legacyMode; + } + + /** + * Configure the request parameter to lookup sort information from. Defaults to {@code sort}. + * + * @param sortParameter must not be {@literal null} or empty. + */ + public void setSortParameter(String parameter) { + + Assert.hasText(parameter); + this.sortParameter = parameter; + } + + /** + * Configures the delimiter used to separate property references and the direction to be sorted by. Defaults to + * {@code}, which means sort values look like this: {@code firstname,lastname,asc}. + * + * @param propertyDelimiter must not be {@literal null} or empty. + */ + public void setPropertyDelimiter(String propertyDelimiter) { + + Assert.hasText(propertyDelimiter, "Property delimiter must not be null or empty!"); + this.propertyDelimiter = propertyDelimiter; + } + + /** + * Configures the delimiter used to separate the qualifier from the sort parameter. Defaults to {@code _}, so a + * qualified sort property would look like {@code qualifier_sort}. + * + * @param qualifierDelimiter the qualifier delimiter to be used or {@literal null} to reset to the default. + */ + public void setQualifierDelimiter(String qualifierDelimiter) { + this.qualifierDelimiter = qualifierDelimiter == null ? DEFAULT_QUALIFIER_DELIMITER : qualifierDelimiter; + } + + /* + * (non-Javadoc) + * @see org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter(org.springframework.core.MethodParameter) + */ + public boolean supportsParameter(MethodParameter parameter) { + return Sort.class.equals(parameter.getParameterType()); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.mvc.UriComponentsContributor#enhance(org.springframework.web.util.UriComponentsBuilder, org.springframework.core.MethodParameter, java.lang.Object) + */ + public void enhance(UriComponentsBuilder builder, MethodParameter parameter, Object value) { + + if (!(value instanceof Sort)) { + return; + } + + Sort sort = (Sort) value; + + if (legacyMode) { + + List expressions = legacyFoldExpressions(sort); + Assert.isTrue(expressions.size() == 2, + String.format("Expected 2 sort expressions (fields, direction) but got %d!", expressions.size())); + builder.queryParam(getSortParameter(parameter), expressions.get(0)); + builder.queryParam(getLegacyDirectionParameter(parameter), expressions.get(1)); + + } else { + + for (String expression : foldIntoExpressions(sort)) { + builder.queryParam(getSortParameter(parameter), expression); + } + } + } + + /* + * (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 Sort resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + + String[] directionParameter = webRequest.getParameterValues(getSortParameter(parameter)); + + if (directionParameter != null && directionParameter.length != 0) { + return legacyMode ? parseLegacyParameterIntoSort(webRequest, parameter) : parseParameterIntoSort( + directionParameter, propertyDelimiter); + } else { + return getDefaults(parameter); + } + } + + /** + * Reads the default {@link Sort} to be used from the given {@link MethodParameter}. Rejects the parameter if both an + * {@link SortDefaults} and {@link SortDefault} annotation is found as we cannot build a reliable {@link Sort} + * instance then (property ordering). + * + * @param parameter will never be {@literal null}. + * @return the default {@link Sort} instance derived from the parameter annotations or {@literal null}. + */ + private Sort getDefaults(MethodParameter parameter) { + + SortDefaults annotatedDefaults = parameter.getParameterAnnotation(SortDefaults.class); + Sort sort = null; + + if (annotatedDefaults != null) { + for (SortDefault annotatedDefault : annotatedDefaults.value()) { + sort = appendOrCreateSortTo(annotatedDefault, sort); + } + } + + SortDefault annotatedDefault = parameter.getParameterAnnotation(SortDefault.class); + + if (annotatedDefault == null) { + return sort; + } + + if (sort != null && annotatedDefault != null) { + throw new IllegalArgumentException(String.format( + "Cannot use both @%s and @%s on parameter %s! Move %s into %s to define sorting order!", SORT_DEFAULTS_NAME, + SORT_DEFAULT_NAME, parameter.toString(), SORT_DEFAULT_NAME, SORT_DEFAULTS_NAME)); + } + + return appendOrCreateSortTo(annotatedDefault, sort); + } + + /** + * Creates a new {@link Sort} instance from the given {@link SortDefault} or appends it to the given {@link Sort} + * instance if it's not {@literal null}. + * + * @param sortDefault + * @param sortOrNull + * @return + */ + private Sort appendOrCreateSortTo(SortDefault sortDefault, Sort sortOrNull) { + + String[] fields = SpringDataAnnotationUtils.getSpecificPropertyOrDefaultFromValue(sortDefault, "sort"); + + if (fields.length == 0) { + return null; + } + + Sort sort = new Sort(sortDefault.direction(), fields); + return sortOrNull == null ? sort : sortOrNull.and(sort); + } + + /** + * Returns the sort parameter to be looked up from the request. Potentially applies qualifiers to it. + * + * @param parameter will never be {@literal null}. + * @return + */ + private String getSortParameter(MethodParameter parameter) { + + StringBuilder builder = new StringBuilder(); + + if (parameter.hasParameterAnnotation(Qualifier.class)) { + builder.append(parameter.getParameterAnnotation(Qualifier.class).value()).append(qualifierDelimiter); + } + + return builder.append(sortParameter).toString(); + } + + /** + * Creates a {@link Sort} instance from the given request expecting the {@link Direction} being encoded in a parameter + * with an appended {@code .dir}. + * + * @param request must not be {@literal null}. + * @param parameter must not be {@literal null}. + * @return + */ + private Sort parseLegacyParameterIntoSort(WebRequest request, MethodParameter parameter) { + + String property = getSortParameter(parameter); + String fields = request.getParameter(property); + String directions = request.getParameter(getLegacyDirectionParameter(parameter)); + + return new Sort(Direction.fromStringOrNull(directions), fields.split(",")); + } + + private String getLegacyDirectionParameter(MethodParameter parameter) { + return getSortParameter(parameter) + ".dir"; + } + + /** + * Parses the given sort expressions into a {@link Sort} instance. The implementation expects the sources to be a + * concatenation of Strings using the given delimiter. If the last element can be parsed into a {@link Direction} it's + * considered a {@link Direction} and a simple property otherwise. + * + * @param source will never be {@literal null}. + * @param delimiter the delimiter to be used to split up the source elements, will never be {@literal null}. + * @return + */ + Sort parseParameterIntoSort(String[] source, String delimiter) { + + List allOrders = new ArrayList(); + + for (String part : source) { + + if (part == null) { + continue; + } + + String[] elements = part.split(delimiter); + Direction direction = Direction.fromStringOrNull(elements[elements.length - 1]); + + for (int i = 0; i < elements.length; i++) { + + if (i == elements.length - 1 && direction != null) { + continue; + } + + allOrders.add(new Order(direction, elements[i])); + } + } + + return allOrders.isEmpty() ? null : new Sort(allOrders); + } + + /** + * Folds the given {@link Sort} instance into a {@link List} of sort expressions, accumulating {@link Order} instances + * of the same direction into a single expression if they are in order. + * + * @param sort must not be {@literal null}. + * @return + */ + private List foldIntoExpressions(Sort sort) { + + List expressions = new ArrayList(); + ExpressionBuilder builder = null; + + for (Order order : sort) { + + Direction direction = order.getDirection(); + + if (builder == null) { + builder = new ExpressionBuilder(direction); + } else if (!builder.hasSameDirectionAs(order)) { + builder.dumpExpressionIfPresentInto(expressions); + builder = new ExpressionBuilder(direction); + } + + builder.add(order.getProperty()); + } + + return builder.dumpExpressionIfPresentInto(expressions); + } + + /** + * Folds the given {@link Sort} instance into two expressions. The first being the property list, the second being the + * direction. + * + * @throws IllegalArgumentException if a {@link Sort} with multiple {@link Direction}s has been handed in. + * @param sort must not be {@literal null}. + * @return + */ + private List legacyFoldExpressions(Sort sort) { + + List expressions = new ArrayList(); + ExpressionBuilder builder = null; + + for (Order order : sort) { + + Direction direction = order.getDirection(); + + if (builder == null) { + builder = new ExpressionBuilder(direction); + } else if (!builder.hasSameDirectionAs(order)) { + throw new IllegalArgumentException(String.format( + "%s in legacy configuration only supports a single direction to sort by!", getClass().getSimpleName())); + } + + builder.add(order.getProperty()); + } + + return builder.dumpExpressionIfPresentInto(expressions); + } + + /** + * Helper to easily build request parameter expressions for {@link Sort} instances. + * + * @author Oliver Gierke + */ + class ExpressionBuilder { + + private final List elements = new ArrayList(); + private final Direction direction; + + /** + * Sets up a new {@link ExpressionBuilder} for properties to be sorted in the given {@link Direction}. + * + * @param direction must not be {@literal null}. + */ + public ExpressionBuilder(Direction direction) { + + Assert.notNull(direction, "Direction must not be null!"); + this.direction = direction; + } + + /** + * Returns whether the given {@link Order} has the same direction as the current {@link ExpressionBuilder}. + * + * @param order must not be {@literal null}. + * @return + */ + public boolean hasSameDirectionAs(Order order) { + return this.direction == order.getDirection(); + } + + /** + * Adds the given property to the expression to be built. + * + * @param property + */ + public void add(String property) { + this.elements.add(property); + } + + /** + * Dumps the expression currently in build into the given {@link List} of {@link String}s. Will only dump it in case + * there are properties piled up currently. + * + * @param expressions + * @return + */ + public List dumpExpressionIfPresentInto(List expressions) { + + if (elements.isEmpty()) { + return expressions; + } + + if (legacyMode) { + expressions.add(StringUtils.collectionToDelimitedString(elements, propertyDelimiter)); + expressions.add(direction.name().toLowerCase()); + } else { + elements.add(direction.name().toLowerCase()); + expressions.add(StringUtils.collectionToDelimitedString(elements, propertyDelimiter)); + } + + return expressions; + } + } +} diff --git a/src/main/java/org/springframework/data/web/SpringDataAnnotationUtils.java b/src/main/java/org/springframework/data/web/SpringDataAnnotationUtils.java new file mode 100644 index 000000000..552676b88 --- /dev/null +++ b/src/main/java/org/springframework/data/web/SpringDataAnnotationUtils.java @@ -0,0 +1,142 @@ +/* + * 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.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.data.domain.Pageable; +import org.springframework.util.ObjectUtils; + +/** + * Helper class to ease sharing code between legacy {@link PageableArgumentResolver} and + * {@link PageableHandlerMethodArgumentResolver}. + * + * @author Oliver Gierke + */ +class SpringDataAnnotationUtils { + + /** + * Asserts uniqueness of all {@link Pageable} parameters of the method of the given {@link MethodParameter}. + * + * @param parameter must not be {@literal null}. + */ + public static 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 must not be {@literal null}. + * @return + */ + private static 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; + } + + /** + * Returns the value of the given specific property of the given annotation. If the value of that property is the + * properties default, we fall back to the value of the {@code value} attribute. + * + * @param annotation must not be {@literal null}. + * @param property must not be {@literal null} or empty. + * @return + */ + @SuppressWarnings("unchecked") + public static T getSpecificPropertyOrDefaultFromValue(Annotation annotation, String property) { + + Object propertyDefaultValue = AnnotationUtils.getDefaultValue(annotation, property); + Object propertyValue = AnnotationUtils.getValue(annotation, property); + + return (T) (ObjectUtils.nullSafeEquals(propertyDefaultValue, propertyValue) ? AnnotationUtils.getValue(annotation) + : propertyValue); + } + + /** + * Asserts that every {@link Pageable} parameter of the given parameters carries an {@link Qualifier} annotation to + * distinguish them from each other. + * + * @param parameterTypes must not be {@literal null}. + * @param annotations must not be {@literal null}. + */ + public static 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 must not be {@literal null}. + * @return + */ + public static Qualifier findAnnotation(Annotation[] annotations) { + + for (Annotation annotation : annotations) { + if (annotation instanceof Qualifier) { + return (Qualifier) annotation; + } + } + + return null; + } +} diff --git a/src/test/java/org/springframework/data/web/PageableHandlerArgumentResolverUnitTests.java b/src/test/java/org/springframework/data/web/LegacyPageableHandlerArgumentResolverUnitTests.java similarity index 54% rename from src/test/java/org/springframework/data/web/PageableHandlerArgumentResolverUnitTests.java rename to src/test/java/org/springframework/data/web/LegacyPageableHandlerArgumentResolverUnitTests.java index cf9da3538..a379e6b09 100644 --- a/src/test/java/org/springframework/data/web/PageableHandlerArgumentResolverUnitTests.java +++ b/src/test/java/org/springframework/data/web/LegacyPageableHandlerArgumentResolverUnitTests.java @@ -17,6 +17,7 @@ package org.springframework.data.web; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; +import static org.springframework.data.web.PageableHandlerMethodArgumentResolver.*; import java.lang.reflect.Method; @@ -27,19 +28,22 @@ 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.data.web.SortDefault.SortDefaults; 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}. + * Unit tests for {@link PageableHandlerMethodArgumentResolver} in it's legacy mode. Essentially a copy of + * {@link PageableArgumentResolverUnitTests} but but executed against {@link PageableHandlerMethodArgumentResolver}. * * @since 1.6 * @author Oliver Gierke */ -public class PageableHandlerArgumentResolverUnitTests { +@SuppressWarnings("deprecation") +public class LegacyPageableHandlerArgumentResolverUnitTests extends PageableDefaultUnitTest { - Method correctMethod, failedMethod, invalidQualifiers, defaultsMethod, defaultsMethodWithSort, + Method correctMethod, noQualifiers, invalidQualifiers, defaultsMethod, defaultsMethodWithSort, defaultsMethodWithSortAndDirection, otherMethod; MockHttpServletRequest request; @@ -48,13 +52,13 @@ public class PageableHandlerArgumentResolverUnitTests { public void setUp() throws SecurityException, NoSuchMethodException { correctMethod = SampleController.class.getMethod("correctMethod", Pageable.class, Pageable.class); - failedMethod = SampleController.class.getMethod("failedMethod", Pageable.class, Pageable.class); + noQualifiers = SampleController.class.getMethod("noQualifiers", Pageable.class, Pageable.class); invalidQualifiers = SampleController.class.getMethod("invalidQualifiers", Pageable.class, Pageable.class); - otherMethod = SampleController.class.getMethod("otherMethod", String.class); + otherMethod = SampleController.class.getMethod("unsupportedMethod", String.class); - defaultsMethod = SampleController.class.getMethod("defaultsMethod", Pageable.class); - defaultsMethodWithSort = SampleController.class.getMethod("defaultsMethodWithSort", Pageable.class); - defaultsMethodWithSortAndDirection = SampleController.class.getMethod("defaultsMethodWithSortAndDirection", + defaultsMethod = SampleController.class.getMethod("simpleDefault", Pageable.class); + defaultsMethodWithSort = SampleController.class.getMethod("simpleDefaultWithSort", Pageable.class); + defaultsMethodWithSortAndDirection = SampleController.class.getMethod("simpleDefaultWithSortAndDirection", Pageable.class); request = new MockHttpServletRequest(); @@ -71,14 +75,14 @@ public class PageableHandlerArgumentResolverUnitTests { @Test public void supportsPageableParameter() { - PageableHandlerArgumentResolver resolver = new PageableHandlerArgumentResolver(); + PageableHandlerMethodArgumentResolver resolver = new PageableHandlerMethodArgumentResolver(); resolver.supportsParameter(new MethodParameter(correctMethod, 0)); } @Test public void doesNotSupportNonPageableParameter() { - PageableHandlerArgumentResolver resolver = new PageableHandlerArgumentResolver(); + PageableHandlerMethodArgumentResolver resolver = new PageableHandlerMethodArgumentResolver(); resolver.supportsParameter(new MethodParameter(otherMethod, 0)); } @@ -92,10 +96,10 @@ public class PageableHandlerArgumentResolverUnitTests { @Test(expected = IllegalStateException.class) public void rejectsInvalidlyMappedPageables() throws Exception { - MethodParameter parameter = new MethodParameter(failedMethod, 0); + MethodParameter parameter = new MethodParameter(noQualifiers, 0); NativeWebRequest webRequest = new ServletWebRequest(request); - new PageableHandlerArgumentResolver().resolveArgument(parameter, null, webRequest, null); + new PageableHandlerMethodArgumentResolver().resolveArgument(parameter, null, webRequest, null); } @Test(expected = IllegalStateException.class) @@ -104,7 +108,7 @@ public class PageableHandlerArgumentResolverUnitTests { MethodParameter parameter = new MethodParameter(invalidQualifiers, 0); NativeWebRequest webRequest = new ServletWebRequest(request); - new PageableHandlerArgumentResolver().resolveArgument(parameter, null, webRequest, null); + new PageableHandlerMethodArgumentResolver().resolveArgument(parameter, null, webRequest, null); } @Test @@ -115,8 +119,8 @@ public class PageableHandlerArgumentResolverUnitTests { 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.getPageSize(), is(PAGE_SIZE)); + assertThat(pageable.getPageNumber(), is(PAGE_NUMBER)); assertThat(pageable.getSort(), is(nullValue())); } @@ -130,12 +134,12 @@ public class PageableHandlerArgumentResolverUnitTests { mockRequest.addParameter("page.page", sizeParam.toString()); NativeWebRequest webRequest = new ServletWebRequest(mockRequest); - Object argument = new PageableHandlerArgumentResolver().resolveArgument(parameter, null, webRequest, null); + Object argument = LEGACY.resolveArgument(parameter, null, webRequest, null); assertTrue(argument instanceof Pageable); Pageable pageable = (Pageable) argument; - assertEquals(SampleController.DEFAULT_PAGESIZE, pageable.getPageSize()); + assertEquals(PAGE_SIZE, pageable.getPageSize()); assertEquals(sizeParam - 1, pageable.getPageNumber()); } @@ -147,9 +151,9 @@ public class PageableHandlerArgumentResolverUnitTests { 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"))); + assertThat(pageable.getPageSize(), is(PAGE_SIZE)); + assertThat(pageable.getPageNumber(), is(PAGE_NUMBER)); + assertThat(pageable.getSort(), is(new Sort("firstname", "lastname"))); } @Test @@ -160,9 +164,20 @@ public class PageableHandlerArgumentResolverUnitTests { 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"))); + assertThat(pageable.getPageSize(), is(PAGE_SIZE)); + assertThat(pageable.getPageNumber(), is(PAGE_NUMBER)); + assertThat(pageable.getSort(), is(new Sort(Direction.DESC, "firstname", "lastname"))); + } + + @Test + public void buildsUpRequestParameters() { + + // Set up basic page representation based on 1-indexed page numbers + String basicString = String.format("page.page=%d&page.size=%d", PAGE_NUMBER + 1, PAGE_SIZE); + + assertUriStringFor(REFERENCE_WITHOUT_SORT, basicString); + assertUriStringFor(REFERENCE_WITH_SORT, basicString + "&page.sort=firstname,lastname&page.sort.dir=desc"); + assertUriStringFor(REFERENCE_WITH_SORT_FIELDS, basicString + "&page.sort=firstname,lastname&page.sort.dir=asc"); } private void assertSizeForPrefix(int size, Sort sort, int index) throws Exception { @@ -170,7 +185,7 @@ public class PageableHandlerArgumentResolverUnitTests { MethodParameter parameter = new MethodParameter(correctMethod, index); NativeWebRequest webRequest = new ServletWebRequest(request); - Object argument = new PageableHandlerArgumentResolver().resolveArgument(parameter, null, webRequest, null); + Object argument = LEGACY.resolveArgument(parameter, null, webRequest, null); assertThat(argument, is(instanceOf(Pageable.class))); Pageable pageable = (Pageable) argument; @@ -185,43 +200,45 @@ public class PageableHandlerArgumentResolverUnitTests { 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) { + return LEGACY.resolveArgument(parameter, null, webRequest, null); + } - } + @Override + protected Class getControllerClass() { + return SampleController.class; + } - public void defaultsMethodWithSort( - @PageableDefaults(value = DEFAULT_PAGESIZE, pageNumber = DEFAULT_PAGENUMBER, sort = "foo") Pageable pageable) { + @Override + protected PageableHandlerMethodArgumentResolver getResolver() { + return PageableHandlerMethodArgumentResolver.LEGACY; + } - } + static interface SampleController { - public void defaultsMethodWithSortAndDirection( - @PageableDefaults(value = DEFAULT_PAGESIZE, pageNumber = DEFAULT_PAGENUMBER, sort = "foo", sortDir = Direction.DESC) Pageable pageable) { + void simpleDefault(@PageableDefaults(value = PAGE_SIZE, pageNumber = PAGE_NUMBER) Pageable pageable); - } + void simpleDefaultWithSort(@PageableDefaults(value = PAGE_SIZE, pageNumber = PAGE_NUMBER, sort = { "firstname", + "lastname" }) Pageable pageable); - public void correctMethod(@Qualifier("foo") Pageable first, @Qualifier("bar") Pageable second) { + void simpleDefaultWithSortAndDirection(@PageableDefaults(value = PAGE_SIZE, pageNumber = PAGE_NUMBER, sort = { + "firstname", "lastname" }, sortDir = Direction.DESC) Pageable pageable); - } + void simpleDefaultWithExternalSort( + @PageableDefaults(value = PAGE_SIZE, pageNumber = PAGE_NUMBER) @SortDefault(sort = { "firstname", "lastname" }, direction = Direction.DESC) Pageable pageable); - public void failedMethod(Pageable first, Pageable second) { + void simpleDefaultWithContaineredExternalSort( + @PageableDefaults(value = PAGE_SIZE, pageNumber = PAGE_NUMBER) @SortDefaults(@SortDefault(sort = { "firstname", + "lastname" }, direction = Direction.DESC)) Pageable pageable); - } + void correctMethod(@Qualifier("foo") Pageable first, @Qualifier("bar") Pageable second); - public void invalidQualifiers(@Qualifier("foo") Pageable first, @Qualifier("foo") Pageable second) { + void invalidQualifiers(@Qualifier("foo") Pageable first, @Qualifier("foo") Pageable second); - } + void noQualifiers(Pageable first, Pageable second); - public void otherMethod(String foo) { + void supportedMethod(Pageable pageable); - } + void unsupportedMethod(String foo); } } diff --git a/src/test/java/org/springframework/data/web/PageableArgumentResolverUnitTests.java b/src/test/java/org/springframework/data/web/PageableArgumentResolverUnitTests.java index d67d3a92a..d5b268c6c 100644 --- a/src/test/java/org/springframework/data/web/PageableArgumentResolverUnitTests.java +++ b/src/test/java/org/springframework/data/web/PageableArgumentResolverUnitTests.java @@ -36,6 +36,7 @@ import org.springframework.web.context.request.ServletWebRequest; * * @author Oliver Gierke */ +@SuppressWarnings("deprecation") public class PageableArgumentResolverUnitTests { Method correctMethod, failedMethod, invalidQualifiers, defaultsMethod, defaultsMethodWithSort, @@ -47,7 +48,7 @@ public class PageableArgumentResolverUnitTests { public void setUp() throws SecurityException, NoSuchMethodException { correctMethod = SampleController.class.getMethod("correctMethod", Pageable.class, Pageable.class); - failedMethod = SampleController.class.getMethod("failedMethod", Pageable.class, Pageable.class); + failedMethod = SampleController.class.getMethod("noQualifiers", Pageable.class, Pageable.class); invalidQualifiers = SampleController.class.getMethod("invalidQualifiers", Pageable.class, Pageable.class); defaultsMethod = SampleController.class.getMethod("defaultsMethod", Pageable.class); @@ -206,7 +207,7 @@ public class PageableArgumentResolverUnitTests { } - public void failedMethod(Pageable first, Pageable second) { + public void noQualifiers(Pageable first, Pageable second) { } diff --git a/src/test/java/org/springframework/data/web/PageableDefaultUnitTest.java b/src/test/java/org/springframework/data/web/PageableDefaultUnitTest.java new file mode 100644 index 000000000..439e147fa --- /dev/null +++ b/src/test/java/org/springframework/data/web/PageableDefaultUnitTest.java @@ -0,0 +1,155 @@ +/* + * 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 static org.springframework.data.web.SortDefaultUnitTest.*; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +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.test.util.ReflectionTestUtils; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * Base test class to test supporting of a {@link HandlerMethodArgumentResolver} implementation defaulting + * {@link Pageable} method parameters. Expects the {@link HandlerMethodArgumentResolver} to be tested returned from + * {@link #getResolver()} and expects methods to be present in the controller class returned from + * {@link #getControllerClass()}. For sample usage see {@link PageableHandlerMethodArgumentResolver}. + * + * @since 1.6 + * @author Oliver Gierke + */ +public abstract class PageableDefaultUnitTest { + + static final int PAGE_SIZE = 47; + static final int PAGE_NUMBER = 23; + + static final PageRequest REFERENCE_WITHOUT_SORT = new PageRequest(PAGE_NUMBER, PAGE_SIZE); + static final PageRequest REFERENCE_WITH_SORT = new PageRequest(PAGE_NUMBER, PAGE_SIZE, SORT); + static final PageRequest REFERENCE_WITH_SORT_FIELDS = new PageRequest(PAGE_NUMBER, PAGE_SIZE, new Sort(SORT_FIELDS)); + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Test + public void supportsPageable() { + assertThat(getResolver().supportsParameter(getParameterOfMethod("supportedMethod")), is(true)); + } + + @Test + public void doesNotSupportNonPageable() { + + MethodParameter parameter = TestUtils.getParameterOfMethod(getControllerClass(), "unsupportedMethod", String.class); + assertThat(getResolver().supportsParameter(parameter), is(false)); + } + + @Test + public void returnsDefaultIfNoRequestParametersAndNoDefault() throws Exception { + assertSupportedAndResult(getParameterOfMethod("supportedMethod"), + (Pageable) ReflectionTestUtils.getField(getResolver(), "fallbackPageable")); + } + + @Test + public void simpleDefault() throws Exception { + assertSupportedAndResult(getParameterOfMethod("simpleDefault"), REFERENCE_WITHOUT_SORT); + } + + @Test + public void simpleDefaultWithSort() throws Exception { + assertSupportedAndResult(getParameterOfMethod("simpleDefaultWithSort"), REFERENCE_WITH_SORT_FIELDS); + } + + @Test + public void simpleDefaultWithSortAndDirection() throws Exception { + assertSupportedAndResult(getParameterOfMethod("simpleDefaultWithSortAndDirection"), REFERENCE_WITH_SORT); + } + + @Test + public void simpleDefaultWithExternalSort() throws Exception { + assertSupportedAndResult(getParameterOfMethod("simpleDefaultWithExternalSort"), REFERENCE_WITH_SORT); + } + + @Test + public void simpleDefaultWithContaineredExternalSort() throws Exception { + assertSupportedAndResult(getParameterOfMethod("simpleDefaultWithContaineredExternalSort"), REFERENCE_WITH_SORT); + } + + @Test + public void rejectsInvalidQulifiers() throws Exception { + + MethodParameter parameter = TestUtils.getParameterOfMethod(getControllerClass(), "invalidQualifiers", + Pageable.class, Pageable.class); + + HandlerMethodArgumentResolver resolver = getResolver(); + assertThat(resolver.supportsParameter(parameter), is(true)); + + exception.expect(IllegalStateException.class); + exception.expectMessage("unique"); + + resolver.resolveArgument(parameter, null, TestUtils.getWebRequest(), null); + } + + @Test + public void rejectsNoQualifiers() throws Exception { + + MethodParameter parameter = TestUtils.getParameterOfMethod(getControllerClass(), "noQualifiers", Pageable.class, + Pageable.class); + + HandlerMethodArgumentResolver resolver = getResolver(); + assertThat(resolver.supportsParameter(parameter), is(true)); + + exception.expect(IllegalStateException.class); + exception.expectMessage("Ambiguous"); + + resolver.resolveArgument(parameter, null, TestUtils.getWebRequest(), null); + } + + private void assertSupportedAndResult(MethodParameter parameter, Pageable pageable) throws Exception { + + HandlerMethodArgumentResolver resolver = getResolver(); + assertThat(resolver.supportsParameter(parameter), is(true)); + assertThat(resolver.resolveArgument(parameter, null, TestUtils.getWebRequest(), null), is((Object) pageable)); + } + + protected void assertUriStringFor(Pageable pageable, String expected) { + + UriComponentsBuilder builder = UriComponentsBuilder.fromPath("/"); + MethodParameter parameter = getParameterOfMethod("supportedMethod"); + + getResolver().enhance(builder, parameter, pageable); + + assertThat(builder.build().toUriString(), endsWith(expected)); + } + + protected abstract PageableHandlerMethodArgumentResolver getResolver(); + + protected abstract Class getControllerClass(); + + protected MethodParameter getParameterOfMethod(String name) { + return getParameterOfMethod(getControllerClass(), name); + } + + private static MethodParameter getParameterOfMethod(Class controller, String name) { + return TestUtils.getParameterOfMethod(controller, name, Pageable.class); + } +} diff --git a/src/test/java/org/springframework/data/web/PageableHandlerMethodArgumentResolverUnitTest.java b/src/test/java/org/springframework/data/web/PageableHandlerMethodArgumentResolverUnitTest.java new file mode 100644 index 000000000..a63692da6 --- /dev/null +++ b/src/test/java/org/springframework/data/web/PageableHandlerMethodArgumentResolverUnitTest.java @@ -0,0 +1,76 @@ +/* + * 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 org.junit.Test; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.web.SortDefault.SortDefaults; + +/** + * Unit tests for {@link PageableHandlerMethodArgumentResolver}. Pulls in defaulting tests from + * {@link PageableDefaultUnitTest}. + * + * @author Oliver Gierke + */ +public class PageableHandlerMethodArgumentResolverUnitTest extends PageableDefaultUnitTest { + + @Test + public void buildsUpRequestParameters() { + + String basicString = String.format("page=%d&size=%d", PAGE_NUMBER, PAGE_SIZE); + + assertUriStringFor(REFERENCE_WITHOUT_SORT, basicString); + assertUriStringFor(REFERENCE_WITH_SORT, basicString + "&sort=firstname,lastname,desc"); + assertUriStringFor(REFERENCE_WITH_SORT_FIELDS, basicString + "&sort=firstname,lastname,asc"); + } + + @Override + protected PageableHandlerMethodArgumentResolver getResolver() { + return new PageableHandlerMethodArgumentResolver(); + } + + @Override + protected Class getControllerClass() { + return Sample.class; + } + + interface Sample { + + void supportedMethod(Pageable pageable); + + void unsupportedMethod(String string); + + void simpleDefault(@PageableDefault(size = PAGE_SIZE, page = PAGE_NUMBER) Pageable pageable); + + void simpleDefaultWithSort( + @PageableDefault(size = PAGE_SIZE, page = PAGE_NUMBER, sort = { "firstname", "lastname" }) Pageable pageable); + + void simpleDefaultWithSortAndDirection(@PageableDefault(size = PAGE_SIZE, page = PAGE_NUMBER, sort = { "firstname", + "lastname" }, direction = Direction.DESC) Pageable pageable); + + void simpleDefaultWithExternalSort(@PageableDefault(size = PAGE_SIZE, page = PAGE_NUMBER)// + @SortDefault(sort = { "firstname", "lastname" }, direction = Direction.DESC) Pageable pageable); + + void simpleDefaultWithContaineredExternalSort(@PageableDefault(size = PAGE_SIZE, page = PAGE_NUMBER)// + @SortDefaults(@SortDefault(sort = { "firstname", "lastname" }, direction = Direction.DESC)) Pageable pageable); + + void invalidQualifiers(@Qualifier("foo") Pageable first, @Qualifier("foo") Pageable second); + + void noQualifiers(Pageable first, Pageable second); + } +} diff --git a/src/test/java/org/springframework/data/web/SortDefaultUnitTest.java b/src/test/java/org/springframework/data/web/SortDefaultUnitTest.java new file mode 100644 index 000000000..ff0a7c5fc --- /dev/null +++ b/src/test/java/org/springframework/data/web/SortDefaultUnitTest.java @@ -0,0 +1,143 @@ +/* + * 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 static org.springframework.data.domain.Sort.Direction.*; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.core.MethodParameter; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.domain.Sort.Order; +import org.springframework.data.web.SortDefault.SortDefaults; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; + +/** + * Unit tests for {@link SortDefault}. + * + * @since 1.6 + * @author Oliver Gierke + */ +public abstract class SortDefaultUnitTest { + + static final String SORT_0 = "username"; + static final String SORT_1 = "username,asc"; + static final String[] SORT_2 = new String[] { "username,ASC", "lastname,firstname,DESC" }; + static final String SORT_3 = "firstname,lastname"; + + static final String[] SORT_FIELDS = new String[] { "firstname", "lastname" }; + static final Direction SORT_DIRECTION = Direction.DESC; + + static final Sort SORT = new Sort(SORT_DIRECTION, SORT_FIELDS); + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Test + public void parsesSimpleSortStringCorrectly() { + + assertSortStringParsedInto(new Sort(new Order("username")), SORT_1); + assertSortStringParsedInto(new Sort(new Order(ASC, "username")), SORT_1); + assertSortStringParsedInto(new Sort(new Order(ASC, "username"), // + new Order(DESC, "lastname"), new Order(DESC, "firstname")), SORT_2); + assertSortStringParsedInto(new Sort("firstname", "lastname"), SORT_3); + } + + private static void assertSortStringParsedInto(Sort expected, String... source) { + + SortHandlerMethodArgumentResolver resolver = new SortHandlerMethodArgumentResolver(); + Sort sort = resolver.parseParameterIntoSort(source, ","); + + assertThat(sort, is(expected)); + } + + @Test + public void supportsSortParameter() { + assertThat(getResolver().supportsParameter(getParameterOfMethod("supportedMethod")), is(true)); + } + + @Test + public void returnsNullForNoDefault() throws Exception { + assertSupportedAndResolvedTo(getParameterOfMethod("supportedMethod"), null); + } + + @Test + public void discoversSimpleDefault() throws Exception { + assertSupportedAndResolvedTo(getParameterOfMethod("simpleDefault"), new Sort(Direction.ASC, SORT_FIELDS)); + } + + @Test + public void discoversSimpleDefaultWithDirection() throws Exception { + assertSupportedAndResolvedTo(getParameterOfMethod("simpleDefaultWithDirection"), SORT); + } + + @Test + public void rejectsNonSortParameter() { + + MethodParameter parameter = TestUtils.getParameterOfMethod(getControllerClass(), "unsupportedMethod", String.class); + assertThat(getResolver().supportsParameter(parameter), is(false)); + } + + @Test + public void rejectsDoubleAnnotatedMethod() throws Exception { + + MethodParameter parameter = getParameterOfMethod("invalid"); + + HandlerMethodArgumentResolver resolver = new SortHandlerMethodArgumentResolver(); + assertThat(resolver.supportsParameter(parameter), is(true)); + + exception.expect(IllegalArgumentException.class); + exception.expectMessage(SortDefault.class.getSimpleName()); + exception.expectMessage(SortDefaults.class.getSimpleName()); + exception.expectMessage(parameter.toString()); + + resolver.resolveArgument(parameter, null, TestUtils.getWebRequest(), null); + } + + @Test + public void discoversContaineredDefault() throws Exception { + + MethodParameter parameter = getParameterOfMethod("containeredDefault"); + Sort reference = new Sort("foo", "bar"); + + assertSupportedAndResolvedTo(parameter, reference); + } + + protected HandlerMethodArgumentResolver getResolver() { + return new SortHandlerMethodArgumentResolver(); + } + + protected abstract Class getControllerClass(); + + private void assertSupportedAndResolvedTo(MethodParameter parameter, Sort sort) throws Exception { + + HandlerMethodArgumentResolver resolver = getResolver(); + assertThat(resolver.supportsParameter(parameter), is(true)); + assertThat(resolver.resolveArgument(parameter, null, TestUtils.getWebRequest(), null), is((Object) sort)); + } + + protected MethodParameter getParameterOfMethod(String name) { + return getParameterOfMethod(getControllerClass(), name); + } + + private static MethodParameter getParameterOfMethod(Class controller, String name) { + return TestUtils.getParameterOfMethod(controller, name, Sort.class); + } +} diff --git a/src/test/java/org/springframework/data/web/SortHandlerArgumentResolverUnitTests.java b/src/test/java/org/springframework/data/web/SortHandlerArgumentResolverUnitTests.java new file mode 100644 index 000000000..7c778ea0d --- /dev/null +++ b/src/test/java/org/springframework/data/web/SortHandlerArgumentResolverUnitTests.java @@ -0,0 +1,164 @@ +/* + * 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 static org.springframework.data.domain.Sort.Direction.*; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.MethodParameter; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.domain.Sort.Order; +import org.springframework.data.web.SortDefault.SortDefaults; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.util.StringUtils; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * Unit tests for {@link SortHandlerMethodArgumentResolver}. + * + * @since 1.6 + * @author Oliver Gierke + */ +public class SortHandlerArgumentResolverUnitTests extends SortDefaultUnitTest { + + static final String SORT_0 = "username"; + static final String SORT_1 = "username,asc"; + static final String[] SORT_2 = new String[] { "username,ASC", "lastname,firstname,DESC" }; + static final String SORT_3 = "firstname,lastname"; + + @Test + public void discoversSimpleSortFromRequest() { + + MethodParameter parameter = getParameterOfMethod("simpleDefault"); + Sort reference = new Sort("bar", "foo"); + NativeWebRequest request = getRequestWithSort(reference); + + assertSupportedAndResolvedTo(request, parameter, reference); + } + + @Test + public void discoversComplexSortFromRequest() { + + MethodParameter parameter = getParameterOfMethod("simpleDefault"); + Sort reference = new Sort("bar", "foo").and(new Sort("fizz", "buzz")); + + assertSupportedAndResolvedTo(getRequestWithSort(reference), parameter, reference); + } + + @Test + public void discoversQualifiedSortFromRequest() { + + MethodParameter parameter = getParameterOfMethod("qualifiedSort"); + Sort reference = new Sort("bar", "foo"); + + assertSupportedAndResolvedTo(getRequestWithSort(reference, "qual"), parameter, reference); + } + + @Test + public void returnsNullForSortParameterSetToNothing() throws Exception { + + MethodParameter parameter = getParameterOfMethod("supportedMethod"); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("sort", (String) null); + + SortHandlerMethodArgumentResolver resolver = new SortHandlerMethodArgumentResolver(); + Sort result = resolver.resolveArgument(parameter, null, new ServletWebRequest(request), null); + assertThat(result, is(nullValue())); + } + + @Test + public void buildsUpRequestParameters() { + assertUriStringFor(SORT, "sort=firstname,lastname,desc"); + assertUriStringFor(new Sort(ASC, "foo").and(new Sort(DESC, "bar").and(new Sort(ASC, "foobar"))), + "sort=foo,asc&sort=bar,desc&sort=foobar,asc"); + assertUriStringFor(new Sort(ASC, "foo").and(new Sort(ASC, "bar").and(new Sort(DESC, "foobar"))), + "sort=foo,bar,asc&sort=foobar,desc"); + } + + private void assertUriStringFor(Sort sort, String expected) { + + UriComponentsBuilder builder = UriComponentsBuilder.fromPath("/"); + MethodParameter parameter = getParameterOfMethod("supportedMethod"); + + new SortHandlerMethodArgumentResolver().enhance(builder, parameter, sort); + + assertThat(builder.build().toUriString(), endsWith(expected)); + } + + private static void assertSupportedAndResolvedTo(NativeWebRequest request, MethodParameter parameter, Sort sort) { + + SortHandlerMethodArgumentResolver resolver = new SortHandlerMethodArgumentResolver(); + assertThat(resolver.supportsParameter(parameter), is(true)); + + try { + assertThat(resolver.resolveArgument(parameter, null, request, null), is(sort)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static NativeWebRequest getRequestWithSort(Sort sort) { + + return getRequestWithSort(sort, null); + } + + private static NativeWebRequest getRequestWithSort(Sort sort, String qualifier) { + + MockHttpServletRequest request = new MockHttpServletRequest(); + + if (sort == null) { + return new ServletWebRequest(request); + } + + for (Order order : sort) { + + String prefix = StringUtils.hasText(qualifier) ? qualifier + "_" : ""; + request.addParameter(prefix + "sort", String.format("%s,%s", order.getProperty(), order.getDirection().name())); + } + + return new ServletWebRequest(request); + } + + @Override + protected Class getControllerClass() { + return Controller.class; + } + + interface Controller { + + void supportedMethod(Sort sort); + + void unsupportedMethod(String string); + + void qualifiedSort(@Qualifier("qual") Sort sort); + + void simpleDefault(@SortDefault({ "firstname", "lastname" }) Sort sort); + + void simpleDefaultWithDirection( + @SortDefault(sort = { "firstname", "lastname" }, direction = Direction.DESC) Sort sort); + + void containeredDefault(@SortDefaults(@SortDefault({ "foo", "bar" })) Sort sort); + + void invalid(@SortDefaults(@SortDefault({ "foo", "bar" })) @SortDefault({ "bar", "foo" }) Sort sort); + } +} diff --git a/src/test/java/org/springframework/data/web/TestUtils.java b/src/test/java/org/springframework/data/web/TestUtils.java new file mode 100644 index 000000000..f7db87219 --- /dev/null +++ b/src/test/java/org/springframework/data/web/TestUtils.java @@ -0,0 +1,51 @@ +/* + * 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.lang.reflect.Method; + +import org.springframework.core.MethodParameter; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.ServletWebRequest; + +/** + * General test utilities. + * + * @since 1.6 + * @author Oliver Gierke + */ +class TestUtils { + + public static NativeWebRequest getWebRequest() { + return new ServletWebRequest(new MockHttpServletRequest()); + } + + public static MethodParameter getParameterOfMethod(Class controller, String name, Class... argumentTypes) { + + Method method = getMethod(controller, name, argumentTypes); + return new MethodParameter(method, 0); + } + + public static Method getMethod(Class controller, String name, Class... argumentTypes) { + + try { + return controller.getMethod(name, argumentTypes); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +}