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