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