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