();
-
- 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);
+ }
+ }
+}