• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 The Guava Authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.common.collect.testing.features;
18 
19 import com.google.common.annotations.GwtIncompatible;
20 import com.google.common.collect.testing.Helpers;
21 import com.google.errorprone.annotations.CanIgnoreReturnValue;
22 import java.lang.annotation.Annotation;
23 import java.lang.reflect.AnnotatedElement;
24 import java.lang.reflect.Method;
25 import java.util.ArrayDeque;
26 import java.util.ArrayList;
27 import java.util.Collections;
28 import java.util.HashMap;
29 import java.util.LinkedHashSet;
30 import java.util.List;
31 import java.util.Locale;
32 import java.util.Map;
33 import java.util.Queue;
34 import java.util.Set;
35 
36 /**
37  * Utilities for collecting and validating tester requirements from annotations.
38  *
39  * @author George van den Driessche
40  */
41 @GwtIncompatible
42 public class FeatureUtil {
43   /** A cache of annotated objects (typically a Class or Method) to its set of annotations. */
44   private static Map<AnnotatedElement, List<Annotation>> annotationCache = new HashMap<>();
45 
46   private static final Map<Class<?>, TesterRequirements> classTesterRequirementsCache =
47       new HashMap<>();
48 
49   private static final Map<Method, TesterRequirements> methodTesterRequirementsCache =
50       new HashMap<>();
51 
52   /**
53    * Given a set of features, add to it all the features directly or indirectly implied by any of
54    * them, and return it.
55    *
56    * @param features the set of features to expand
57    * @return the same set of features, expanded with all implied features
58    */
59   @CanIgnoreReturnValue
addImpliedFeatures(Set<Feature<?>> features)60   public static Set<Feature<?>> addImpliedFeatures(Set<Feature<?>> features) {
61     Queue<Feature<?>> queue = new ArrayDeque<>(features);
62     while (!queue.isEmpty()) {
63       Feature<?> feature = queue.remove();
64       for (Feature<?> implied : feature.getImpliedFeatures()) {
65         if (features.add(implied)) {
66           queue.add(implied);
67         }
68       }
69     }
70     return features;
71   }
72 
73   /**
74    * Given a set of features, return a new set of all features directly or indirectly implied by any
75    * of them.
76    *
77    * @param features the set of features whose implications to find
78    * @return the implied set of features
79    */
impliedFeatures(Set<Feature<?>> features)80   public static Set<Feature<?>> impliedFeatures(Set<Feature<?>> features) {
81     Set<Feature<?>> impliedSet = new LinkedHashSet<>();
82     Queue<Feature<?>> queue = new ArrayDeque<>(features);
83     while (!queue.isEmpty()) {
84       Feature<?> feature = queue.remove();
85       for (Feature<?> implied : feature.getImpliedFeatures()) {
86         if (!features.contains(implied) && impliedSet.add(implied)) {
87           queue.add(implied);
88         }
89       }
90     }
91     return impliedSet;
92   }
93 
94   /**
95    * Get the full set of requirements for a tester class.
96    *
97    * @param testerClass a tester class
98    * @return all the constraints implicitly or explicitly required by the class or any of its
99    *     superclasses.
100    * @throws ConflictingRequirementsException if the requirements are mutually inconsistent.
101    */
getTesterRequirements(Class<?> testerClass)102   public static TesterRequirements getTesterRequirements(Class<?> testerClass)
103       throws ConflictingRequirementsException {
104     synchronized (classTesterRequirementsCache) {
105       TesterRequirements requirements = classTesterRequirementsCache.get(testerClass);
106       if (requirements == null) {
107         requirements = buildTesterRequirements(testerClass);
108         classTesterRequirementsCache.put(testerClass, requirements);
109       }
110       return requirements;
111     }
112   }
113 
114   /**
115    * Get the full set of requirements for a tester class.
116    *
117    * @param testerMethod a test method of a tester class
118    * @return all the constraints implicitly or explicitly required by the method, its declaring
119    *     class, or any of its superclasses.
120    * @throws ConflictingRequirementsException if the requirements are mutually inconsistent.
121    */
getTesterRequirements(Method testerMethod)122   public static TesterRequirements getTesterRequirements(Method testerMethod)
123       throws ConflictingRequirementsException {
124     synchronized (methodTesterRequirementsCache) {
125       TesterRequirements requirements = methodTesterRequirementsCache.get(testerMethod);
126       if (requirements == null) {
127         requirements = buildTesterRequirements(testerMethod);
128         methodTesterRequirementsCache.put(testerMethod, requirements);
129       }
130       return requirements;
131     }
132   }
133 
134   /**
135    * Construct the full set of requirements for a tester class.
136    *
137    * @param testerClass a tester class
138    * @return all the constraints implicitly or explicitly required by the class or any of its
139    *     superclasses.
140    * @throws ConflictingRequirementsException if the requirements are mutually inconsistent.
141    */
buildTesterRequirements(Class<?> testerClass)142   static TesterRequirements buildTesterRequirements(Class<?> testerClass)
143       throws ConflictingRequirementsException {
144     TesterRequirements declaredRequirements = buildDeclaredTesterRequirements(testerClass);
145     Class<?> baseClass = testerClass.getSuperclass();
146     if (baseClass == null) {
147       return declaredRequirements;
148     } else {
149       TesterRequirements clonedBaseRequirements =
150           new TesterRequirements(getTesterRequirements(baseClass));
151       return incorporateRequirements(clonedBaseRequirements, declaredRequirements, testerClass);
152     }
153   }
154 
155   /**
156    * Construct the full set of requirements for a tester method.
157    *
158    * @param testerMethod a test method of a tester class
159    * @return all the constraints implicitly or explicitly required by the method, its declaring
160    *     class, or any of its superclasses.
161    * @throws ConflictingRequirementsException if the requirements are mutually inconsistent.
162    */
buildTesterRequirements(Method testerMethod)163   static TesterRequirements buildTesterRequirements(Method testerMethod)
164       throws ConflictingRequirementsException {
165     TesterRequirements clonedClassRequirements =
166         new TesterRequirements(getTesterRequirements(testerMethod.getDeclaringClass()));
167     TesterRequirements declaredRequirements = buildDeclaredTesterRequirements(testerMethod);
168     return incorporateRequirements(clonedClassRequirements, declaredRequirements, testerMethod);
169   }
170 
171   /**
172    * Find all the constraints explicitly or implicitly specified by a single tester annotation.
173    *
174    * @param testerAnnotation a tester annotation
175    * @return the requirements specified by the annotation
176    * @throws ConflictingRequirementsException if the requirements are mutually inconsistent.
177    */
buildTesterRequirements(Annotation testerAnnotation)178   private static TesterRequirements buildTesterRequirements(Annotation testerAnnotation)
179       throws ConflictingRequirementsException {
180     Class<? extends Annotation> annotationClass = testerAnnotation.annotationType();
181     Feature<?>[] presentFeatures;
182     Feature<?>[] absentFeatures;
183     try {
184       presentFeatures = (Feature[]) annotationClass.getMethod("value").invoke(testerAnnotation);
185       absentFeatures = (Feature[]) annotationClass.getMethod("absent").invoke(testerAnnotation);
186     } catch (Exception e) {
187       throw new IllegalArgumentException("Error extracting features from tester annotation.", e);
188     }
189     Set<Feature<?>> allPresentFeatures =
190         addImpliedFeatures(Helpers.<Feature<?>>copyToSet(presentFeatures));
191     Set<Feature<?>> allAbsentFeatures =
192         addImpliedFeatures(Helpers.<Feature<?>>copyToSet(absentFeatures));
193     if (!Collections.disjoint(allPresentFeatures, allAbsentFeatures)) {
194       throw new ConflictingRequirementsException(
195           "Annotation explicitly or "
196               + "implicitly requires one or more features to be both present "
197               + "and absent.",
198           intersection(allPresentFeatures, allAbsentFeatures),
199           testerAnnotation);
200     }
201     return new TesterRequirements(allPresentFeatures, allAbsentFeatures);
202   }
203 
204   /**
205    * Construct the set of requirements specified by annotations directly on a tester class or
206    * method.
207    *
208    * @param classOrMethod a tester class or a test method thereof
209    * @return all the constraints implicitly or explicitly required by annotations on the class or
210    *     method.
211    * @throws ConflictingRequirementsException if the requirements are mutually inconsistent.
212    */
buildDeclaredTesterRequirements(AnnotatedElement classOrMethod)213   public static TesterRequirements buildDeclaredTesterRequirements(AnnotatedElement classOrMethod)
214       throws ConflictingRequirementsException {
215     TesterRequirements requirements = new TesterRequirements();
216 
217     Iterable<Annotation> testerAnnotations = getTesterAnnotations(classOrMethod);
218     for (Annotation testerAnnotation : testerAnnotations) {
219       TesterRequirements moreRequirements = buildTesterRequirements(testerAnnotation);
220       incorporateRequirements(requirements, moreRequirements, testerAnnotation);
221     }
222 
223     return requirements;
224   }
225 
226   /**
227    * Find all the tester annotations declared on a tester class or method.
228    *
229    * @param classOrMethod a class or method whose tester annotations to find
230    * @return an iterable sequence of tester annotations on the class
231    */
getTesterAnnotations(AnnotatedElement classOrMethod)232   public static Iterable<Annotation> getTesterAnnotations(AnnotatedElement classOrMethod) {
233     synchronized (annotationCache) {
234       List<Annotation> annotations = annotationCache.get(classOrMethod);
235       if (annotations == null) {
236         annotations = new ArrayList<>();
237         for (Annotation a : classOrMethod.getDeclaredAnnotations()) {
238           if (a.annotationType().isAnnotationPresent(TesterAnnotation.class)) {
239             annotations.add(a);
240           }
241         }
242         annotations = Collections.unmodifiableList(annotations);
243         annotationCache.put(classOrMethod, annotations);
244       }
245       return annotations;
246     }
247   }
248 
249   /**
250    * Incorporate additional requirements into an existing requirements object.
251    *
252    * @param requirements the existing requirements object
253    * @param moreRequirements more requirements to incorporate
254    * @param source the source of the additional requirements (used only for error reporting)
255    * @return the existing requirements object, modified to include the additional requirements
256    * @throws ConflictingRequirementsException if the additional requirements are inconsistent with
257    *     the existing requirements
258    */
259   @CanIgnoreReturnValue
incorporateRequirements( TesterRequirements requirements, TesterRequirements moreRequirements, Object source)260   private static TesterRequirements incorporateRequirements(
261       TesterRequirements requirements, TesterRequirements moreRequirements, Object source)
262       throws ConflictingRequirementsException {
263     Set<Feature<?>> presentFeatures = requirements.getPresentFeatures();
264     Set<Feature<?>> absentFeatures = requirements.getAbsentFeatures();
265     Set<Feature<?>> morePresentFeatures = moreRequirements.getPresentFeatures();
266     Set<Feature<?>> moreAbsentFeatures = moreRequirements.getAbsentFeatures();
267     checkConflict("absent", absentFeatures, "present", morePresentFeatures, source);
268     checkConflict("present", presentFeatures, "absent", moreAbsentFeatures, source);
269     presentFeatures.addAll(morePresentFeatures);
270     absentFeatures.addAll(moreAbsentFeatures);
271     return requirements;
272   }
273 
274   // Used by incorporateRequirements() only
checkConflict( String earlierRequirement, Set<Feature<?>> earlierFeatures, String newRequirement, Set<Feature<?>> newFeatures, Object source)275   private static void checkConflict(
276       String earlierRequirement,
277       Set<Feature<?>> earlierFeatures,
278       String newRequirement,
279       Set<Feature<?>> newFeatures,
280       Object source)
281       throws ConflictingRequirementsException {
282     if (!Collections.disjoint(newFeatures, earlierFeatures)) {
283       throw new ConflictingRequirementsException(
284           String.format(
285               Locale.ROOT,
286               "Annotation requires to be %s features that earlier "
287                   + "annotations required to be %s.",
288               newRequirement,
289               earlierRequirement),
290           intersection(newFeatures, earlierFeatures),
291           source);
292     }
293   }
294 
295   /** Construct a new {@link java.util.Set} that is the intersection of the given sets. */
intersection(Set<? extends T> set1, Set<? extends T> set2)296   public static <T> Set<T> intersection(Set<? extends T> set1, Set<? extends T> set2) {
297     Set<T> result = Helpers.<T>copyToSet(set1);
298     result.retainAll(set2);
299     return result;
300   }
301 }
302