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 * <p>This class can be referenced in GWT tests.
35 *
36 * @author George van den Driessche
37 */
38public 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   */
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   */
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   */
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   */
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   */
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   */
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   */
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   */
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   */
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   */
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
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")
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   */
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