From 54d9f016198198e268b5fe3647985b0a2d7b5dca Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 11 Mar 2020 08:46:50 +0100 Subject: [PATCH] DATACMNS-658 - Polishing. Introduce caseSensitive() flag to `@SortDefault`. Move sort spec parsing into parser class. Update documentation. Original pull request: #172. --- src/main/asciidoc/repositories.adoc | 4 +- .../springframework/data/web/SortDefault.java | 8 + ...tHandlerMethodArgumentResolverSupport.java | 139 +++++++++++++++--- ...andlerMethodArgumentResolverUnitTests.java | 83 +++++++---- 4 files changed, 179 insertions(+), 55 deletions(-) diff --git a/src/main/asciidoc/repositories.adoc b/src/main/asciidoc/repositories.adoc index af8519d6b..979a75d82 100644 --- a/src/main/asciidoc/repositories.adoc +++ b/src/main/asciidoc/repositories.adoc @@ -448,7 +448,7 @@ For a more type-safe way of defining sort expressions, start with the type to de ---- TypedSort person = Sort.sort(Person.class); -TypedSort sort = person.by(Person::getFirstname).ascending() +TypedSort sort = person.by(Person::getFirstname).ascending() .and(person.by(Person::getLastname).descending()); ---- ==== @@ -1249,7 +1249,7 @@ The preceding method signature causes Spring MVC try to derive a `Pageable` inst |=============== |`page`|Page you want to retrieve. 0-indexed and defaults to 0. |`size`|Size of the page you want to retrieve. Defaults to 20. -|`sort`|Properties that should be sorted by in the format `property,property(,ASC\|DESC)`. Default sort direction is ascending. Use multiple `sort` parameters if you want to switch directions -- for example, `?sort=firstname&sort=lastname,asc`. +|`sort`|Properties that should be sorted by in the format `property,property(,ASC\|DESC)(,IgnoreCase)`. Default sort direction is case-sensitive ascending. Use multiple `sort` parameters if you want to switch direction or case sensitivity -- for example, `?sort=firstname&sort=lastname,asc&sort=city,ignorecase`. |=============== To customize this behavior, register a bean implementing the `PageableHandlerMethodArgumentResolverCustomizer` interface or the `SortHandlerMethodArgumentResolverCustomizer` interface, respectively. Its `customize()` method gets called, letting you change settings, as shown in the following example: diff --git a/src/main/java/org/springframework/data/web/SortDefault.java b/src/main/java/org/springframework/data/web/SortDefault.java index 72cd55ece..f89d316de 100644 --- a/src/main/java/org/springframework/data/web/SortDefault.java +++ b/src/main/java/org/springframework/data/web/SortDefault.java @@ -57,6 +57,14 @@ public @interface SortDefault { */ Direction direction() default Direction.ASC; + /** + * Specifies whether to apply case-sensitive sorting. Defaults to {@literal true}. + * + * @return + * @since 2.3 + */ + boolean caseSensitive() default true; + /** * Wrapper annotation to allow declaring multiple {@link SortDefault} annotations on a method parameter. * diff --git a/src/main/java/org/springframework/data/web/SortHandlerMethodArgumentResolverSupport.java b/src/main/java/org/springframework/data/web/SortHandlerMethodArgumentResolverSupport.java index 3c5ecb237..e02326d5f 100644 --- a/src/main/java/org/springframework/data/web/SortHandlerMethodArgumentResolverSupport.java +++ b/src/main/java/org/springframework/data/web/SortHandlerMethodArgumentResolverSupport.java @@ -20,6 +20,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.function.Consumer; import javax.annotation.Nullable; @@ -163,7 +164,14 @@ public abstract class SortHandlerMethodArgumentResolverSupport { return Sort.unsorted(); } - return sortOrNull.and(Sort.by(sortDefault.direction(), fields)); + List orders = new ArrayList<>(fields.length); + for (String field : fields) { + + Order order = new Order(sortDefault.direction(), field); + orders.add(sortDefault.caseSensitive() ? order : order.ignoreCase()); + } + + return sortOrNull.and(Sort.by(orders)); } /** @@ -187,9 +195,9 @@ public abstract class SortHandlerMethodArgumentResolverSupport { /** * 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 is equal to "ignorecase" (when using a + * concatenation of Strings using the given delimiter. If the last element is equal {@code ignorecase} (when using a * case-insensitive comparison), the sort order will be performed without respect to case. If the last element (or the - * penultimate element if the last is "ignorecase") can be parsed into a {@link Direction} it's considered a + * penultimate element if the last is {@code ignorecase}) 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}. @@ -206,32 +214,15 @@ public abstract class SortHandlerMethodArgumentResolverSupport { continue; } - String[] elements = Arrays.stream(part.split(delimiter)) // - .filter(SortHandlerMethodArgumentResolver::notOnlyDots) // - .toArray(String[]::new); - - Optional direction = elements.length == 0 ? Optional.empty() - : Direction.fromOptionalString(elements[elements.length - 1]); - - int lastIndex = direction.map(it -> elements.length - 1).orElseGet(() -> elements.length); - - for (int i = 0; i < lastIndex; i++) { - toOrder(elements[i], direction).ifPresent(allOrders::add); - } + SortOrderParser.parse(part, delimiter) // + .parseIgnoreCase() // + .parseDirection() // + .forEachOrder(allOrders::add); } return allOrders.isEmpty() ? Sort.unsorted() : Sort.by(allOrders); } - private static Optional toOrder(String property, Optional direction) { - - if (!StringUtils.hasText(property)) { - return Optional.empty(); - } - - return Optional.of(direction.map(it -> new Order(it, property)).orElseGet(() -> Order.by(property))); - } - /** * Folds the given {@link Sort} instance into a {@link List} of sort expressions, accumulating {@link Order} instances * of the same direction into a single expression if they are in order. @@ -360,4 +351,104 @@ public abstract class SortHandlerMethodArgumentResolverSupport { return expressions; } } + + /** + * Parser for sort {@link Order}. + * + * @author Mark Paluch + * @since 2.3 + */ + static class SortOrderParser { + + private static final String IGNORECASE = "ignorecase"; + + private final String[] elements; + private final int lastIndex; + private final Optional direction; + private final Optional ignoreCase; + + private SortOrderParser(String[] elements) { + this(elements, elements.length, Optional.empty(), Optional.empty()); + } + + private SortOrderParser(String[] elements, int lastIndex, Optional direction, + Optional ignoreCase) { + this.elements = elements; + this.lastIndex = Math.max(0, lastIndex); + this.direction = direction; + this.ignoreCase = ignoreCase; + } + + /** + * Parse the raw sort string delimited by {@code delimiter}. + * + * @param part sort part to parse. + * @param delimiter the delimiter to be used to split up the source elements, will never be {@literal null}. + * @return the parsing state object. + */ + public static SortOrderParser parse(String part, String delimiter) { + + String[] elements = Arrays.stream(part.split(delimiter)) // + .filter(SortHandlerMethodArgumentResolver::notOnlyDots) // + .toArray(String[]::new); + + return new SortOrderParser(elements); + } + + /** + * Parse the {@code ignoreCase} portion of the sort specification. + * + * @return a new parsing state object. + */ + public SortOrderParser parseIgnoreCase() { + + Optional ignoreCase = lastIndex > 0 ? fromOptionalString(elements[lastIndex - 1]) : Optional.empty(); + + return new SortOrderParser(elements, lastIndex - (ignoreCase.isPresent() ? 1 : 0), direction, ignoreCase); + } + + /** + * Parse the {@link Order} portion of the sort specification. + * + * @return a new parsing state object. + */ + public SortOrderParser parseDirection() { + + Optional direction = lastIndex > 0 ? Direction.fromOptionalString(elements[lastIndex - 1]) + : Optional.empty(); + + return new SortOrderParser(elements, lastIndex - (direction.isPresent() ? 1 : 0), direction, ignoreCase); + } + + /** + * Notify a {@link Consumer callback function} for each parsed {@link Order} object. + * + * @param callback block to be executed. + */ + public void forEachOrder(Consumer callback) { + + for (int i = 0; i < lastIndex; i++) { + toOrder(elements[i]).ifPresent(callback); + } + } + + private Optional fromOptionalString(String value) { + return IGNORECASE.equalsIgnoreCase(value) ? Optional.of(true) : Optional.empty(); + } + + private Optional toOrder(String property) { + + if (!StringUtils.hasText(property)) { + return Optional.empty(); + } + + Order order = direction.map(it -> new Order(it, property)).orElseGet(() -> Order.by(property)); + + if (ignoreCase.isPresent()) { + return Optional.of(order.ignoreCase()); + } + + return Optional.of(order); + } + } } diff --git a/src/test/java/org/springframework/data/web/SortHandlerMethodArgumentResolverUnitTests.java b/src/test/java/org/springframework/data/web/SortHandlerMethodArgumentResolverUnitTests.java index 1bd12455a..2a1bca2fd 100755 --- a/src/test/java/org/springframework/data/web/SortHandlerMethodArgumentResolverUnitTests.java +++ b/src/test/java/org/springframework/data/web/SortHandlerMethodArgumentResolverUnitTests.java @@ -18,19 +18,20 @@ package org.springframework.data.web; import static org.assertj.core.api.Assertions.*; import static org.springframework.data.domain.Sort.Direction.*; -import java.util.Arrays; import java.util.stream.Stream; import javax.servlet.http.HttpServletRequest; import org.junit.BeforeClass; import org.junit.Test; + import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.core.MethodParameter; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.domain.Sort.Order; import org.springframework.data.web.SortDefault.SortDefaults; +import org.springframework.lang.Nullable; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.util.StringUtils; import org.springframework.web.context.request.NativeWebRequest; @@ -43,6 +44,7 @@ import org.springframework.web.context.request.ServletWebRequest; * @author Oliver Gierke * @author Thomas Darimont * @author Nick Williams + * @author Mark Paluch */ public class SortHandlerMethodArgumentResolverUnitTests extends SortDefaultUnitTests { @@ -54,7 +56,7 @@ public class SortHandlerMethodArgumentResolverUnitTests extends SortDefaultUnitT } @Test // DATACMNS-351 - public void fallbackToGivenDefaultSort() throws Exception { + public void fallbackToGivenDefaultSort() { MethodParameter parameter = TestUtils.getParameterOfMethod(getControllerClass(), "unsupportedMethod", String.class); SortHandlerMethodArgumentResolver resolver = new SortHandlerMethodArgumentResolver(); @@ -66,7 +68,7 @@ public class SortHandlerMethodArgumentResolverUnitTests extends SortDefaultUnitT } @Test // DATACMNS-351 - public void fallbackToDefaultDefaultSort() throws Exception { + public void fallbackToDefaultDefaultSort() { MethodParameter parameter = TestUtils.getParameterOfMethod(getControllerClass(), "unsupportedMethod", String.class); SortHandlerMethodArgumentResolver resolver = new SortHandlerMethodArgumentResolver(); @@ -75,28 +77,6 @@ public class SortHandlerMethodArgumentResolverUnitTests extends SortDefaultUnitT assertThat(sort.isSorted()).isFalse(); } - @Test - public void sortParamHandlesSortOrderAndIgnoreCase() throws Exception { - - MockHttpServletRequest request = new MockHttpServletRequest(); - request.addParameter("sort", "property,DESC,IgnoreCase"); - request.addParameter("sort", ""); - - assertThat(resolveSort(request, PARAMETER)) - .isEqualTo(Sort.by(Arrays.asList(new Order(DESC, "property").ignoreCase()))); - } - - @Test - public void sortParamHandlesIgnoreCase() throws Exception { - - MockHttpServletRequest request = new MockHttpServletRequest(); - request.addParameter("sort", "property,IgnoreCase"); - request.addParameter("sort", ""); - - assertThat(resolveSort(request, PARAMETER)) - .isEqualTo(Sort.by(Arrays.asList(new Order(ASC, "property").ignoreCase()))); - } - @Test public void discoversSimpleSortFromRequest() { @@ -126,7 +106,7 @@ public class SortHandlerMethodArgumentResolverUnitTests extends SortDefaultUnitT } @Test - public void returnsNullForSortParameterSetToNothing() throws Exception { + public void returnsNullForSortParameterSetToNothing() { MethodParameter parameter = getParameterOfMethod("supportedMethod"); @@ -139,7 +119,7 @@ public class SortHandlerMethodArgumentResolverUnitTests extends SortDefaultUnitT } @Test // DATACMNS-366 - public void requestForMultipleSortPropertiesIsUnmarshalledCorrectly() throws Exception { + public void requestForMultipleSortPropertiesIsUnmarshalledCorrectly() { MethodParameter parameter = getParameterOfMethod("supportedMethod"); @@ -188,6 +168,46 @@ public class SortHandlerMethodArgumentResolverUnitTests extends SortDefaultUnitT assertThat(resolveSort(request, PARAMETER)).isEqualTo(Sort.by(DESC, "property")); } + @Test // DATACMNS-658 + public void sortParamHandlesSortOrderAndIgnoreCase() throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("sort", "property,DESC,IgnoreCase"); + request.addParameter("sort", ""); + + assertThat(resolveSort(request, PARAMETER)).isEqualTo(Sort.by(new Order(DESC, "property").ignoreCase())); + } + + @Test // DATACMNS-658 + public void sortParamHandlesMultiplePropertiesWithSortOrderAndIgnoreCase() throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("sort", "property1,property2,DESC,IgnoreCase"); + + assertThat(resolveSort(request, PARAMETER)) + .isEqualTo(Sort.by(new Order(DESC, "property1").ignoreCase(), new Order(DESC, "property2").ignoreCase())); + } + + @Test // DATACMNS-658 + public void sortParamHandlesIgnoreCase() throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("sort", "property,IgnoreCase"); + request.addParameter("sort", ""); + + assertThat(resolveSort(request, PARAMETER)).isEqualTo(Sort.by(new Order(ASC, "property").ignoreCase())); + } + + @Test // DATACMNS-658 + public void returnsDefaultCaseInsensitive() throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("sort", ""); + + assertThat(resolveSort(request, getParameterOfMethod("simpleDefaultWithDirectionCaseInsensitive"))) + .isEqualTo(Sort.by(new Order(DESC, "firstname").ignoreCase(), new Order(DESC, "lastname").ignoreCase())); + } + @Test // DATACMNS-379 public void parsesCommaParameterForSort() throws Exception { @@ -243,7 +263,7 @@ public class SortHandlerMethodArgumentResolverUnitTests extends SortDefaultUnitT return getRequestWithSort(sort, null); } - private static NativeWebRequest getRequestWithSort(Sort sort, String qualifier) { + private static NativeWebRequest getRequestWithSort(@Nullable Sort sort, @Nullable String qualifier) { MockHttpServletRequest request = new MockHttpServletRequest(); @@ -254,7 +274,9 @@ public class SortHandlerMethodArgumentResolverUnitTests extends SortDefaultUnitT for (Order order : sort) { String prefix = StringUtils.hasText(qualifier) ? qualifier + "_" : ""; - request.addParameter(prefix + "sort", String.format("%s,%s", order.getProperty(), order.getDirection().name())); + String suffix = order.isIgnoreCase() ? ",IgnoreCase" : ""; + request.addParameter(prefix + "sort", + String.format("%s,%s%s", order.getProperty(), order.getDirection().name(), suffix)); } return new ServletWebRequest(request); @@ -278,6 +300,9 @@ public class SortHandlerMethodArgumentResolverUnitTests extends SortDefaultUnitT void simpleDefaultWithDirection( @SortDefault(sort = { "firstname", "lastname" }, direction = Direction.DESC) Sort sort); + void simpleDefaultWithDirectionCaseInsensitive( + @SortDefault(sort = { "firstname", "lastname" }, direction = Direction.DESC, caseSensitive = false) Sort sort); + void containeredDefault(@SortDefaults(@SortDefault({ "foo", "bar" })) Sort sort); void invalid(@SortDefaults(@SortDefault({ "foo", "bar" })) @SortDefault({ "bar", "foo" }) Sort sort);