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