| // 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()); |
| } |
| } |
| } |
| } |