diff --git a/src/main/java/it/aboutbits/springboot/toolbox/util/CollectUtil.java b/src/main/java/it/aboutbits/springboot/toolbox/util/CollectUtil.java
new file mode 100644
index 0000000..42f7ed0
--- /dev/null
+++ b/src/main/java/it/aboutbits/springboot/toolbox/util/CollectUtil.java
@@ -0,0 +1,114 @@
+package it.aboutbits.springboot.toolbox.util;
+
+import lombok.NonNull;
+import org.springframework.data.util.Streamable;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * This is a collection of tools to simply collect a property to a new collection or stream.
+ * The common usage would, for example, be to collect the ids of all items in a list to a set.
+ *
+ * The returned collections are unmodifiable.
+ */
+public final class CollectUtil {
+ private CollectUtil() {
+ }
+
+ public static Set collectToSet(@NonNull Collection items, @NonNull Function mapper) {
+ return items.stream()
+ .map(mapper)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toUnmodifiableSet());
+ }
+
+ public static Set collectToSet(@NonNull Streamable items, @NonNull Function mapper) {
+ return items.stream()
+ .map(mapper)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toUnmodifiableSet());
+ }
+
+ public static Set collectToSet(@NonNull Stream items, @NonNull Function mapper) {
+ return items
+ .map(mapper)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toUnmodifiableSet());
+ }
+
+ public static List collectToList(@NonNull Collection items, @NonNull Function mapper) {
+ return items.stream()
+ .map(mapper)
+ .filter(Objects::nonNull)
+ .toList();
+ }
+
+ public static List collectToList(@NonNull Streamable items, @NonNull Function mapper) {
+ return items.stream()
+ .map(mapper)
+ .filter(Objects::nonNull)
+ .toList();
+ }
+
+ public static List collectToList(@NonNull Stream items, @NonNull Function mapper) {
+ return items
+ .map(mapper)
+ .filter(Objects::nonNull)
+ .toList();
+ }
+
+ public static Stream collectToStream(@NonNull Collection items, @NonNull Function mapper) {
+ return items.stream()
+ .map(mapper)
+ .filter(Objects::nonNull);
+ }
+
+ public static Stream collectToStream(@NonNull Streamable items, @NonNull Function mapper) {
+ return items.stream()
+ .map(mapper)
+ .filter(Objects::nonNull);
+ }
+
+ public static Stream collectToStream(@NonNull Stream items, @NonNull Function mapper) {
+ return items
+ .map(mapper)
+ .filter(Objects::nonNull);
+ }
+
+ public static Map collectToMap(
+ @NonNull Collection items,
+ @NonNull Function keyMapper,
+ @NonNull Function valueMapper
+ ) {
+ return items.stream()
+ .filter(Objects::nonNull)
+ .collect(Collectors.toMap(keyMapper, valueMapper));
+ }
+
+ public static Map collectToMap(
+ @NonNull Streamable items,
+ @NonNull Function keyMapper,
+ @NonNull Function valueMapper
+ ) {
+ return items.stream()
+ .filter(Objects::nonNull)
+ .collect(Collectors.toMap(keyMapper, valueMapper));
+ }
+
+ public static Map collectToMap(
+ @NonNull Stream items,
+ @NonNull Function keyMapper,
+ @NonNull Function valueMapper
+ ) {
+ return items
+ .filter(Objects::nonNull)
+ .collect(Collectors.toMap(keyMapper, valueMapper));
+ }
+}
diff --git a/src/test/java/it/aboutbits/springboot/toolbox/util/CollectUtilTest.java b/src/test/java/it/aboutbits/springboot/toolbox/util/CollectUtilTest.java
new file mode 100644
index 0000000..dfb3a17
--- /dev/null
+++ b/src/test/java/it/aboutbits/springboot/toolbox/util/CollectUtilTest.java
@@ -0,0 +1,481 @@
+package it.aboutbits.springboot.toolbox.util;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.springframework.data.util.Streamable;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class CollectUtilTest {
+ @Nested
+ class CollectToSet {
+ @Test
+ @DisplayName("Should convert Collection to Set using mapper")
+ void shouldConvertCollectionToSetUsingMapper() {
+ // given
+ var numbers = Arrays.asList(1, 2, 3, 4, 5);
+ var mapper = (Function) Object::toString;
+
+ // when
+ var result = CollectUtil.collectToSet(numbers, mapper);
+
+ // then
+ assertThat(result).containsExactlyInAnyOrder("1", "2", "3", "4", "5");
+ }
+
+ @Test
+ @DisplayName("Should convert Collection to Set using mapper")
+ void shouldConvertCollectionToSetUsingMapperAndFilterNullValues() {
+ // given
+ var numbers = Arrays.asList("a", "b", "c", "d");
+ var mapper = (Function) Object::toString;
+
+ // when
+ var result = CollectUtil.collectToSet(numbers, mapper);
+
+ // then
+ assertThat(result).containsExactlyInAnyOrder("a", "b", "c", "d");
+ }
+
+ @Test
+ @DisplayName("Should handle empty Collection when converting to Set")
+ void shouldHandleEmptyCollectionWhenConvertingToSet() {
+ // given
+ var emptyList = Collections.emptyList();
+ var mapper = (Function) Object::toString;
+
+ // when
+ var result = CollectUtil.collectToSet(emptyList, mapper);
+
+ // then
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ @DisplayName("Should remove duplicates when converting Collection to Set")
+ void shouldRemoveDuplicatesWhenConvertingCollectionToSet() {
+ // given
+ var numbersWithDuplicates = Arrays.asList(1, 2, 2, 3, 3, 3);
+ var mapper = (Function) Object::toString;
+
+ // when
+ var result = CollectUtil.collectToSet(numbersWithDuplicates, mapper);
+
+ // then
+ assertThat(result).containsExactlyInAnyOrder("1", "2", "3");
+ assertEquals(3, result.size());
+ }
+
+ @Test
+ @DisplayName("Should convert Streamable to Set using mapper")
+ void shouldConvertStreamableToSetUsingMapper() {
+ // given
+ var numbers = Streamable.of(1, 2, 3, 4, 5);
+ var mapper = (Function) Object::toString;
+
+ // when
+ var result = CollectUtil.collectToSet(numbers, mapper);
+
+ // then
+ assertThat(result).containsExactlyInAnyOrder("1", "2", "3", "4", "5");
+ }
+
+ @Test
+ @DisplayName("Should convert Streamable to Set using mapper")
+ void shouldConvertStreamableToSetUsingMapperAndFilterNullValues() {
+ // given
+ var numbers = Streamable.of("a", "b", "c", "d");
+ var mapper = (Function) Object::toString;
+
+ // when
+ var result = CollectUtil.collectToSet(numbers, mapper);
+
+ // then
+ assertThat(result).containsExactlyInAnyOrder("a", "b", "c", "d");
+ }
+
+ @Test
+ @DisplayName("Should handle empty Streamable when converting to Set")
+ void shouldHandleEmptyStreamableWhenConvertingToSet() {
+ // given
+ var emptyStreamable = Streamable.empty();
+ var mapper = (Function) Object::toString;
+
+ // when
+ var result = CollectUtil.collectToSet(emptyStreamable, mapper);
+
+ // then
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ @DisplayName("Should convert Stream to Set using mapper")
+ void shouldConvertStreamToSetUsingMapper() {
+ // given
+ var numbers = java.util.stream.Stream.of(1, 2, 3, 4, 5);
+ var mapper = (Function) Object::toString;
+
+ // when
+ var result = CollectUtil.collectToSet(numbers, mapper);
+
+ // then
+ assertThat(result).containsExactlyInAnyOrder("1", "2", "3", "4", "5");
+ }
+
+ @Test
+ @DisplayName("Should handle empty Stream when converting to Set")
+ void shouldHandleEmptyStreamWhenConvertingToSet() {
+ // given
+ var emptyStream = java.util.stream.Stream.empty();
+ var mapper = (Function) Object::toString;
+
+ // when
+ var result = CollectUtil.collectToSet(emptyStream, mapper);
+
+ // then
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ @DisplayName("Should remove duplicates when converting Stream to Set")
+ void shouldRemoveDuplicatesWhenConvertingStreamToSet() {
+ // given
+ var numbersWithDuplicates = java.util.stream.Stream.of(1, 2, 2, 3, 3, 3);
+ var mapper = (Function) Object::toString;
+
+ // when
+ var result = CollectUtil.collectToSet(numbersWithDuplicates, mapper);
+
+ // then
+ assertThat(result).containsExactlyInAnyOrder("1", "2", "3");
+ assertEquals(3, result.size());
+ }
+ }
+
+ @Nested
+ class CollectToList {
+ @Test
+ @DisplayName("Should convert Collection to List using mapper")
+ void shouldConvertCollectionToListUsingMapper() {
+ // given
+ var numbers = Arrays.asList(1, 2, 3, 4, 5);
+ var mapper = (Function) Object::toString;
+
+ // when
+ var result = CollectUtil.collectToList(numbers, mapper);
+
+ // then
+ assertThat(result).containsExactly("1", "2", "3", "4", "5");
+ }
+
+ @Test
+ @DisplayName("Should handle empty Collection when converting to List")
+ void shouldHandleEmptyCollectionWhenConvertingToList() {
+ // given
+ var emptyList = Collections.emptyList();
+ var mapper = (Function) Object::toString;
+
+ // when
+ var result = CollectUtil.collectToList(emptyList, mapper);
+
+ // then
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ @DisplayName("Should preserve duplicates when converting Collection to List")
+ void shouldPreserveDuplicatesWhenConvertingCollectionToList() {
+ // given
+ var numbersWithDuplicates = Arrays.asList(1, 2, 2, 3, 3, 3);
+ var mapper = (Function) Object::toString;
+
+ // when
+ var result = CollectUtil.collectToList(numbersWithDuplicates, mapper);
+
+ // then
+ assertThat(result).containsExactly("1", "2", "2", "3", "3", "3");
+ assertEquals(6, result.size());
+ }
+
+ @Test
+ @DisplayName("Should convert Streamable to List using mapper")
+ void shouldConvertStreamableToListUsingMapper() {
+ // given
+ var numbers = Streamable.of(1, 2, 3, 4, 5);
+ var mapper = (Function) Object::toString;
+
+ // when
+ var result = CollectUtil.collectToList(numbers, mapper);
+
+ // then
+ assertThat(result).containsExactly("1", "2", "3", "4", "5");
+ }
+
+ @Test
+ @DisplayName("Should handle empty Streamable when converting to List")
+ void shouldHandleEmptyStreamableWhenConvertingToList() {
+ // given
+ var emptyStreamable = Streamable.empty();
+ var mapper = (Function) Object::toString;
+
+ // when
+ var result = CollectUtil.collectToList(emptyStreamable, mapper);
+
+ // then
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ @DisplayName("Should convert Stream to List using mapper")
+ void shouldConvertStreamToListUsingMapper() {
+ // given
+ var numbers = java.util.stream.Stream.of(1, 2, 3, 4, 5);
+ var mapper = (Function) Object::toString;
+
+ // when
+ var result = CollectUtil.collectToList(numbers, mapper);
+
+ // then
+ assertThat(result).containsExactly("1", "2", "3", "4", "5");
+ }
+
+ @Test
+ @DisplayName("Should handle empty Stream when converting to List")
+ void shouldHandleEmptyStreamWhenConvertingToList() {
+ // given
+ var emptyStream = java.util.stream.Stream.empty();
+ var mapper = (Function) Object::toString;
+
+ // when
+ var result = CollectUtil.collectToList(emptyStream, mapper);
+
+ // then
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ @DisplayName("Should preserve duplicates when converting Stream to List")
+ void shouldPreserveDuplicatesWhenConvertingStreamToList() {
+ // given
+ var numbersWithDuplicates = java.util.stream.Stream.of(1, 2, 2, 3, 3, 3);
+ var mapper = (Function) Object::toString;
+
+ // when
+ var result = CollectUtil.collectToList(numbersWithDuplicates, mapper);
+
+ // then
+ assertThat(result).containsExactly("1", "2", "2", "3", "3", "3");
+ assertEquals(6, result.size());
+ }
+ }
+
+ @Nested
+ class CollectToStream {
+ @Test
+ @DisplayName("Should convert Collection to Stream using mapper")
+ void shouldConvertCollectionToStreamUsingMapper() {
+ // given
+ var numbers = Arrays.asList(1, 2, 3, 4, 5);
+ var mapper = (Function) Object::toString;
+
+ // when
+ var resultStream = CollectUtil.collectToStream(numbers, mapper);
+ var result = resultStream.toList();
+
+ // then
+ assertThat(result).containsExactly("1", "2", "3", "4", "5");
+ }
+
+ @Test
+ @DisplayName("Should handle empty Collection when converting to Stream")
+ void shouldHandleEmptyCollectionWhenConvertingToStream() {
+ // given
+ var emptyList = Collections.emptyList();
+ var mapper = (Function) Object::toString;
+
+ // when
+ var resultStream = CollectUtil.collectToStream(emptyList, mapper);
+ var result = resultStream.toList();
+
+ // then
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ @DisplayName("Should convert Streamable to Stream using mapper")
+ void shouldConvertStreamableToStreamUsingMapper() {
+ // given
+ var numbers = Streamable.of(1, 2, 3, 4, 5);
+ var mapper = (Function) Object::toString;
+
+ // when
+ var resultStream = CollectUtil.collectToStream(numbers, mapper);
+ var result = resultStream.toList();
+
+ // then
+ assertThat(result).containsExactly("1", "2", "3", "4", "5");
+ }
+
+ @Test
+ @DisplayName("Should handle empty Streamable when converting to Stream")
+ void shouldHandleEmptyStreamableWhenConvertingToStream() {
+ // given
+ var emptyStreamable = Streamable.empty();
+ var mapper = (Function) Object::toString;
+
+ // when
+ var resultStream = CollectUtil.collectToStream(emptyStreamable, mapper);
+ var result = resultStream.toList();
+
+ // then
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ @DisplayName("Should convert Stream to Stream using mapper")
+ void shouldConvertStreamToStreamUsingMapper() {
+ // given
+ var numbers = java.util.stream.Stream.of(1, 2, 3, 4, 5);
+ var mapper = (Function) Object::toString;
+
+ // when
+ var resultStream = CollectUtil.collectToStream(numbers, mapper);
+ var result = resultStream.toList();
+
+ // then
+ assertThat(result).containsExactly("1", "2", "3", "4", "5");
+ }
+
+ @Test
+ @DisplayName("Should handle empty Stream when converting to Stream")
+ void shouldHandleEmptyStreamWhenConvertingToStream() {
+ // given
+ var emptyStream = java.util.stream.Stream.empty();
+ var mapper = (Function) Object::toString;
+
+ // when
+ var resultStream = CollectUtil.collectToStream(emptyStream, mapper);
+ var result = resultStream.toList();
+
+ // then
+ assertTrue(result.isEmpty());
+ }
+ }
+
+ @Nested
+ class CollectToMap {
+ @Test
+ @DisplayName("Should convert Collection to Map using key and value mappers")
+ void shouldConvertCollectionToMapUsingMappers() {
+ // given
+ var items = Arrays.asList("a", "bb", "ccc");
+
+ // when
+ var result = CollectUtil.collectToMap(items, String::length, Function.identity());
+
+ // then
+ assertThat(result)
+ .containsEntry(1, "a")
+ .containsEntry(2, "bb")
+ .containsEntry(3, "ccc")
+ .hasSize(3);
+ }
+
+ @Test
+ @DisplayName("Should throw on duplicate keys according to Collectors.toMap default behavior")
+ void shouldThrowOnDuplicateKeys() {
+ // given
+ var items = Arrays.asList("a", "b"); // both have length of 1
+
+ // when / then
+ assertThatIllegalStateException().isThrownBy(
+ () ->
+ CollectUtil.collectToMap(
+ items,
+ String::length,
+ Function.identity()
+ )
+ );
+ }
+
+ @Test
+ @DisplayName("Should convert Streamable to Map using key and value mappers")
+ void shouldConvertStreamableToMapUsingMappers() {
+ // given
+ var items = Streamable.of("x", "yy");
+
+ // when
+ var result = CollectUtil.collectToMap(items, String::length, Function.identity());
+
+ // then
+ assertThat(result)
+ .containsEntry(1, "x")
+ .containsEntry(2, "yy")
+ .hasSize(2);
+ }
+
+ @Test
+ @DisplayName("Should convert Stream to Map using key and value mappers")
+ void shouldConvertStreamToMapUsingMappers() {
+ // given
+ var items = Stream.of("m", "nn");
+
+ // when
+ var result = CollectUtil.collectToMap(items, String::length, Function.identity());
+
+ // then
+ assertThat(result)
+ .containsEntry(1, "m")
+ .containsEntry(2, "nn")
+ .hasSize(2);
+ }
+
+ @Test
+ @DisplayName("Should return empty map for empty collection")
+ void shouldReturnEmptyMapForEmptyCollection() {
+ // given
+ var items = Collections.emptyList();
+
+ // when
+ var result = CollectUtil.collectToMap(items, String::length, Function.identity());
+
+ // then
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ @DisplayName("Should return empty map for empty streamable")
+ void shouldReturnEmptyMapForEmptyStreamable() {
+ // given
+ var items = Streamable.empty();
+
+ // when
+ var result = CollectUtil.collectToMap(items, String::length, Function.identity());
+
+ // then
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ @DisplayName("Should return empty map for empty stream")
+ void shouldReturnEmptyMapForEmptyStream() {
+ // given
+ var items = Stream.empty();
+
+ // when
+ var result = CollectUtil.collectToMap(items, String::length, Function.identity());
+
+ // then
+ assertThat(result).isEmpty();
+ }
+ }
+}