DATACMNS-236, DATACMNS-117 - Added (Pageable|Sort)HanderMethodArgumentResolvers.
Added HandlerMethodArgumentResolver implementations for Pageable and Sort exposing a new default set of properties (page, size, sort) to resolve pagination and sorting information from the request. To mimic the legacy behavior we expose a (deprecated) LEGACY constant in PageableHandlerMethodArgumentResolver. Clients should move to the new properties structure ASAP. Added unit tests to verify old and new defaulting behavior. Introduced new annotations @SortDefault (with @SortDefaults wrapper annotation) and @PageableDefault (superseding the legacy @PageableDefaults). The new annotations have more speaking attribute names and the HMAR implementations transparently alias the generic value attribute into a more semantic one (size). The HMAR implementations implement Spring HATEOAS' UriComponentsContributor to be able to turn Pageable / Sort instances back into URIs created through the MethodLinkBuilderFactory API in Spring HATEOAS. Extracted common API between legacy PageableArgumentResolver and PageableHandlerMethodArgumentResolver into common helper class. Upgraded to Spring Hateoas 0.5.0.BUILD-SNAPSHOT.
This commit is contained in:
@@ -15,11 +15,10 @@
|
||||
*/
|
||||
package org.springframework.data.web;
|
||||
|
||||
import static org.springframework.data.web.SpringDataAnnotationUtils.*;
|
||||
|
||||
import java.beans.PropertyEditorSupport;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.servlet.ServletRequest;
|
||||
|
||||
@@ -131,7 +130,7 @@ public class PageableArgumentResolver implements WebArgumentResolver {
|
||||
return new PageRequest(fallbackPagable.getPageNumber(), fallbackPagable.getPageSize(), fallbackPagable.getSort());
|
||||
}
|
||||
|
||||
private static Pageable getDefaultPageRequestFrom(PageableDefaults defaults) {
|
||||
static Pageable getDefaultPageRequestFrom(PageableDefaults defaults) {
|
||||
|
||||
// +1 is because we substract 1 later
|
||||
int defaultPageNumber = defaults.pageNumber() + 1;
|
||||
@@ -162,94 +161,6 @@ public class PageableArgumentResolver implements WebArgumentResolver {
|
||||
return prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts uniqueness of all {@link Pageable} parameters of the method of the given {@link MethodParameter}.
|
||||
*
|
||||
* @param parameter
|
||||
*/
|
||||
private void assertPageableUniqueness(MethodParameter parameter) {
|
||||
|
||||
Method method = parameter.getMethod();
|
||||
|
||||
if (containsMoreThanOnePageableParameter(method)) {
|
||||
Annotation[][] annotations = method.getParameterAnnotations();
|
||||
assertQualifiersFor(method.getParameterTypes(), annotations);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given {@link Method} has more than one {@link Pageable} parameter.
|
||||
*
|
||||
* @param method
|
||||
* @return
|
||||
*/
|
||||
private boolean containsMoreThanOnePageableParameter(Method method) {
|
||||
|
||||
boolean pageableFound = false;
|
||||
|
||||
for (Class<?> type : method.getParameterTypes()) {
|
||||
|
||||
if (pageableFound && type.equals(Pageable.class)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type.equals(Pageable.class)) {
|
||||
pageableFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that every {@link Pageable} parameter of the given parameters carries an {@link Qualifier} annotation to
|
||||
* distinguish them from each other.
|
||||
*
|
||||
* @param parameterTypes
|
||||
* @param annotations
|
||||
*/
|
||||
private void assertQualifiersFor(Class<?>[] parameterTypes, Annotation[][] annotations) {
|
||||
|
||||
Set<String> values = new HashSet<String>();
|
||||
|
||||
for (int i = 0; i < annotations.length; i++) {
|
||||
|
||||
if (Pageable.class.equals(parameterTypes[i])) {
|
||||
|
||||
Qualifier qualifier = findAnnotation(annotations[i]);
|
||||
|
||||
if (null == qualifier) {
|
||||
throw new IllegalStateException(
|
||||
"Ambiguous Pageable arguments in handler method. If you use multiple parameters of type Pageable you need to qualify them with @Qualifier");
|
||||
}
|
||||
|
||||
if (values.contains(qualifier.value())) {
|
||||
throw new IllegalStateException("Values of the user Qualifiers must be unique!");
|
||||
}
|
||||
|
||||
values.add(qualifier.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Qualifier} annotation from the given array of {@link Annotation}s. Returns {@literal null} if the
|
||||
* array does not contain a {@link Qualifier} annotation.
|
||||
*
|
||||
* @param annotations
|
||||
* @return
|
||||
*/
|
||||
private Qualifier findAnnotation(Annotation[] annotations) {
|
||||
|
||||
for (Annotation annotation : annotations) {
|
||||
if (annotation instanceof Qualifier) {
|
||||
return (Qualifier) annotation;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link java.beans.PropertyEditor} to create {@link Sort} instances from textual representations. The implementation
|
||||
* interprets the string as a comma separated list where the first entry is the sort direction ( {@code asc},
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.web;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.data.domain.Sort.Direction;
|
||||
import org.springframework.data.web.SortDefault.SortDefaults;
|
||||
|
||||
/**
|
||||
* Annotation to set defaults when injecting a {@link org.springframework.data.domain.Pageable} into a controller
|
||||
* method. Instead of configuring {@link #sort()} and {@link #direction()} you can also use {@link SortDefault} or
|
||||
* {@link SortDefaults}.
|
||||
*
|
||||
* @since 1.6
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.PARAMETER)
|
||||
public @interface PageableDefault {
|
||||
|
||||
/**
|
||||
* Alias for {@link #size()}. Prefer to use the {@link #size()} method as it makes the annotation declaration more
|
||||
* expressive and you'll probably want to configure the {@link #page()} anyway.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
int value() default 10;
|
||||
|
||||
/**
|
||||
* The default-size the injected {@link org.springframework.data.domain.Pageable} should get if no corresponding
|
||||
* parameter defined in request (default is 10).
|
||||
*/
|
||||
int size() default 10;
|
||||
|
||||
/**
|
||||
* The default-pagenumber the injected {@link org.springframework.data.domain.Pageable} should get if no corresponding
|
||||
* parameter defined in request (default is 0).
|
||||
*/
|
||||
int page() default 0;
|
||||
|
||||
/**
|
||||
* The properties to sort by by default. If unset, no sorting will be applied at all.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String[] sort() default {};
|
||||
|
||||
/**
|
||||
* The direction to sort by. Defaults to {@link Direction#ASC}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
Direction direction() default Direction.ASC;
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.web;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
@@ -26,10 +27,13 @@ import org.springframework.data.domain.Sort.Direction;
|
||||
* Annotation to set defaults when injecting a {@link org.springframework.data.domain.Pageable} into a controller
|
||||
* method.
|
||||
*
|
||||
* @deprecated use {@link PageableDefault} instead.
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.PARAMETER)
|
||||
@Deprecated
|
||||
public @interface PageableDefaults {
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,295 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.web;
|
||||
|
||||
import java.beans.PropertyEditorSupport;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.servlet.ServletRequest;
|
||||
|
||||
import org.springframework.beans.PropertyValue;
|
||||
import org.springframework.beans.PropertyValues;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.domain.Sort.Direction;
|
||||
import org.springframework.validation.DataBinder;
|
||||
import org.springframework.web.bind.ServletRequestDataBinder;
|
||||
import org.springframework.web.bind.ServletRequestParameterPropertyValues;
|
||||
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
|
||||
/**
|
||||
* Extracts paging information from web requests and thus allows injecting {@link Pageable} instances into controller
|
||||
* methods. Request properties to be parsed can be configured. Default configuration uses request properties beginning
|
||||
* with {@link #DEFAULT_PREFIX}{@link #DEFAULT_SEPARATOR}.
|
||||
*
|
||||
* @since 1.6
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
public class PageableHandlerArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
private static final Pageable DEFAULT_PAGE_REQUEST = new PageRequest(0, 10);
|
||||
private static final String DEFAULT_PREFIX = "page";
|
||||
private static final String DEFAULT_SEPARATOR = ".";
|
||||
|
||||
private Pageable fallbackPagable = DEFAULT_PAGE_REQUEST;
|
||||
private String prefix = DEFAULT_PREFIX;
|
||||
private String separator = DEFAULT_SEPARATOR;
|
||||
|
||||
/**
|
||||
* Setter to configure a fallback instance of {@link Pageable} that is being used to back missing parameters. Defaults
|
||||
* to {@link #DEFAULT_PAGE_REQUEST}.
|
||||
*
|
||||
* @param fallbackPagable the fallbackPagable to set
|
||||
*/
|
||||
public void setFallbackPagable(Pageable fallbackPagable) {
|
||||
this.fallbackPagable = null == fallbackPagable ? DEFAULT_PAGE_REQUEST : fallbackPagable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter to configure the prefix of request parameters to be used to retrieve paging information. Defaults to
|
||||
* {@link #DEFAULT_PREFIX}.
|
||||
*
|
||||
* @param prefix the prefix to set
|
||||
*/
|
||||
public void setPrefix(String prefix) {
|
||||
this.prefix = null == prefix ? DEFAULT_PREFIX : prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter to configure the separator between prefix and actual property value. Defaults to {@link #DEFAULT_SEPARATOR}.
|
||||
*
|
||||
* @param separator the separator to set. Will default to {@link #DEFAULT_SEPEARATOR} if set to {@literal null}.
|
||||
*/
|
||||
public void setSeparator(String separator) {
|
||||
this.separator = null == separator ? DEFAULT_SEPARATOR : separator;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter(org.springframework.core.MethodParameter)
|
||||
*/
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return Pageable.class.equals(parameter.getParameterType());
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.web.method.support.HandlerMethodArgumentResolver#resolveArgument(org.springframework.core.MethodParameter, org.springframework.web.method.support.ModelAndViewContainer, org.springframework.web.context.request.NativeWebRequest, org.springframework.web.bind.support.WebDataBinderFactory)
|
||||
*/
|
||||
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer mavContainer,
|
||||
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
|
||||
|
||||
assertPageableUniqueness(methodParameter);
|
||||
|
||||
Pageable request = getDefaultFromAnnotationOrFallback(methodParameter);
|
||||
ServletRequest servletRequest = (ServletRequest) webRequest.getNativeRequest();
|
||||
PropertyValues propertyValues = new ServletRequestParameterPropertyValues(servletRequest,
|
||||
getPrefix(methodParameter), separator);
|
||||
|
||||
DataBinder binder = new ServletRequestDataBinder(request);
|
||||
|
||||
binder.initDirectFieldAccess();
|
||||
binder.registerCustomEditor(Sort.class, new SortPropertyEditor("sort.dir", propertyValues));
|
||||
binder.bind(propertyValues);
|
||||
|
||||
if (request.getPageNumber() > 0) {
|
||||
request = new PageRequest(request.getPageNumber() - 1, request.getPageSize(), request.getSort());
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
private Pageable getDefaultFromAnnotationOrFallback(MethodParameter methodParameter) {
|
||||
|
||||
// search for PageableDefaults annotation
|
||||
for (Annotation annotation : methodParameter.getParameterAnnotations()) {
|
||||
if (annotation instanceof PageableDefaults) {
|
||||
return getDefaultPageRequestFrom((PageableDefaults) annotation);
|
||||
}
|
||||
}
|
||||
|
||||
// Construct request with fallback request to ensure sensible
|
||||
// default values. Create fresh copy as Spring will manipulate the
|
||||
// instance under the covers
|
||||
return new PageRequest(fallbackPagable.getPageNumber(), fallbackPagable.getPageSize(), fallbackPagable.getSort());
|
||||
}
|
||||
|
||||
private static Pageable getDefaultPageRequestFrom(PageableDefaults defaults) {
|
||||
|
||||
// +1 is because we substract 1 later
|
||||
int defaultPageNumber = defaults.pageNumber() + 1;
|
||||
int defaultPageSize = defaults.value();
|
||||
|
||||
if (defaults.sort().length == 0) {
|
||||
return new PageRequest(defaultPageNumber, defaultPageSize);
|
||||
}
|
||||
|
||||
return new PageRequest(defaultPageNumber, defaultPageSize, defaults.sortDir(), defaults.sort());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the prefix to use to bind properties from. Will prepend a possible {@link Qualifier} if available or
|
||||
* return the configured prefix otherwise.
|
||||
*
|
||||
* @param parameter
|
||||
* @return
|
||||
*/
|
||||
private String getPrefix(MethodParameter parameter) {
|
||||
|
||||
for (Annotation annotation : parameter.getParameterAnnotations()) {
|
||||
if (annotation instanceof Qualifier) {
|
||||
return new StringBuilder(((Qualifier) annotation).value()).append("_").append(prefix).toString();
|
||||
}
|
||||
}
|
||||
|
||||
return prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts uniqueness of all {@link Pageable} parameters of the method of the given {@link MethodParameter}.
|
||||
*
|
||||
* @param parameter
|
||||
*/
|
||||
private void assertPageableUniqueness(MethodParameter parameter) {
|
||||
|
||||
Method method = parameter.getMethod();
|
||||
|
||||
if (containsMoreThanOnePageableParameter(method)) {
|
||||
Annotation[][] annotations = method.getParameterAnnotations();
|
||||
assertQualifiersFor(method.getParameterTypes(), annotations);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given {@link Method} has more than one {@link Pageable} parameter.
|
||||
*
|
||||
* @param method
|
||||
* @return
|
||||
*/
|
||||
private boolean containsMoreThanOnePageableParameter(Method method) {
|
||||
|
||||
boolean pageableFound = false;
|
||||
|
||||
for (Class<?> type : method.getParameterTypes()) {
|
||||
|
||||
if (pageableFound && type.equals(Pageable.class)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type.equals(Pageable.class)) {
|
||||
pageableFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that every {@link Pageable} parameter of the given parameters carries an {@link Qualifier} annotation to
|
||||
* distinguish them from each other.
|
||||
*
|
||||
* @param parameterTypes
|
||||
* @param annotations
|
||||
*/
|
||||
private void assertQualifiersFor(Class<?>[] parameterTypes, Annotation[][] annotations) {
|
||||
|
||||
Set<String> values = new HashSet<String>();
|
||||
|
||||
for (int i = 0; i < annotations.length; i++) {
|
||||
|
||||
if (Pageable.class.equals(parameterTypes[i])) {
|
||||
|
||||
Qualifier qualifier = findAnnotation(annotations[i]);
|
||||
|
||||
if (null == qualifier) {
|
||||
throw new IllegalStateException(
|
||||
"Ambiguous Pageable arguments in handler method. If you use multiple parameters of type Pageable you need to qualify them with @Qualifier");
|
||||
}
|
||||
|
||||
if (values.contains(qualifier.value())) {
|
||||
throw new IllegalStateException("Values of the user Qualifiers must be unique!");
|
||||
}
|
||||
|
||||
values.add(qualifier.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Qualifier} annotation from the given array of {@link Annotation}s. Returns {@literal null} if the
|
||||
* array does not contain a {@link Qualifier} annotation.
|
||||
*
|
||||
* @param annotations
|
||||
* @return
|
||||
*/
|
||||
private Qualifier findAnnotation(Annotation[] annotations) {
|
||||
|
||||
for (Annotation annotation : annotations) {
|
||||
if (annotation instanceof Qualifier) {
|
||||
return (Qualifier) annotation;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link java.beans.PropertyEditor} to create {@link Sort} instances from textual representations. The implementation
|
||||
* interprets the string as a comma separated list where the first entry is the sort direction ( {@code asc},
|
||||
* {@code desc}) followed by the properties to sort by.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
private static class SortPropertyEditor extends PropertyEditorSupport {
|
||||
|
||||
private final String orderProperty;
|
||||
private final PropertyValues values;
|
||||
|
||||
/**
|
||||
* Creates a new {@link SortPropertyEditor}.
|
||||
*
|
||||
* @param orderProperty
|
||||
* @param values
|
||||
*/
|
||||
public SortPropertyEditor(String orderProperty, PropertyValues values) {
|
||||
|
||||
this.orderProperty = orderProperty;
|
||||
this.values = values;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.beans.PropertyEditorSupport#setAsText(java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public void setAsText(String text) {
|
||||
|
||||
PropertyValue rawOrder = values.getPropertyValue(orderProperty);
|
||||
Direction order = null == rawOrder ? Direction.ASC : Direction.fromString(rawOrder.getValue().toString());
|
||||
|
||||
setValue(new Sort(order, text));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
/*
|
||||
* Copyright 2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.web;
|
||||
|
||||
import static org.springframework.data.web.SpringDataAnnotationUtils.*;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.hateoas.mvc.UriComponentsContributor;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* Extracts paging information from web requests and thus allows injecting {@link Pageable} instances into controller
|
||||
* methods. Request properties to be parsed can be configured. Default configuration uses request properties beginning
|
||||
* with {@link #PAGE_PROPERTY}{@link #DEFAULT_SEPARATOR}.
|
||||
*
|
||||
* @since 1.6
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public class PageableHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver, UriComponentsContributor {
|
||||
|
||||
/**
|
||||
* A {@link PageableHandlerMethodArgumentResolver} preconfigured to the setup of {@link PageableArgumentResolver}. Use
|
||||
* that if you need to stick to the former request parameters an 1-indexed behavior. This will be removed in the next
|
||||
* major version (1.7). So consider migrating to the new way of exposing request parameters.
|
||||
*/
|
||||
@Deprecated
|
||||
public static final PageableHandlerMethodArgumentResolver LEGACY;
|
||||
|
||||
static {
|
||||
LEGACY = new PageableHandlerMethodArgumentResolver();
|
||||
LEGACY.pageProperty = "page.page";
|
||||
LEGACY.sizeProperty = "page.size";
|
||||
LEGACY.fallbackPageable = new PageRequest(1, 10);
|
||||
LEGACY.oneIndexedParameters = true;
|
||||
LEGACY.sortResolver.setLegacyMode(true);
|
||||
LEGACY.sortResolver.setSortParameter("page.sort");
|
||||
}
|
||||
|
||||
private static final Pageable DEFAULT_PAGE_REQUEST = new PageRequest(0, 20);
|
||||
private static final String DEFAULT_PAGE_PROPERTY = "page";
|
||||
private static final String DEFAULT_SIZE_PROPERTY = "size";
|
||||
private static final String DEFAULT_PREFIX = "";
|
||||
private static final String DEFAULT_QUALIFIER_SEPARATOR = "_";
|
||||
|
||||
private Pageable fallbackPageable = DEFAULT_PAGE_REQUEST;
|
||||
private SortHandlerMethodArgumentResolver sortResolver = new SortHandlerMethodArgumentResolver();
|
||||
private String pageProperty = DEFAULT_PAGE_PROPERTY;
|
||||
private String sizeProperty = DEFAULT_SIZE_PROPERTY;
|
||||
private String prefix = DEFAULT_PREFIX;
|
||||
private String qualifierSeparator = DEFAULT_QUALIFIER_SEPARATOR;
|
||||
private boolean oneIndexedParameters = false;
|
||||
|
||||
/**
|
||||
* Configures the {@link Pageable} to be used as fallback in case no {@link PageableDefault} or
|
||||
* {@link PageableDefaults} (the latter only supported in legacy mode) can be found at the method parameter to be
|
||||
* resolved.
|
||||
* <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 Pageable} data can be found in the request.
|
||||
*
|
||||
* @param fallbackPageable the {@link Pageable} to be used as general fallback.
|
||||
*/
|
||||
public void setFallbackPageable(Pageable fallbackPageable) {
|
||||
this.fallbackPageable = fallbackPageable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the parameter name to be used to find the page number in the request. Defaults to {@code page}.
|
||||
*
|
||||
* @param pageProperty the parameter name to be used, must not be {@literal null} or empty.
|
||||
*/
|
||||
public void setPageProperty(String pageProperty) {
|
||||
|
||||
Assert.hasText(pageProperty, "Page parameter name must not be null or empty!");
|
||||
this.pageProperty = pageProperty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the parameter name to be used to find the page size in the request. Defaults to {@code size}.
|
||||
*
|
||||
* @param sizeProperty the parameter name to be used, must not be {@literal null} or empty.
|
||||
*/
|
||||
public void setSizeProperty(String sizeProperty) {
|
||||
|
||||
Assert.hasText(sizeProperty, "Size parameter name must not be null or empty!");
|
||||
this.sizeProperty = sizeProperty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures a general prefix to be prepended to the page number and page size parameters. Useful to namespace the
|
||||
* property names used in case they are clashing with ones used by your application. By default, no prefix is used.
|
||||
*
|
||||
* @param prefix the prefix to be used or {@literal null} to reset to the default.
|
||||
*/
|
||||
public void setPrefix(String prefix) {
|
||||
this.prefix = prefix == null ? DEFAULT_PREFIX : prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* The separator to be used between the qualifier and the actual page number and size properties. Defaults to
|
||||
* {@code _}. So a qualifier of {@code foo} will result in a page number parameter of {@code foo_page}.
|
||||
*
|
||||
* @param qualifierSeparator the qualifierSeparator to be used or {@literal null} to reset to the default.
|
||||
*/
|
||||
public void setQualifierSeparator(String qualifierSeparator) {
|
||||
this.qualifierSeparator = qualifierSeparator == null ? DEFAULT_QUALIFIER_SEPARATOR : qualifierSeparator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the {@link SortHandlerMethodArgumentResolver} to be used with the
|
||||
* {@link PageableHandlerMethodArgumentResolver}.
|
||||
*
|
||||
* @param sortResolver the {@link SortHandlerMethodArgumentResolver} to be used ot {@literal null} to reset it to the
|
||||
* default one.
|
||||
*/
|
||||
public void setSortResolver(SortHandlerMethodArgumentResolver sortResolver) {
|
||||
this.sortResolver = sortResolver == null ? new SortHandlerMethodArgumentResolver() : sortResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures whether to expose and assume 1-based page number indexes in the request parameters. Defaults to
|
||||
* {@literal false}, meaning a page number of 0 in the request equals the first page. If this is set to
|
||||
* {@literal true}, a page number of 1 in the request will be considered the first page.
|
||||
*
|
||||
* @param oneIndexedParameters the oneIndexedParameters to set
|
||||
*/
|
||||
public void setOneIndexedParameters(boolean oneIndexedParameters) {
|
||||
this.oneIndexedParameters = oneIndexedParameters;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter(org.springframework.core.MethodParameter)
|
||||
*/
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return Pageable.class.equals(parameter.getParameterType());
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.hateoas.mvc.UriComponentsContributor#enhance(org.springframework.web.util.UriComponentsBuilder, org.springframework.core.MethodParameter, java.lang.Object)
|
||||
*/
|
||||
public void enhance(UriComponentsBuilder builder, MethodParameter parameter, Object value) {
|
||||
|
||||
if (!(value instanceof Pageable)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Pageable pageable = (Pageable) value;
|
||||
|
||||
String pagePropertyName = getParameterNameToUse(pageProperty, parameter);
|
||||
String propertyToLookup = getParameterNameToUse(sizeProperty, parameter);
|
||||
|
||||
int pageNumber = pageable.getPageNumber();
|
||||
|
||||
builder.queryParam(pagePropertyName, oneIndexedParameters ? pageNumber + 1 : pageNumber);
|
||||
builder.queryParam(propertyToLookup, pageable.getPageSize());
|
||||
|
||||
sortResolver.enhance(builder, parameter, pageable.getSort());
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.web.method.support.HandlerMethodArgumentResolver#resolveArgument(org.springframework.core.MethodParameter, org.springframework.web.method.support.ModelAndViewContainer, org.springframework.web.context.request.NativeWebRequest, org.springframework.web.bind.support.WebDataBinderFactory)
|
||||
*/
|
||||
public Pageable resolveArgument(MethodParameter methodParameter, ModelAndViewContainer mavContainer,
|
||||
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
|
||||
|
||||
assertPageableUniqueness(methodParameter);
|
||||
|
||||
Pageable defaultOrFallback = getDefaultFromAnnotationOrFallback(methodParameter);
|
||||
|
||||
String pageString = webRequest.getParameter(getParameterNameToUse(pageProperty, methodParameter));
|
||||
String pageSizeString = webRequest.getParameter(getParameterNameToUse(sizeProperty, methodParameter));
|
||||
|
||||
int page = StringUtils.hasText(pageString) ? Integer.parseInt(pageString) - (oneIndexedParameters ? 1 : 0)
|
||||
: defaultOrFallback.getPageNumber();
|
||||
int pageSize = StringUtils.hasText(pageSizeString) ? Integer.parseInt(pageSizeString) : defaultOrFallback
|
||||
.getPageSize();
|
||||
|
||||
Sort sort = sortResolver.resolveArgument(methodParameter, mavContainer, webRequest, binderFactory);
|
||||
return new PageRequest(page, pageSize, sort == null ? defaultOrFallback.getSort() : sort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the request parameter to find the {@link Pageable} information in. Inspects the given
|
||||
* {@link MethodParameter} for {@link Qualifier} present and prefixes the given source parameter name with it.
|
||||
*
|
||||
* @param source the basic parameter name.
|
||||
* @param parameter the {@link MethodParameter} potentially qualified.
|
||||
* @return
|
||||
*/
|
||||
private String getParameterNameToUse(String source, MethodParameter parameter) {
|
||||
|
||||
StringBuilder builder = new StringBuilder(prefix);
|
||||
|
||||
if (parameter.hasParameterAnnotation(Qualifier.class)) {
|
||||
builder.append(parameter.getParameterAnnotation(Qualifier.class).value());
|
||||
builder.append(qualifierSeparator);
|
||||
}
|
||||
|
||||
return builder.append(source).toString();
|
||||
}
|
||||
|
||||
private Pageable getDefaultFromAnnotationOrFallback(MethodParameter methodParameter) {
|
||||
|
||||
if (sortResolver.legacyMode && methodParameter.hasParameterAnnotation(PageableDefaults.class)) {
|
||||
Pageable pageable = PageableArgumentResolver.getDefaultPageRequestFrom(methodParameter
|
||||
.getParameterAnnotation(PageableDefaults.class));
|
||||
return new PageRequest(pageable.getPageNumber() - 1, pageable.getPageSize(), pageable.getSort());
|
||||
}
|
||||
|
||||
if (methodParameter.hasParameterAnnotation(PageableDefault.class)) {
|
||||
return getDefaultPageRequestFrom(methodParameter.getParameterAnnotation(PageableDefault.class));
|
||||
}
|
||||
|
||||
return fallbackPageable;
|
||||
}
|
||||
|
||||
private static Pageable getDefaultPageRequestFrom(PageableDefault defaults) {
|
||||
|
||||
int defaultPageNumber = defaults.page();
|
||||
int defaultPageSize = getSpecificPropertyOrDefaultFromValue(defaults, "size");
|
||||
|
||||
if (defaults.sort().length == 0) {
|
||||
return new PageRequest(defaultPageNumber, defaultPageSize);
|
||||
}
|
||||
|
||||
return new PageRequest(defaultPageNumber, defaultPageSize, defaults.direction(), defaults.sort());
|
||||
}
|
||||
}
|
||||
78
src/main/java/org/springframework/data/web/SortDefault.java
Normal file
78
src/main/java/org/springframework/data/web/SortDefault.java
Normal file
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.web;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.domain.Sort.Direction;
|
||||
|
||||
/**
|
||||
* Annotation to define the default {@link Sort} options to be used when injecting a {@link Sort} instance into a
|
||||
* controller handler method.
|
||||
*
|
||||
* @since 1.6
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.PARAMETER)
|
||||
public @interface SortDefault {
|
||||
|
||||
/**
|
||||
* Alias for {@link #sort()} to make a declaration configuring fields only more concise.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String[] value() default {};
|
||||
|
||||
/**
|
||||
* The properties to sort by by default. If unset, no sorting will be applied at all.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String[] sort() default {};
|
||||
|
||||
/**
|
||||
* The direction to sort by. Defaults to {@link Direction#ASC}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
Direction direction() default Direction.ASC;
|
||||
|
||||
/**
|
||||
* Wrapper annotation to allow declaring multiple {@link SortDefault} annotations on a method parameter.
|
||||
*
|
||||
* @since 1.6
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.PARAMETER)
|
||||
public @interface SortDefaults {
|
||||
|
||||
/**
|
||||
* The individual {@link SortDefault} declarations to be sorted by.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
SortDefault[] value();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
/*
|
||||
* Copyright 2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.web;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.domain.Sort.Direction;
|
||||
import org.springframework.data.domain.Sort.Order;
|
||||
import org.springframework.data.web.SortDefault.SortDefaults;
|
||||
import org.springframework.hateoas.mvc.UriComponentsContributor;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
import org.springframework.web.context.request.WebRequest;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* {@link HandlerMethodArgumentResolver} to automatically create {@link Sort} instances from request parameters or
|
||||
* {@link SortDefault} annotations.
|
||||
*
|
||||
* @since 1.6
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
public class SortHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver, UriComponentsContributor {
|
||||
|
||||
private static final String DEFAULT_PARAMETER = "sort";
|
||||
private static final String DEFAULT_PROPERTY_DELIMITER = ",";
|
||||
private static final String DEFAULT_QUALIFIER_DELIMITER = "_";
|
||||
|
||||
private static final String SORT_DEFAULTS_NAME = SortDefaults.class.getSimpleName();
|
||||
private static final String SORT_DEFAULT_NAME = SortDefault.class.getSimpleName();
|
||||
|
||||
private String sortParameter = DEFAULT_PARAMETER;
|
||||
private String propertyDelimiter = DEFAULT_PROPERTY_DELIMITER;
|
||||
private String qualifierDelimiter = DEFAULT_QUALIFIER_DELIMITER;
|
||||
|
||||
boolean legacyMode = false;
|
||||
|
||||
/**
|
||||
* Enables legacy mode parsing of the sorting parameter from the incoming request. Uses the sort property configured
|
||||
* to lookup the fields to sort on and {@code $sortParameter.dir} for the direction.
|
||||
*
|
||||
* @param legacyMode whether to enable the legacy mode or not.
|
||||
*/
|
||||
@Deprecated
|
||||
void setLegacyMode(boolean legacyMode) {
|
||||
this.legacyMode = legacyMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the request parameter to lookup sort information from. Defaults to {@code sort}.
|
||||
*
|
||||
* @param sortParameter must not be {@literal null} or empty.
|
||||
*/
|
||||
public void setSortParameter(String parameter) {
|
||||
|
||||
Assert.hasText(parameter);
|
||||
this.sortParameter = parameter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the delimiter used to separate property references and the direction to be sorted by. Defaults to
|
||||
* {@code}, which means sort values look like this: {@code firstname,lastname,asc}.
|
||||
*
|
||||
* @param propertyDelimiter must not be {@literal null} or empty.
|
||||
*/
|
||||
public void setPropertyDelimiter(String propertyDelimiter) {
|
||||
|
||||
Assert.hasText(propertyDelimiter, "Property delimiter must not be null or empty!");
|
||||
this.propertyDelimiter = propertyDelimiter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the delimiter used to separate the qualifier from the sort parameter. Defaults to {@code _}, so a
|
||||
* qualified sort property would look like {@code qualifier_sort}.
|
||||
*
|
||||
* @param qualifierDelimiter the qualifier delimiter to be used or {@literal null} to reset to the default.
|
||||
*/
|
||||
public void setQualifierDelimiter(String qualifierDelimiter) {
|
||||
this.qualifierDelimiter = qualifierDelimiter == null ? DEFAULT_QUALIFIER_DELIMITER : qualifierDelimiter;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter(org.springframework.core.MethodParameter)
|
||||
*/
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return Sort.class.equals(parameter.getParameterType());
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.hateoas.mvc.UriComponentsContributor#enhance(org.springframework.web.util.UriComponentsBuilder, org.springframework.core.MethodParameter, java.lang.Object)
|
||||
*/
|
||||
public void enhance(UriComponentsBuilder builder, MethodParameter parameter, Object value) {
|
||||
|
||||
if (!(value instanceof Sort)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Sort sort = (Sort) value;
|
||||
|
||||
if (legacyMode) {
|
||||
|
||||
List<String> expressions = legacyFoldExpressions(sort);
|
||||
Assert.isTrue(expressions.size() == 2,
|
||||
String.format("Expected 2 sort expressions (fields, direction) but got %d!", expressions.size()));
|
||||
builder.queryParam(getSortParameter(parameter), expressions.get(0));
|
||||
builder.queryParam(getLegacyDirectionParameter(parameter), expressions.get(1));
|
||||
|
||||
} else {
|
||||
|
||||
for (String expression : foldIntoExpressions(sort)) {
|
||||
builder.queryParam(getSortParameter(parameter), expression);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.web.method.support.HandlerMethodArgumentResolver#resolveArgument(org.springframework.core.MethodParameter, org.springframework.web.method.support.ModelAndViewContainer, org.springframework.web.context.request.NativeWebRequest, org.springframework.web.bind.support.WebDataBinderFactory)
|
||||
*/
|
||||
public Sort resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
|
||||
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
|
||||
|
||||
String[] directionParameter = webRequest.getParameterValues(getSortParameter(parameter));
|
||||
|
||||
if (directionParameter != null && directionParameter.length != 0) {
|
||||
return legacyMode ? parseLegacyParameterIntoSort(webRequest, parameter) : parseParameterIntoSort(
|
||||
directionParameter, propertyDelimiter);
|
||||
} else {
|
||||
return getDefaults(parameter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the default {@link Sort} to be used from the given {@link MethodParameter}. Rejects the parameter if both an
|
||||
* {@link SortDefaults} and {@link SortDefault} annotation is found as we cannot build a reliable {@link Sort}
|
||||
* instance then (property ordering).
|
||||
*
|
||||
* @param parameter will never be {@literal null}.
|
||||
* @return the default {@link Sort} instance derived from the parameter annotations or {@literal null}.
|
||||
*/
|
||||
private Sort getDefaults(MethodParameter parameter) {
|
||||
|
||||
SortDefaults annotatedDefaults = parameter.getParameterAnnotation(SortDefaults.class);
|
||||
Sort sort = null;
|
||||
|
||||
if (annotatedDefaults != null) {
|
||||
for (SortDefault annotatedDefault : annotatedDefaults.value()) {
|
||||
sort = appendOrCreateSortTo(annotatedDefault, sort);
|
||||
}
|
||||
}
|
||||
|
||||
SortDefault annotatedDefault = parameter.getParameterAnnotation(SortDefault.class);
|
||||
|
||||
if (annotatedDefault == null) {
|
||||
return sort;
|
||||
}
|
||||
|
||||
if (sort != null && annotatedDefault != null) {
|
||||
throw new IllegalArgumentException(String.format(
|
||||
"Cannot use both @%s and @%s on parameter %s! Move %s into %s to define sorting order!", SORT_DEFAULTS_NAME,
|
||||
SORT_DEFAULT_NAME, parameter.toString(), SORT_DEFAULT_NAME, SORT_DEFAULTS_NAME));
|
||||
}
|
||||
|
||||
return appendOrCreateSortTo(annotatedDefault, sort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link Sort} instance from the given {@link SortDefault} or appends it to the given {@link Sort}
|
||||
* instance if it's not {@literal null}.
|
||||
*
|
||||
* @param sortDefault
|
||||
* @param sortOrNull
|
||||
* @return
|
||||
*/
|
||||
private Sort appendOrCreateSortTo(SortDefault sortDefault, Sort sortOrNull) {
|
||||
|
||||
String[] fields = SpringDataAnnotationUtils.getSpecificPropertyOrDefaultFromValue(sortDefault, "sort");
|
||||
|
||||
if (fields.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Sort sort = new Sort(sortDefault.direction(), fields);
|
||||
return sortOrNull == null ? sort : sortOrNull.and(sort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sort parameter to be looked up from the request. Potentially applies qualifiers to it.
|
||||
*
|
||||
* @param parameter will never be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
private String getSortParameter(MethodParameter parameter) {
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
if (parameter.hasParameterAnnotation(Qualifier.class)) {
|
||||
builder.append(parameter.getParameterAnnotation(Qualifier.class).value()).append(qualifierDelimiter);
|
||||
}
|
||||
|
||||
return builder.append(sortParameter).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link Sort} instance from the given request expecting the {@link Direction} being encoded in a parameter
|
||||
* with an appended {@code .dir}.
|
||||
*
|
||||
* @param request must not be {@literal null}.
|
||||
* @param parameter must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
private Sort parseLegacyParameterIntoSort(WebRequest request, MethodParameter parameter) {
|
||||
|
||||
String property = getSortParameter(parameter);
|
||||
String fields = request.getParameter(property);
|
||||
String directions = request.getParameter(getLegacyDirectionParameter(parameter));
|
||||
|
||||
return new Sort(Direction.fromStringOrNull(directions), fields.split(","));
|
||||
}
|
||||
|
||||
private String getLegacyDirectionParameter(MethodParameter parameter) {
|
||||
return getSortParameter(parameter) + ".dir";
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given sort expressions into a {@link Sort} instance. The implementation expects the sources to be a
|
||||
* concatenation of Strings using the given delimiter. If the last element can be parsed into a {@link Direction} it's
|
||||
* considered a {@link Direction} and a simple property otherwise.
|
||||
*
|
||||
* @param source will never be {@literal null}.
|
||||
* @param delimiter the delimiter to be used to split up the source elements, will never be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
Sort parseParameterIntoSort(String[] source, String delimiter) {
|
||||
|
||||
List<Order> allOrders = new ArrayList<Sort.Order>();
|
||||
|
||||
for (String part : source) {
|
||||
|
||||
if (part == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String[] elements = part.split(delimiter);
|
||||
Direction direction = Direction.fromStringOrNull(elements[elements.length - 1]);
|
||||
|
||||
for (int i = 0; i < elements.length; i++) {
|
||||
|
||||
if (i == elements.length - 1 && direction != null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
allOrders.add(new Order(direction, elements[i]));
|
||||
}
|
||||
}
|
||||
|
||||
return allOrders.isEmpty() ? null : new Sort(allOrders);
|
||||
}
|
||||
|
||||
/**
|
||||
* Folds the given {@link Sort} instance into a {@link List} of sort expressions, accumulating {@link Order} instances
|
||||
* of the same direction into a single expression if they are in order.
|
||||
*
|
||||
* @param sort must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
private List<String> foldIntoExpressions(Sort sort) {
|
||||
|
||||
List<String> expressions = new ArrayList<String>();
|
||||
ExpressionBuilder builder = null;
|
||||
|
||||
for (Order order : sort) {
|
||||
|
||||
Direction direction = order.getDirection();
|
||||
|
||||
if (builder == null) {
|
||||
builder = new ExpressionBuilder(direction);
|
||||
} else if (!builder.hasSameDirectionAs(order)) {
|
||||
builder.dumpExpressionIfPresentInto(expressions);
|
||||
builder = new ExpressionBuilder(direction);
|
||||
}
|
||||
|
||||
builder.add(order.getProperty());
|
||||
}
|
||||
|
||||
return builder.dumpExpressionIfPresentInto(expressions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Folds the given {@link Sort} instance into two expressions. The first being the property list, the second being the
|
||||
* direction.
|
||||
*
|
||||
* @throws IllegalArgumentException if a {@link Sort} with multiple {@link Direction}s has been handed in.
|
||||
* @param sort must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
private List<String> legacyFoldExpressions(Sort sort) {
|
||||
|
||||
List<String> expressions = new ArrayList<String>();
|
||||
ExpressionBuilder builder = null;
|
||||
|
||||
for (Order order : sort) {
|
||||
|
||||
Direction direction = order.getDirection();
|
||||
|
||||
if (builder == null) {
|
||||
builder = new ExpressionBuilder(direction);
|
||||
} else if (!builder.hasSameDirectionAs(order)) {
|
||||
throw new IllegalArgumentException(String.format(
|
||||
"%s in legacy configuration only supports a single direction to sort by!", getClass().getSimpleName()));
|
||||
}
|
||||
|
||||
builder.add(order.getProperty());
|
||||
}
|
||||
|
||||
return builder.dumpExpressionIfPresentInto(expressions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to easily build request parameter expressions for {@link Sort} instances.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
class ExpressionBuilder {
|
||||
|
||||
private final List<String> elements = new ArrayList<String>();
|
||||
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;
|
||||
}
|
||||
|
||||
if (legacyMode) {
|
||||
expressions.add(StringUtils.collectionToDelimitedString(elements, propertyDelimiter));
|
||||
expressions.add(direction.name().toLowerCase());
|
||||
} else {
|
||||
elements.add(direction.name().toLowerCase());
|
||||
expressions.add(StringUtils.collectionToDelimitedString(elements, propertyDelimiter));
|
||||
}
|
||||
|
||||
return expressions;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright 2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.web;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Helper class to ease sharing code between legacy {@link PageableArgumentResolver} and
|
||||
* {@link PageableHandlerMethodArgumentResolver}.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
class SpringDataAnnotationUtils {
|
||||
|
||||
/**
|
||||
* Asserts uniqueness of all {@link Pageable} parameters of the method of the given {@link MethodParameter}.
|
||||
*
|
||||
* @param parameter must not be {@literal null}.
|
||||
*/
|
||||
public static void assertPageableUniqueness(MethodParameter parameter) {
|
||||
|
||||
Method method = parameter.getMethod();
|
||||
|
||||
if (containsMoreThanOnePageableParameter(method)) {
|
||||
Annotation[][] annotations = method.getParameterAnnotations();
|
||||
assertQualifiersFor(method.getParameterTypes(), annotations);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given {@link Method} has more than one {@link Pageable} parameter.
|
||||
*
|
||||
* @param method must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
private static boolean containsMoreThanOnePageableParameter(Method method) {
|
||||
|
||||
boolean pageableFound = false;
|
||||
|
||||
for (Class<?> type : method.getParameterTypes()) {
|
||||
|
||||
if (pageableFound && type.equals(Pageable.class)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type.equals(Pageable.class)) {
|
||||
pageableFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the given specific property of the given annotation. If the value of that property is the
|
||||
* properties default, we fall back to the value of the {@code value} attribute.
|
||||
*
|
||||
* @param annotation must not be {@literal null}.
|
||||
* @param property must not be {@literal null} or empty.
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T getSpecificPropertyOrDefaultFromValue(Annotation annotation, String property) {
|
||||
|
||||
Object propertyDefaultValue = AnnotationUtils.getDefaultValue(annotation, property);
|
||||
Object propertyValue = AnnotationUtils.getValue(annotation, property);
|
||||
|
||||
return (T) (ObjectUtils.nullSafeEquals(propertyDefaultValue, propertyValue) ? AnnotationUtils.getValue(annotation)
|
||||
: propertyValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that every {@link Pageable} parameter of the given parameters carries an {@link Qualifier} annotation to
|
||||
* distinguish them from each other.
|
||||
*
|
||||
* @param parameterTypes must not be {@literal null}.
|
||||
* @param annotations must not be {@literal null}.
|
||||
*/
|
||||
public static void assertQualifiersFor(Class<?>[] parameterTypes, Annotation[][] annotations) {
|
||||
|
||||
Set<String> values = new HashSet<String>();
|
||||
|
||||
for (int i = 0; i < annotations.length; i++) {
|
||||
|
||||
if (Pageable.class.equals(parameterTypes[i])) {
|
||||
|
||||
Qualifier qualifier = findAnnotation(annotations[i]);
|
||||
|
||||
if (null == qualifier) {
|
||||
throw new IllegalStateException(
|
||||
"Ambiguous Pageable arguments in handler method. If you use multiple parameters of type Pageable you need to qualify them with @Qualifier");
|
||||
}
|
||||
|
||||
if (values.contains(qualifier.value())) {
|
||||
throw new IllegalStateException("Values of the user Qualifiers must be unique!");
|
||||
}
|
||||
|
||||
values.add(qualifier.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Qualifier} annotation from the given array of {@link Annotation}s. Returns {@literal null} if the
|
||||
* array does not contain a {@link Qualifier} annotation.
|
||||
*
|
||||
* @param annotations must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public static Qualifier findAnnotation(Annotation[] annotations) {
|
||||
|
||||
for (Annotation annotation : annotations) {
|
||||
if (annotation instanceof Qualifier) {
|
||||
return (Qualifier) annotation;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user