diff --git a/src/main/java/org/kiwiproject/collect/KiwiMaps.java b/src/main/java/org/kiwiproject/collect/KiwiMaps.java index cad46f3d..b68b5406 100644 --- a/src/main/java/org/kiwiproject/collect/KiwiMaps.java +++ b/src/main/java/org/kiwiproject/collect/KiwiMaps.java @@ -2,7 +2,9 @@ import static java.util.Objects.isNull; import static java.util.Objects.nonNull; +import static org.kiwiproject.base.KiwiPreconditions.checkArgumentNotNull; import static org.kiwiproject.base.KiwiPreconditions.checkEvenItemCount; +import static org.kiwiproject.base.KiwiStrings.f; import lombok.experimental.UtilityClass; @@ -10,10 +12,12 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; +import java.util.NoSuchElementException; import java.util.SortedMap; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.function.Supplier; /** * Utility methods for working with {@link Map} instances @@ -255,4 +259,96 @@ public static boolean keyExistsWithNonNullValue(Map map, K key) { private static boolean keyExists(Map map, K key) { return isNotNullOrEmpty(map) && map.containsKey(key); } + + /** + * Returns the value to which the specified key is mapped. + *

+ * If the map is null or empty, or it does not contain the specified key, or the value + * associated with the key is null, a {@link NoSuchElementException} is thrown. + * + * @param map the map + * @param key the key whose associated value is to be returned + * @param the type of the keys in the map + * @param the type of the values in the map + * @return the value to which the specified key is mapped + * @throws IllegalArgumentException if either argument is null + * @throws NoSuchElementException if the map is null or empty, does not contain the specified key, + * or the value associated with the key is null + */ + public static V getOrThrow(Map map, K key) { + checkMapAndKeyArgsNotNull(map, key); + + if (map.containsKey(key)) { + var v = map.get(key); + if (isNull(v)) { + throw new NoSuchElementException(f("value associated with key '{}' is null", key)); + } + return v; + } + + throw new NoSuchElementException(f("key '{}' does not exist in map", key)); + } + + /** + * Returns the value to which the specified key is mapped. + *

+ * If the map is null or empty, or it does not contain the specified key, or the value + * associated with the key is null, the exception provided by the given Supplier is thrown. + * + * @param map the map + * @param key the key whose associated value is to be returned + * @param exceptionSupplier supplies a RuntimeException when there is no value to return + * @param the type of the keys in the map + * @param the type of the values in the map + * @param the type of RuntimeException + * @return the value to which the specified key is mapped + * @throws IllegalArgumentException if any of the arguments is null + * @throws E if the map is null or empty, does not contain the specified key, + * or the value associated with the key is null + */ + public static V getOrThrow(Map map, + K key, + Supplier exceptionSupplier) { + + return getOrThrowChecked(map, key, exceptionSupplier); + } + + /** + * Returns the value to which the specified key is mapped. + *

+ * If the map is null or empty, or it does not contain the specified key, or the value + * associated with the key is null, the exception provided by the given Supplier is thrown. + * + * @param map the map + * @param key the key whose associated value is to be returned + * @param exceptionSupplier supplies an Exception when there is no value to return + * @param the type of the keys in the map + * @param the type of the values in the map + * @param the type of Exception + * @return the value to which the specified key is mapped + * @throws IllegalArgumentException if any of the arguments is null + * @throws E if the map is null or empty, does not contain the specified key, + * or the value associated with the key is null + */ + public static V getOrThrowChecked(Map map, + K key, + Supplier exceptionSupplier) throws E { + checkMapAndKeyArgsNotNull(map, key); + checkArgumentNotNull(exceptionSupplier, "exceptionSupplier must not be null"); + + if (map.containsKey(key)) { + var v = map.get(key); + if (isNull(v)) { + throw exceptionSupplier.get(); + } + return v; + } + + throw exceptionSupplier.get(); + } + + private static void checkMapAndKeyArgsNotNull(Map map, K key) { + checkArgumentNotNull(map, "map must not be null"); + checkArgumentNotNull(key, "key must not be null"); + } } diff --git a/src/test/java/org/kiwiproject/collect/KiwiMapsTest.java b/src/test/java/org/kiwiproject/collect/KiwiMapsTest.java index 86caedfa..289995f3 100644 --- a/src/test/java/org/kiwiproject/collect/KiwiMapsTest.java +++ b/src/test/java/org/kiwiproject/collect/KiwiMapsTest.java @@ -2,9 +2,14 @@ import static java.util.stream.Collectors.toList; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.entry; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.kiwiproject.collect.KiwiMaps.newHashMap; +import lombok.experimental.StandardException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -20,10 +25,12 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import java.util.SortedMap; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.function.Supplier; @DisplayName("KiwiMaps") class KiwiMapsTest { @@ -65,7 +72,7 @@ void testIsNotNullOrEmpty_WhenContainsSomeMappings() { @Test void testNewHashMap() { Object[] items = wordToNumberArray(); - Map hashMap = KiwiMaps.newHashMap(items); + Map hashMap = newHashMap(items); assertThat(hashMap) .isExactlyInstanceOf(HashMap.class) .containsAllEntriesOf(newWordNumberMap()); @@ -73,12 +80,12 @@ void testNewHashMap() { @Test void testNewHashMap_WhenNoItems() { - assertThat(KiwiMaps.newHashMap()).isEmpty(); + assertThat(newHashMap()).isEmpty(); } @Test void testNewHashMap_WhenOddNumberOfItems() { - assertThatThrownBy(() -> KiwiMaps.newHashMap("one", 1, "two")) + assertThatThrownBy(() -> newHashMap("one", 1, "two")) .isExactlyInstanceOf(IllegalArgumentException.class) .hasMessage("must be an even number of items (received 3)"); } @@ -225,7 +232,7 @@ void whenKeyExistsAndHasNullValue() { @Test void whenObjectKeyExistsAndHasNullValue() { var key = new Object(); - var map = KiwiMaps.newHashMap(key, null); + var map = newHashMap(key, null); assertThat(KiwiMaps.keyExistsWithNullValue(map, key)).isTrue(); } } @@ -419,4 +426,156 @@ void whenObjectKeyExistsAndHasNullValue() { } } } + + @Nested + class GetOrThrow { + + @Test + void shouldRequireArguments() { + assertAll( + () -> assertThatIllegalArgumentException() + .isThrownBy(() -> KiwiMaps.getOrThrow(null, "aKey")), + () -> assertThatIllegalArgumentException() + .isThrownBy(() -> KiwiMaps.getOrThrow(Map.of("count", 42), null)) + ); + } + + @Test + void shouldReturnValue() { + var fruitCounts = Map.of("orange", 12, "apple", 6); + + assertAll( + () -> assertThat(KiwiMaps.getOrThrow(fruitCounts, "orange")) + .isEqualTo(12), + () -> assertThat(KiwiMaps.getOrThrow(fruitCounts, "apple")) + .isEqualTo(6) + ); + } + + @Test + void shouldThrowNoSuchElementException_WhenKeyDoesNotExist() { + var fruitCounts = newHashMap("orange", 12, "apple", 6); + + assertThatExceptionOfType(NoSuchElementException.class) + .isThrownBy(() -> KiwiMaps.getOrThrow(fruitCounts, "dragon fruit")) + .withMessage("key 'dragon fruit' does not exist in map"); + } + + @Test + void shouldThrowNoSuchElementException_WhenValueIsNull() { + var fruitCounts = newHashMap("orange", 12, "apple", 6, "papaya", null); + + assertThatExceptionOfType(NoSuchElementException.class) + .isThrownBy(() -> KiwiMaps.getOrThrow(fruitCounts, "papaya")) + .withMessage("value associated with key 'papaya' is null"); + } + } + + @Nested + class GetOrThrowCustomRuntimeException { + + @Test + void shouldRequireArguments() { + assertAll( + () -> assertThatIllegalArgumentException() + .isThrownBy(() -> KiwiMaps.getOrThrow(null, "aKey", IllegalStateException::new)), + () -> assertThatIllegalArgumentException() + .isThrownBy(() -> KiwiMaps.getOrThrow(Map.of("count", 42), null, IllegalStateException::new)), + () -> assertThatIllegalArgumentException() + .isThrownBy(() -> KiwiMaps.getOrThrow(Map.of("count", 42), "count", null)) + ); + } + + @Test + void shouldReturnValue() { + var fruitCounts = Map.of("mango", 10, "kiwi", 7); + + assertAll( + () -> assertThat(KiwiMaps.getOrThrow(fruitCounts, "mango", RuntimeException::new)) + .isEqualTo(10), + () -> assertThat(KiwiMaps.getOrThrow(fruitCounts, "kiwi", IllegalStateException::new)) + .isEqualTo(7) + ); + } + + @Test + void shouldThrowCustomRuntimeException_WhenKeyDoesNotExist() { + var fruitCounts = newHashMap("orange", 12, "apple", 6); + + Supplier exceptionSupplier = + () -> new IllegalStateException("missing key or null value"); + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> KiwiMaps.getOrThrow(fruitCounts, "dragon fruit", exceptionSupplier)) + .withMessage("missing key or null value"); + } + + @Test + void shouldThrowCustomRuntimeException_WhenValueIsNull() { + var fruitCounts = newHashMap("orange", 12, "apple", 6, "papaya", null); + + Supplier exceptionSupplier = + () -> new MyRuntimeException("missing key or null value"); + assertThatExceptionOfType(MyRuntimeException.class) + .isThrownBy(() -> KiwiMaps.getOrThrow(fruitCounts, "papaya", exceptionSupplier)) + .withMessage("missing key or null value"); + } + + @StandardException + static class MyRuntimeException extends RuntimeException { + } + } + + @Nested + class GetOrThrowCustomCheckedException { + + @Test + void shouldRequireArguments() { + assertAll( + () -> assertThatIllegalArgumentException() + .isThrownBy(() -> KiwiMaps.getOrThrowChecked(null, "aKey", Exception::new)), + () -> assertThatIllegalArgumentException() + .isThrownBy(() -> KiwiMaps.getOrThrowChecked(Map.of("count", 42), null, Exception::new)), + () -> assertThatIllegalArgumentException() + .isThrownBy(() -> KiwiMaps.getOrThrowChecked(Map.of("count", 42), "count", null)) + ); + } + + @Test + void shouldReturnValue() { + var fruitCounts = Map.of("papaya", 4, "guava", 3); + + assertAll( + () -> assertThat(KiwiMaps.getOrThrowChecked(fruitCounts, "papaya", Exception::new)) + .isEqualTo(4), + () -> assertThat(KiwiMaps.getOrThrowChecked(fruitCounts, "guava", Exception::new)) + .isEqualTo(3) + ); + } + + @Test + void shouldThrowCustomException_WhenKeyDoesNotExist() { + var fruitCounts = newHashMap("orange", 12, "apple", 6); + + Supplier exceptionSupplier = + () -> new Exception("missing key or null value"); + assertThatExceptionOfType(Exception.class) + .isThrownBy(() -> KiwiMaps.getOrThrowChecked(fruitCounts, "dragon fruit", exceptionSupplier)) + .withMessage("missing key or null value"); + } + + @Test + void shouldThrowCustomException_WhenValueIsNull() { + var fruitCounts = newHashMap("orange", 12, "apple", 6, "papaya", null); + + Supplier exceptionSupplier = + () -> new MyException("missing key or null value"); + assertThatExceptionOfType(MyException.class) + .isThrownBy(() -> KiwiMaps.getOrThrowChecked(fruitCounts, "papaya", exceptionSupplier)) + .withMessage("missing key or null value"); + } + + @StandardException + static class MyException extends Exception { + } + } }