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