parseAndApplyBoundaries(@Nullable String parameter, int upper, boolean shiftIndex) {
-
- if (!StringUtils.hasText(parameter)) {
- return Optional.empty();
- }
-
- try {
- int parsed = Integer.parseInt(parameter) - (oneIndexedParameters && shiftIndex ? 1 : 0);
- return Optional.of(parsed < 0 ? 0 : parsed > upper ? upper : parsed);
- } catch (NumberFormatException e) {
- return Optional.of(0);
- }
+ return pageable;
}
}
diff --git a/src/main/java/org/springframework/data/web/PageableHandlerMethodArgumentResolverSupport.java b/src/main/java/org/springframework/data/web/PageableHandlerMethodArgumentResolverSupport.java
new file mode 100644
index 000000000..9ccb84aaf
--- /dev/null
+++ b/src/main/java/org/springframework/data/web/PageableHandlerMethodArgumentResolverSupport.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright 2017 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 java.lang.reflect.Method;
+import java.util.Optional;
+
+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.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * Base class providing methods for handler method argument resolvers to create 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 parameters beginning with
+ * {@link #DEFAULT_PAGE_PARAMETER}{@link #DEFAULT_QUALIFIER_DELIMITER}.
+ *
+ * @since 2.1
+ * @author Mark Paluch
+ */
+public abstract class PageableHandlerMethodArgumentResolverSupport {
+
+ private static final String INVALID_DEFAULT_PAGE_SIZE = "Invalid default page size configured for method %s! Must not be less than one!";
+
+ private static final String DEFAULT_PAGE_PARAMETER = "page";
+ private static final String DEFAULT_SIZE_PARAMETER = "size";
+ private static final String DEFAULT_PREFIX = "";
+ private static final String DEFAULT_QUALIFIER_DELIMITER = "_";
+ private static final int DEFAULT_MAX_PAGE_SIZE = 2000;
+ static final Pageable DEFAULT_PAGE_REQUEST = PageRequest.of(0, 20);
+
+ private Pageable fallbackPageable = DEFAULT_PAGE_REQUEST;
+ private String pageParameterName = DEFAULT_PAGE_PARAMETER;
+ private String sizeParameterName = DEFAULT_SIZE_PARAMETER;
+ private String prefix = DEFAULT_PREFIX;
+ private String qualifierDelimiter = DEFAULT_QUALIFIER_DELIMITER;
+ private int maxPageSize = DEFAULT_MAX_PAGE_SIZE;
+ private boolean oneIndexedParameters = false;
+
+ /**
+ * Configures the {@link Pageable} to be used as fallback in case no {@link PageableDefault} or
+ * {@link PageableDefault} (the latter only supported in legacy mode) can be found at the method parameter to be
+ * resolved.
+ *
+ * If you set this to {@literal Optional#empty()}, 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. Note, that doing so will require you
+ * supply bot the page and the size parameter with the requests as there will be no default for any of the
+ * parameters available.
+ *
+ * @param fallbackPageable the {@link Pageable} to be used as general fallback.
+ */
+ public void setFallbackPageable(Pageable fallbackPageable) {
+
+ Assert.notNull(fallbackPageable, "Fallback Pageable must not be null!");
+
+ this.fallbackPageable = fallbackPageable;
+ }
+
+ /**
+ * Returns whether the given {@link Pageable} is the fallback one.
+ *
+ * @param pageable can be {@literal null}.
+ * @return
+ */
+ public boolean isFallbackPageable(Pageable pageable) {
+ return fallbackPageable == null ? false : fallbackPageable.equals(pageable);
+ }
+
+ /**
+ * Configures the maximum page size to be accepted. This allows to put an upper boundary of the page size to prevent
+ * potential attacks trying to issue an {@link OutOfMemoryError}. Defaults to {@link #DEFAULT_MAX_PAGE_SIZE}.
+ *
+ * @param maxPageSize the maxPageSize to set
+ */
+ public void setMaxPageSize(int maxPageSize) {
+ this.maxPageSize = maxPageSize;
+ }
+
+ /**
+ * Retrieves the maximum page size to be accepted. This allows to put an upper boundary of the page size to prevent
+ * potential attacks trying to issue an {@link OutOfMemoryError}. Defaults to {@link #DEFAULT_MAX_PAGE_SIZE}.
+ *
+ * @return the maximum page size allowed.
+ */
+ protected int getMaxPageSize() {
+ return this.maxPageSize;
+ }
+
+ /**
+ * Configures the parameter name to be used to find the page number in the request. Defaults to {@code page}.
+ *
+ * @param pageParameterName the parameter name to be used, must not be {@literal null} or empty.
+ */
+ public void setPageParameterName(String pageParameterName) {
+
+ Assert.hasText(pageParameterName, "Page parameter name must not be null or empty!");
+ this.pageParameterName = pageParameterName;
+ }
+
+ /**
+ * Retrieves the parameter name to be used to find the page number in the request. Defaults to {@code page}.
+ *
+ * @return the parameter name to be used, never {@literal null} or empty.
+ */
+ protected String getPageParameterName() {
+ return this.pageParameterName;
+ }
+
+ /**
+ * Configures the parameter name to be used to find the page size in the request. Defaults to {@code size}.
+ *
+ * @param sizeParameterName the parameter name to be used, must not be {@literal null} or empty.
+ */
+ public void setSizeParameterName(String sizeParameterName) {
+
+ Assert.hasText(sizeParameterName, "Size parameter name must not be null or empty!");
+ this.sizeParameterName = sizeParameterName;
+ }
+
+ /**
+ * Retrieves the parameter name to be used to find the page size in the request. Defaults to {@code size}.
+ *
+ * @return the parameter name to be used, never {@literal null} or empty.
+ */
+ protected String getSizeParameterName() {
+ return this.sizeParameterName;
+ }
+
+ /**
+ * 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 delimiter 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 qualifierDelimiter the 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;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Indicates 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.
+ *
+ * @return whether to assume 1-based page number indexes in the request parameters.
+ */
+ protected boolean isOneIndexedParameters() {
+ return this.oneIndexedParameters;
+ }
+
+ protected Pageable getPageable(MethodParameter methodParameter, @Nullable String pageString,
+ @Nullable String pageSizeString) {
+ assertPageableUniqueness(methodParameter);
+
+ Optional defaultOrFallback = getDefaultFromAnnotationOrFallback(methodParameter).toOptional();
+
+ Optional page = parseAndApplyBoundaries(pageString, Integer.MAX_VALUE, true);
+ Optional pageSize = parseAndApplyBoundaries(pageSizeString, maxPageSize, false);
+
+ if (!(page.isPresent() && pageSize.isPresent()) && !defaultOrFallback.isPresent()) {
+ return Pageable.unpaged();
+ }
+
+ int p = page
+ .orElseGet(() -> defaultOrFallback.map(Pageable::getPageNumber).orElseThrow(IllegalStateException::new));
+ int ps = pageSize
+ .orElseGet(() -> defaultOrFallback.map(Pageable::getPageSize).orElseThrow(IllegalStateException::new));
+
+ // Limit lower bound
+ ps = ps < 1 ? defaultOrFallback.map(Pageable::getPageSize).orElseThrow(IllegalStateException::new) : ps;
+ // Limit upper bound
+ ps = ps > maxPageSize ? maxPageSize : ps;
+
+ return PageRequest.of(p, ps, defaultOrFallback.map(Pageable::getSort).orElseGet(Sort::unsorted));
+ }
+
+ /**
+ * 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 the name of the request parameter.
+ */
+ protected String getParameterNameToUse(String source, @Nullable MethodParameter parameter) {
+
+ StringBuilder builder = new StringBuilder(prefix);
+
+ Qualifier qualifier = parameter == null ? null : parameter.getParameterAnnotation(Qualifier.class);
+
+ if (qualifier != null) {
+ builder.append(qualifier.value());
+ builder.append(qualifierDelimiter);
+ }
+
+ return builder.append(source).toString();
+ }
+
+ private Pageable getDefaultFromAnnotationOrFallback(MethodParameter methodParameter) {
+
+ PageableDefault defaults = methodParameter.getParameterAnnotation(PageableDefault.class);
+
+ if (defaults != null) {
+ return getDefaultPageRequestFrom(methodParameter, defaults);
+ }
+
+ return fallbackPageable;
+ }
+
+ private static Pageable getDefaultPageRequestFrom(MethodParameter parameter, PageableDefault defaults) {
+
+ Integer defaultPageNumber = defaults.page();
+ Integer defaultPageSize = getSpecificPropertyOrDefaultFromValue(defaults, "size");
+
+ if (defaultPageSize < 1) {
+ Method annotatedMethod = parameter.getMethod();
+ throw new IllegalStateException(String.format(INVALID_DEFAULT_PAGE_SIZE, annotatedMethod));
+ }
+
+ if (defaults.sort().length == 0) {
+ return PageRequest.of(defaultPageNumber, defaultPageSize);
+ }
+
+ return PageRequest.of(defaultPageNumber, defaultPageSize, defaults.direction(), defaults.sort());
+ }
+
+ /**
+ * Tries to parse the given {@link String} into an integer and applies the given boundaries. Will return 0 if the
+ * {@link String} cannot be parsed.
+ *
+ * @param parameter the parameter value.
+ * @param upper the upper bound to be applied.
+ * @param shiftIndex whether to shift the index if {@link #oneIndexedParameters} is set to true.
+ * @return
+ */
+ private Optional parseAndApplyBoundaries(@Nullable String parameter, int upper, boolean shiftIndex) {
+
+ if (!StringUtils.hasText(parameter)) {
+ return Optional.empty();
+ }
+
+ try {
+ int parsed = Integer.parseInt(parameter) - (oneIndexedParameters && shiftIndex ? 1 : 0);
+ return Optional.of(parsed < 0 ? 0 : parsed > upper ? upper : parsed);
+ } catch (NumberFormatException e) {
+ return Optional.of(0);
+ }
+ }
+}
diff --git a/src/main/java/org/springframework/data/web/ReactivePageableHandlerMethodArgumentResolver.java b/src/main/java/org/springframework/data/web/ReactivePageableHandlerMethodArgumentResolver.java
new file mode 100644
index 000000000..9994c0ca8
--- /dev/null
+++ b/src/main/java/org/springframework/data/web/ReactivePageableHandlerMethodArgumentResolver.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2017 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 javax.annotation.Nonnull;
+
+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.util.Assert;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.reactive.BindingContext;
+import org.springframework.web.reactive.result.method.SyncHandlerMethodArgumentResolver;
+import org.springframework.web.server.ServerWebExchange;
+
+/**
+ * Extracts paging information from web requests and thus allows injecting {@link Pageable} instances into WebFlux
+ * controller methods. Request properties to be parsed can be configured. Default configuration uses request parameters
+ * beginning with {@link #DEFAULT_PAGE_PARAMETER}{@link #DEFAULT_QUALIFIER_DELIMITER}.
+ *
+ * @since 2.1
+ * @author Mark Paluch
+ */
+public class ReactivePageableHandlerMethodArgumentResolver extends PageableHandlerMethodArgumentResolverSupport
+ implements SyncHandlerMethodArgumentResolver {
+
+ private static final ReactiveSortHandlerMethodArgumentResolver DEFAULT_SORT_RESOLVER = new ReactiveSortHandlerMethodArgumentResolver();
+
+ private ReactiveSortHandlerMethodArgumentResolver sortResolver;
+
+ /**
+ * Constructs an instance of this resolved with a default {@link ReactiveSortHandlerMethodArgumentResolver}.
+ */
+ public ReactivePageableHandlerMethodArgumentResolver() {
+ this(DEFAULT_SORT_RESOLVER);
+ }
+
+ /**
+ * Constructs an instance of this resolver with the specified {@link SortArgumentResolver}.
+ *
+ * @param sortResolver the sort resolver to use.
+ */
+ public ReactivePageableHandlerMethodArgumentResolver(ReactiveSortHandlerMethodArgumentResolver sortResolver) {
+
+ Assert.notNull(sortResolver, "ReactiveSortHandlerMethodArgumentResolver must not be null!");
+
+ this.sortResolver = sortResolver;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver#supportsParameter(org.springframework.core.MethodParameter)
+ */
+ @Override
+ public boolean supportsParameter(MethodParameter parameter) {
+ return Pageable.class.equals(parameter.getParameterType());
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.web.reactive.result.method.SyncHandlerMethodArgumentResolver#resolveArgumentValue(org.springframework.core.MethodParameter, org.springframework.web.reactive.BindingContext, org.springframework.web.server.ServerWebExchange)
+ */
+ @Nonnull
+ @Override
+ public Pageable resolveArgumentValue(MethodParameter parameter, BindingContext bindingContext,
+ ServerWebExchange exchange) {
+
+ MultiValueMap queryParams = exchange.getRequest().getQueryParams();
+ String page = queryParams.getFirst(getParameterNameToUse(getPageParameterName(), parameter));
+ String pageSize = queryParams.getFirst(getParameterNameToUse(getSizeParameterName(), parameter));
+
+ Sort sort = sortResolver.resolveArgumentValue(parameter, bindingContext, exchange);
+
+ Pageable pageable = getPageable(parameter, page, pageSize);
+
+ return sort.isSorted() ? PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), sort) : pageable;
+ }
+}
diff --git a/src/main/java/org/springframework/data/web/ReactiveSortHandlerMethodArgumentResolver.java b/src/main/java/org/springframework/data/web/ReactiveSortHandlerMethodArgumentResolver.java
new file mode 100644
index 000000000..d5c52af31
--- /dev/null
+++ b/src/main/java/org/springframework/data/web/ReactiveSortHandlerMethodArgumentResolver.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2017 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.List;
+
+import javax.annotation.Nonnull;
+
+import org.springframework.core.MethodParameter;
+import org.springframework.data.domain.Sort;
+import org.springframework.util.StringUtils;
+import org.springframework.web.reactive.BindingContext;
+import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
+import org.springframework.web.reactive.result.method.SyncHandlerMethodArgumentResolver;
+import org.springframework.web.server.ServerWebExchange;
+
+/**
+ * Reactive {@link HandlerMethodArgumentResolver} to create {@link Sort} instances from query string parameters or
+ * {@link SortDefault} annotations.
+ *
+ * @since 2.1
+ * @author Mark Paluch
+ */
+public class ReactiveSortHandlerMethodArgumentResolver extends SortHandlerMethodArgumentResolverSupport
+ implements SyncHandlerMethodArgumentResolver {
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter(org.springframework.core.MethodParameter)
+ */
+ @Override
+ public boolean supportsParameter(MethodParameter parameter) {
+ return Sort.class.equals(parameter.getParameterType());
+ }
+
+ /*
+ *(non-Javadoc)
+ * @see org.springframework.web.reactive.result.method.SyncHandlerMethodArgumentResolver#resolveArgumentValue(org.springframework.core.MethodParameter, org.springframework.web.reactive.BindingContext, org.springframework.web.server.ServerWebExchange)
+ */
+ @Nonnull
+ @Override
+ public Sort resolveArgumentValue(MethodParameter parameter, BindingContext bindingContext,
+ ServerWebExchange exchange) {
+
+ List directionParameter = exchange.getRequest().getQueryParams().get(getSortParameter(parameter));
+
+ // No parameter
+ if (directionParameter == null) {
+ return getDefaultFromAnnotationOrFallback(parameter);
+ }
+
+ // Single empty parameter, e.g "sort="
+ if (directionParameter.size() == 1 && !StringUtils.hasText(directionParameter.get(0))) {
+ return getDefaultFromAnnotationOrFallback(parameter);
+ }
+
+ return parseParameterIntoSort(directionParameter, getPropertyDelimiter());
+ }
+}
diff --git a/src/main/java/org/springframework/data/web/SortHandlerMethodArgumentResolver.java b/src/main/java/org/springframework/data/web/SortHandlerMethodArgumentResolver.java
index 7bafca892..53fcfd5ba 100644
--- a/src/main/java/org/springframework/data/web/SortHandlerMethodArgumentResolver.java
+++ b/src/main/java/org/springframework/data/web/SortHandlerMethodArgumentResolver.java
@@ -17,18 +17,12 @@ package org.springframework.data.web;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
import java.util.List;
-import java.util.Optional;
import javax.annotation.Nullable;
-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.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.support.WebDataBinderFactory;
@@ -47,14 +41,14 @@ import org.springframework.web.method.support.ModelAndViewContainer;
* @author Mark Paluch
* @author Christoph Strobl
*/
-public class SortHandlerMethodArgumentResolver implements SortArgumentResolver {
+public class SortHandlerMethodArgumentResolver extends SortHandlerMethodArgumentResolverSupport {
private static final String DEFAULT_PARAMETER = "sort";
private static final String DEFAULT_PROPERTY_DELIMITER = ",";
private static final String DEFAULT_QUALIFIER_DELIMITER = "_";
private static final Sort DEFAULT_SORT = Sort.unsorted();
- private static final String SORT_DEFAULTS_NAME = SortDefaults.class.getSimpleName();
+ private static final String SORT_DEFAULTS_NAME = SortDefault.SortDefaults.class.getSimpleName();
private static final String SORT_DEFAULT_NAME = SortDefault.class.getSimpleName();
private Sort fallbackSort = DEFAULT_SORT;
@@ -137,255 +131,6 @@ public class SortHandlerMethodArgumentResolver implements SortArgumentResolver {
return getDefaultFromAnnotationOrFallback(parameter);
}
- return parseParameterIntoSort(directionParameter, propertyDelimiter);
- }
-
- /**
- * Returns the sort parameter to be looked up from the request. Potentially applies qualifiers to it.
- *
- * @param parameter can be {@literal null}.
- * @return
- */
- protected String getSortParameter(@Nullable MethodParameter parameter) {
-
- StringBuilder builder = new StringBuilder();
-
- Qualifier qualifier = parameter != null ? parameter.getParameterAnnotation(Qualifier.class) : null;
-
- if (qualifier != null) {
- builder.append(qualifier.value()).append(qualifierDelimiter);
- }
-
- return builder.append(sortParameter).toString();
- }
-
- /**
- * 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
- */
- protected 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 == null ? Collections.emptyList() : 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
- */
- protected 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 == null ? Collections.emptyList() : builder.dumpExpressionIfPresentInto(expressions);
- }
-
- /**
- * 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 = Arrays.stream(part.split(delimiter)) //
- .filter(SortHandlerMethodArgumentResolver::notOnlyDots) //
- .toArray(String[]::new);
-
- Optional direction = elements.length == 0 //
- ? Optional.empty() //
- : Direction.fromOptionalString(elements[elements.length - 1]);
-
- int lastIndex = direction.map(it -> elements.length - 1) //
- .orElseGet(() -> elements.length);
-
- for (int i = 0; i < lastIndex; i++) {
- toOrder(elements[i], direction).ifPresent(allOrders::add);
- }
- }
-
- return allOrders.isEmpty() ? Sort.unsorted() : Sort.by(allOrders);
- }
-
- /**
- * 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 the configured fallback-sort
- * {@link #setFallbackSort(Sort)}.
- */
- private Sort getDefaultFromAnnotationOrFallback(MethodParameter parameter) {
-
- SortDefaults annotatedDefaults = parameter.getParameterAnnotation(SortDefaults.class);
- SortDefault annotatedDefault = parameter.getParameterAnnotation(SortDefault.class);
-
- if (annotatedDefault != null && annotatedDefaults != 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));
- }
-
- if (annotatedDefault != null) {
- return appendOrCreateSortTo(annotatedDefault, Sort.unsorted());
- }
-
- if (annotatedDefaults != null) {
-
- Sort sort = Sort.unsorted();
-
- for (SortDefault currentAnnotatedDefault : annotatedDefaults.value()) {
- sort = appendOrCreateSortTo(currentAnnotatedDefault, sort);
- }
-
- return sort;
- }
-
- return fallbackSort;
- }
-
- /**
- * 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");
-
- return fields.length == 0 //
- ? Sort.unsorted()
- : sortOrNull.and(Sort.by(sortDefault.direction(), fields));
- }
-
- /**
- * Returns whether the given source {@link String} consists of dots only.
- *
- * @param source must not be {@literal null}.
- * @return
- */
- private static boolean notOnlyDots(String source) {
- return StringUtils.hasText(source.replace(".", ""));
- }
-
- private static Optional toOrder(String property, Optional direction) {
-
- return !StringUtils.hasText(property) //
- ? Optional.empty() //
- : Optional.of(direction.map(it -> new Order(it, property)) //
- .orElseGet(() -> Order.by(property)));
- }
-
- /**
- * 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;
- }
-
- elements.add(direction.name().toLowerCase());
- expressions.add(StringUtils.collectionToDelimitedString(elements, propertyDelimiter));
-
- return expressions;
- }
+ return parseParameterIntoSort(Arrays.asList(directionParameter), getPropertyDelimiter());
}
}
diff --git a/src/main/java/org/springframework/data/web/SortHandlerMethodArgumentResolverSupport.java b/src/main/java/org/springframework/data/web/SortHandlerMethodArgumentResolverSupport.java
new file mode 100644
index 000000000..127fd6f4d
--- /dev/null
+++ b/src/main/java/org/springframework/data/web/SortHandlerMethodArgumentResolverSupport.java
@@ -0,0 +1,348 @@
+/*
+ * Copyright 2017 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.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import javax.annotation.Nullable;
+
+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.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * Base class providing methods for handler method argument resolvers to create {@link Sort} instances from request
+ * parameters or {@link SortDefault} annotations.
+ *
+ * @since 2.1
+ * @see SortHandlerMethodArgumentResolver
+ * @see ReactiveSortHandlerMethodArgumentResolver
+ * @author Mark Paluch
+ */
+public abstract class SortHandlerMethodArgumentResolverSupport {
+
+ private static final String DEFAULT_PARAMETER = "sort";
+ private static final String DEFAULT_PROPERTY_DELIMITER = ",";
+ private static final String DEFAULT_QUALIFIER_DELIMITER = "_";
+ private static final Sort DEFAULT_SORT = Sort.unsorted();
+
+ private static final String SORT_DEFAULTS_NAME = SortDefaults.class.getSimpleName();
+ private static final String SORT_DEFAULT_NAME = SortDefault.class.getSimpleName();
+
+ private Sort fallbackSort = DEFAULT_SORT;
+ private String sortParameter = DEFAULT_PARAMETER;
+ private String propertyDelimiter = DEFAULT_PROPERTY_DELIMITER;
+ private String qualifierDelimiter = DEFAULT_QUALIFIER_DELIMITER;
+
+ /**
+ * propertyDel 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 sortParameter) {
+
+ Assert.hasText(sortParameter, "SortParameter must not be null nor empty!");
+ this.sortParameter = sortParameter;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * @return the configured delimiter used to separate property references and the direction to be sorted by
+ */
+ public String getPropertyDelimiter() {
+ return 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;
+ }
+
+ /**
+ * Configures the {@link Sort} to be used as fallback in case no {@link SortDefault} or {@link SortDefaults} (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 Sort} data can be found in the request.
+ *
+ * @param fallbackSort the {@link Sort} to be used as general fallback.
+ */
+ public void setFallbackSort(Sort fallbackSort) {
+ this.fallbackSort = fallbackSort;
+ }
+
+ /**
+ * 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 the configured fallback-sort
+ * {@link #setFallbackSort(Sort)}.
+ */
+ protected Sort getDefaultFromAnnotationOrFallback(MethodParameter parameter) {
+
+ SortDefaults annotatedDefaults = parameter.getParameterAnnotation(SortDefaults.class);
+ SortDefault annotatedDefault = parameter.getParameterAnnotation(SortDefault.class);
+
+ if (annotatedDefault != null && annotatedDefaults != 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));
+ }
+
+ if (annotatedDefault != null) {
+ return appendOrCreateSortTo(annotatedDefault, Sort.unsorted());
+ }
+
+ if (annotatedDefaults != null) {
+
+ Sort sort = Sort.unsorted();
+
+ for (SortDefault currentAnnotatedDefault : annotatedDefaults.value()) {
+ sort = appendOrCreateSortTo(currentAnnotatedDefault, sort);
+ }
+
+ return sort;
+ }
+
+ return fallbackSort;
+ }
+
+ /**
+ * 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 Sort.unsorted();
+ }
+
+ return sortOrNull.and(Sort.by(sortDefault.direction(), fields));
+ }
+
+ /**
+ * Returns the sort parameter to be looked up from the request. Potentially applies qualifiers to it.
+ *
+ * @param parameter can be {@literal null}.
+ * @return
+ */
+ protected String getSortParameter(@Nullable MethodParameter parameter) {
+
+ StringBuilder builder = new StringBuilder();
+
+ Qualifier qualifier = parameter != null ? parameter.getParameterAnnotation(Qualifier.class) : null;
+
+ if (qualifier != null) {
+ builder.append(qualifier.value()).append(qualifierDelimiter);
+ }
+
+ return builder.append(sortParameter).toString();
+ }
+
+ /**
+ * 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(List source, String delimiter) {
+
+ List allOrders = new ArrayList<>();
+
+ for (String part : source) {
+
+ if (part == null) {
+ continue;
+ }
+
+ String[] elements = part.split(delimiter);
+
+ Optional direction = elements.length == 0 ? Optional.empty()
+ : Direction.fromOptionalString(elements[elements.length - 1]);
+
+ int lastIndex = direction.map(it -> elements.length - 1).orElseGet(() -> elements.length);
+
+ for (int i = 0; i < lastIndex; i++) {
+ toOrder(elements[i], direction).ifPresent(allOrders::add);
+ }
+ }
+
+ return allOrders.isEmpty() ? Sort.unsorted() : Sort.by(allOrders);
+ }
+
+ private static Optional toOrder(String property, Optional direction) {
+
+ if (!StringUtils.hasText(property)) {
+ return Optional.empty();
+ }
+
+ return Optional.of(direction.map(it -> new Order(it, property)).orElseGet(() -> Order.by(property)));
+ }
+
+ /**
+ * 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
+ */
+ protected 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 == null ? Collections.emptyList() : 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
+ */
+ protected 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 == null ? Collections.emptyList() : 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}.
+ */
+ 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
+ */
+ boolean hasSameDirectionAs(Order order) {
+ return this.direction == order.getDirection();
+ }
+
+ /**
+ * Adds the given property to the expression to be built.
+ *
+ * @param property
+ */
+ 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
+ */
+ List dumpExpressionIfPresentInto(List expressions) {
+
+ if (elements.isEmpty()) {
+ return expressions;
+ }
+
+ elements.add(direction.name().toLowerCase());
+ expressions.add(StringUtils.collectionToDelimitedString(elements, propertyDelimiter));
+
+ return expressions;
+ }
+ }
+}
diff --git a/src/test/java/org/springframework/data/web/ReactivePageableHandlerMethodArgumentResolverUnitTests.java b/src/test/java/org/springframework/data/web/ReactivePageableHandlerMethodArgumentResolverUnitTests.java
new file mode 100755
index 000000000..0fb9d1e22
--- /dev/null
+++ b/src/test/java/org/springframework/data/web/ReactivePageableHandlerMethodArgumentResolverUnitTests.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright 2017 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.assertj.core.api.Assertions.*;
+import static org.springframework.data.web.PageableDefaultUnitTests.*;
+import static org.springframework.data.web.PageableHandlerMethodArgumentResolver.*;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+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.Direction;
+import org.springframework.data.web.SortDefault.SortDefaults;
+import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
+import org.springframework.mock.web.server.MockServerWebExchange;
+import org.springframework.web.reactive.result.method.SyncHandlerMethodArgumentResolver;
+
+/**
+ * Unit tests for {@link ReactivePageableHandlerMethodArgumentResolver}.
+ *
+ * @author Mark Paluch
+ */
+public class ReactivePageableHandlerMethodArgumentResolverUnitTests {
+
+ @Rule public final ExpectedException exception = ExpectedException.none();
+
+ MethodParameter supportedMethodParameter;
+
+ @Before
+ public void setUp() throws Exception {
+ this.supportedMethodParameter = new MethodParameter(Sample.class.getMethod("supportedMethod", Pageable.class), 0);
+ }
+
+ @Test // DATACMNS-1211
+ public void preventsPageSizeFromExceedingMayValueIfConfigured() {
+
+ // Read side
+ MockServerHttpRequest request = MockServerHttpRequest.get("foo?page=0&size=200").build();
+
+ assertSupportedAndResult(supportedMethodParameter, PageRequest.of(0, 100), request);
+ }
+
+ @Test(expected = IllegalArgumentException.class) // DATACMNS-1211
+ public void rejectsEmptyPageParameterName() {
+ new ReactivePageableHandlerMethodArgumentResolver().setPageParameterName("");
+ }
+
+ @Test(expected = IllegalArgumentException.class) // DATACMNS-1211
+ public void rejectsNullPageParameterName() {
+ new ReactivePageableHandlerMethodArgumentResolver().setPageParameterName(null);
+ }
+
+ @Test(expected = IllegalArgumentException.class) // DATACMNS-1211
+ public void rejectsEmptySizeParameterName() {
+ new ReactivePageableHandlerMethodArgumentResolver().setSizeParameterName("");
+ }
+
+ @Test(expected = IllegalArgumentException.class) // DATACMNS-1211
+ public void rejectsNullSizeParameterName() {
+ new ReactivePageableHandlerMethodArgumentResolver().setSizeParameterName(null);
+ }
+
+ @Test // DATACMNS-1211
+ public void qualifierIsUsedInParameterLookup() throws Exception {
+
+ MethodParameter parameter = new MethodParameter(Sample.class.getMethod("validQualifier", Pageable.class), 0);
+
+ MockServerHttpRequest request = MockServerHttpRequest.get("foo?foo_page=2&foo_size=10").build();
+
+ assertSupportedAndResult(parameter, PageRequest.of(2, 10), request);
+ }
+
+ @Test // DATACMNS-1211
+ public void usesDefaultPageSizeIfRequestPageSizeIsLessThanOne() {
+
+ MockServerHttpRequest request = MockServerHttpRequest.get("foo?page=0&size=0").build();
+
+ assertSupportedAndResult(supportedMethodParameter, DEFAULT_PAGE_REQUEST, request);
+ }
+
+ @Test // DATACMNS-1211
+ public void rejectsInvalidCustomDefaultForPageSize() throws Exception {
+
+ MethodParameter parameter = new MethodParameter(Sample.class.getMethod("invalidDefaultPageSize", Pageable.class),
+ 0);
+
+ exception.expect(IllegalStateException.class);
+ exception.expectMessage("invalidDefaultPageSize");
+
+ assertSupportedAndResult(parameter, DEFAULT_PAGE_REQUEST);
+ }
+
+ @Test // DATACMNS-1211
+ public void fallsBackToFirstPageIfNegativePageNumberIsGiven() {
+
+ MockServerHttpRequest request = MockServerHttpRequest.get("foo?page=-1").build();
+
+ assertSupportedAndResult(supportedMethodParameter, DEFAULT_PAGE_REQUEST, request);
+ }
+
+ @Test // DATACMNS-1211
+ public void pageParamIsNotNumeric() {
+
+ MockServerHttpRequest request = MockServerHttpRequest.get("foo?page=a").build();
+
+ assertSupportedAndResult(supportedMethodParameter, DEFAULT_PAGE_REQUEST, request);
+ }
+
+ @Test // DATACMNS-1211
+ public void sizeParamIsNotNumeric() {
+
+ MockServerHttpRequest request = MockServerHttpRequest.get("foo?size=a").build();
+
+ assertSupportedAndResult(supportedMethodParameter, DEFAULT_PAGE_REQUEST, request);
+ }
+
+ @Test // DATACMNS-1211
+ public void returnsNullIfFallbackIsUnpagedAndNoParametersGiven() {
+
+ ReactivePageableHandlerMethodArgumentResolver resolver = getReactiveResolver();
+ resolver.setFallbackPageable(Pageable.unpaged());
+
+ assertSupportedAndResult(supportedMethodParameter, Pageable.unpaged(), TestUtils.getWebfluxRequest(), resolver);
+ }
+
+ @Test // DATACMNS-1211
+ public void returnsFallbackIfOnlyPageIsGiven() {
+
+ ReactivePageableHandlerMethodArgumentResolver resolver = getReactiveResolver();
+ resolver.setFallbackPageable(Pageable.unpaged());
+
+ MockServerHttpRequest request = MockServerHttpRequest.get("foo?page=20").build();
+
+ assertThat(resolve(resolver, request)).isEqualTo(Pageable.unpaged());
+ }
+
+ @Test // DATACMNS-1211
+ public void returnsFallbackIfFallbackIsUnpagedAndOnlySizeIsGiven() {
+
+ ReactivePageableHandlerMethodArgumentResolver resolver = getReactiveResolver();
+ resolver.setFallbackPageable(Pageable.unpaged());
+
+ MockServerHttpRequest request = MockServerHttpRequest.get("foo?size=10").build();
+
+ assertThat(resolve(resolver, request)).isEqualTo(Pageable.unpaged());
+ }
+
+ @Test // DATACMNS-1211
+ public void considersOneIndexedParametersSetting() {
+
+ ReactivePageableHandlerMethodArgumentResolver resolver = getReactiveResolver();
+ resolver.setOneIndexedParameters(true);
+
+ MockServerHttpRequest request = MockServerHttpRequest.get("foo?page=1").build();
+
+ assertThat(resolve(resolver, request).getPageNumber()).isEqualTo(0);
+ }
+
+ @Test // DATACMNS-1211
+ public void usesNullSortIfNoDefaultIsConfiguredAndPageAndSizeAreGiven() {
+
+ ReactivePageableHandlerMethodArgumentResolver resolver = getReactiveResolver();
+ resolver.setFallbackPageable(Pageable.unpaged());
+
+ MockServerHttpRequest request = MockServerHttpRequest.get("foo?page=0&size=10").build();
+
+ Pageable result = resolve(resolver, request);
+
+ assertThat(result.getPageNumber()).isEqualTo(0);
+ assertThat(result.getPageSize()).isEqualTo(10);
+ assertThat(result.getSort().isSorted()).isFalse();
+ }
+
+ @Test // DATACMNS-1211
+ public void oneIndexedParametersDefaultsIndexOutOfRange() {
+
+ ReactivePageableHandlerMethodArgumentResolver resolver = getReactiveResolver();
+ resolver.setOneIndexedParameters(true);
+
+ MockServerHttpRequest request = MockServerHttpRequest.get("foo?page=0").build();
+
+ assertThat(resolve(resolver, request).getPageNumber()).isEqualTo(0);
+ }
+
+ @Test // DATACMNS-1211
+ public void returnsCorrectPageSizeForOneIndexParameters() {
+
+ ReactivePageableHandlerMethodArgumentResolver resolver = getReactiveResolver();
+ resolver.setOneIndexedParameters(true);
+
+ MockServerHttpRequest request = MockServerHttpRequest.get("foo?size=10").build();
+
+ assertThat(resolve(resolver, request).getPageSize()).isEqualTo(10);
+ }
+
+ @Test // DATACMNS-1211
+ public void detectsFallbackPageableIfNullOneIsConfigured() {
+
+ ReactivePageableHandlerMethodArgumentResolver resolver = getReactiveResolver();
+ resolver.setFallbackPageable(Pageable.unpaged());
+
+ assertThat(resolver.isFallbackPageable(null)).isFalse();
+ assertThat(resolver.isFallbackPageable(PageRequest.of(0, 10))).isFalse();
+ }
+
+ private ReactivePageableHandlerMethodArgumentResolver getReactiveResolver() {
+
+ ReactivePageableHandlerMethodArgumentResolver resolver = new ReactivePageableHandlerMethodArgumentResolver();
+ resolver.setMaxPageSize(100);
+ return resolver;
+ }
+
+ private void assertSupportedAndResult(MethodParameter parameter, Pageable pageable) {
+ assertSupportedAndResult(parameter, pageable, TestUtils.getWebfluxRequest());
+ }
+
+ private void assertSupportedAndResult(MethodParameter parameter, Pageable pageable, MockServerHttpRequest request) {
+ assertSupportedAndResult(parameter, pageable, request, getReactiveResolver());
+ }
+
+ private void assertSupportedAndResult(MethodParameter parameter, Pageable pageable, MockServerHttpRequest request,
+ SyncHandlerMethodArgumentResolver resolver) {
+
+ assertThat(resolver.supportsParameter(parameter)).isTrue();
+
+ Object value = resolver.resolveArgumentValue(parameter, null, MockServerWebExchange.from(request));
+ assertThat(value).isEqualTo(pageable);
+ }
+
+ private Pageable resolve(ReactivePageableHandlerMethodArgumentResolver resolver, MockServerHttpRequest request) {
+ return resolver.resolveArgumentValue(supportedMethodParameter, null, MockServerWebExchange.from(request));
+ }
+
+ interface Sample {
+
+ void supportedMethod(Pageable pageable);
+
+ void unsupportedMethod(String string);
+
+ void invalidDefaultPageSize(@PageableDefault(size = 0) Pageable pageable);
+
+ 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 validQualifier(@Qualifier("foo") Pageable pageable);
+
+ void noQualifiers(Pageable first, Pageable second);
+ }
+}
diff --git a/src/test/java/org/springframework/data/web/ReactiveSortHandlerMethodArgumentResolverUnitTests.java b/src/test/java/org/springframework/data/web/ReactiveSortHandlerMethodArgumentResolverUnitTests.java
new file mode 100755
index 000000000..ca074b767
--- /dev/null
+++ b/src/test/java/org/springframework/data/web/ReactiveSortHandlerMethodArgumentResolverUnitTests.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright 2017 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.assertj.core.api.Assertions.*;
+import static org.springframework.data.domain.Sort.Direction.*;
+import static org.springframework.data.web.SortDefaultUnitTests.*;
+
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+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.lang.Nullable;
+import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
+import org.springframework.mock.web.server.MockServerWebExchange;
+import org.springframework.util.StringUtils;
+
+/**
+ * Unit tests for {@link ReactiveSortHandlerMethodArgumentResolver}.
+ *
+ * @author Mark Paluch
+ */
+public class ReactiveSortHandlerMethodArgumentResolverUnitTests {
+
+ static MethodParameter PARAMETER;
+
+ @Rule public final ExpectedException exception = ExpectedException.none();
+
+ @BeforeClass
+ public static void setUp() throws Exception {
+ PARAMETER = new MethodParameter(Controller.class.getMethod("supportedMethod", Sort.class), 0);
+ }
+
+ @Test // DATACMNS-1211
+ public void supportsSortParameter() {
+
+ ReactiveSortHandlerMethodArgumentResolver resolver = new ReactiveSortHandlerMethodArgumentResolver();
+
+ assertThat(resolver.supportsParameter(getParameterOfMethod("supportedMethod"))).isTrue();
+ }
+
+ @Test // DATACMNS-1211
+ public void returnsNullForNoDefault() {
+ assertSupportedAndResolvedTo(getParameterOfMethod("supportedMethod"), Sort.unsorted());
+ }
+
+ @Test // DATACMNS-1211
+ public void discoversSimpleDefault() {
+ assertSupportedAndResolvedTo(getParameterOfMethod("simpleDefault"), Sort.by(SORT_FIELDS).ascending());
+ }
+
+ @Test // DATACMNS-1211
+ public void discoversSimpleDefaultWithDirection() {
+ assertSupportedAndResolvedTo(getParameterOfMethod("simpleDefaultWithDirection"), SORT);
+ }
+
+ @Test // DATACMNS-1211
+ public void fallbackToGivenDefaultSort() {
+
+ MethodParameter parameter = TestUtils.getParameterOfMethod(Controller.class, "unsupportedMethod", String.class);
+ ReactiveSortHandlerMethodArgumentResolver resolver = new ReactiveSortHandlerMethodArgumentResolver();
+ Sort fallbackSort = Sort.by(Direction.ASC, "ID");
+ resolver.setFallbackSort(fallbackSort);
+
+ assertThat(resolve(resolver, TestUtils.getWebfluxRequest(), parameter)).isEqualTo(fallbackSort);
+ }
+
+ @Test // DATACMNS-1211
+ public void fallbackToDefaultDefaultSort() {
+
+ MethodParameter parameter = TestUtils.getParameterOfMethod(Controller.class, "unsupportedMethod", String.class);
+ ReactiveSortHandlerMethodArgumentResolver resolver = new ReactiveSortHandlerMethodArgumentResolver();
+
+ assertThat(resolve(resolver, TestUtils.getWebfluxRequest(), parameter).isSorted()).isFalse();
+ }
+
+ @Test // DATACMNS-1211
+ public void discoversSimpleSortFromRequest() {
+
+ MethodParameter parameter = getParameterOfMethod("simpleDefault");
+ Sort reference = Sort.by("bar", "foo");
+ MockServerHttpRequest request = getRequestWithSort(reference);
+
+ assertSupportedAndResolvedTo(request, parameter, reference);
+ }
+
+ @Test // DATACMNS-1211
+ public void discoversComplexSortFromRequest() {
+
+ MethodParameter parameter = getParameterOfMethod("simpleDefault");
+ Sort reference = Sort.by("bar", "foo").and(Sort.by("fizz", "buzz"));
+
+ assertSupportedAndResolvedTo(getRequestWithSort(reference), parameter, reference);
+ }
+
+ @Test // DATACMNS-1211
+ public void discoversQualifiedSortFromRequest() {
+
+ MethodParameter parameter = getParameterOfMethod("qualifiedSort");
+ Sort reference = Sort.by("bar", "foo");
+
+ assertSupportedAndResolvedTo(getRequestWithSort(reference, "qual"), parameter, reference);
+ }
+
+ @Test // DATACMNS-1211
+ public void requestForMultipleSortPropertiesIsUnmarshalledCorrectly() {
+
+ MockServerHttpRequest request = MockServerHttpRequest.get(String.format("foo?sort=%s", SortDefaultUnitTests.SORT_3))
+ .build();
+
+ ReactiveSortHandlerMethodArgumentResolver resolver = new ReactiveSortHandlerMethodArgumentResolver();
+ Sort result = resolve(resolver, request, PARAMETER);
+
+ assertThat(result).isEqualTo(Sort.by(Direction.ASC, "firstname", "lastname"));
+ }
+
+ @Test // DATACMNS-1211
+ public void parsesEmptySortToNull() {
+
+ MockServerHttpRequest request = MockServerHttpRequest.get("foo?sort=").build();
+
+ assertThat(resolve(request, PARAMETER).isSorted()).isFalse();
+ }
+
+ @Test // DATACMNS-1211
+ public void sortParamIsInvalidProperty() {
+
+ MockServerHttpRequest request = MockServerHttpRequest.get("foo?sort=,DESC").build();
+
+ assertThat(resolve(request, PARAMETER).isSorted()).isFalse();
+ }
+
+ @Test // DATACMNS-1211
+ public void sortParamIsInvalidPropertyWhenMultiProperty() {
+
+ MockServerHttpRequest request = MockServerHttpRequest.get("foo?sort=property1,,DESC").build();
+
+ assertThat(resolve(request, PARAMETER)).isEqualTo(Sort.by(DESC, "property1"));
+ }
+
+ @Test // DATACMNS-1211
+ public void rejectsDoubleAnnotatedMethod() {
+
+ MethodParameter parameter = getParameterOfMethod("invalid");
+
+ ReactiveSortHandlerMethodArgumentResolver resolver = new ReactiveSortHandlerMethodArgumentResolver();
+ assertThat(resolver.supportsParameter(parameter)).isTrue();
+
+ exception.expect(IllegalArgumentException.class);
+ exception.expectMessage(SortDefault.class.getSimpleName());
+ exception.expectMessage(SortDefaults.class.getSimpleName());
+ exception.expectMessage(parameter.toString());
+
+ resolver.resolveArgumentValue(parameter, null,
+ MockServerWebExchange.from(TestUtils.getWebfluxRequest()));
+ }
+
+ @Test // DATACMNS-1211
+ public void sortParamIsEmptyWhenMultiParams() {
+
+ MockServerHttpRequest request = MockServerHttpRequest.get("foo?sort=property,DESC&sort=").build();
+
+ assertThat(resolve(request, PARAMETER)).isEqualTo(Sort.by(DESC, "property"));
+ }
+
+ @Test // DATACMNS-1211
+ public void parsesCommaParameterForSort() {
+
+ MockServerHttpRequest request = MockServerHttpRequest.get("foo?sort=,").build();
+
+ assertThat(resolve(request, PARAMETER).isSorted()).isFalse();
+ }
+
+ @Test // DATACMNS-1211
+ public void doesNotReturnNullWhenAnnotatedWithSortDefault() {
+
+ MockServerHttpRequest request = MockServerHttpRequest.get("foo?sort=").build();
+
+ assertThat(resolve(request, getParameterOfMethod("simpleDefault"))).isEqualTo(Sort.by("firstname", "lastname"));
+ assertThat(resolve(request, getParameterOfMethod("containeredDefault"))).isEqualTo(Sort.by("foo", "bar"));
+ }
+
+ private static Sort resolve(MockServerHttpRequest request, MethodParameter parameter) {
+ return resolve(new ReactiveSortHandlerMethodArgumentResolver(), request, parameter);
+ }
+
+ private static Sort resolve(ReactiveSortHandlerMethodArgumentResolver resolver, MockServerHttpRequest request,
+ MethodParameter parameter) {
+ return resolver.resolveArgumentValue(parameter, null, MockServerWebExchange.from(request));
+ }
+
+ private static void assertSupportedAndResolvedTo(MethodParameter parameter, Sort sort) {
+ assertSupportedAndResolvedTo(TestUtils.getWebfluxRequest(), parameter, sort);
+ }
+
+ private static void assertSupportedAndResolvedTo(MockServerHttpRequest request, MethodParameter parameter,
+ Sort sort) {
+
+ ReactiveSortHandlerMethodArgumentResolver resolver = new ReactiveSortHandlerMethodArgumentResolver();
+ assertThat(resolver.supportsParameter(parameter)).isTrue();
+
+ Sort resolved = resolve(resolver, request, parameter);
+
+ assertThat(resolved).isEqualTo(sort);
+ }
+
+ private static MockServerHttpRequest getRequestWithSort(Sort sort) {
+ return getRequestWithSort(sort, null);
+ }
+
+ private static MockServerHttpRequest getRequestWithSort(@Nullable Sort sort, @Nullable String qualifier) {
+
+ if (sort == null) {
+ return TestUtils.getWebfluxRequest();
+ }
+
+ StringBuilder queryString = new StringBuilder();
+ for (Order order : sort) {
+
+ String prefix = StringUtils.hasText(qualifier) ? qualifier + "_" : "";
+
+ if (queryString.length() != 0) {
+ queryString.append('&');
+ }
+ queryString.append(String.format("%ssort=%s,%s", prefix, order.getProperty(), order.getDirection().name()));
+ }
+
+ return MockServerHttpRequest.get(String.format("foo?%s", queryString.toString())).build();
+ }
+
+ private static MethodParameter getParameterOfMethod(String name) {
+ return TestUtils.getParameterOfMethod(Controller.class, name, Sort.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/SortDefaultUnitTests.java b/src/test/java/org/springframework/data/web/SortDefaultUnitTests.java
index 566a752af..59fd365ef 100755
--- a/src/test/java/org/springframework/data/web/SortDefaultUnitTests.java
+++ b/src/test/java/org/springframework/data/web/SortDefaultUnitTests.java
@@ -17,6 +17,8 @@ package org.springframework.data.web;
import static org.assertj.core.api.Assertions.*;
+import java.util.Arrays;
+
import org.junit.Test;
import org.springframework.core.MethodParameter;
@@ -57,7 +59,7 @@ public abstract class SortDefaultUnitTests {
private static void assertSortStringParsedInto(Sort expected, String... source) {
SortHandlerMethodArgumentResolver resolver = new SortHandlerMethodArgumentResolver();
- Sort sort = resolver.parseParameterIntoSort(source, ",");
+ Sort sort = resolver.parseParameterIntoSort(Arrays.asList(source), ",");
assertThat(sort).isEqualTo(expected);
}
diff --git a/src/test/java/org/springframework/data/web/TestUtils.java b/src/test/java/org/springframework/data/web/TestUtils.java
index 22e378eed..56e3f25aa 100644
--- a/src/test/java/org/springframework/data/web/TestUtils.java
+++ b/src/test/java/org/springframework/data/web/TestUtils.java
@@ -18,6 +18,7 @@ package org.springframework.data.web;
import java.lang.reflect.Method;
import org.springframework.core.MethodParameter;
+import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
@@ -27,6 +28,7 @@ import org.springframework.web.context.request.ServletWebRequest;
*
* @since 1.6
* @author Oliver Gierke
+ * @author Mark Paluch
*/
class TestUtils {
@@ -34,6 +36,10 @@ class TestUtils {
return new ServletWebRequest(new MockHttpServletRequest());
}
+ public static MockServerHttpRequest getWebfluxRequest() {
+ return MockServerHttpRequest.get("foo").build();
+ }
+
public static MethodParameter getParameterOfMethod(Class> controller, String name, Class>... argumentTypes) {
Method method = getMethod(controller, name, argumentTypes);