001// Generated by delombok at Sat Feb 25 17:58:34 UTC 2023 002package io.cui.tools.reflect; 003 004import static io.cui.tools.collect.MoreCollections.requireNotEmpty; 005import static java.util.Objects.requireNonNull; 006import java.lang.annotation.Annotation; 007import java.lang.reflect.Field; 008import java.lang.reflect.InvocationHandler; 009import java.lang.reflect.Method; 010import java.lang.reflect.Modifier; 011import java.lang.reflect.ParameterizedType; 012import java.lang.reflect.Proxy; 013import java.lang.reflect.Type; 014import java.util.ArrayList; 015import java.util.Collection; 016import java.util.Collections; 017import java.util.HashMap; 018import java.util.List; 019import java.util.Map; 020import java.util.Objects; 021import java.util.Optional; 022import java.util.WeakHashMap; 023import io.cui.tools.base.Preconditions; 024import io.cui.tools.collect.CollectionBuilder; 025import io.cui.tools.logging.CuiLogger; 026 027/** 028 * Provides a number of methods simplifying the usage of Reflection-based access. 029 * <h2>Caution</h2> 030 * <p> 031 * Use reflection only if there is no other way. Even if some of the problems are 032 * minimized by using this type. It should be used either in test-code, what is we actually do, and 033 * not production code. An other reason could be framework code. as for that you should exactly know 034 * what you do. 035 * </p> 036 * 037 * @author Oliver Wolff 038 */ 039public final class MoreReflection { 040 @java.lang.SuppressWarnings("all") 041 @lombok.Generated 042 private static final java.lang.Object $LOCK = new java.lang.Object[0]; 043 private static final String IGNORING_METHOD_ON_CLASS = "Ignoring method \'{}\' on class \'{}\'"; 044 private static final CuiLogger log = new CuiLogger(MoreReflection.class); 045 /** 046 * We use {@link WeakHashMap} in order to allow the garbage collector to do its job 047 */ 048 private static final Map<Class<?>, List<Method>> publicObjectMethodCache = new WeakHashMap<>(); 049 private static final Map<Class<?>, Map<String, Field>> fieldCache = new WeakHashMap<>(); 050 051 // owolff: computeIfAbsent is not an option because we add null 052 // to the field 053 /** 054 * Tries to access a field on a given type. If none can be found it recursively calls itself 055 * with the corresponding parent until {@link Object}. 056 * <em>Caution:</em> 057 * <p> 058 * The field elements are shared between requests (cached), therefore you must 059 * ensure that changes to the instance, like {@link Field#setAccessible(boolean)} are reseted 060 * by the client. This can be simplified by using {@link FieldWrapper} 061 * </p> 062 * 063 * @param type to be checked, must not be null 064 * @param fieldName to be checked, must not be null 065 * @return an {@link Optional} {@link Field} if it can be found 066 */ 067 @SuppressWarnings("java:S3824") 068 public static Optional<Field> accessField(final Class<?> type, final String fieldName) { 069 synchronized (MoreReflection.$LOCK) { 070 requireNonNull(type); 071 requireNonNull(fieldName); 072 if (!fieldCache.containsKey(type)) { 073 fieldCache.put(type, new HashMap<>()); 074 } 075 final Map<String, Field> typeMap = fieldCache.get(type); 076 if (!typeMap.containsKey(fieldName)) { 077 typeMap.put(fieldName, resolveField(type, fieldName).orElse(null)); 078 } 079 return Optional.ofNullable(typeMap.get(fieldName)); 080 } 081 } 082 083 private static Optional<Field> resolveField(final Class<?> type, final String fieldName) { 084 try { 085 return Optional.of(type.getDeclaredField(fieldName)); 086 } catch (final NoSuchFieldException | SecurityException e) { 087 log.trace("Error while trying to read field {} on type {}", type, fieldName, e); 088 if (Object.class.equals(type.getClass()) || null == type.getSuperclass()) { 089 return Optional.empty(); 090 } 091 return resolveField(type.getSuperclass(), fieldName); 092 } 093 } 094 095 /** 096 * Determines the public not static methods of a given {@link Class}. {@link Object#getClass()} 097 * will implicitly ignore 098 * 099 * @param clazz to be checked 100 * @return the found public-methods. 101 */ 102 public static List<Method> retrievePublicObjectMethods(final Class<?> clazz) { 103 synchronized (MoreReflection.$LOCK) { 104 requireNonNull(clazz); 105 if (!publicObjectMethodCache.containsKey(clazz)) { 106 final List<Method> found = new ArrayList<>(); 107 for (final Method method : clazz.getMethods()) { 108 final int modifiers = method.getModifiers(); 109 if (Modifier.isPublic(modifiers) && !Modifier.isStatic(modifiers) && !"getClass".equals(method.getName())) { 110 found.add(method); 111 } 112 } 113 publicObjectMethodCache.put(clazz, found); 114 return found; 115 } 116 return publicObjectMethodCache.get(clazz); 117 } 118 } 119 120 /** 121 * Determines the access methods of a given class. An access method is defined as being a public 122 * not static zero-argument method that is prefixed with either "get" or "is". The Method 123 * "getClass" is explicitly filtered 124 * 125 * @param clazz to be checked 126 * @return the found access-methods. 127 */ 128 public static List<Method> retrieveAccessMethods(final Class<?> clazz) { 129 final List<Method> found = new ArrayList<>(); 130 for (final Method method : retrievePublicObjectMethods(clazz)) { 131 if (0 == method.getParameterCount()) { 132 final var name = method.getName(); 133 if (name.startsWith("get") || name.startsWith("is")) { 134 log.debug("Adding found method \'{}\' on class \'{}\'", name, clazz); 135 found.add(method); 136 } 137 } else { 138 log.trace(IGNORING_METHOD_ON_CLASS, method.getName(), clazz); 139 } 140 } 141 return found; 142 } 143 144 /** 145 * Determines the access methods of a given class. An access method is defined as being a public 146 * not static zero-argument method that is prefixed with either "get" or "is". The Method 147 * "getClass" is explicitly filtered 148 * 149 * @param clazz to be checked 150 * @param ignoreProperties identifies the property by name that must be filtered from the result 151 * @return the found access-methods. 152 */ 153 public static List<Method> retrieveAccessMethods(final Class<?> clazz, final Collection<String> ignoreProperties) { 154 final List<Method> found = new ArrayList<>(); 155 for (final Method method : retrieveAccessMethods(clazz)) { 156 final var propertyName = computePropertyNameFromMethodName(method.getName()); 157 if (!ignoreProperties.contains(propertyName)) { 158 found.add(method); 159 } 160 } 161 return found; 162 } 163 164 /** 165 * Determines the modifier methods of a given class. A modifier method is defined as being a 166 * public not static single-argument method that is prefixed with either "set" or consists of 167 * the propertyName only. 168 * 169 * @param clazz to be checked 170 * @param propertyName to be checked, must not be null 171 * @param parameterType identifying the parameter to be passed to the given method, must not be 172 * null 173 * @return the found modifier-method or {@link Optional#empty()} if none could be found 174 */ 175 public static Optional<Method> retrieveWriteMethod(final Class<?> clazz, final String propertyName, final Class<?> parameterType) { 176 requireNonNull(parameterType); 177 for (final Method method : retrieveWriteMethodCandidates(clazz, propertyName)) { 178 if (checkWhetherParameterIsAssignable(method.getParameterTypes()[0], parameterType)) { 179 return Optional.of(method); 180 } 181 log.trace(IGNORING_METHOD_ON_CLASS, method.getName(), clazz); 182 } 183 return Optional.empty(); 184 } 185 186 /** 187 * @param assignableSource the type to be checked 188 * @param queryType to be checked for 189 * @return boolean indicating whether the given parameter, identified by their class attributes 190 * match 191 */ 192 public static boolean checkWhetherParameterIsAssignable(final Class<?> assignableSource, final Class<?> queryType) { 193 requireNonNull(assignableSource); 194 requireNonNull(queryType); 195 if (assignableSource.equals(queryType)) { 196 log.trace("Parameter-type matches exactly \'%s\'", assignableSource); 197 return true; 198 } 199 if (assignableSource.isAssignableFrom(queryType)) { 200 log.trace("Parameter \'%s\' is assignable from \'%s\'", assignableSource, queryType); 201 return true; 202 } 203 final Class<?> boxedSource = resolveWrapperTypeForPrimitive(assignableSource); 204 final Class<?> boxedQuery = resolveWrapperTypeForPrimitive(queryType); 205 if (boxedSource.equals(boxedQuery)) { 206 log.trace("Parameter-type matches exactly after autoboxing \'%s\'", assignableSource); 207 return true; 208 } 209 return boxedSource.isAssignableFrom(boxedQuery); 210 } 211 212 /** 213 * Helper class for converting a primitive to a wrapper type. 214 * 215 * @param check must not be null 216 * @return the wrapper type if the given type represents a primitive, the given type otherwise. 217 */ 218 static Class<?> resolveWrapperTypeForPrimitive(final Class<?> check) { 219 if (!check.isPrimitive()) { 220 return check; 221 } 222 switch (check.getName()) { 223 case "boolean": 224 return Boolean.class; 225 case "byte": 226 return Byte.class; 227 case "char": 228 return Character.class; 229 case "short": 230 return Short.class; 231 case "int": 232 return Integer.class; 233 case "long": 234 return Long.class; 235 case "double": 236 return Double.class; 237 case "float": 238 return Float.class; 239 default: 240 log.warn("Unable to determine wrapper type for \'{}\', ", check); 241 return check; 242 } 243 } 244 245 /** 246 * Determines the modifier methods of a given class for a property. A modifier method is defined 247 * as being a public not static single-argument method that is prefixed with either "set" or 248 * consists of the ropertyName only. This 249 * will implicitly return all possible setter or builder methods, e.g. 250 * {@code setPropertyName(String name)}, {@code propertyName(String name)} and 251 * {@code setPropertyName(Collection<String> name)} will all be part of 252 * the result. 253 * 254 * @param clazz to be checked 255 * @param propertyName to be checked, must not be null 256 * @return the found modifier-methods or an empty {@link Collection} if none could be found 257 */ 258 public static Collection<Method> retrieveWriteMethodCandidates(final Class<?> clazz, final String propertyName) { 259 requireNotEmpty(propertyName); 260 final var builder = new CollectionBuilder<Method>(); 261 for (final Method method : retrievePublicObjectMethods(clazz)) { 262 if (1 == method.getParameterCount()) { 263 final var name = method.getName(); 264 if (propertyName.equals(name)) { 265 log.debug("Returning found method \'{}\' on class \'{}\'", name, clazz); 266 builder.add(method); 267 } 268 if (name.startsWith("set") && computePropertyNameFromMethodName(name).equalsIgnoreCase(propertyName)) { 269 log.debug("Returning found method \'{}\' on class \'{}\'", name, clazz); 270 builder.add(method); 271 } 272 } else { 273 log.trace(IGNORING_METHOD_ON_CLASS, method.getName(), clazz); 274 } 275 } 276 return builder.toImmutableList(); 277 } 278 279 /** 280 * Retrieves the access-method for a given property Name. See 281 * {@link #retrieveAccessMethods(Class)} for the 282 * definition of an access-method 283 * 284 * @param clazz must not be null 285 * @param propertyName to be accessed 286 * @return {@link Optional#empty()} in case no method could be found, an {@link Optional} with 287 * the found method 288 * otherwise. 289 */ 290 public static Optional<Method> retrieveAccessMethod(final Class<?> clazz, final String propertyName) { 291 requireNotEmpty(propertyName); 292 for (final Method method : retrieveAccessMethods(clazz)) { 293 if (computePropertyNameFromMethodName(method.getName()).equalsIgnoreCase(propertyName)) { 294 return Optional.of(method); 295 } 296 } 297 return Optional.empty(); 298 } 299 300 /** 301 * Helper method that extract the property-name from a given accessor-method name. 302 * 303 * @param methodName must not be null nor empty 304 * @return the possible attribute name of a given method-name, e.g. it return 'name' for 305 * getName/setName/isName. If 306 * none of the prefixes 'get', 'set', 'is' is found it returns the passed String. 307 */ 308 public static String computePropertyNameFromMethodName(final String methodName) { 309 requireNotEmpty(methodName); 310 if (methodName.startsWith("get") || methodName.startsWith("set")) { 311 if (methodName.length() > 3) { 312 return methodName.substring(3, 4).toLowerCase() + methodName.substring(4); 313 } 314 log.debug("Name to short for extracting attributeName \'{}\'", methodName); 315 } 316 if (methodName.startsWith("is")) { 317 if (methodName.length() > 2) { 318 return methodName.substring(2, 3).toLowerCase() + methodName.substring(3); 319 } 320 log.debug("Name to short for extracting attributeName \'{}\'", methodName); 321 } 322 return methodName; 323 } 324 325 /** 326 * Helper class for extracting <em>all</em> annotations of a given class including from their 327 * ancestors. 328 * 329 * @param <A> the concrete annotation type 330 * @param annotatedType the (possibly) annotated type. If it is null or 331 * {@link Object#getClass()} it will return an 332 * empty list 333 * @param annotation the annotation to be extracted, must not be null 334 * @return an immutable List with all annotations found at the given object or one of its 335 * ancestors. May be empty 336 * but never null 337 */ 338 public static <A extends Annotation> List<A> extractAllAnnotations(final Class<?> annotatedType, final Class<A> annotation) { 339 if (null == annotatedType || Object.class.equals(annotatedType.getClass())) { 340 return Collections.emptyList(); 341 } 342 final var builder = new CollectionBuilder<A>(); 343 builder.add(annotatedType.getAnnotationsByType(annotation)); 344 builder.add(extractAllAnnotations(annotatedType.getSuperclass(), annotation)); 345 return builder.toImmutableList(); 346 } 347 348 /** 349 * Helper class for extracting an annotation of a given class including from their ancestors. 350 * 351 * @param <A> the concrete annotation type 352 * @param annotatedType the (possibly) annotated type. If it is null or 353 * {@link Object#getClass()} {@link 354 * Optional#empty()} 355 * @param annotation the annotation to be extracted, must not be null 356 * @return an {@link Optional} on the annotated Object if the annotation can be found. In case 357 * the annotation is found multiple times the first element will be returned. 358 */ 359 public static <A extends Annotation> Optional<A> extractAnnotation(final Class<?> annotatedType, final Class<A> annotation) { 360 requireNonNull(annotation); 361 final List<A> extracted = extractAllAnnotations(annotatedType, annotation); 362 if (extracted.isEmpty()) { 363 return Optional.empty(); 364 } 365 return Optional.of(extracted.iterator().next()); 366 } 367 368 // owolff: The unchecked casting is necessary 369 /** 370 * Extracts the first generic type argument for the given type. 371 * 372 * @param <T> identifying the type to be looked for 373 * @param typeToBeExtractedFrom must not be null 374 * @return an {@link Optional} of the KeyStoreType-Argument of the given class. 375 * @throws IllegalArgumentException in case the given type does not represent a generic. 376 */ 377 @SuppressWarnings("unchecked") 378 public static <T> Class<T> extractFirstGenericTypeArgument(final Class<?> typeToBeExtractedFrom) { 379 final var parameterizedType = extractParameterizedType(typeToBeExtractedFrom).orElseThrow(() -> new IllegalArgumentException("Given type defines no generic KeyStoreType: " + typeToBeExtractedFrom)); 380 requireNotEmpty(parameterizedType.getActualTypeArguments(), "No type argument found for " + typeToBeExtractedFrom.getName()); 381 final Class<?> firstType = extractGenericTypeCovariantly(parameterizedType.getActualTypeArguments()[0]).orElseThrow(() -> new IllegalArgumentException("Unable to determine genric type for " + typeToBeExtractedFrom)); 382 try { 383 return (Class<T>) firstType; 384 } catch (final ClassCastException e) { 385 throw new IllegalArgumentException("No type argument can be extracted from " + typeToBeExtractedFrom.getName(), e); 386 } 387 } 388 389 /** 390 * @param type to be extracted from 391 * @return if applicable the actual type argument for the given type. If the type represents 392 * already a {@link Class} 393 * it will be returned directly. Otherwise, the super-type will be checked by calling 394 * the superclass 395 */ 396 @SuppressWarnings("rawtypes") 397 public static Optional<Class<?>> extractGenericTypeCovariantly(final Type type) { 398 if (null == type) { 399 log.trace("No KeyStoreType given, returning empty"); 400 return Optional.empty(); 401 } 402 if (type instanceof Class) { 403 log.debug("Found actual class returning as result {}", type); 404 return Optional.of((Class) type); 405 } 406 if (type instanceof ParameterizedType) { 407 log.debug("found Parameterized type, for {}, calling recursively", type); 408 return extractGenericTypeCovariantly(((ParameterizedType) type).getRawType()); 409 } 410 log.warn("Unable to determines generic-type for {}", type); 411 return Optional.empty(); 412 } 413 414 /** 415 * Extracts a {@link ParameterizedType} view for the given type 416 * 417 * @param typeToBeExtractedFrom must not be null 418 * @return an {@link Optional} of the {@link ParameterizedType} view of the given class. 419 */ 420 public static Optional<ParameterizedType> extractParameterizedType(final Class<?> typeToBeExtractedFrom) { 421 log.debug("Extracting ParameterizedType from {}", typeToBeExtractedFrom); 422 if (null == typeToBeExtractedFrom) { 423 return Optional.empty(); 424 } 425 if (Object.class.equals(typeToBeExtractedFrom)) { 426 log.debug("java.lang.Object is not a ParameterizedType"); 427 return Optional.empty(); 428 } 429 final var genericSuperclass = typeToBeExtractedFrom.getGenericSuperclass(); 430 if (genericSuperclass instanceof ParameterizedType) { 431 return Optional.of((ParameterizedType) genericSuperclass); 432 } 433 // Check the tree 434 return extractParameterizedType(typeToBeExtractedFrom.getSuperclass()); 435 } 436 437 /** 438 * Returns a proxy instance that implements {@code interfaceType} by dispatching method 439 * invocations to {@code handler}. The class loader of {@code interfaceType} will be used to 440 * define the proxy class. To implement multiple 441 * interfaces or specify a class loader, use Proxy#newProxyInstance(Class, Constructor, 442 * InvocationHandler). 443 * 444 * @param interfaceType must not be null 445 * @param handler the invocation handler 446 * @param <T> the target type of the proxy 447 * @return the created Proxy-instance 448 * @throws IllegalArgumentException if {@code interfaceType} does not specify the type of Java 449 * interface 450 * @author https://github.com/google/guava/blob/master/guava/src/com/google/common/reflect/Reflection.java 451 */ 452 public static <T> T newProxy(final Class<T> interfaceType, final InvocationHandler handler) { 453 requireNonNull(handler); 454 Preconditions.checkArgument(interfaceType.isInterface(), "%s is not an interface", interfaceType); 455 final var object = Proxy.newProxyInstance(interfaceType.getClassLoader(), new Class<?>[] {interfaceType}, handler); 456 return interfaceType.cast(object); 457 } 458 459 /** 460 * Try to detect class from call stack which was the previous, before the marker class name 461 * 462 * @param markerClasses class names which could be used as marker before the real caller name. 463 * Collection must not 464 * be {@code null}. 465 * @return option of detected caller class name 466 */ 467 public static Optional<String> findCaller(final Collection<String> markerClasses) { 468 final var callerElement = findCallerElement(null, markerClasses); 469 return callerElement.map(StackTraceElement::getClassName); 470 } 471 472 /** 473 * Tries to detect class from call stack which was the previous, before the marker class name 474 * 475 * @param throwable is an optional parameter, will be used to access the stack 476 * @param markerClasses class names which could be used as marker before the real caller name. 477 * Collection must not be {@code null}. 478 * @return option of detected caller class name 479 */ 480 public static Optional<StackTraceElement> findCallerElement(final Throwable throwable, final Collection<String> markerClasses) { 481 Objects.requireNonNull(markerClasses, "Marker class names are missing"); 482 final StackTraceElement[] stackTraceElements; 483 if (null == throwable) { 484 stackTraceElements = Thread.currentThread().getStackTrace(); 485 } else { 486 stackTraceElements = throwable.getStackTrace(); 487 } 488 if (null == stackTraceElements || stackTraceElements.length < 5) { 489 return Optional.empty(); 490 } 491 for (var index = 2; index < stackTraceElements.length; index++) { 492 final var element = stackTraceElements[index]; 493 if (markerClasses.contains(element.getClassName())) { 494 if (stackTraceElements.length > index + 1) { 495 return Optional.of(stackTraceElements[index + 1]); 496 } 497 return Optional.empty(); 498 } 499 } 500 return Optional.empty(); 501 } 502 503 @java.lang.SuppressWarnings("all") 504 @lombok.Generated 505 private MoreReflection() { 506 throw new java.lang.UnsupportedOperationException("This is a utility class and cannot be instantiated"); 507 } 508}