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