org.junitpioneer.internal.PioneerAnnotationUtils Maven / Gradle / Ivy
/*
* Copyright 2016-2022 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* http://www.eclipse.org/legal/epl-v20.html
*/
package org.junitpioneer.internal;
import java.lang.annotation.Annotation;
import java.lang.annotation.Inherited;
import java.lang.annotation.Repeatable;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.provider.ArgumentsSource;
import org.junit.platform.commons.support.AnnotationSupport;
import org.junitpioneer.jupiter.cartesian.CartesianArgumentsSource;
/**
* Pioneer-internal utility class to handle annotations.
* DO NOT USE THIS CLASS - IT MAY CHANGE SIGNIFICANTLY IN ANY MINOR UPDATE.
*
* It uses the following terminology to describe annotations that are not
* immediately present on an element:
*
*
* - indirectly present if a supertype of the element is annotated
* - meta-present if an annotation that is present on the element is itself annotated
* - enclosing-present if an enclosing type (think opposite of
* {@link org.junit.jupiter.api.Nested @Nested}) is annotated
*
*
* All of the above mechanisms apply recursively, meaning that, e.g., for an annotation to be
* meta-present it can present on an annotation that is present on another annotation
* that is present on the element.
*/
public class PioneerAnnotationUtils {
private PioneerAnnotationUtils() {
// private constructor to prevent instantiation of utility class
}
/**
* Determines whether an annotation of the specified {@code annotationType} is either
* present, indirectly present, meta-present, or
* enclosing-present on the test element (method or class) belonging to the
* specified {@code context}.
*/
public static boolean isAnnotationPresent(ExtensionContext context, Class extends Annotation> annotationType) {
return findClosestEnclosingAnnotation(context, annotationType).isPresent();
}
/**
* Determines whether an annotation of the specified repeatable {@code annotationType}
* is either present, indirectly present, meta-present, or
* enclosing-present on the test element (method or class) belonging to the specified
* {@code context}.
*/
public static boolean isAnyRepeatableAnnotationPresent(ExtensionContext context,
Class extends Annotation> annotationType) {
return findClosestEnclosingRepeatableAnnotations(context, annotationType).iterator().hasNext();
}
/**
* Returns the specified annotation if it is either present, meta-present,
* enclosing-present, or indirectly present on the test element (method or class) belonging
* to the specified {@code context}. If the annotations are present on more than one enclosing type,
* the closest ones are returned.
*/
public static Optional findClosestEnclosingAnnotation(ExtensionContext context,
Class annotationType) {
return findAnnotations(context, annotationType, false, false).findFirst();
}
/**
* Returns the specified repeatable annotations if they are either present,
* indirectly present, meta-present, or enclosing-present on the test
* element (method or class) belonging to the specified {@code context}. If the annotations are
* present on more than one enclosing type, the instances on the closest one are returned.
*/
public static Stream findClosestEnclosingRepeatableAnnotations(ExtensionContext context,
Class annotationType) {
return findAnnotations(context, annotationType, true, false);
}
/**
* Returns the specified annotations if they are either present, indirectly present,
* meta-present, or enclosing-present on the test element (method or class) belonging
* to the specified {@code context}. If the annotations are present on more than one enclosing type,
* all instances are returned.
*/
public static Stream findAllEnclosingAnnotations(ExtensionContext context,
Class annotationType) {
return findAnnotations(context, annotationType, false, true);
}
/**
* Returns the specified repeatable annotations if they are either present,
* indirectly present, meta-present, or enclosing-present on the test
* element (method or class) belonging to the specified {@code context}. If the annotation is
* present on more than one enclosing type, all instances are returned.
*/
public static Stream findAllEnclosingRepeatableAnnotations(ExtensionContext context,
Class annotationType) {
return findAnnotations(context, annotationType, true, true);
}
/**
* Returns the annotations present on the {@code AnnotatedElement}
* that are themselves annotated with the specified annotation. The meta-annotation can be present,
* indirectly present, or meta-present.
*/
public static List findAnnotatedAnnotations(AnnotatedElement element,
Class annotation) {
boolean isRepeatable = annotation.isAnnotationPresent(Repeatable.class);
return Arrays
.stream(element.getDeclaredAnnotations())
// flatten @Repeatable aggregator annotations
.flatMap(PioneerAnnotationUtils::flatten)
.filter(a -> !(findOnType(a.annotationType(), annotation, isRepeatable, false).isEmpty()))
.collect(Collectors.toList());
}
private static Stream flatten(Annotation annotation) {
try {
if (isContainerAnnotation(annotation)) {
Method value = annotation.annotationType().getDeclaredMethod("value");
Annotation[] invoke = (Annotation[]) value.invoke(annotation);
return Stream.of(invoke).flatMap(PioneerAnnotationUtils::flatten);
} else {
return Stream.of(annotation);
}
}
catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
throw new RuntimeException("Failed to flatten annotation stream.", e); //NOSONAR
}
}
public static boolean isContainerAnnotation(Annotation annotation) {
// See https://docs.oracle.com/javase/specs/jls/se8/html/jls-9.html#jls-9.6.3
try {
Method value = annotation.annotationType().getDeclaredMethod("value");
return value.getReturnType().isArray() && value.getReturnType().getComponentType().isAnnotation()
&& isContainerAnnotationOf(annotation, value.getReturnType().getComponentType());
}
catch (NoSuchMethodException e) {
return false;
}
}
private static boolean isContainerAnnotationOf(Annotation potentialContainer, Class> potentialRepeatable) {
Repeatable repeatable = potentialRepeatable.getAnnotation(Repeatable.class);
return repeatable != null && repeatable.value().equals(potentialContainer.annotationType());
}
static Stream findAnnotations(ExtensionContext context, Class annotationType,
boolean findRepeated, boolean findAllEnclosing) {
/*
* Implementation notes:
*
* This method starts with the specified element and, if not happy with the results (depends on the
* arguments and whether the annotation is present) kicks off a recursive search. The recursion steps
* through enclosing types (if required by the arguments, thus handling _enclosing-presence_) and
* eventually calls either `AnnotationSupport::findRepeatableAnnotations` or
* `AnnotationSupport::findAnnotation` (depending on arguments, thus handling the repeatable case).
* Both of these methods check for _meta-presence_ and _indirect presence_.
*/
List onMethod = context
.getTestMethod()
.map(method -> findOnMethod(method, annotationType, findRepeated))
.orElse(Collections.emptyList());
if (!findAllEnclosing && !onMethod.isEmpty())
return onMethod.stream();
Stream onClass = findOnOuterClasses(context.getTestClass(), annotationType, findRepeated, findAllEnclosing);
return Stream.concat(onMethod.stream(), onClass);
}
private static List findOnMethod(Method element, Class annotationType,
boolean findRepeated) {
if (findRepeated)
return AnnotationSupport.findRepeatableAnnotations(element, annotationType);
else
return AnnotationSupport
.findAnnotation(element, annotationType)
.map(Collections::singletonList)
.orElse(Collections.emptyList());
}
private static Stream findOnOuterClasses(Optional> type, Class annotationType,
boolean findRepeated, boolean findAllEnclosing) {
if (!type.isPresent())
return Stream.empty();
List onThisClass = Arrays.asList(type.get().getAnnotationsByType(annotationType));
if (!findAllEnclosing && !onThisClass.isEmpty())
return onThisClass.stream();
List onClass = findOnType(type.get(), annotationType, findRepeated, findAllEnclosing);
Stream onParentClass = findOnOuterClasses(type.map(Class::getEnclosingClass), annotationType, findRepeated,
findAllEnclosing);
return Stream.concat(onClass.stream(), onParentClass);
}
private static List findOnType(Class> element, Class annotationType,
boolean findRepeated, boolean findAllEnclosing) {
if (element == null || element == Object.class)
return Collections.emptyList();
if (findRepeated)
return AnnotationSupport.findRepeatableAnnotations(element, annotationType);
List onElement = AnnotationSupport
.findAnnotation(element, annotationType)
.map(Collections::singletonList)
.orElse(Collections.emptyList());
List onInterfaces = Arrays
.stream(element.getInterfaces())
.flatMap(clazz -> findOnType(clazz, annotationType, false, findAllEnclosing).stream())
.collect(Collectors.toList());
if (!annotationType.isAnnotationPresent(Inherited.class)) {
if (!findAllEnclosing)
return onElement;
else
return Stream
.of(onElement, onInterfaces)
.flatMap(Collection::stream)
.distinct()
.collect(Collectors.toList());
}
List onSuperclass = findOnType(element.getSuperclass(), annotationType, false, findAllEnclosing);
return Stream
.of(onElement, onInterfaces, onSuperclass)
.flatMap(Collection::stream)
.distinct()
.collect(Collectors.toList());
}
public static List findParameterArgumentsSources(Method testMethod) {
return Arrays
.stream(testMethod.getParameters())
.map(PioneerAnnotationUtils::collectArgumentSources)
.filter(list -> !list.isEmpty())
.map(annotations -> annotations.get(0))
.collect(Collectors.toList());
}
private static List collectArgumentSources(Parameter parameter) {
List annotations = new ArrayList<>();
AnnotationSupport.findAnnotation(parameter, CartesianArgumentsSource.class).ifPresent(annotations::add);
// ArgumentSource meta-annotations are allowed on parameters for
// CartesianTest because there is no overlap with ParameterizedTest
annotations.addAll(AnnotationSupport.findRepeatableAnnotations(parameter, ArgumentsSource.class));
return annotations;
}
public static List findMethodArgumentsSources(Method testMethod) {
return Arrays
.stream(testMethod.getAnnotations())
.filter(annotation -> AnnotationSupport
.findAnnotation(annotation.annotationType(), CartesianArgumentsSource.class)
.isPresent())
.collect(Collectors.toList());
}
}