ExUnwrap.java

package net.dapete.exceptional.wrap;

import org.jspecify.annotations.Nullable;

import java.util.HashSet;
import java.util.Set;
import java.util.function.Supplier;

public final class ExUnwrap {

    private static final ThreadLocal<@Nullable Set<Class<? extends Exception>>> activeExceptionClasses = new ThreadLocal<>();

    // Utility class with private constructor
    private ExUnwrap() {
    }

    static void unwrapScope(Set<Class<? extends Exception>> exceptionClasses, Runnable runnable) {
        final var previousExceptionClasses = getActiveExceptionClasses();
        try {
            addActiveExceptionClasses(previousExceptionClasses, exceptionClasses);
            runnable.run();
        } finally {
            setActiveExceptionClasses(previousExceptionClasses);
        }
    }

    static <T> T unwrapScope(Set<Class<? extends Exception>> exceptionClasses, Supplier<T> supplier) {
        final var previousExceptionClasses = getActiveExceptionClasses();
        try {
            addActiveExceptionClasses(previousExceptionClasses, exceptionClasses);
            return supplier.get();
        } finally {
            setActiveExceptionClasses(previousExceptionClasses);
        }
    }

    private static @Nullable Set<Class<? extends Exception>> getActiveExceptionClasses() {
        return activeExceptionClasses.get();
    }

    private static void setActiveExceptionClasses(@Nullable Set<Class<? extends Exception>> exceptionClasses) {
        if (exceptionClasses == null || exceptionClasses.isEmpty()) {
            activeExceptionClasses.remove();
        } else {
            activeExceptionClasses.set(exceptionClasses);
        }
    }

    private static void addActiveExceptionClasses(@Nullable Set<Class<? extends Exception>> previousClasses,
                                                  Set<Class<? extends Exception>> exceptionClasses) {
        final HashSet<Class<? extends Exception>> allClasses;
        if (previousClasses == null) {
            allClasses = new HashSet<>(exceptionClasses);
        } else {
            allClasses = new HashSet<>(previousClasses.size() + exceptionClasses.size());
            allClasses.addAll(previousClasses);
            allClasses.addAll(exceptionClasses);
        }
        setActiveExceptionClasses(allClasses);
    }

    /**
     * Check if unwrapping is currently active.
     *
     * @return {@code true} if unwrapping is active, {@code false} otherwise.
     */
    public static boolean isUnwrapActive() {
        final var activeExceptionClasses = getActiveExceptionClasses();
        return isUnwrapActive(activeExceptionClasses);
    }

    /**
     * Verify unwrapping for the supplied {@code exceptionClass} is currently active.
     *
     * @param exceptionClass exception class.
     * @throws IllegalArgumentException if unwrapping for the supplied {@code exceptionClass} is not currently active.
     */
    public static void verifyUnwrapActive(Class<? extends Exception> exceptionClass) {
        final Set<Class<? extends Exception>> activeExceptionClasses = getActiveExceptionClasses();
        verifyUnwrapActive(activeExceptionClasses, exceptionClass);
    }

    /**
     * If unwrapping is active, verify it is active for the supplied {@code exceptionClass}.
     *
     * @param exceptionClass exception class.
     * @throws IllegalArgumentException if unwrapping is active, but not for the supplied {@code exceptionClass}.
     */
    public static void verifyExceptionAllowed(Class<? extends Exception> exceptionClass) {
        final var activeExceptionClasses = getActiveExceptionClasses();
        if (isUnwrapActive(activeExceptionClasses)) {
            verifyUnwrapActive(activeExceptionClasses, exceptionClass);
        }
    }

    private static boolean isUnwrapActive(@Nullable Set<Class<? extends Exception>> activeExceptionClasses) {
        return activeExceptionClasses != null
               && !activeExceptionClasses.isEmpty();
    }

    private static void verifyUnwrapActive(@Nullable Set<Class<? extends Exception>> activeExceptionClasses, Class<? extends Exception> exceptionClass) {
        if (activeExceptionClasses == null ||
            activeExceptionClasses.stream().noneMatch(activeClass -> activeClass.isAssignableFrom(exceptionClass))) {
            throw new IllegalArgumentException("Exception %s is not allowed here, must be included in ExUnwrapper.of(...) invocation"
                    .formatted(exceptionClass.getName()));
        }
    }

}