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