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 */ addImpliedFeatures(Set<Feature<?>> features)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 */ impliedFeatures(Set<Feature<?>> features)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 */ getTesterRequirements(Class<?> testerClass)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 */ getTesterRequirements(Method testerMethod)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 */ buildTesterRequirements(Class<?> testerClass)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 */ buildTesterRequirements(Method testerMethod)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 */ buildDeclaredTesterRequirements( AnnotatedElement classOrMethod)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 */ getTesterAnnotations( AnnotatedElement classOrMethod)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 */ buildTesterRequirements( Annotation testerAnnotation)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 */ incorporateRequirements( TesterRequirements requirements, TesterRequirements moreRequirements, Object source)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 checkConflict( String earlierRequirement, Set<Feature<?>> earlierFeatures, String newRequirement, Set<Feature<?>> newFeatures, Object source)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") intersection( Set<? extends T> set1, Set<? extends T> set2)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 */ intersection(Set<? extends T> .... sets)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