DATACMNS-1211 - Add ReactiveSortHandlerMethodArgumentResolver.
Add ReactiveSortHandlerMethodArgumentResolver and extract shared code from imperative SortHandlerMethodArgumentResolver into SortHandlerMethodArgumentResolverSupport. Original pull request: #264.
This commit is contained in:
@@ -15,19 +15,11 @@
|
||||
*/
|
||||
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;
|
||||
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
@@ -43,26 +35,11 @@ import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
* @author Mark Paluch
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
public class PageableHandlerMethodArgumentResolver implements PageableArgumentResolver {
|
||||
public class PageableHandlerMethodArgumentResolver extends PageableHandlerMethodArgumentResolverSupport
|
||||
implements PageableArgumentResolver {
|
||||
|
||||
private static final SortHandlerMethodArgumentResolver DEFAULT_SORT_RESOLVER = new SortHandlerMethodArgumentResolver();
|
||||
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 SortArgumentResolver sortResolver;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Constructs an instance of this resolved with a default {@link SortHandlerMethodArgumentResolver}.
|
||||
@@ -239,106 +216,17 @@ public class PageableHandlerMethodArgumentResolver implements PageableArgumentRe
|
||||
public Pageable resolveArgument(MethodParameter methodParameter, @Nullable ModelAndViewContainer mavContainer,
|
||||
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) {
|
||||
|
||||
assertPageableUniqueness(methodParameter);
|
||||
String page = webRequest.getParameter(getParameterNameToUse(getPageParameterName(), methodParameter));
|
||||
String pageSize = webRequest.getParameter(getParameterNameToUse(getSizeParameterName(), methodParameter));
|
||||
|
||||
Optional<Pageable> defaultOrFallback = getDefaultFromAnnotationOrFallback(methodParameter).toOptional();
|
||||
|
||||
String pageString = webRequest.getParameter(getParameterNameToUse(pageParameterName, methodParameter));
|
||||
String pageSizeString = webRequest.getParameter(getParameterNameToUse(sizeParameterName, methodParameter));
|
||||
|
||||
Optional<Integer> page = parseAndApplyBoundaries(pageString, Integer.MAX_VALUE, true);
|
||||
Optional<Integer> 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;
|
||||
|
||||
Sort sort = sortResolver.resolveArgument(methodParameter, mavContainer, webRequest, binderFactory);
|
||||
Pageable pageable = getPageable(methodParameter, page, pageSize);
|
||||
|
||||
return PageRequest.of(p, ps,
|
||||
sort.isSorted() ? sort : 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);
|
||||
if (sort.isSorted()) {
|
||||
return PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), sort);
|
||||
}
|
||||
|
||||
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<Integer> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
* <p>
|
||||
* 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 <em>and</em> 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<Pageable> defaultOrFallback = getDefaultFromAnnotationOrFallback(methodParameter).toOptional();
|
||||
|
||||
Optional<Integer> page = parseAndApplyBoundaries(pageString, Integer.MAX_VALUE, true);
|
||||
Optional<Integer> 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<Integer> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String, String> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<String> 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());
|
||||
}
|
||||
}
|
||||
@@ -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<String> foldIntoExpressions(Sort sort) {
|
||||
|
||||
List<String> 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<String> legacyFoldExpressions(Sort sort) {
|
||||
|
||||
List<String> 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<Order> 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> 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<Order> toOrder(String property, Optional<Direction> 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<String> 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<String> dumpExpressionIfPresentInto(List<String> 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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
* <p>
|
||||
* 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<String> source, String delimiter) {
|
||||
|
||||
List<Order> allOrders = new ArrayList<>();
|
||||
|
||||
for (String part : source) {
|
||||
|
||||
if (part == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String[] elements = part.split(delimiter);
|
||||
|
||||
Optional<Direction> 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<Order> toOrder(String property, Optional<Direction> 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<String> foldIntoExpressions(Sort sort) {
|
||||
|
||||
List<String> 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<String> legacyFoldExpressions(Sort sort) {
|
||||
|
||||
List<String> 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<String> 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<String> dumpExpressionIfPresentInto(List<String> expressions) {
|
||||
|
||||
if (elements.isEmpty()) {
|
||||
return expressions;
|
||||
}
|
||||
|
||||
elements.add(direction.name().toLowerCase());
|
||||
expressions.add(StringUtils.collectionToDelimitedString(elements, propertyDelimiter));
|
||||
|
||||
return expressions;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user