001/*
002 * Copyright (C) 2008 The Guava Authors
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.google.common.collect.testing.features;
018
019import com.google.common.annotations.GwtIncompatible;
020import com.google.common.collect.testing.Helpers;
021import java.lang.annotation.Annotation;
022import java.lang.reflect.AnnotatedElement;
023import java.lang.reflect.Method;
024import java.util.ArrayDeque;
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.HashMap;
028import java.util.LinkedHashSet;
029import java.util.List;
030import java.util.Locale;
031import java.util.Map;
032import java.util.Queue;
033import java.util.Set;
034
035/**
036 * Utilities for collecting and validating tester requirements from annotations.
037 *
038 * @author George van den Driessche
039 */
040@GwtIncompatible
041public class FeatureUtil {
042  /** A cache of annotated objects (typically a Class or Method) to its set of annotations. */
043  private static Map<AnnotatedElement, List<Annotation>> annotationCache = new HashMap<>();
044
045  private static final Map<Class<?>, TesterRequirements> classTesterRequirementsCache =
046      new HashMap<>();
047
048  private static final Map<Method, TesterRequirements> methodTesterRequirementsCache =
049      new HashMap<>();
050
051  /**
052   * Given a set of features, add to it all the features directly or indirectly implied by any of
053   * them, and return it.
054   *
055   * @param features the set of features to expand
056   * @return the same set of features, expanded with all implied features
057   */
058  public static Set<Feature<?>> addImpliedFeatures(Set<Feature<?>> features) {
059    Queue<Feature<?>> queue = new ArrayDeque<>(features);
060    while (!queue.isEmpty()) {
061      Feature<?> feature = queue.remove();
062      for (Feature<?> implied : feature.getImpliedFeatures()) {
063        if (features.add(implied)) {
064          queue.add(implied);
065        }
066      }
067    }
068    return features;
069  }
070
071  /**
072   * Given a set of features, return a new set of all features directly or indirectly implied by any
073   * of them.
074   *
075   * @param features the set of features whose implications to find
076   * @return the implied set of features
077   */
078  public static Set<Feature<?>> impliedFeatures(Set<Feature<?>> features) {
079    Set<Feature<?>> impliedSet = new LinkedHashSet<>();
080    Queue<Feature<?>> queue = new ArrayDeque<>(features);
081    while (!queue.isEmpty()) {
082      Feature<?> feature = queue.remove();
083      for (Feature<?> implied : feature.getImpliedFeatures()) {
084        if (!features.contains(implied) && impliedSet.add(implied)) {
085          queue.add(implied);
086        }
087      }
088    }
089    return impliedSet;
090  }
091
092  /**
093   * Get the full set of requirements for a tester class.
094   *
095   * @param testerClass a tester class
096   * @return all the constraints implicitly or explicitly required by the class or any of its
097   *     superclasses.
098   * @throws ConflictingRequirementsException if the requirements are mutually inconsistent.
099   */
100  public static TesterRequirements getTesterRequirements(Class<?> testerClass)
101      throws ConflictingRequirementsException {
102    synchronized (classTesterRequirementsCache) {
103      TesterRequirements requirements = classTesterRequirementsCache.get(testerClass);
104      if (requirements == null) {
105        requirements = buildTesterRequirements(testerClass);
106        classTesterRequirementsCache.put(testerClass, requirements);
107      }
108      return requirements;
109    }
110  }
111
112  /**
113   * Get the full set of requirements for a tester class.
114   *
115   * @param testerMethod a test method of a tester class
116   * @return all the constraints implicitly or explicitly required by the method, its declaring
117   *     class, or any of its superclasses.
118   * @throws ConflictingRequirementsException if the requirements are mutually inconsistent.
119   */
120  public static TesterRequirements getTesterRequirements(Method testerMethod)
121      throws ConflictingRequirementsException {
122    synchronized (methodTesterRequirementsCache) {
123      TesterRequirements requirements = methodTesterRequirementsCache.get(testerMethod);
124      if (requirements == null) {
125        requirements = buildTesterRequirements(testerMethod);
126        methodTesterRequirementsCache.put(testerMethod, requirements);
127      }
128      return requirements;
129    }
130  }
131
132  /**
133   * Construct the full set of requirements for a tester class.
134   *
135   * @param testerClass a tester class
136   * @return all the constraints implicitly or explicitly required by the class or any of its
137   *     superclasses.
138   * @throws ConflictingRequirementsException if the requirements are mutually inconsistent.
139   */
140  static TesterRequirements buildTesterRequirements(Class<?> testerClass)
141      throws ConflictingRequirementsException {
142    final TesterRequirements declaredRequirements = buildDeclaredTesterRequirements(testerClass);
143    Class<?> baseClass = testerClass.getSuperclass();
144    if (baseClass == null) {
145      return declaredRequirements;
146    } else {
147      final TesterRequirements clonedBaseRequirements =
148          new TesterRequirements(getTesterRequirements(baseClass));
149      return incorporateRequirements(clonedBaseRequirements, declaredRequirements, testerClass);
150    }
151  }
152
153  /**
154   * Construct the full set of requirements for a tester method.
155   *
156   * @param testerMethod a test method of a tester class
157   * @return all the constraints implicitly or explicitly required by the method, its declaring
158   *     class, or any of its superclasses.
159   * @throws ConflictingRequirementsException if the requirements are mutually inconsistent.
160   */
161  static TesterRequirements buildTesterRequirements(Method testerMethod)
162      throws ConflictingRequirementsException {
163    TesterRequirements clonedClassRequirements =
164        new TesterRequirements(getTesterRequirements(testerMethod.getDeclaringClass()));
165    TesterRequirements declaredRequirements = buildDeclaredTesterRequirements(testerMethod);
166    return incorporateRequirements(clonedClassRequirements, declaredRequirements, testerMethod);
167  }
168
169  /**
170   * Find all the constraints explicitly or implicitly specified by a single tester annotation.
171   *
172   * @param testerAnnotation a tester annotation
173   * @return the requirements specified by the annotation
174   * @throws ConflictingRequirementsException if the requirements are mutually inconsistent.
175   */
176  private static TesterRequirements buildTesterRequirements(Annotation testerAnnotation)
177      throws ConflictingRequirementsException {
178    Class<? extends Annotation> annotationClass = testerAnnotation.annotationType();
179    final Feature<?>[] presentFeatures;
180    final Feature<?>[] absentFeatures;
181    try {
182      presentFeatures = (Feature[]) annotationClass.getMethod("value").invoke(testerAnnotation);
183      absentFeatures = (Feature[]) annotationClass.getMethod("absent").invoke(testerAnnotation);
184    } catch (Exception e) {
185      throw new IllegalArgumentException("Error extracting features from tester annotation.", e);
186    }
187    Set<Feature<?>> allPresentFeatures =
188        addImpliedFeatures(Helpers.<Feature<?>>copyToSet(presentFeatures));
189    Set<Feature<?>> allAbsentFeatures =
190        addImpliedFeatures(Helpers.<Feature<?>>copyToSet(absentFeatures));
191    if (!Collections.disjoint(allPresentFeatures, allAbsentFeatures)) {
192      throw new ConflictingRequirementsException(
193          "Annotation explicitly or "
194              + "implicitly requires one or more features to be both present "
195              + "and absent.",
196          intersection(allPresentFeatures, allAbsentFeatures),
197          testerAnnotation);
198    }
199    return new TesterRequirements(allPresentFeatures, allAbsentFeatures);
200  }
201
202  /**
203   * Construct the set of requirements specified by annotations directly on a tester class or
204   * method.
205   *
206   * @param classOrMethod a tester class or a test method thereof
207   * @return all the constraints implicitly or explicitly required by annotations on the class or
208   *     method.
209   * @throws ConflictingRequirementsException if the requirements are mutually inconsistent.
210   */
211  public static TesterRequirements buildDeclaredTesterRequirements(AnnotatedElement classOrMethod)
212      throws ConflictingRequirementsException {
213    TesterRequirements requirements = new TesterRequirements();
214
215    Iterable<Annotation> testerAnnotations = getTesterAnnotations(classOrMethod);
216    for (Annotation testerAnnotation : testerAnnotations) {
217      TesterRequirements moreRequirements = buildTesterRequirements(testerAnnotation);
218      incorporateRequirements(requirements, moreRequirements, testerAnnotation);
219    }
220
221    return requirements;
222  }
223
224  /**
225   * Find all the tester annotations declared on a tester class or method.
226   *
227   * @param classOrMethod a class or method whose tester annotations to find
228   * @return an iterable sequence of tester annotations on the class
229   */
230  public static Iterable<Annotation> getTesterAnnotations(AnnotatedElement classOrMethod) {
231    synchronized (annotationCache) {
232      List<Annotation> annotations = annotationCache.get(classOrMethod);
233      if (annotations == null) {
234        annotations = new ArrayList<>();
235        for (Annotation a : classOrMethod.getDeclaredAnnotations()) {
236          if (a.annotationType().isAnnotationPresent(TesterAnnotation.class)) {
237            annotations.add(a);
238          }
239        }
240        annotations = Collections.unmodifiableList(annotations);
241        annotationCache.put(classOrMethod, annotations);
242      }
243      return annotations;
244    }
245  }
246
247  /**
248   * Incorporate additional requirements into an existing requirements object.
249   *
250   * @param requirements the existing requirements object
251   * @param moreRequirements more requirements to incorporate
252   * @param source the source of the additional requirements (used only for error reporting)
253   * @return the existing requirements object, modified to include the additional requirements
254   * @throws ConflictingRequirementsException if the additional requirements are inconsistent with
255   *     the existing requirements
256   */
257  private static TesterRequirements incorporateRequirements(
258      TesterRequirements requirements, TesterRequirements moreRequirements, Object source)
259      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("absent", absentFeatures, "present", morePresentFeatures, source);
265    checkConflict("present", presentFeatures, "absent", moreAbsentFeatures, source);
266    presentFeatures.addAll(morePresentFeatures);
267    absentFeatures.addAll(moreAbsentFeatures);
268    return requirements;
269  }
270
271  // Used by incorporateRequirements() only
272  private static void checkConflict(
273      String earlierRequirement,
274      Set<Feature<?>> earlierFeatures,
275      String newRequirement,
276      Set<Feature<?>> newFeatures,
277      Object source)
278      throws ConflictingRequirementsException {
279    if (!Collections.disjoint(newFeatures, earlierFeatures)) {
280      throw new ConflictingRequirementsException(
281          String.format(
282              Locale.ROOT,
283              "Annotation requires to be %s features that earlier "
284                  + "annotations required to be %s.",
285              newRequirement,
286              earlierRequirement),
287          intersection(newFeatures, earlierFeatures),
288          source);
289    }
290  }
291
292  /** Construct a new {@link java.util.Set} that is the intersection of the given sets. */
293  public static <T> Set<T> intersection(Set<? extends T> set1, Set<? extends T> set2) {
294    Set<T> result = Helpers.<T>copyToSet(set1);
295    result.retainAll(set2);
296    return result;
297  }
298}