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 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..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,25 +1,62 @@ 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; import java.util.stream.Stream; -public record SortParameter & SortParameter.Definition>(List sortFields) { +/** + * 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. + */ +@EqualsAndHashCode +public final class SortParameter & SortParameter.Definition> { 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; + @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. + * + * @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()); + return new SortParameter<>(); } + /** + * 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 +73,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 +124,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; + return 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 +233,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( @@ -75,34 +253,37 @@ 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()) { - return withDefault ? DEFAULT_SORT : Sort.unsorted(); + 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) { 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 +291,9 @@ public record SortField( ) { } + /** + * Interface to give enums the purpose of listing sortable keys. + */ public interface Definition { } } 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 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 { -}