Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.6</version>
<version>3.5.7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

Expand Down Expand Up @@ -78,7 +79,7 @@
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.8.13</version>
<version>2.8.14</version>
</dependency>

<!-- Testing -->
Expand Down Expand Up @@ -115,7 +116,7 @@
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.21.3</version>
<version>2.0.2</version>
<scope>test</scope>
<exclusions>
<exclusion>
Expand All @@ -136,14 +137,14 @@
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.21.3</version>
<artifactId>testcontainers-junit-jupiter</artifactId>
<version>2.0.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.21.3</version>
<artifactId>testcontainers-postgresql</artifactId>
<version>2.0.2</version>
<scope>test</scope>
</dependency>
</dependencies>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<T extends Enum<?> & SortParameter.Definition>(List<SortField> 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<T extends Enum<?> & 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<SortField> sortFields = new ArrayList<>();

public SortParameter(List<SortField> sortFields) {
if (sortFields == null) {
return;
}
this.sortFields.addAll(sortFields);
}

private SortParameter() {
}

/**
* Creates a {@link SortParameter} that represents an unsorted state.
*
* @param <T> a type that extends both {@link Enum} and {@link Definition}.
* @return an instance of {@link SortParameter} configured with no sorting fields.
*/
public static <T extends Enum<?> & Definition> SortParameter<T> 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 <T> 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 <T extends Enum<?> & Definition> SortParameter<T> by(
@NonNull T... sortDefinitions
Expand All @@ -36,6 +73,42 @@ public static <T extends Enum<?> & Definition> SortParameter<T> 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 <T> 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 <T extends Enum<?> & Definition> SortParameter<T> 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 <T> 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 <T extends Enum<?> & Definition> SortParameter<T> by(
@NonNull T sortDefinition,
@NonNull Sort.Direction direction,
Expand All @@ -51,10 +124,105 @@ public static <T extends Enum<?> & Definition> SortParameter<T> 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<T> 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<T> 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<T> 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<T> or(@NonNull SortParameter<T> 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<T, String> mapping) {
var stringMapping = mapping.entrySet().stream()
.collect(Collectors.toMap(
Expand All @@ -65,6 +233,16 @@ public Sort buildSortWithoutDefault(@NonNull Map<T, String> 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.
* <p>
* 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<T, String> mapping) {
var stringMapping = mapping.entrySet().stream()
.collect(Collectors.toMap(
Expand All @@ -75,41 +253,47 @@ public Sort buildSort(@NonNull Map<T, String> 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<String, String> 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()
)
Comment on lines +262 to +271

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds additional overhead.

The previous code worked and returned a mutable ArrayList directly, but now we produce an immutable one and then copy it into a new mutable ArrayList.

We could simply remove the comment and create a test that modifies the returned sort to validate that it is mutable.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Collectors.toList() explicitly says that

[...]
There are no guarantees on the type, mutability,
     * serializability, or thread-safety of the {@code List} returned;
[...]

So this is the only way to make sure that it is mutable.

@ThoSap ThoSap Nov 27, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know about this line.

All the OpenJDK implementations return a mutable ArrayList.
There is no JDK in the wild that returns an immutable list.

This is why .toList() and .collect(Collectors.toUnmodifiableList()) exist.

https://gemini.google.com/share/c6bc01c56a4c

Comment on lines -86 to +271

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had Hide whitespace on, so the marked code did not include the previous code.

);

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<String, String> 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,
@NonNull Sort.NullHandling nullHandling
) {
}

/**
* Interface to give enums the purpose of listing sortable keys.
*/
public interface Definition {
}
}
Loading