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