001/* 002 * Copyright (C) 2008 The Guava Authors 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.google.common.collect.testing.features; 018 019import static com.google.common.collect.testing.Helpers.copyToSet; 020import static java.util.Collections.disjoint; 021import static java.util.Collections.unmodifiableList; 022 023import com.google.common.annotations.GwtIncompatible; 024import com.google.errorprone.annotations.CanIgnoreReturnValue; 025import com.google.errorprone.annotations.concurrent.GuardedBy; 026import java.lang.annotation.Annotation; 027import java.lang.reflect.AnnotatedElement; 028import java.lang.reflect.Method; 029import java.util.ArrayDeque; 030import java.util.ArrayList; 031import java.util.HashMap; 032import java.util.LinkedHashSet; 033import java.util.List; 034import java.util.Locale; 035import java.util.Map; 036import java.util.Queue; 037import java.util.Set; 038import org.jspecify.annotations.NullMarked; 039 040/** 041 * Utilities for collecting and validating tester requirements from annotations. 042 * 043 * @author George van den Driessche 044 */ 045@GwtIncompatible 046public final class FeatureUtil { 047 /** A cache of annotated objects (typically a Class or Method) to its set of annotations. */ 048 @GuardedBy("annotationCache") 049 private static final Map<AnnotatedElement, List<Annotation>> annotationCache = new HashMap<>(); 050 051 @GuardedBy("classTesterRequirementsCache") 052 private static final Map<Class<?>, TesterRequirements> classTesterRequirementsCache = 053 new HashMap<>(); 054 055 @GuardedBy("methodTesterRequirementsCache") 056 private static final Map<Method, TesterRequirements> methodTesterRequirementsCache = 057 new HashMap<>(); 058 059 /** 060 * Given a set of features, add to it all the features directly or indirectly implied by any of 061 * them, and return it. 062 * 063 * @param features the set of features to expand 064 * @return the same set of features, expanded with all implied features 065 */ 066 @CanIgnoreReturnValue 067 public static Set<Feature<?>> addImpliedFeatures(Set<Feature<?>> features) { 068 Queue<Feature<?>> queue = new ArrayDeque<>(features); 069 while (!queue.isEmpty()) { 070 Feature<?> feature = queue.remove(); 071 for (Feature<?> implied : feature.getImpliedFeatures()) { 072 if (features.add(implied)) { 073 queue.add(implied); 074 } 075 } 076 } 077 return features; 078 } 079 080 /** 081 * Given a set of features, return a new set of all features directly or indirectly implied by any 082 * of them. 083 * 084 * @param features the set of features whose implications to find 085 * @return the implied set of features 086 */ 087 public static Set<Feature<?>> impliedFeatures(Set<Feature<?>> features) { 088 Set<Feature<?>> impliedSet = new LinkedHashSet<>(); 089 Queue<Feature<?>> queue = new ArrayDeque<>(features); 090 while (!queue.isEmpty()) { 091 Feature<?> feature = queue.remove(); 092 for (Feature<?> implied : feature.getImpliedFeatures()) { 093 if (!features.contains(implied) && impliedSet.add(implied)) { 094 queue.add(implied); 095 } 096 } 097 } 098 return impliedSet; 099 } 100 101 /** 102 * Get the full set of requirements for a tester class. 103 * 104 * @param testerClass a tester class 105 * @return all the constraints implicitly or explicitly required by the class or any of its 106 * superclasses. 107 * @throws ConflictingRequirementsException if the requirements are mutually inconsistent. 108 */ 109 public static TesterRequirements getTesterRequirements(Class<?> testerClass) 110 throws ConflictingRequirementsException { 111 synchronized (classTesterRequirementsCache) { 112 TesterRequirements requirements = classTesterRequirementsCache.get(testerClass); 113 if (requirements == null) { 114 requirements = buildTesterRequirements(testerClass); 115 classTesterRequirementsCache.put(testerClass, requirements); 116 } 117 return requirements; 118 } 119 } 120 121 /** 122 * Get the full set of requirements for a tester class. 123 * 124 * @param testerMethod a test method of a tester class 125 * @return all the constraints implicitly or explicitly required by the method, its declaring 126 * class, or any of its superclasses. 127 * @throws ConflictingRequirementsException if the requirements are mutually inconsistent. 128 */ 129 public static TesterRequirements getTesterRequirements(Method testerMethod) 130 throws ConflictingRequirementsException { 131 synchronized (methodTesterRequirementsCache) { 132 TesterRequirements requirements = methodTesterRequirementsCache.get(testerMethod); 133 if (requirements == null) { 134 requirements = buildTesterRequirements(testerMethod); 135 methodTesterRequirementsCache.put(testerMethod, requirements); 136 } 137 return requirements; 138 } 139 } 140 141 /** 142 * Construct the full set of requirements for a tester class. 143 * 144 * @param testerClass a tester class 145 * @return all the constraints implicitly or explicitly required by the class or any of its 146 * superclasses. 147 * @throws ConflictingRequirementsException if the requirements are mutually inconsistent. 148 */ 149 static TesterRequirements buildTesterRequirements(Class<?> testerClass) 150 throws ConflictingRequirementsException { 151 TesterRequirements declaredRequirements = buildDeclaredTesterRequirements(testerClass); 152 Class<?> baseClass = testerClass.getSuperclass(); 153 if (baseClass == null) { 154 return declaredRequirements; 155 } else { 156 TesterRequirements clonedBaseRequirements = 157 new TesterRequirements(getTesterRequirements(baseClass)); 158 return incorporateRequirements(clonedBaseRequirements, declaredRequirements, testerClass); 159 } 160 } 161 162 /** 163 * Construct the full set of requirements for a tester method. 164 * 165 * @param testerMethod a test method of a tester class 166 * @return all the constraints implicitly or explicitly required by the method, its declaring 167 * class, or any of its superclasses. 168 * @throws ConflictingRequirementsException if the requirements are mutually inconsistent. 169 */ 170 static TesterRequirements buildTesterRequirements(Method testerMethod) 171 throws ConflictingRequirementsException { 172 TesterRequirements clonedClassRequirements = 173 new TesterRequirements(getTesterRequirements(testerMethod.getDeclaringClass())); 174 TesterRequirements declaredRequirements = buildDeclaredTesterRequirements(testerMethod); 175 return incorporateRequirements(clonedClassRequirements, declaredRequirements, testerMethod); 176 } 177 178 /** 179 * Find all the constraints explicitly or implicitly specified by a single tester annotation. 180 * 181 * @param testerAnnotation a tester annotation 182 * @return the requirements specified by the annotation 183 * @throws ConflictingRequirementsException if the requirements are mutually inconsistent. 184 */ 185 private static TesterRequirements buildTesterRequirements(Annotation testerAnnotation) 186 throws ConflictingRequirementsException { 187 Class<? extends Annotation> annotationClass = testerAnnotation.annotationType(); 188 Feature<?>[] presentFeatures; 189 Feature<?>[] absentFeatures; 190 try { 191 presentFeatures = (Feature<?>[]) annotationClass.getMethod("value").invoke(testerAnnotation); 192 absentFeatures = (Feature<?>[]) annotationClass.getMethod("absent").invoke(testerAnnotation); 193 } catch (Exception e) { 194 throw new IllegalArgumentException("Error extracting features from tester annotation.", e); 195 } 196 Set<Feature<?>> allPresentFeatures = addImpliedFeatures(copyToSet(presentFeatures)); 197 Set<Feature<?>> allAbsentFeatures = copyToSet(absentFeatures); 198 if (!disjoint(allPresentFeatures, allAbsentFeatures)) { 199 throw new ConflictingRequirementsException( 200 "Annotation explicitly or " 201 + "implicitly requires one or more features to be both present " 202 + "and absent.", 203 intersection(allPresentFeatures, allAbsentFeatures), 204 testerAnnotation); 205 } 206 return new TesterRequirements(allPresentFeatures, allAbsentFeatures); 207 } 208 209 /** 210 * Construct the set of requirements specified by annotations directly on a tester class or 211 * method. 212 * 213 * @param classOrMethod a tester class or a test method thereof 214 * @return all the constraints implicitly or explicitly required by annotations on the class or 215 * method. 216 * @throws ConflictingRequirementsException if the requirements are mutually inconsistent. 217 */ 218 public static TesterRequirements buildDeclaredTesterRequirements(AnnotatedElement classOrMethod) 219 throws ConflictingRequirementsException { 220 TesterRequirements requirements = new TesterRequirements(); 221 222 Iterable<Annotation> testerAnnotations = getTesterAnnotations(classOrMethod); 223 for (Annotation testerAnnotation : testerAnnotations) { 224 TesterRequirements moreRequirements = buildTesterRequirements(testerAnnotation); 225 incorporateRequirements(requirements, moreRequirements, testerAnnotation); 226 } 227 228 return requirements; 229 } 230 231 /** 232 * Find all the tester annotations declared on a tester class or method. 233 * 234 * @param classOrMethod a class or method whose tester annotations to find 235 * @return an iterable sequence of tester annotations on the class 236 */ 237 public static Iterable<Annotation> getTesterAnnotations(AnnotatedElement classOrMethod) { 238 synchronized (annotationCache) { 239 List<Annotation> annotations = annotationCache.get(classOrMethod); 240 if (annotations == null) { 241 annotations = new ArrayList<>(); 242 for (Annotation a : classOrMethod.getDeclaredAnnotations()) { 243 /* 244 * We avoid reflecting on NullMarked because its @Target(..., MODULE) causes problems 245 * under JDK 8. 246 */ 247 if (!(a instanceof NullMarked) 248 && a.annotationType().isAnnotationPresent(TesterAnnotation.class)) { 249 annotations.add(a); 250 } 251 } 252 annotations = unmodifiableList(annotations); 253 annotationCache.put(classOrMethod, annotations); 254 } 255 return annotations; 256 } 257 } 258 259 /** 260 * Incorporate additional requirements into an existing requirements object. 261 * 262 * @param requirements the existing requirements object 263 * @param moreRequirements more requirements to incorporate 264 * @param source the source of the additional requirements (used only for error reporting) 265 * @return the existing requirements object, modified to include the additional requirements 266 * @throws ConflictingRequirementsException if the additional requirements are inconsistent with 267 * the existing requirements 268 */ 269 @CanIgnoreReturnValue 270 private static TesterRequirements incorporateRequirements( 271 TesterRequirements requirements, TesterRequirements moreRequirements, Object source) 272 throws ConflictingRequirementsException { 273 Set<Feature<?>> presentFeatures = requirements.getPresentFeatures(); 274 Set<Feature<?>> absentFeatures = requirements.getAbsentFeatures(); 275 Set<Feature<?>> morePresentFeatures = moreRequirements.getPresentFeatures(); 276 Set<Feature<?>> moreAbsentFeatures = moreRequirements.getAbsentFeatures(); 277 checkConflict("absent", absentFeatures, "present", morePresentFeatures, source); 278 checkConflict("present", presentFeatures, "absent", moreAbsentFeatures, source); 279 presentFeatures.addAll(morePresentFeatures); 280 absentFeatures.addAll(moreAbsentFeatures); 281 return requirements; 282 } 283 284 // Used by incorporateRequirements() only 285 private static void checkConflict( 286 String earlierRequirement, 287 Set<Feature<?>> earlierFeatures, 288 String newRequirement, 289 Set<Feature<?>> newFeatures, 290 Object source) 291 throws ConflictingRequirementsException { 292 if (!disjoint(newFeatures, earlierFeatures)) { 293 throw new ConflictingRequirementsException( 294 String.format( 295 Locale.ROOT, 296 "Annotation requires to be %s features that earlier " 297 + "annotations required to be %s.", 298 newRequirement, 299 earlierRequirement), 300 intersection(newFeatures, earlierFeatures), 301 source); 302 } 303 } 304 305 /** 306 * Construct a new {@link java.util.Set} that is the intersection of the given sets. 307 * 308 * @deprecated Use {@link com.google.common.collect.Sets#intersection(Set, Set)} instead. 309 */ 310 @Deprecated 311 public static <T> Set<T> intersection(Set<? extends T> set1, Set<? extends T> set2) { 312 Set<T> result = copyToSet(set1); 313 result.retainAll(set2); 314 return result; 315 } 316 317 private FeatureUtil() {} 318}