Refine @PageableDefault and @SortDefault annotations.

We now use MergedAnnotations to evaluate annotation attributes and to use aliasing across attributes. Also, SortDefault is not repeatable.

Closes #2657
This commit is contained in:
Mark Paluch
2022-07-27 09:45:28 +02:00
parent 3a6b04dfe1
commit 7e1712a5de
10 changed files with 71 additions and 68 deletions

View File

@@ -21,6 +21,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.web.SortDefault.SortDefaults;
@@ -31,6 +32,7 @@ import org.springframework.data.web.SortDefault.SortDefaults;
*
* @since 1.6
* @author Oliver Gierke
* @author Mark Paluch
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@@ -43,22 +45,24 @@ public @interface PageableDefault {
*
* @return
*/
@AliasFor("size")
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).
* parameter defined in request (default is {@code 10}).
*/
@AliasFor("value")
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).
* The default page number the injected {@link org.springframework.data.domain.Pageable} should use if no
* corresponding parameter defined in request (default is {@code 0}).
*/
int page() default 0;
/**
* The properties to sort by by default. If unset, no sorting will be applied at all.
* The properties to sort by default. If unset, no sorting will be applied at all.
*
* @return
*/

View File

@@ -22,6 +22,8 @@ import java.util.Optional;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
@@ -238,30 +240,34 @@ public abstract class PageableHandlerMethodArgumentResolverSupport {
private Pageable getDefaultFromAnnotationOrFallback(MethodParameter methodParameter) {
PageableDefault defaults = methodParameter.getParameterAnnotation(PageableDefault.class);
MergedAnnotation<PageableDefault> defaults = MergedAnnotations.from(methodParameter.getParameterAnnotations())
.get(PageableDefault.class);
if (defaults != null) {
if (defaults.isPresent()) {
return getDefaultPageRequestFrom(methodParameter, defaults);
}
return fallbackPageable;
}
private static Pageable getDefaultPageRequestFrom(MethodParameter parameter, PageableDefault defaults) {
private static Pageable getDefaultPageRequestFrom(MethodParameter parameter,
MergedAnnotation<PageableDefault> defaults) {
int defaultPageNumber = defaults.page();
Integer defaultPageSize = getSpecificPropertyOrDefaultFromValue(defaults, "size");
int defaultPageNumber = defaults.getInt("page");
int defaultPageSize = defaults.getInt("size");
if (defaultPageSize < 1) {
Method annotatedMethod = parameter.getMethod();
throw new IllegalStateException(String.format(INVALID_DEFAULT_PAGE_SIZE, annotatedMethod));
}
if (defaults.sort().length == 0) {
String[] sort = defaults.getStringArray("sort");
if (sort.length == 0) {
return PageRequest.of(defaultPageNumber, defaultPageSize);
}
return PageRequest.of(defaultPageNumber, defaultPageSize, defaults.direction(), defaults.sort());
return PageRequest.of(defaultPageNumber, defaultPageSize, defaults.getEnum("direction", Sort.Direction.class),
sort);
}
/**
@@ -287,5 +293,4 @@ public abstract class PageableHandlerMethodArgumentResolverSupport {
}
}
}

View File

@@ -17,10 +17,12 @@ package org.springframework.data.web;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
@@ -30,10 +32,12 @@ import org.springframework.data.domain.Sort.Direction;
*
* @since 1.6
* @author Oliver Gierke
* @author Mark Palich
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@Repeatable(SortDefault.SortDefaults.class)
public @interface SortDefault {
/**
@@ -41,13 +45,15 @@ public @interface SortDefault {
*
* @return
*/
@AliasFor("sort")
String[] value() default {};
/**
* The properties to sort by by default. If unset, no sorting will be applied at all.
* The properties to sort by default. If unset, no sorting will be applied at all.
*
* @return
*/
@AliasFor("value")
String[] sort() default {};
/**

View File

@@ -23,6 +23,9 @@ import java.util.Optional;
import java.util.function.Consumer;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.RepeatableContainers;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.domain.Sort.Order;
@@ -121,31 +124,26 @@ public abstract class SortHandlerMethodArgumentResolverSupport {
*/
protected Sort getDefaultFromAnnotationOrFallback(MethodParameter parameter) {
SortDefaults annotatedDefaults = parameter.getParameterAnnotation(SortDefaults.class);
SortDefault annotatedDefault = parameter.getParameterAnnotation(SortDefault.class);
MergedAnnotations mergedAnnotations = MergedAnnotations.from(parameter, parameter.getParameterAnnotations(),
RepeatableContainers.of(SortDefault.class, SortDefaults.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));
List<MergedAnnotation<SortDefault>> annotations = mergedAnnotations.stream(SortDefault.class).toList();
if (annotations.isEmpty()) {
return fallbackSort;
}
if (annotatedDefault != null) {
return appendOrCreateSortTo(annotatedDefault, Sort.unsorted());
if (annotations.size() == 1) {
return appendOrCreateSortTo(annotations.get(0), Sort.unsorted());
}
if (annotatedDefaults != null) {
Sort sort = Sort.unsorted();
Sort sort = Sort.unsorted();
for (SortDefault currentAnnotatedDefault : annotatedDefaults.value()) {
sort = appendOrCreateSortTo(currentAnnotatedDefault, sort);
}
return sort;
for (MergedAnnotation<SortDefault> currentAnnotatedDefault : annotations) {
sort = appendOrCreateSortTo(currentAnnotatedDefault, sort);
}
return fallbackSort;
return sort;
}
/**
@@ -156,9 +154,9 @@ public abstract class SortHandlerMethodArgumentResolverSupport {
* @param sortOrNull
* @return
*/
private Sort appendOrCreateSortTo(SortDefault sortDefault, Sort sortOrNull) {
private Sort appendOrCreateSortTo(MergedAnnotation<SortDefault> sortDefault, Sort sortOrNull) {
String[] fields = SpringDataAnnotationUtils.getSpecificPropertyOrDefaultFromValue(sortDefault, "sort");
String[] fields = sortDefault.getStringArray("sort");
if (fields.length == 0) {
return Sort.unsorted();
@@ -167,8 +165,8 @@ public abstract class SortHandlerMethodArgumentResolverSupport {
List<Order> orders = new ArrayList<>(fields.length);
for (String field : fields) {
Order order = new Order(sortDefault.direction(), field);
orders.add(sortDefault.caseSensitive() ? order : order.ignoreCase());
Order order = new Order(sortDefault.getEnum("direction", Sort.Direction.class), field);
orders.add(sortDefault.getBoolean("caseSensitive") ? order : order.ignoreCase());
}
return sortOrNull.and(Sort.by(orders));

View File

@@ -91,8 +91,10 @@ abstract class SpringDataAnnotationUtils {
* @param annotation must not be {@literal null}.
* @param property must not be {@literal null} or empty.
* @return
* @deprecated since 3.0 as this method is no longer used within the Framework.
*/
@SuppressWarnings("unchecked")
@Deprecated
public static <T> T getSpecificPropertyOrDefaultFromValue(Annotation annotation, String property) {
Object propertyDefaultValue = AnnotationUtils.getDefaultValue(annotation, property);

View File

@@ -96,7 +96,7 @@ abstract class PageableDefaultUnitTests {
}
@Test
void rejectsInvalidQulifiers() throws Exception {
void rejectsInvalidQualifiers() {
var parameter = TestUtils.getParameterOfMethod(getControllerClass(), "invalidQualifiers",
Pageable.class, Pageable.class);
@@ -110,7 +110,7 @@ abstract class PageableDefaultUnitTests {
}
@Test
void rejectsNoQualifiers() throws Exception {
void rejectsNoQualifiers() {
var parameter = TestUtils.getParameterOfMethod(getControllerClass(), "noQualifiers", Pageable.class,
Pageable.class);

View File

@@ -36,6 +36,7 @@ import org.springframework.web.context.request.ServletWebRequest;
* @author Oliver Gierke
* @author Nick Williams
* @author Vedran Pavic
* @author Mark Paluch
*/
class PageableHandlerMethodArgumentResolverUnitTests extends PageableDefaultUnitTests {
@@ -106,13 +107,20 @@ class PageableHandlerMethodArgumentResolverUnitTests extends PageableDefaultUnit
@Test // DATACMNS-377
void rejectsInvalidCustomDefaultForPageSize() throws Exception {
var parameter = new MethodParameter(Sample.class.getMethod("invalidDefaultPageSize", Pageable.class),
0);
var parameter = new MethodParameter(Sample.class.getMethod("invalidDefaultPageSize", Pageable.class), 0);
assertThatIllegalStateException().isThrownBy(() -> assertSupportedAndResult(parameter, DEFAULT_PAGE_REQUEST)) //
.withMessageContaining("invalidDefaultPageSize");
}
@Test // GH-2657
void considersValueAlias() throws Exception {
var parameter = new MethodParameter(Sample.class.getMethod("valuePageSize", Pageable.class), 0);
assertSupportedAndResult(parameter, PageRequest.of(0, 2));
}
@Test // DATACMNS-408
void fallsBackToFirstPageIfNegativePageNumberIsGiven() throws Exception {
@@ -289,6 +297,8 @@ class PageableHandlerMethodArgumentResolverUnitTests extends PageableDefaultUnit
void invalidDefaultPageSize(@PageableDefault(size = 0) Pageable pageable);
void valuePageSize(@PageableDefault(2) Pageable pageable);
void simpleDefault(@PageableDefault(size = PAGE_SIZE, page = PAGE_NUMBER) Pageable pageable);
void simpleDefaultWithSort(

View File

@@ -153,20 +153,9 @@ class ReactiveSortHandlerMethodArgumentResolverUnitTests {
assertThat(resolve(request, PARAMETER)).isEqualTo(Sort.by(DESC, "property1"));
}
@Test // DATACMNS-1211
void rejectsDoubleAnnotatedMethod() {
var parameter = getParameterOfMethod("invalid");
var resolver = new ReactiveSortHandlerMethodArgumentResolver();
assertThat(resolver.supportsParameter(parameter)).isTrue();
assertThatIllegalArgumentException()
.isThrownBy(() ->
resolver.resolveArgumentValue(parameter, null,
MockServerWebExchange.from(TestUtils.getWebfluxRequest())))
.withMessageContaining(SortDefault.class.getSimpleName())
.withMessageContaining(SortDefaults.class.getSimpleName()).withMessageContaining(parameter.toString());
@Test // GH-2657
void considersRepeatableAnnotation() {
assertSupportedAndResolvedTo(getParameterOfMethod("repeatable"), Sort.by("one", "two", "three").ascending());
}
@Test // DATACMNS-1211
@@ -261,6 +250,6 @@ class ReactiveSortHandlerMethodArgumentResolverUnitTests {
void containeredDefault(@SortDefaults(@SortDefault({ "foo", "bar" })) Sort sort);
void invalid(@SortDefaults(@SortDefault({ "foo", "bar" })) @SortDefault({ "bar", "foo" }) Sort sort);
void repeatable(@SortDefault({ "one", "two" }) @SortDefault({ "three" }) Sort sort);
}
}

View File

@@ -25,7 +25,6 @@ import org.springframework.core.MethodParameter;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.web.SortDefault.SortDefaults;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
/**
@@ -91,19 +90,9 @@ abstract class SortDefaultUnitTests {
assertThat(getResolver().supportsParameter(parameter)).isFalse();
}
@Test
void rejectsDoubleAnnotatedMethod() {
var parameter = getParameterOfMethod("invalid");
HandlerMethodArgumentResolver resolver = new SortHandlerMethodArgumentResolver();
assertThat(resolver.supportsParameter(parameter)).isTrue();
assertThatIllegalArgumentException()
.isThrownBy(() -> resolver.resolveArgument(parameter, null, TestUtils.getWebRequest(), null)) //
.withMessageContaining(SortDefault.class.getSimpleName()) //
.withMessageContaining(SortDefaults.class.getSimpleName()) //
.withMessageContaining(parameter.toString());
@Test // GH-2657
void considersRepeatableAnnotation() throws Exception {
assertSupportedAndResolvedTo(getParameterOfMethod("repeatable"), Sort.by("one", "two", "three").ascending());
}
@Test

View File

@@ -324,7 +324,7 @@ class SortHandlerMethodArgumentResolverUnitTests extends SortDefaultUnitTests {
void containeredDefault(@SortDefaults(@SortDefault({ "foo", "bar" })) Sort sort);
void invalid(@SortDefaults(@SortDefault({ "foo", "bar" })) @SortDefault({ "bar", "foo" }) Sort sort);
void repeatable(@SortDefault({ "one", "two" }) @SortDefault({ "three" }) Sort sort);
void emptyQualifier(@Qualifier Sort sort);