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