From a5463ff8d22fe4792c3e0895a23104a60a9c6eba Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Wed, 26 Nov 2025 16:53:17 +0100 Subject: [PATCH 1/5] remove obsolete files --- src/test/java/org/junit/rules/TestRule.java | 10 ---------- src/test/java/org/junit/runners/model/Statement.java | 10 ---------- 2 files changed, 20 deletions(-) delete mode 100644 src/test/java/org/junit/rules/TestRule.java delete mode 100644 src/test/java/org/junit/runners/model/Statement.java diff --git a/src/test/java/org/junit/rules/TestRule.java b/src/test/java/org/junit/rules/TestRule.java deleted file mode 100644 index e499646..0000000 --- a/src/test/java/org/junit/rules/TestRule.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.junit.rules; - -/** - * This is required by Testcontainers but actually unused. - * By defining this here, we can avoid loading Junit-4. - * See: https://github.com/testcontainers/testcontainers-java/issues/970#issuecomment-625044008 - */ -@SuppressWarnings("unused") -public interface TestRule { -} diff --git a/src/test/java/org/junit/runners/model/Statement.java b/src/test/java/org/junit/runners/model/Statement.java deleted file mode 100644 index 2a72b00..0000000 --- a/src/test/java/org/junit/runners/model/Statement.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.junit.runners.model; - -/** - * This is required by Testcontainers but actually unused. - * By defining this here, we can avoid loading Junit-4. - * See: https://github.com/testcontainers/testcontainers-java/issues/970#issuecomment-625044008 - */ -@SuppressWarnings("unused") -public class Statement { -} From 23ce0215f137c3d055f6d580c337795edd5b0b2b Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Wed, 26 Nov 2025 16:54:16 +0100 Subject: [PATCH 2/5] update deps --- pom.xml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index b93d735..9a1e065 100644 --- a/pom.xml +++ b/pom.xml @@ -1,11 +1,12 @@ - + 4.0.0 org.springframework.boot spring-boot-starter-parent - 3.5.6 + 3.5.7 @@ -78,7 +79,7 @@ org.springdoc springdoc-openapi-starter-webmvc-ui - 2.8.13 + 2.8.14 @@ -115,7 +116,7 @@ org.testcontainers testcontainers - 1.21.3 + 2.0.2 test @@ -136,14 +137,14 @@ org.testcontainers - junit-jupiter - 1.21.3 + testcontainers-junit-jupiter + 2.0.2 test org.testcontainers - postgresql - 1.21.3 + testcontainers-postgresql + 2.0.2 test From 9fae6b9cfff06bd7639507c0915d661e1fd8da59 Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Wed, 26 Nov 2025 16:54:23 +0100 Subject: [PATCH 3/5] update sort parameter --- .../toolbox/parameter/SortParameter.java | 179 +++++++++++++++++- 1 file changed, 173 insertions(+), 6 deletions(-) diff --git a/src/main/java/it/aboutbits/springboot/toolbox/parameter/SortParameter.java b/src/main/java/it/aboutbits/springboot/toolbox/parameter/SortParameter.java index 25f9791..d027837 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/parameter/SortParameter.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/parameter/SortParameter.java @@ -9,17 +9,36 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +/** + * Represents sorting parameters for data retrieval and manipulation. + * This class provides methods for creating, customizing, and applying sorting criteria + * based on enum constants and associated sort properties. Sorting criteria can be defined + * with various configurations, including direction and null-handling behavior. + */ public record SortParameter & SortParameter.Definition>(List sortFields) { private static final String DEFAULT_SORT_PROPERTY = "id"; - private static final Sort DEFAULT_SORT = Sort.by( - Sort.Direction.ASC, - DEFAULT_SORT_PROPERTY - ); + private static final Sort.Direction DEFAULT_SORT_DIRETION = Sort.Direction.ASC; + /** + * Creates a {@link SortParameter} that represents an unsorted state. + * + * @param a type that extends both {@link Enum} and {@link Definition}. + * @return an instance of {@link SortParameter} configured with no sorting fields. + */ public static & Definition> SortParameter unsorted() { return new SortParameter<>(Collections.emptyList()); } + /** + * Creates a {@link SortParameter} initialized with the provided sort definitions. + * Each provided enum constant is converted into a {@link SortField} with ascending + * order direction and default null-handling behavior. This method allows the + * specification of multiple sorting criteria. + * + * @param a type parameter representing an enum that implements the {@link Definition} interface. + * @param sortDefinitions an array of enum constants defining the sort properties. Must not be null. + * @return a {@link SortParameter} instance configured with the given sort definitions. + */ @SafeVarargs public static & Definition> SortParameter by( @NonNull T... sortDefinitions @@ -36,6 +55,42 @@ public static & Definition> SortParameter by( ); } + /** + * Creates a {@link SortParameter} initialized with a single sort definition. + * This method allows specifying the property to sort by, the direction of sorting, + * and uses the default null-handling behavior ({@link Sort.NullHandling#NATIVE}). + * + * @param a type that extends both {@link Enum} and {@link Definition}. + * @param sortDefinition an enum constant defining the property to sort by. Must not be null. + * @param direction the direction of sorting, either {@link Sort.Direction#ASC} or {@link Sort.Direction#DESC}. + * Must not be null. + * @return an instance of {@link SortParameter} configured with the given sort definition and direction. + */ + public static & Definition> SortParameter by( + @NonNull T sortDefinition, + @NonNull Sort.Direction direction + ) { + return new SortParameter<>( + List.of(new SortField( + sortDefinition.name(), + direction, + Sort.NullHandling.NATIVE + ) + ) + ); + } + + /** + * Creates a {@link SortParameter} instance configured with a single sort definition. + * This method allows specifying the property to sort by, the direction of sorting, + * and null-handling behavior. + * + * @param the type parameter extending both {@link Enum} and {@link Definition}. + * @param sortDefinition an enum constant defining the property to sort by. Must not be null. + * @param direction the direction of sorting, either {@link Sort.Direction#ASC} or {@link Sort.Direction#DESC}. Must not be null. + * @param nullHandling the strategy for handling null values during sorting, specified by {@link Sort.NullHandling}. Must not be null. + * @return an instance of {@link SortParameter} configured with the given sort definition, direction, and null-handling behavior. + */ public static & Definition> SortParameter by( @NonNull T sortDefinition, @NonNull Sort.Direction direction, @@ -51,10 +106,105 @@ public static & Definition> SortParameter by( ); } + /** + * Adds additional sorting criteria to the existing {@link SortParameter}. + * Each provided enum constant is converted into a {@link SortField} with ascending + * order direction and default null-handling behavior. + * + * @param sortDefinitions an array of enum constants defining the additional sort properties. Must not be null. + * @return the updated {@link SortParameter} instance containing the new sort definitions. + */ + @SafeVarargs + public final SortParameter and( + @NonNull T... sortDefinitions + ) { + sortFields.addAll( + Stream.of(sortDefinitions) + .map(sortDefinition -> new SortField( + sortDefinition.name(), + Sort.Direction.ASC, + Sort.NullHandling.NATIVE + ) + ) + .toList() + ); + + return this; + } + + /** + * Adds a sorting criterion to the current {@link SortParameter} instance. + * The provided sort definition and direction are converted into a {@link SortField} + * with default null-handling behavior and appended to the existing sort fields. + * + * @param sortDefinition the enum constant defining the property to sort by. Must not be null. + * @param direction the direction of sorting, either {@link Sort.Direction#ASC} or {@link Sort.Direction#DESC}. Must not be null. + * @return the updated {@link SortParameter} instance containing the new sorting criterion. + */ + public SortParameter and( + @NonNull T sortDefinition, + @NonNull Sort.Direction direction + ) { + sortFields.add( + new SortField( + sortDefinition.name(), + direction, + Sort.NullHandling.NATIVE + ) + ); + + return this; + } + + /** + * Adds a sorting criterion to the current {@link SortParameter} instance. The provided sort definition, + * direction, and null-handling behavior are converted into a {@link SortField} and appended to the + * existing sort fields. + * + * @param sortDefinition the enum constant defining the property to sort by. Must not be null. + * @param direction the direction of sorting, either {@link Sort.Direction#ASC} or {@link Sort.Direction#DESC}. Must not be null. + * @param nullHandling the strategy for handling null values during sorting, specified by {@link Sort.NullHandling}. Must not be null. + * @return the updated {@link SortParameter} instance containing the new sorting criterion. + */ + public SortParameter and( + @NonNull T sortDefinition, + @NonNull Sort.Direction direction, + @NonNull Sort.NullHandling nullHandling + ) { + sortFields.add( + new SortField( + sortDefinition.name(), + direction, + nullHandling + ) + ); + + return this; + } + + /** + * Returns the current {@link SortParameter} instance if it has defined sorting fields. + * Otherwise, returns the provided fallback {@link SortParameter}. + * + * @param fallback the {@link SortParameter} to use as a fallback in case the current instance + * has no defined sorting fields. Must not be null. + * @return the current {@link SortParameter} if it has defined sorting fields, + * or the provided fallback if it does not. + */ public SortParameter or(@NonNull SortParameter fallback) { return sortFields == null || sortFields.isEmpty() ? fallback : this; } + /** + * Builds a {@link Sort} object without applying any default sorting parameters. Converts the enum keys + * of the provided map to their string names and generates the sort object. + * + * @param mapping a non-null map where the keys are enumeration values representing sort properties + * and the values are their associated sort directions. The enumeration keys must + * have a `name()` method for string conversion. Must not be null. + * @return an instance of {@link Sort} created using the transformed key-value mapping, + * excluding default sorting behavior. + */ public Sort buildSortWithoutDefault(@NonNull Map mapping) { var stringMapping = mapping.entrySet().stream() .collect(Collectors.toMap( @@ -65,6 +215,16 @@ public Sort buildSortWithoutDefault(@NonNull Map mapping) { return buildSort(stringMapping, false); } + /** + * Builds a {@link Sort} object based on the provided mapping of enumeration values to string properties. + * Converts enum keys to their respective string names and generates the sort object. + *

+ * If a sort mapping for "id" is provided, it will be used as the default sort property. Unless specified, this sort will be applied last. + * + * @param mapping a non-null map where the keys represent enumeration values and the values represent sort property names. + * The enumeration keys must implement the `name()` method to retrieve their string representation. + * @return an instance of {@link Sort} created using the transformed key-value mapping with default sorting behavior. + */ public Sort buildSort(@NonNull Map mapping) { var stringMapping = mapping.entrySet().stream() .collect(Collectors.toMap( @@ -79,7 +239,7 @@ public Sort buildSort(@NonNull Map mapping) { @SuppressWarnings("java:S6204") private Sort buildSort(@NonNull Map mapping, boolean withDefault) { if (sortFields == null || sortFields.isEmpty()) { - return withDefault ? DEFAULT_SORT : Sort.unsorted(); + return withDefault ? getMappedDefaultSort(mapping) : Sort.unsorted(); } var additionalSort = Sort.by( @@ -97,12 +257,16 @@ private Sort buildSort(@NonNull Map mapping, boolean withDefault if (withDefault) { var includesDefault = additionalSort.getOrderFor(DEFAULT_SORT_PROPERTY) != null; if (!includesDefault) { - return additionalSort.and(DEFAULT_SORT); + return additionalSort.and(getMappedDefaultSort(mapping)); } } return additionalSort; } + private static Sort getMappedDefaultSort(Map mapping) { + return Sort.by(DEFAULT_SORT_DIRETION, mapping.getOrDefault(DEFAULT_SORT_PROPERTY, DEFAULT_SORT_PROPERTY)); + } + public record SortField( @NonNull String property, @NonNull Sort.Direction direction, @@ -110,6 +274,9 @@ public record SortField( ) { } + /** + * Interface to give enums the purpose of listing sortable keys. + */ public interface Definition { } } From 98524c7db48503af04d46c5ea8cd92dee762a038 Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Wed, 26 Nov 2025 17:07:47 +0100 Subject: [PATCH 4/5] update sort parameter --- .../toolbox/parameter/SortParameter.java | 49 +++++++++++++------ 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/src/main/java/it/aboutbits/springboot/toolbox/parameter/SortParameter.java b/src/main/java/it/aboutbits/springboot/toolbox/parameter/SortParameter.java index d027837..12ae84e 100644 --- a/src/main/java/it/aboutbits/springboot/toolbox/parameter/SortParameter.java +++ b/src/main/java/it/aboutbits/springboot/toolbox/parameter/SortParameter.java @@ -1,9 +1,12 @@ package it.aboutbits.springboot.toolbox.parameter; +import lombok.EqualsAndHashCode; +import lombok.Getter; import lombok.NonNull; +import lombok.experimental.Accessors; import org.springframework.data.domain.Sort; -import java.util.Collections; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -15,10 +18,25 @@ * based on enum constants and associated sort properties. Sorting criteria can be defined * with various configurations, including direction and null-handling behavior. */ -public record SortParameter & SortParameter.Definition>(List sortFields) { +@EqualsAndHashCode +public final class SortParameter & SortParameter.Definition> { private static final String DEFAULT_SORT_PROPERTY = "id"; private static final Sort.Direction DEFAULT_SORT_DIRETION = Sort.Direction.ASC; + @Accessors(fluent = true) + @Getter + private final List sortFields = new ArrayList<>(); + + public SortParameter(List sortFields) { + if (sortFields == null) { + return; + } + this.sortFields.addAll(sortFields); + } + + private SortParameter() { + } + /** * Creates a {@link SortParameter} that represents an unsorted state. * @@ -26,7 +44,7 @@ public record SortParameter & SortParameter.Definition>(List & Definition> SortParameter unsorted() { - return new SortParameter<>(Collections.emptyList()); + return new SortParameter<>(); } /** @@ -192,7 +210,7 @@ public SortParameter and( * or the provided fallback if it does not. */ public SortParameter or(@NonNull SortParameter fallback) { - return sortFields == null || sortFields.isEmpty() ? fallback : this; + return sortFields.isEmpty() ? fallback : this; } /** @@ -235,23 +253,22 @@ public Sort buildSort(@NonNull Map mapping) { return buildSort(stringMapping, true); } - // SonarLint: Replace this usage of 'Stream.collect(Collectors.toList())' with 'Stream.toList()' and ensure that the list is unmodified. - @SuppressWarnings("java:S6204") private Sort buildSort(@NonNull Map mapping, boolean withDefault) { - if (sortFields == null || sortFields.isEmpty()) { + if (sortFields.isEmpty()) { return withDefault ? getMappedDefaultSort(mapping) : Sort.unsorted(); } var additionalSort = Sort.by( - sortFields.stream() - .filter(sortField -> mapping.containsKey(sortField.property())) - .map(sortField -> new Sort.Order( - sortField.direction(), - mapping.get(sortField.property()), - sortField.nullHandling() - )) - // We do not use .toList() here as we potentially want to modify the sort list later in the StoreImpl - .collect(Collectors.toList()) + new ArrayList<>( + sortFields.stream() + .filter(sortField -> mapping.containsKey(sortField.property())) + .map(sortField -> new Sort.Order( + sortField.direction(), + mapping.get(sortField.property()), + sortField.nullHandling() + )) + .toList() + ) ); if (withDefault) { From 1ee0ce73c7c2710dcc11714d6ae950ce8db816bf Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Wed, 26 Nov 2025 17:08:05 +0100 Subject: [PATCH 5/5] update tests --- .../toolbox/parameter/SortParameterTest.java | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) diff --git a/src/test/java/it/aboutbits/springboot/toolbox/parameter/SortParameterTest.java b/src/test/java/it/aboutbits/springboot/toolbox/parameter/SortParameterTest.java index ae99eb4..9307540 100644 --- a/src/test/java/it/aboutbits/springboot/toolbox/parameter/SortParameterTest.java +++ b/src/test/java/it/aboutbits/springboot/toolbox/parameter/SortParameterTest.java @@ -43,6 +43,182 @@ void unsorted() { } } + @Nested + class By { + @Test + void byMultipleDefinitions_shouldCreateSortParameter() { + // when + var item = SortParameter.by(ESort.Property1, ESort.property3, ESort.Property2); + + // then + assertThat(item.sortFields()).hasSize(3); + assertThat(item.sortFields().get(0).property()).isEqualTo("Property1"); + assertThat(item.sortFields().get(0).direction()).isEqualTo(Sort.Direction.ASC); + assertThat(item.sortFields().get(0).nullHandling()).isEqualTo(Sort.NullHandling.NATIVE); + assertThat(item.sortFields().get(1).property()).isEqualTo("property3"); + assertThat(item.sortFields().get(1).direction()).isEqualTo(Sort.Direction.ASC); + assertThat(item.sortFields().get(1).nullHandling()).isEqualTo(Sort.NullHandling.NATIVE); + assertThat(item.sortFields().get(2).property()).isEqualTo("Property2"); + assertThat(item.sortFields().get(2).direction()).isEqualTo(Sort.Direction.ASC); + assertThat(item.sortFields().get(2).nullHandling()).isEqualTo(Sort.NullHandling.NATIVE); + } + + @Test + void bySingleDefinition_shouldCreateSortParameter() { + // when + var item = SortParameter.by(ESort.Property1); + + // then + assertThat(item.sortFields()).hasSize(1); + assertThat(item.sortFields().get(0).property()).isEqualTo("Property1"); + assertThat(item.sortFields().get(0).direction()).isEqualTo(Sort.Direction.ASC); + assertThat(item.sortFields().get(0).nullHandling()).isEqualTo(Sort.NullHandling.NATIVE); + } + + @Test + void byDefinitionAndDirection_shouldCreateSortParameter() { + // when + var item = SortParameter.by(ESort.Property1, Sort.Direction.DESC); + + // then + assertThat(item.sortFields()).hasSize(1); + assertThat(item.sortFields().get(0).property()).isEqualTo("Property1"); + assertThat(item.sortFields().get(0).direction()).isEqualTo(Sort.Direction.DESC); + assertThat(item.sortFields().get(0).nullHandling()).isEqualTo(Sort.NullHandling.NATIVE); + } + + @Test + void byDefinitionDirectionAndNullHandling_shouldCreateSortParameter() { + // when + var item = SortParameter.by(ESort.Property1, Sort.Direction.DESC, Sort.NullHandling.NULLS_FIRST); + + // then + assertThat(item.sortFields()).hasSize(1); + assertThat(item.sortFields().get(0).property()).isEqualTo("Property1"); + assertThat(item.sortFields().get(0).direction()).isEqualTo(Sort.Direction.DESC); + assertThat(item.sortFields().get(0).nullHandling()).isEqualTo(Sort.NullHandling.NULLS_FIRST); + } + } + + @Nested + class And { + @Test + void andMultipleDefinitions_shouldAddSortFields() { + // given + var item = SortParameter.by(ESort.Property1); + + // when + item.and(ESort.property3, ESort.Property2); + + // then + assertThat(item.sortFields()).hasSize(3); + assertThat(item.sortFields().get(0).property()).isEqualTo("Property1"); + assertThat(item.sortFields().get(1).property()).isEqualTo("property3"); + assertThat(item.sortFields().get(1).direction()).isEqualTo(Sort.Direction.ASC); + assertThat(item.sortFields().get(1).nullHandling()).isEqualTo(Sort.NullHandling.NATIVE); + assertThat(item.sortFields().get(2).property()).isEqualTo("Property2"); + assertThat(item.sortFields().get(2).direction()).isEqualTo(Sort.Direction.ASC); + assertThat(item.sortFields().get(2).nullHandling()).isEqualTo(Sort.NullHandling.NATIVE); + } + + @Test + void andDefinitionAndDirection_shouldAddSortField() { + // given + var item = SortParameter.by(ESort.Property1); + + // when + item.and(ESort.property3, Sort.Direction.DESC); + + // then + assertThat(item.sortFields()).hasSize(2); + assertThat(item.sortFields().get(0).property()).isEqualTo("Property1"); + assertThat(item.sortFields().get(1).property()).isEqualTo("property3"); + assertThat(item.sortFields().get(1).direction()).isEqualTo(Sort.Direction.DESC); + assertThat(item.sortFields().get(1).nullHandling()).isEqualTo(Sort.NullHandling.NATIVE); + } + + @Test + void andDefinitionDirectionAndNullHandling_shouldAddSortField() { + // given + var item = SortParameter.by(ESort.Property1); + + // when + item.and(ESort.property3, Sort.Direction.DESC, Sort.NullHandling.NULLS_LAST); + + // then + assertThat(item.sortFields()).hasSize(2); + assertThat(item.sortFields().get(0).property()).isEqualTo("Property1"); + assertThat(item.sortFields().get(1).property()).isEqualTo("property3"); + assertThat(item.sortFields().get(1).direction()).isEqualTo(Sort.Direction.DESC); + assertThat(item.sortFields().get(1).nullHandling()).isEqualTo(Sort.NullHandling.NULLS_LAST); + } + + @Test + void andChaining_shouldAddMultipleSortFields() { + // given + var item = SortParameter.by(ESort.Property1) + .and(ESort.property3, Sort.Direction.DESC) + .and(ESort.Property2, Sort.Direction.ASC, Sort.NullHandling.NULLS_FIRST); + + // then + assertThat(item.sortFields()).hasSize(3); + assertThat(item.sortFields().get(0).property()).isEqualTo("Property1"); + assertThat(item.sortFields().get(1).property()).isEqualTo("property3"); + assertThat(item.sortFields().get(1).direction()).isEqualTo(Sort.Direction.DESC); + assertThat(item.sortFields().get(2).property()).isEqualTo("Property2"); + assertThat(item.sortFields().get(2).direction()).isEqualTo(Sort.Direction.ASC); + assertThat(item.sortFields().get(2).nullHandling()).isEqualTo(Sort.NullHandling.NULLS_FIRST); + } + } + + @Nested + class Or { + @Test + void orWithEmptySortFields_shouldReturnFallback() { + // given + var item = SortParameter.unsorted(); + var fallback = SortParameter.by(ESort.Property1); + + // when + var result = item.or(fallback); + + // then + assertThat(result).isEqualTo(fallback); + assertThat(result.sortFields()).hasSize(1); + assertThat(result.sortFields().get(0).property()).isEqualTo("Property1"); + } + + @Test + void orWithNullSortFields_shouldReturnFallback() { + // given + var item = new SortParameter(null); + var fallback = SortParameter.by(ESort.Property1); + + // when + var result = item.or(fallback); + + // then + assertThat(result).isEqualTo(fallback); + assertThat(result.sortFields()).hasSize(1); + assertThat(result.sortFields().get(0).property()).isEqualTo("Property1"); + } + + @Test + void orWithNonEmptySortFields_shouldReturnThis() { + // given + var item = SortParameter.by(ESort.property3); + var fallback = SortParameter.by(ESort.Property1); + + // when + var result = item.or(fallback); + + // then + assertThat(result).isEqualTo(item); + assertThat(result.sortFields()).hasSize(1); + assertThat(result.sortFields().get(0).property()).isEqualTo("property3"); + } + } + @Nested class BuildSortWithoutDefault { @Test