Skip to content

Commit

Permalink
Add KiwiMaps methods to get a non-null value or throw an exception (#…
Browse files Browse the repository at this point in the history
…1097)

* Add getOrThrow that accepts a map and key
* Add getOrThrow that accepts a map, key, and RuntimeException supplier
* Add getOrThrowChecked that accepts a map, key, and Exception supplier

Closes #1094
  • Loading branch information
sleberknight authored Jan 13, 2024
1 parent 3f27ff4 commit 5114458
Show file tree
Hide file tree
Showing 2 changed files with 259 additions and 4 deletions.
96 changes: 96 additions & 0 deletions src/main/java/org/kiwiproject/collect/KiwiMaps.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@

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;

import java.util.Collections;
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
Expand Down Expand Up @@ -255,4 +259,96 @@ public static <K, V> boolean keyExistsWithNonNullValue(Map<K, V> map, K key) {
private static <K, V> boolean keyExists(Map<K, V> map, K key) {
return isNotNullOrEmpty(map) && map.containsKey(key);
}

/**
* Returns the value to which the specified key is mapped.
* <p>
* 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 <K> the type of the keys in the map
* @param <V> 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 <K, V> V getOrThrow(Map<K, V> 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.
* <p>
* 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 <K> the type of the keys in the map
* @param <V> the type of the values in the map
* @param <E> 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 <K, V, E extends RuntimeException> V getOrThrow(Map<K, V> map,
K key,
Supplier<E> exceptionSupplier) {

return getOrThrowChecked(map, key, exceptionSupplier);
}

/**
* Returns the value to which the specified key is mapped.
* <p>
* 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 <K> the type of the keys in the map
* @param <V> the type of the values in the map
* @param <E> 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 <K, V, E extends Exception> V getOrThrowChecked(Map<K, V> map,
K key,
Supplier<E> 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 <K, V> void checkMapAndKeyArgsNotNull(Map<K, V> map, K key) {
checkArgumentNotNull(map, "map must not be null");
checkArgumentNotNull(key, "key must not be null");
}
}
167 changes: 163 additions & 4 deletions src/test/java/org/kiwiproject/collect/KiwiMapsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -65,20 +72,20 @@ void testIsNotNullOrEmpty_WhenContainsSomeMappings() {
@Test
void testNewHashMap() {
Object[] items = wordToNumberArray();
Map<String, Integer> hashMap = KiwiMaps.newHashMap(items);
Map<String, Integer> hashMap = newHashMap(items);
assertThat(hashMap)
.isExactlyInstanceOf(HashMap.class)
.containsAllEntriesOf(newWordNumberMap());
}

@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)");
}
Expand Down Expand Up @@ -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();
}
}
Expand Down Expand Up @@ -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<IllegalStateException> 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<MyRuntimeException> 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<Exception> 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<MyException> 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 {
}
}
}

0 comments on commit 5114458

Please sign in to comment.