DATACMNS-658 - Polishing.
Introduce caseSensitive() flag to `@SortDefault`. Move sort spec parsing into parser class. Update documentation. Original pull request: #172.
This commit is contained in:
@@ -448,7 +448,7 @@ For a more type-safe way of defining sort expressions, start with the type to de
|
||||
----
|
||||
TypedSort<Person> person = Sort.sort(Person.class);
|
||||
|
||||
TypedSort<Person> sort = person.by(Person::getFirstname).ascending()
|
||||
TypedSort<Person> 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:
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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<Order> 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> 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<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.
|
||||
@@ -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> direction;
|
||||
private final Optional<Boolean> ignoreCase;
|
||||
|
||||
private SortOrderParser(String[] elements) {
|
||||
this(elements, elements.length, Optional.empty(), Optional.empty());
|
||||
}
|
||||
|
||||
private SortOrderParser(String[] elements, int lastIndex, Optional<Direction> direction,
|
||||
Optional<Boolean> 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<Boolean> 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> 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<? super Order> callback) {
|
||||
|
||||
for (int i = 0; i < lastIndex; i++) {
|
||||
toOrder(elements[i]).ifPresent(callback);
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<Boolean> fromOptionalString(String value) {
|
||||
return IGNORECASE.equalsIgnoreCase(value) ? Optional.of(true) : Optional.empty();
|
||||
}
|
||||
|
||||
private Optional<Order> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user