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