SearchUtils.java

package com.renomad.minum.utils;

import java.util.List;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.function.Predicate;
import java.util.stream.Stream;

import static com.renomad.minum.utils.Invariants.mustBeTrue;

/**
 * Utilities for searching collections of data
 */
public final class SearchUtils {

    private SearchUtils() {
        // not meant to be instantiated
    }

    /**
     * This helper method will give you the one item in this list, or
     * null if there are none.  If there's more than 1, it will throw
     * an exception.  This is for those times when we absolutely expect
     * there to be just one of a thing in a database, like if we're searching
     * for Persons by id.
     * @param searchPredicate a {@link Predicate} run to search for an element in the stream.
     * @throws InvariantException if there are two or more results found
     */
    public static <T> T findExactlyOne(Stream<T> streamOfSomething, Predicate<? super T> searchPredicate) {
        return findExactlyOne(streamOfSomething, searchPredicate, () -> null);
    }

    /**
     * This is similar to {@link #findExactlyOne(Stream, Predicate)} except that you
     * can provide what gets returned if there are none found - so instead of
     * returning null, it can return something else.
     * <br>
     * The values will be pre-filtered to skip any null values.
     * <br>
     * @param alternate a {@link Callable} that will be run when no elements were found.
     * @param searchPredicate a {@link Predicate} run to search for an element in the stream.
     * @throws InvariantException if there are two or more results found
     */
    public static <T> T findExactlyOne(Stream<T> streamOfSomething, Predicate<? super T> searchPredicate, Callable<T> alternate) {
        List<T> listOfThings = streamOfSomething.filter(Objects::nonNull).filter(searchPredicate).toList();
        mustBeTrue(listOfThings.isEmpty() || listOfThings.size() == 1, "Must be zero or one of this thing, or it's a bug.  We found a size of " + listOfThings.size());
        if (listOfThings.isEmpty()) {
            T returnValue;
            try {
                returnValue = alternate.call();
            } catch (Exception ex) {
                throw new UtilsException(ex);
            }
            return returnValue;
        } else {
            return listOfThings.getFirst();
        }
    }
}