AnnotationUtil.java

package net.morimekta.testing.junit5;

import org.junit.jupiter.api.extension.ExtensionContext;

import java.lang.annotation.Annotation;
import java.util.Optional;
import java.util.stream.Stream;

/**
 * Internal utility for resolving annotations from JUnit 5 extension contexts, searching
 * method-level first and then walking up the class hierarchy.
 */
public final class AnnotationUtil {
    /**
     * Check if the given annotation is present on the test method or any enclosing class.
     *
     * @param context    The JUnit extension context.
     * @param annotation The annotation type to look for.
     * @param <A>        The annotation type.
     * @return True if the annotation is found.
     */
    public static <A extends Annotation> boolean isAnnotationPresent(ExtensionContext context, Class<A> annotation) {
        return getTopAnnotation(context, annotation).isPresent();
    }

    /**
     * Find the most specific (method-first, then class hierarchy) instance of an annotation.
     *
     * @param context    The JUnit extension context.
     * @param annotation The annotation type to look for.
     * @param <A>        The annotation type.
     * @return The annotation if found, empty otherwise.
     */
    public static <A extends Annotation> Optional<A> getTopAnnotation(
            ExtensionContext context, Class<A> annotation) {
        return context
                .getTestMethod()
                .map(m -> m.getDeclaredAnnotation(annotation))
                .or(() -> context.getTestClass().flatMap(t -> getTopAnnotation(t, annotation)));
    }

    /**
     * Collect all instances of an annotation from the class hierarchy (superclass first)
     * and then the test method, in bottom-up order.
     *
     * @param context    The JUnit extension context.
     * @param annotation The annotation type to collect.
     * @param <T>        The annotation type.
     * @return A stream of annotations in superclass-to-method order.
     */
    public static <T extends Annotation> Stream<T> getAnnotationsBottomUp(
            ExtensionContext context, Class<T> annotation) {
        return Stream.concat(
                context.getTestClass()
                       .stream()
                       .flatMap(t -> getAnnotationsBottomUp(t, annotation)),
                context.getTestMethod()
                       .map(m -> m.getDeclaredAnnotation(annotation))
                       .stream());
    }

    // --- Private ---

    private static <T extends Annotation> Optional<T> getTopAnnotation(
            Class<?> type, Class<T> annotation) {
        return Optional.ofNullable(type.getDeclaredAnnotation(annotation))
                       .or(() -> Optional.ofNullable(type.getSuperclass())
                                         .flatMap(t -> getTopAnnotation(t, annotation)));
    }

    private static <T extends Annotation> Stream<T> getAnnotationsBottomUp(
            Class<?> type, Class<T> annotation) {
        return Stream.concat(
                Optional.ofNullable(type.getSuperclass())
                        .stream()
                        .flatMap(t -> getAnnotationsBottomUp(t, annotation)),
                Optional.ofNullable(type.getDeclaredAnnotation(annotation))
                        .stream());
    }

    private AnnotationUtil() {}
}