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