Skip to content

Commit afb6d7f

Browse files
committed
Add null handling to sort query parameters
Closes spring-projects#3152
1 parent f305308 commit afb6d7f

File tree

3 files changed

+137
-7
lines changed

3 files changed

+137
-7
lines changed

src/main/java/org/springframework/data/web/SortDefault.java

+10
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.springframework.core.annotation.AliasFor;
2626
import org.springframework.data.domain.Sort;
2727
import org.springframework.data.domain.Sort.Direction;
28+
import org.springframework.data.domain.Sort.NullHandling;
2829

2930
/**
3031
* Annotation to define the default {@link Sort} options to be used when injecting a {@link Sort} instance into a
@@ -33,6 +34,7 @@
3334
* @since 1.6
3435
* @author Oliver Gierke
3536
* @author Mark Palich
37+
* @author Petar Heyken
3638
*/
3739
@Documented
3840
@Retention(RetentionPolicy.RUNTIME)
@@ -71,6 +73,14 @@
7173
*/
7274
boolean caseSensitive() default true;
7375

76+
/**
77+
* Specifies which null handling to apply. Defaults to {@link NullHandling#NATIVE}.
78+
*
79+
* @return
80+
* @since 3.4
81+
*/
82+
NullHandling nullHandling() default NullHandling.NATIVE;
83+
7484
/**
7585
* Wrapper annotation to allow declaring multiple {@link SortDefault} annotations on a method parameter.
7686
*

src/main/java/org/springframework/data/web/SortHandlerMethodArgumentResolverSupport.java

+67-7
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.core.annotation.RepeatableContainers;
2929
import org.springframework.data.domain.Sort;
3030
import org.springframework.data.domain.Sort.Direction;
31+
import org.springframework.data.domain.Sort.NullHandling;
3132
import org.springframework.data.domain.Sort.Order;
3233
import org.springframework.data.web.SortDefault.SortDefaults;
3334
import org.springframework.lang.Nullable;
@@ -41,6 +42,7 @@
4142
* @author Mark Paluch
4243
* @author Vedran Pavic
4344
* @author Johannes Englmeier
45+
* @author Petar Heyken
4446
* @see SortHandlerMethodArgumentResolver
4547
* @see ReactiveSortHandlerMethodArgumentResolver
4648
* @since 2.2
@@ -165,7 +167,14 @@ private Sort appendOrCreateSortTo(MergedAnnotation<SortDefault> sortDefault, Sor
165167
List<Order> orders = new ArrayList<>(fields.length);
166168
for (String field : fields) {
167169

168-
Order order = new Order(sortDefault.getEnum("direction", Sort.Direction.class), field);
170+
Order order = new Order(sortDefault.getEnum("direction", Direction.class), field);
171+
172+
order = switch (sortDefault.getEnum("nullHandling", NullHandling.class)) {
173+
case NATIVE -> order.nullsNative();
174+
case NULLS_FIRST -> order.nullsFirst();
175+
case NULLS_LAST -> order.nullsLast();
176+
};
177+
169178
orders.add(sortDefault.getBoolean("caseSensitive") ? order : order.ignoreCase());
170179
}
171180

@@ -214,6 +223,7 @@ Sort parseParameterIntoSort(List<String> source, String delimiter) {
214223
}
215224

216225
SortOrderParser.parse(part, delimiter) //
226+
.parseNullHandling() //
217227
.parseIgnoreCase() //
218228
.parseDirection() //
219229
.forEachOrder(allOrders::add);
@@ -360,22 +370,28 @@ List<String> dumpExpressionIfPresentInto(List<String> expressions) {
360370
static class SortOrderParser {
361371

362372
private static final String IGNORECASE = "ignorecase";
373+
private static final String NULLSNATIVE = "nullsnative";
374+
private static final String NULLSFIRST = "nullsfirst";
375+
private static final String NULLSLAST = "nullslast";
363376

364377
private final String[] elements;
365378
private final int lastIndex;
366379
private final Optional<Direction> direction;
367380
private final Optional<Boolean> ignoreCase;
381+
private final Optional<NullHandling> nullHandling;
368382

369383
private SortOrderParser(String[] elements) {
370-
this(elements, elements.length, Optional.empty(), Optional.empty());
384+
this(elements, elements.length, Optional.empty(), Optional.empty(), Optional.empty());
371385
}
372386

373387
private SortOrderParser(String[] elements, int lastIndex, Optional<Direction> direction,
374-
Optional<Boolean> ignoreCase) {
388+
Optional<Boolean> ignoreCase, Optional<NullHandling> nullHandling) {
389+
375390
this.elements = elements;
376391
this.lastIndex = Math.max(0, lastIndex);
377392
this.direction = direction;
378393
this.ignoreCase = ignoreCase;
394+
this.nullHandling = nullHandling;
379395
}
380396

381397
/**
@@ -394,16 +410,34 @@ public static SortOrderParser parse(String part, String delimiter) {
394410
return new SortOrderParser(elements);
395411
}
396412

413+
/**
414+
* Parse the {@link NullHandling} portion of the sort specification.
415+
*
416+
* @return a new parsing state object.
417+
*/
418+
public SortOrderParser parseNullHandling() {
419+
420+
Optional<NullHandling> nullHandling = lastIndex > 0 ?
421+
fromOptionalNullHandlingString(elements[lastIndex - 1]) :
422+
Optional.empty();
423+
424+
return new SortOrderParser(elements, lastIndex - (nullHandling.isPresent() ? 1 : 0), direction, ignoreCase,
425+
nullHandling);
426+
}
427+
397428
/**
398429
* Parse the {@code ignoreCase} portion of the sort specification.
399430
*
400431
* @return a new parsing state object.
401432
*/
402433
public SortOrderParser parseIgnoreCase() {
403434

404-
Optional<Boolean> ignoreCase = lastIndex > 0 ? fromOptionalString(elements[lastIndex - 1]) : Optional.empty();
435+
Optional<Boolean> ignoreCase = lastIndex > 0 ?
436+
fromOptionalIgnoreCaseString(elements[lastIndex - 1]) :
437+
Optional.empty();
405438

406-
return new SortOrderParser(elements, lastIndex - (ignoreCase.isPresent() ? 1 : 0), direction, ignoreCase);
439+
return new SortOrderParser(elements, lastIndex - (ignoreCase.isPresent() ? 1 : 0), direction, ignoreCase,
440+
nullHandling);
407441
}
408442

409443
/**
@@ -416,7 +450,8 @@ public SortOrderParser parseDirection() {
416450
Optional<Direction> direction = lastIndex > 0 ? Direction.fromOptionalString(elements[lastIndex - 1])
417451
: Optional.empty();
418452

419-
return new SortOrderParser(elements, lastIndex - (direction.isPresent() ? 1 : 0), direction, ignoreCase);
453+
return new SortOrderParser(elements, lastIndex - (direction.isPresent() ? 1 : 0), direction, ignoreCase,
454+
nullHandling);
420455
}
421456

422457
/**
@@ -431,7 +466,24 @@ public void forEachOrder(Consumer<? super Order> callback) {
431466
}
432467
}
433468

434-
private Optional<Boolean> fromOptionalString(String value) {
469+
private Optional<NullHandling> fromOptionalNullHandlingString(String value) {
470+
471+
if (NULLSNATIVE.equalsIgnoreCase(value)) {
472+
return Optional.of(NullHandling.NATIVE);
473+
}
474+
475+
if (NULLSFIRST.equalsIgnoreCase(value)) {
476+
return Optional.of(NullHandling.NULLS_FIRST);
477+
}
478+
479+
if (NULLSLAST.equalsIgnoreCase(value)) {
480+
return Optional.of(NullHandling.NULLS_LAST);
481+
}
482+
483+
return Optional.empty();
484+
}
485+
486+
private Optional<Boolean> fromOptionalIgnoreCaseString(String value) {
435487
return IGNORECASE.equalsIgnoreCase(value) ? Optional.of(true) : Optional.empty();
436488
}
437489

@@ -443,6 +495,14 @@ private Optional<Order> toOrder(String property) {
443495

444496
Order order = direction.map(it -> new Order(it, property)).orElseGet(() -> Order.by(property));
445497

498+
if (nullHandling.isPresent()) {
499+
order = switch (nullHandling.get()) {
500+
case NATIVE -> order.nullsNative();
501+
case NULLS_FIRST -> order.nullsFirst();
502+
case NULLS_LAST -> order.nullsLast();
503+
};
504+
}
505+
446506
if (ignoreCase.isPresent()) {
447507
return Optional.of(order.ignoreCase());
448508
}

src/test/java/org/springframework/data/web/SortHandlerMethodArgumentResolverUnitTests.java

+60
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
* @author Nick Williams
4848
* @author Mark Paluch
4949
* @author Vedran Pavic
50+
* @author Petar Heyken
5051
*/
5152
class SortHandlerMethodArgumentResolverUnitTests extends SortDefaultUnitTests {
5253

@@ -210,6 +211,16 @@ void returnsDefaultCaseInsensitive() throws Exception {
210211
.isEqualTo(Sort.by(new Order(DESC, "firstname").ignoreCase(), new Order(DESC, "lastname").ignoreCase()));
211212
}
212213

214+
@Test // GH-3152
215+
void returnsDefaultNullHandling() throws Exception {
216+
217+
final var request = new MockHttpServletRequest();
218+
request.addParameter("sort", "");
219+
220+
assertThat(resolveSort(request, getParameterOfMethod("simpleDefaultWithDirectionAndNullHandling"))).isEqualTo(
221+
Sort.by(new Order(DESC, "firstname").nullsLast(), new Order(DESC, "lastname").nullsLast()));
222+
}
223+
213224
@Test // DATACMNS-379
214225
void parsesCommaParameterForSort() throws Exception {
215226

@@ -272,6 +283,52 @@ void readsEncodedSort() {
272283
assertSupportedAndResolvedTo(new ServletWebRequest(request), parameter, Sort.by("foo").descending());
273284
}
274285

286+
@Test // GH-3152
287+
void sortParamHandlesMultiplePropertiesWithSortOrderAndIgnoreCaseAndNullsLast() throws Exception {
288+
289+
final var request = new MockHttpServletRequest();
290+
request.addParameter("sort", "property1,property2,DESC,IgnoreCase,NullsLast");
291+
292+
assertThat(resolveSort(request, PARAMETER)).isEqualTo(Sort.by(new Order(DESC, "property1").ignoreCase().nullsLast(),
293+
new Order(DESC, "property2").ignoreCase().nullsLast()));
294+
}
295+
296+
@Test // GH-3152
297+
void sortParamHandlesSinglePropertyWithIgnoreCaseAndNullsLast() throws Exception {
298+
299+
final var request = new MockHttpServletRequest();
300+
request.addParameter("sort", "property,IgnoreCase,NullsLast");
301+
302+
assertThat(resolveSort(request, PARAMETER)).isEqualTo(Sort.by(new Order(ASC, "property").ignoreCase().nullsLast()));
303+
}
304+
305+
@Test // GH-3152
306+
void sortParamHandlesSinglePropertyWithNullsFirst() throws Exception {
307+
308+
final var request = new MockHttpServletRequest();
309+
request.addParameter("sort", "property,nullsfirst");
310+
311+
assertThat(resolveSort(request, PARAMETER)).isEqualTo(Sort.by(new Order(ASC, "property").nullsFirst()));
312+
}
313+
314+
@Test // GH-3152
315+
void sortParamHandlesSinglePropertyWithSortOrderAndWithNullsFirst() throws Exception {
316+
317+
final var request = new MockHttpServletRequest();
318+
request.addParameter("sort", "property,DESC,nullsfirst");
319+
320+
assertThat(resolveSort(request, PARAMETER)).isEqualTo(Sort.by(new Order(DESC, "property").nullsFirst()));
321+
}
322+
323+
@Test // GH-3152
324+
void sortParamHandlesSinglePropertyWithSortOrderAndWithNullsNative() throws Exception {
325+
326+
final var request = new MockHttpServletRequest();
327+
request.addParameter("sort", "property,DESC,nullsnative");
328+
329+
assertThat(resolveSort(request, PARAMETER)).isEqualTo(Sort.by(new Order(DESC, "property").nullsNative()));
330+
}
331+
275332
private static Sort resolveSort(HttpServletRequest request, MethodParameter parameter) throws Exception {
276333

277334
var resolver = new SortHandlerMethodArgumentResolver();
@@ -334,6 +391,9 @@ void simpleDefaultWithDirection(
334391
void simpleDefaultWithDirectionCaseInsensitive(
335392
@SortDefault(sort = { "firstname", "lastname" }, direction = Direction.DESC, caseSensitive = false) Sort sort);
336393

394+
void simpleDefaultWithDirectionAndNullHandling(
395+
@SortDefault(sort = { "firstname", "lastname" }, direction = Direction.DESC, nullHandling = Sort.NullHandling.NULLS_LAST) Sort sort);
396+
337397
void containeredDefault(@SortDefaults(@SortDefault({ "foo", "bar" })) Sort sort);
338398

339399
void repeatable(@SortDefault({ "one", "two" }) @SortDefault({ "three" }) Sort sort);

0 commit comments

Comments
 (0)