• 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.collect.testing.Helpers;
20 
21 import java.lang.annotation.Annotation;
22 import java.lang.reflect.AnnotatedElement;
23 import java.lang.reflect.Method;
24 import java.util.ArrayList;
25 import java.util.HashMap;
26 import java.util.LinkedHashSet;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Set;
30 
31 /**
32  * Utilities for collecting and validating tester requirements from annotations.
33  *
34  * <p>This class can be referenced in GWT tests.
35  *
36  * @author George van den Driessche
37  */
38 public class FeatureUtil {
39   /**
40    * A cache of annotated objects (typically a Class or Method) to its
41    * set of annotations.
42    */
43   private static Map<AnnotatedElement, Annotation[]> annotationCache =
44       new HashMap<AnnotatedElement, Annotation[]>();
45 
46   private static final Map<Class<?>, TesterRequirements>
47       classTesterRequirementsCache =
48           new HashMap<Class<?>, TesterRequirements>();
49 
50   /**
51    * Given a set of features, add to it all the features directly or indirectly
52    * implied by any of them, and return it.
53    * @param features the set of features to expand
54    * @return the same set of features, expanded with all implied features
55    */
addImpliedFeatures(Set<Feature<?>> features)56   public static Set<Feature<?>> addImpliedFeatures(Set<Feature<?>> features) {
57     // The base case of the recursion is an empty set of features, which will
58     // occur when the previous set contained only simple features.
59     if (!features.isEmpty()) {
60       features.addAll(impliedFeatures(features));
61     }
62     return features;
63   }
64 
65   /**
66    * Given a set of features, return a new set of all features directly or
67    * indirectly implied by any of them.
68    * @param features the set of features whose implications to find
69    * @return the implied set of features
70    */
impliedFeatures(Set<Feature<?>> features)71   public static Set<Feature<?>> impliedFeatures(Set<Feature<?>> features) {
72     Set<Feature<?>> implied = new LinkedHashSet<Feature<?>>();
73     for (Feature<?> feature : features) {
74       implied.addAll(feature.getImpliedFeatures());
75     }
76     addImpliedFeatures(implied);
77     return implied;
78   }
79 
80   /**
81    * Get the full set of requirements for a tester class.
82    * @param testerClass a tester class
83    * @return all the constraints implicitly or explicitly required by the class
84    * or any of its superclasses.
85    * @throws ConflictingRequirementsException if the requirements are mutually
86    * inconsistent.
87    */
getTesterRequirements(Class<?> testerClass)88   public static TesterRequirements getTesterRequirements(Class<?> testerClass)
89       throws ConflictingRequirementsException {
90     synchronized (classTesterRequirementsCache) {
91       TesterRequirements requirements =
92           classTesterRequirementsCache.get(testerClass);
93       if (requirements == null) {
94         requirements = buildTesterRequirements(testerClass);
95         classTesterRequirementsCache.put(testerClass, requirements);
96       }
97       return requirements;
98     }
99   }
100 
101   /**
102    * Get the full set of requirements for a tester class.
103    * @param testerMethod a test method of a tester class
104    * @return all the constraints implicitly or explicitly required by the
105    * method, its declaring class, or any of its superclasses.
106    * @throws ConflictingRequirementsException if the requirements are
107    * mutually inconsistent.
108    */
getTesterRequirements(Method testerMethod)109   public static TesterRequirements getTesterRequirements(Method testerMethod)
110       throws ConflictingRequirementsException {
111     return buildTesterRequirements(testerMethod);
112   }
113 
114   /**
115    * Construct the full set of requirements for a tester class.
116    * @param testerClass a tester class
117    * @return all the constraints implicitly or explicitly required by the class
118    * or any of its superclasses.
119    * @throws ConflictingRequirementsException if the requirements are mutually
120    * inconsistent.
121    */
buildTesterRequirements(Class<?> testerClass)122   static TesterRequirements buildTesterRequirements(Class<?> testerClass)
123       throws ConflictingRequirementsException {
124     final TesterRequirements declaredRequirements =
125         buildDeclaredTesterRequirements(testerClass);
126     Class<?> baseClass = testerClass.getSuperclass();
127     if (baseClass == null) {
128       return declaredRequirements;
129     } else {
130       final TesterRequirements clonedBaseRequirements =
131           new TesterRequirements(getTesterRequirements(baseClass));
132       return incorporateRequirements(
133           clonedBaseRequirements, declaredRequirements, testerClass);
134     }
135   }
136 
137   /**
138    * Construct the full set of requirements for a tester method.
139    * @param testerMethod a test method of a tester class
140    * @return all the constraints implicitly or explicitly required by the
141    * method, its declaring class, or any of its superclasses.
142    * @throws ConflictingRequirementsException if the requirements are mutually
143    * inconsistent.
144    */
buildTesterRequirements(Method testerMethod)145   static TesterRequirements buildTesterRequirements(Method testerMethod)
146       throws ConflictingRequirementsException {
147     TesterRequirements clonedClassRequirements = new TesterRequirements(
148         getTesterRequirements(testerMethod.getDeclaringClass()));
149     TesterRequirements declaredRequirements =
150         buildDeclaredTesterRequirements(testerMethod);
151     return incorporateRequirements(
152         clonedClassRequirements, declaredRequirements, testerMethod);
153   }
154 
155   /**
156    * Construct the set of requirements specified by annotations
157    * directly on a tester class or method.
158    * @param classOrMethod a tester class or a test method thereof
159    * @return all the constraints implicitly or explicitly required by
160    *         annotations on the class or method.
161    * @throws ConflictingRequirementsException if the requirements are mutually
162    *         inconsistent.
163    */
buildDeclaredTesterRequirements( AnnotatedElement classOrMethod)164   public static TesterRequirements buildDeclaredTesterRequirements(
165       AnnotatedElement classOrMethod)
166       throws ConflictingRequirementsException {
167     TesterRequirements requirements = new TesterRequirements();
168 
169     Iterable<Annotation> testerAnnotations =
170         getTesterAnnotations(classOrMethod);
171     for (Annotation testerAnnotation : testerAnnotations) {
172       TesterRequirements moreRequirements =
173           buildTesterRequirements(testerAnnotation);
174       incorporateRequirements(
175           requirements, moreRequirements, testerAnnotation);
176     }
177 
178     return requirements;
179   }
180 
181   /**
182    * Find all the tester annotations declared on a tester class or method.
183    * @param classOrMethod a class or method whose tester annotations to find
184    * @return an iterable sequence of tester annotations on the class
185    */
getTesterAnnotations( AnnotatedElement classOrMethod)186   public static Iterable<Annotation> getTesterAnnotations(
187       AnnotatedElement classOrMethod) {
188     List<Annotation> result = new ArrayList<Annotation>();
189 
190     Annotation[] annotations;
191     synchronized (annotationCache) {
192       annotations = annotationCache.get(classOrMethod);
193       if (annotations == null) {
194         annotations = classOrMethod.getDeclaredAnnotations();
195         annotationCache.put(classOrMethod, annotations);
196       }
197     }
198 
199     for (Annotation a : annotations) {
200       Class<? extends Annotation> annotationClass = a.annotationType();
201       if (annotationClass.isAnnotationPresent(TesterAnnotation.class)) {
202         result.add(a);
203       }
204     }
205     return result;
206   }
207 
208   /**
209    * Find all the constraints explicitly or implicitly specified by a single
210    * tester annotation.
211    * @param testerAnnotation a tester annotation
212    * @return the requirements specified by the annotation
213    * @throws ConflictingRequirementsException if the requirements are mutually
214    *         inconsistent.
215    */
buildTesterRequirements( Annotation testerAnnotation)216   private static TesterRequirements buildTesterRequirements(
217       Annotation testerAnnotation)
218       throws ConflictingRequirementsException {
219     Class<? extends Annotation> annotationClass = testerAnnotation.getClass();
220     final Feature<?>[] presentFeatures;
221     final Feature<?>[] absentFeatures;
222     try {
223       presentFeatures = (Feature[]) annotationClass.getMethod("value")
224           .invoke(testerAnnotation);
225       absentFeatures = (Feature[]) annotationClass.getMethod("absent")
226           .invoke(testerAnnotation);
227     } catch (Exception e) {
228       throw new IllegalArgumentException(
229           "Error extracting features from tester annotation.", e);
230     }
231     Set<Feature<?>> allPresentFeatures =
232         addImpliedFeatures(Helpers.<Feature<?>>copyToSet(presentFeatures));
233     Set<Feature<?>> allAbsentFeatures =
234         addImpliedFeatures(Helpers.<Feature<?>>copyToSet(absentFeatures));
235     Set<Feature<?>> conflictingFeatures =
236         intersection(allPresentFeatures, allAbsentFeatures);
237     if (!conflictingFeatures.isEmpty()) {
238       throw new ConflictingRequirementsException("Annotation explicitly or " +
239           "implicitly requires one or more features to be both present " +
240           "and absent.",
241           conflictingFeatures, testerAnnotation);
242     }
243     return new TesterRequirements(allPresentFeatures, allAbsentFeatures);
244   }
245 
246   /**
247    * Incorporate additional requirements into an existing requirements object.
248    * @param requirements the existing requirements object
249    * @param moreRequirements more requirements to incorporate
250    * @param source the source of the additional requirements
251    *        (used only for error reporting)
252    * @return the existing requirements object, modified to include the
253    *         additional requirements
254    * @throws ConflictingRequirementsException if the additional requirements
255    *         are inconsistent with the existing requirements
256    */
incorporateRequirements( TesterRequirements requirements, TesterRequirements moreRequirements, Object source)257   private static TesterRequirements incorporateRequirements(
258       TesterRequirements requirements, TesterRequirements moreRequirements,
259       Object source) throws ConflictingRequirementsException {
260     Set<Feature<?>> presentFeatures = requirements.getPresentFeatures();
261     Set<Feature<?>> absentFeatures = requirements.getAbsentFeatures();
262     Set<Feature<?>> morePresentFeatures = moreRequirements.getPresentFeatures();
263     Set<Feature<?>> moreAbsentFeatures = moreRequirements.getAbsentFeatures();
264     checkConflict(
265         "absent", absentFeatures,
266         "present", morePresentFeatures, source);
267     checkConflict(
268         "present", presentFeatures,
269         "absent", moreAbsentFeatures, source);
270     presentFeatures.addAll(morePresentFeatures);
271     absentFeatures.addAll(moreAbsentFeatures);
272     return requirements;
273   }
274 
275   // Used by incorporateRequirements() only
checkConflict( String earlierRequirement, Set<Feature<?>> earlierFeatures, String newRequirement, Set<Feature<?>> newFeatures, Object source)276   private static void checkConflict(
277       String earlierRequirement, Set<Feature<?>> earlierFeatures,
278       String newRequirement, Set<Feature<?>> newFeatures,
279       Object source) throws ConflictingRequirementsException {
280     Set<Feature<?>> conflictingFeatures;
281     conflictingFeatures = intersection(newFeatures, earlierFeatures);
282     if (!conflictingFeatures.isEmpty()) {
283       throw new ConflictingRequirementsException(String.format(
284           "Annotation requires to be %s features that earlier " +
285           "annotations required to be %s.",
286               newRequirement, earlierRequirement),
287           conflictingFeatures, source);
288     }
289   }
290 
291   /**
292    * Construct a new {@link java.util.Set} that is the intersection
293    * of the given sets.
294    */
295   // Calls generic varargs method.
296   @SuppressWarnings("unchecked")
intersection( Set<? extends T> set1, Set<? extends T> set2)297   public static <T> Set<T> intersection(
298       Set<? extends T> set1, Set<? extends T> set2) {
299     return intersection(new Set[] {set1, set2});
300   }
301 
302   /**
303    * Construct a new {@link java.util.Set} that is the intersection
304    * of all the given sets.
305    * @param sets the sets to intersect
306    * @return the intersection of the sets
307    * @throws java.lang.IllegalArgumentException if there are no sets to
308    *         intersect
309    */
intersection(Set<? extends T> .... sets)310   public static <T> Set<T> intersection(Set<? extends T> ... sets) {
311     if (sets.length == 0) {
312       throw new IllegalArgumentException(
313           "Can't intersect no sets; would have to return the universe.");
314     }
315     Set<T> results = Helpers.copyToSet(sets[0]);
316     for (int i = 1; i < sets.length; i++) {
317       Set<? extends T> set = sets[i];
318       results.retainAll(set);
319     }
320     return results;
321   }
322 }
323