blob: d335412e1e503b6f7ee64b6a2ba220bfb1d16b91 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.base.test.util;
import android.support.annotation.Nullable;
import org.junit.runner.Description;
import org.chromium.base.VisibleForTesting;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;
/**
* Utility class to help with processing annotations, going around the code to collect them, etc.
*/
public abstract class AnnotationProcessingUtils {
/**
* Returns the closest instance of the requested annotation or null if there is none.
* See {@link AnnotationExtractor} for context of "closest".
*/
@SuppressWarnings("unchecked")
public static <A extends Annotation> A getAnnotation(Description description, Class<A> clazz) {
AnnotationExtractor extractor = new AnnotationExtractor(clazz);
return (A) extractor.getClosest(extractor.getMatchingAnnotations(description));
}
/**
* Returns the closest instance of the requested annotation or null if there is none.
* See {@link AnnotationExtractor} for context of "closest".
*/
@SuppressWarnings("unchecked")
public static <A extends Annotation> A getAnnotation(AnnotatedElement element, Class<A> clazz) {
AnnotationExtractor extractor = new AnnotationExtractor(clazz);
return (A) extractor.getClosest(extractor.getMatchingAnnotations(element));
}
/** See {@link AnnotationExtractor} for details about the output sorting order. */
@SuppressWarnings("unchecked")
public static <A extends Annotation> List<A> getAnnotations(
Description description, Class<A> annotationType) {
return (List<A>) new AnnotationExtractor(annotationType)
.getMatchingAnnotations(description);
}
/** See {@link AnnotationExtractor} for details about the output sorting order. */
@SuppressWarnings("unchecked")
public static <A extends Annotation> List<A> getAnnotations(
AnnotatedElement annotatedElement, Class<A> annotationType) {
return (List<A>) new AnnotationExtractor(annotationType)
.getMatchingAnnotations(annotatedElement);
}
private static boolean isChromiumAnnotation(Annotation annotation) {
Package pkg = annotation.annotationType().getPackage();
return pkg != null && pkg.getName().startsWith("org.chromium");
}
/**
* Processes various types of annotated elements ({@link Class}es, {@link Annotation}s,
* {@link Description}s, etc.) and extracts the targeted annotations from it. The output will be
* sorted in BFS-like order.
*
* For example, for a method we would get in reverse order:
* - the method annotations
* - the meta-annotations present on the method annotations,
* - the class annotations
* - the meta-annotations present on the class annotations,
* - the annotations present on the super class,
* - the meta-annotations present on the super class annotations,
* - etc.
*
* When multiple annotations are targeted, if more than one is picked up at a given level (for
* example directly on the method), they will be returned in the reverse order that they were
* provided to the constructor.
*
* Note: We return the annotations in reverse order because we assume that if some processing
* is going to be made on related annotations, the later annotations would likely override
* modifications made by the former.
*
* Note: While resolving meta annotations, we don't expand the explorations to annotations types
* that have already been visited. Please file a bug and assign to dgn@ if you think it caused
* an issue.
*/
public static class AnnotationExtractor {
private final List<Class<? extends Annotation>> mAnnotationTypes;
private final Comparator<Class<? extends Annotation>> mAnnotationTypeComparator;
private final Comparator<Annotation> mAnnotationComparator;
@SafeVarargs
public AnnotationExtractor(Class<? extends Annotation>... additionalTypes) {
this(Arrays.asList(additionalTypes));
}
public AnnotationExtractor(List<Class<? extends Annotation>> additionalTypes) {
assert !additionalTypes.isEmpty();
mAnnotationTypes = Collections.unmodifiableList(additionalTypes);
mAnnotationTypeComparator =
(t1, t2) -> mAnnotationTypes.indexOf(t1) - mAnnotationTypes.indexOf(t2);
mAnnotationComparator = (t1, t2)
-> mAnnotationTypeComparator.compare(t1.annotationType(), t2.annotationType());
}
public List<Annotation> getMatchingAnnotations(Description description) {
return getMatchingAnnotations(new AnnotatedNode.DescriptionNode(description));
}
public List<Annotation> getMatchingAnnotations(AnnotatedElement annotatedElement) {
AnnotatedNode annotatedNode;
if (annotatedElement instanceof Method) {
annotatedNode = new AnnotatedNode.MethodNode((Method) annotatedElement);
} else if (annotatedElement instanceof Class) {
annotatedNode = new AnnotatedNode.ClassNode((Class) annotatedElement);
} else {
throw new IllegalArgumentException("Unsupported type for " + annotatedElement);
}
return getMatchingAnnotations(annotatedNode);
}
/**
* For a given list obtained from the extractor, returns the {@link Annotation} that would
* be closest from the extraction point, or {@code null} if the list is empty.
*/
@Nullable
public Annotation getClosest(List<Annotation> annotationList) {
return annotationList.isEmpty() ? null : annotationList.get(annotationList.size() - 1);
}
@VisibleForTesting
Comparator<Class<? extends Annotation>> getTypeComparator() {
return mAnnotationTypeComparator;
}
private List<Annotation> getMatchingAnnotations(AnnotatedNode annotatedNode) {
List<Annotation> collectedAnnotations = new ArrayList<>();
Queue<Annotation> workingSet = new LinkedList<>();
Set<Class<? extends Annotation>> visited = new HashSet<>();
AnnotatedNode currentAnnotationLayer = annotatedNode;
while (currentAnnotationLayer != null) {
queueAnnotations(currentAnnotationLayer.getAnnotations(), workingSet);
while (!workingSet.isEmpty()) {
sweepAnnotations(collectedAnnotations, workingSet, visited);
}
currentAnnotationLayer = currentAnnotationLayer.getParent();
}
return collectedAnnotations;
}
private void queueAnnotations(List<Annotation> annotations, Queue<Annotation> workingSet) {
Collections.sort(annotations, mAnnotationComparator);
workingSet.addAll(annotations);
}
private void sweepAnnotations(List<Annotation> collectedAnnotations,
Queue<Annotation> workingSet, Set<Class<? extends Annotation>> visited) {
// 1. Grab node at the front of the working set.
Annotation annotation = workingSet.remove();
// 2. If it's an annotation of interest, put it aside for the output.
if (mAnnotationTypes.contains(annotation.annotationType())) {
collectedAnnotations.add(0, annotation);
}
// 3. Check if we can get skip some redundant iterations and avoid cycles.
if (!visited.add(annotation.annotationType())) return;
if (!isChromiumAnnotation(annotation)) return;
// 4. Expand the working set
queueAnnotations(Arrays.asList(annotation.annotationType().getDeclaredAnnotations()),
workingSet);
}
}
/**
* Abstraction to hide differences between Class, Method and Description with regards to their
* annotations and what should be analyzed next.
*/
private static abstract class AnnotatedNode {
@Nullable
abstract AnnotatedNode getParent();
abstract List<Annotation> getAnnotations();
static class DescriptionNode extends AnnotatedNode {
final Description mDescription;
DescriptionNode(Description description) {
mDescription = description;
}
@Nullable
@Override
AnnotatedNode getParent() {
return new ClassNode(mDescription.getTestClass());
}
@Override
List<Annotation> getAnnotations() {
return new ArrayList<>(mDescription.getAnnotations());
}
}
static class ClassNode extends AnnotatedNode {
final Class<?> mClass;
ClassNode(Class<?> clazz) {
mClass = clazz;
}
@Nullable
@Override
AnnotatedNode getParent() {
Class<?> superClass = mClass.getSuperclass();
return superClass == null ? null : new ClassNode(superClass);
}
@Override
List<Annotation> getAnnotations() {
return Arrays.asList(mClass.getDeclaredAnnotations());
}
}
static class MethodNode extends AnnotatedNode {
final Method mMethod;
MethodNode(Method method) {
mMethod = method;
}
@Nullable
@Override
AnnotatedNode getParent() {
return new ClassNode(mMethod.getDeclaringClass());
}
@Override
List<Annotation> getAnnotations() {
return Arrays.asList(mMethod.getDeclaredAnnotations());
}
}
}
}