001// Generated by delombok at Mon Jan 02 16:27:27 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 String 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 String 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 } else { 181 log.trace(IGNORING_METHOD_ON_CLASS, method.getName(), clazz); 182 } 183 } 184 return Optional.empty(); 185 } 186 187 /** 188 * @param assignableSource the type to be checked 189 * @param queryType to be checked for 190 * @return boolean indicating whether the given parameter, identified by their class attributes 191 * match 192 */ 193 public static boolean checkWhetherParameterIsAssignable(final Class<?> assignableSource, final Class<?> queryType) { 194 requireNonNull(assignableSource); 195 requireNonNull(queryType); 196 if (assignableSource.equals(queryType)) { 197 log.trace("Parameter-type matches exactly \'%s\'", assignableSource); 198 return true; 199 } 200 if (assignableSource.isAssignableFrom(queryType)) { 201 log.trace("Parameter \'%s\' is assignable from \'%s\'", assignableSource, queryType); 202 return true; 203 } 204 final Class<?> boxedSource = resolveWrapperTypeForPrimitive(assignableSource); 205 final Class<?> boxedQuery = resolveWrapperTypeForPrimitive(queryType); 206 if (boxedSource.equals(boxedQuery)) { 207 log.trace("Parameter-type matches exactly after autoboxing \'%s\'", assignableSource); 208 return true; 209 } 210 return boxedSource.isAssignableFrom(boxedQuery); 211 } 212 213 /** 214 * Helper class for converting a primitive to a wrapper type. 215 * 216 * @param check must not be null 217 * @return the wrapper type if the given type represents a primitive, the given type otherwise. 218 */ 219 static Class<?> resolveWrapperTypeForPrimitive(final Class<?> check) { 220 if (!check.isPrimitive()) { 221 return check; 222 } 223 switch (check.getName()) { 224 case "boolean": 225 return Boolean.class; 226 case "byte": 227 return Byte.class; 228 case "char": 229 return Character.class; 230 case "short": 231 return Short.class; 232 case "int": 233 return Integer.class; 234 case "long": 235 return Long.class; 236 case "double": 237 return Double.class; 238 case "float": 239 return Float.class; 240 default: 241 log.warn("Unable to determine wrapper type for \'{}\', ", check); 242 return check; 243 } 244 } 245 246 /** 247 * Determines the modifier methods of a given class for a property. A modifier method is defined 248 * as being a public not static single-argument method that is prefixed with either "set" or 249 * consists of the ropertyName only. This 250 * will implicitly return all possible setter or builder methods, e.g. 251 * {@code setPropertyName(String name)}, {@code propertyName(String name)} and 252 * {@code setPropertyName(Collection<String> name)} will all be part of 253 * the result. 254 * 255 * @param clazz to be checked 256 * @param propertyName to be checked, must not be null 257 * @return the found modifier-methods or an empty {@link Collection} if none could be found 258 */ 259 public static Collection<Method> retrieveWriteMethodCandidates(final Class<?> clazz, final String propertyName) { 260 requireNotEmpty(propertyName); 261 final CollectionBuilder<Method> builder = new CollectionBuilder<>(); 262 for (final Method method : retrievePublicObjectMethods(clazz)) { 263 if (1 == method.getParameterCount()) { 264 final String name = method.getName(); 265 if (propertyName.equals(name)) { 266 log.debug("Returning found method \'{}\' on class \'{}\'", name, clazz); 267 builder.add(method); 268 } 269 if (name.startsWith("set") && computePropertyNameFromMethodName(name).equalsIgnoreCase(propertyName)) { 270 log.debug("Returning found method \'{}\' on class \'{}\'", name, clazz); 271 builder.add(method); 272 } 273 } else { 274 log.trace(IGNORING_METHOD_ON_CLASS, method.getName(), clazz); 275 } 276 } 277 return builder.toImmutableList(); 278 } 279 280 /** 281 * Retrieves the access-method for a given property Name. See 282 * {@link #retrieveAccessMethods(Class)} for the 283 * definition of an access-method 284 * 285 * @param clazz must not be null 286 * @param propertyName to be accessed 287 * @return {@link Optional#empty()} in case no method could be found, an {@link Optional} with 288 * the found method 289 * otherwise. 290 */ 291 public static Optional<Method> retrieveAccessMethod(final Class<?> clazz, final String propertyName) { 292 requireNotEmpty(propertyName); 293 for (final Method method : retrieveAccessMethods(clazz)) { 294 if (computePropertyNameFromMethodName(method.getName()).equalsIgnoreCase(propertyName)) { 295 return Optional.of(method); 296 } 297 } 298 return Optional.empty(); 299 } 300 301 /** 302 * Helper method that extract the property-name from a given accessor-method name. 303 * 304 * @param methodName must not be null nor empty 305 * @return the possible attribute name of a given method-name, e.g. it return 'name' for 306 * getName/setName/isName. If 307 * none of the prefixes 'get', 'set', 'is' is found it returns the passed String. 308 */ 309 public static String computePropertyNameFromMethodName(final String methodName) { 310 requireNotEmpty(methodName); 311 if (methodName.startsWith("get") || methodName.startsWith("set")) { 312 if (methodName.length() > 3) { 313 return methodName.substring(3, 4).toLowerCase() + methodName.substring(4); 314 } else { 315 log.debug("Name to short for extracting attributeName \'{}\'", methodName); 316 } 317 } 318 if (methodName.startsWith("is")) { 319 if (methodName.length() > 2) { 320 return methodName.substring(2, 3).toLowerCase() + methodName.substring(3); 321 } else { 322 log.debug("Name to short for extracting attributeName \'{}\'", methodName); 323 } 324 } 325 return methodName; 326 } 327 328 /** 329 * Helper class for extracting <em>all</em> annotations of a given class including from their 330 * ancestors. 331 * 332 * @param <A> the concrete annotation type 333 * @param annotatedType the (possibly) annotated type. If it is null or 334 * {@link Object#getClass()} it will return an 335 * empty list 336 * @param annotation the annotation to be extracted, must not be null 337 * @return an immutable List with all annotations found at the given object or one of its 338 * ancestors. May be empty 339 * but never null 340 */ 341 public static <A extends Annotation> List<A> extractAllAnnotations(final Class<?> annotatedType, final Class<A> annotation) { 342 if (null == annotatedType || Object.class.equals(annotatedType.getClass())) { 343 return Collections.emptyList(); 344 } 345 final CollectionBuilder<A> builder = new CollectionBuilder<>(); 346 builder.add(annotatedType.getAnnotationsByType(annotation)); 347 builder.add(extractAllAnnotations(annotatedType.getSuperclass(), annotation)); 348 return builder.toImmutableList(); 349 } 350 351 /** 352 * Helper class for extracting an annotation of a given class including from their ancestors. 353 * 354 * @param <A> the concrete annotation type 355 * @param annotatedType the (possibly) annotated type. If it is null or 356 * {@link Object#getClass()} {@link 357 * Optional#empty()} 358 * @param annotation the annotation to be extracted, must not be null 359 * @return an {@link Optional} on the annotated Object if the annotation can be found. In case 360 * the annotation is found multiple times the first element will be returned. 361 */ 362 public static <A extends Annotation> Optional<A> extractAnnotation(final Class<?> annotatedType, final Class<A> annotation) { 363 requireNonNull(annotation); 364 final List<A> extracted = extractAllAnnotations(annotatedType, annotation); 365 if (extracted.isEmpty()) { 366 return Optional.empty(); 367 } 368 return Optional.of(extracted.iterator().next()); 369 } 370 371 // owolff: The unchecked casting is necessary 372 /** 373 * Extracts the first generic type argument for the given type. 374 * 375 * @param <T> identifying the type to be looked for 376 * @param typeToBeExtractedFrom must not be null 377 * @return an {@link Optional} of the KeyStoreType-Argument of the given class. 378 * @throws IllegalArgumentException in case the given type does not represent a generic. 379 */ 380 @SuppressWarnings("unchecked") 381 public static <T> Class<T> extractFirstGenericTypeArgument(final Class<?> typeToBeExtractedFrom) { 382 final ParameterizedType parameterizedType = extractParameterizedType(typeToBeExtractedFrom).orElseThrow(() -> new IllegalArgumentException("Given type defines no generic KeyStoreType: " + typeToBeExtractedFrom)); 383 requireNotEmpty(parameterizedType.getActualTypeArguments(), "No type argument found for " + typeToBeExtractedFrom.getName()); 384 final Class<?> firstType = extractGenericTypeCovariantly(parameterizedType.getActualTypeArguments()[0]).orElseThrow(() -> new IllegalArgumentException("Unable to determine genric type for " + typeToBeExtractedFrom)); 385 try { 386 return (Class<T>) firstType; 387 } catch (final ClassCastException e) { 388 throw new IllegalArgumentException("No type argument can be extracted from " + typeToBeExtractedFrom.getName(), e); 389 } 390 } 391 392 /** 393 * @param type to be extracted from 394 * @return if applicable the actual type argument for the given type. If the type represents 395 * already a {@link Class} 396 * it will be returned directly. Otherwise, the super-type will be checked by calling 397 * the superclass 398 */ 399 @SuppressWarnings("rawtypes") 400 public static Optional<Class<?>> extractGenericTypeCovariantly(final Type type) { 401 if (null == type) { 402 log.trace("No KeyStoreType given, returning empty"); 403 return Optional.empty(); 404 } 405 if (type instanceof Class) { 406 log.debug("Found actual class returning as result {}", type); 407 return Optional.of((Class) type); 408 } 409 if (type instanceof ParameterizedType) { 410 log.debug("found Parameterized type, for {}, calling recursively", type); 411 return extractGenericTypeCovariantly(((ParameterizedType) type).getRawType()); 412 } 413 log.warn("Unable to determines generic-type for {}", type); 414 return Optional.empty(); 415 } 416 417 /** 418 * Extracts a {@link ParameterizedType} view for the given type 419 * 420 * @param typeToBeExtractedFrom must not be null 421 * @return an {@link Optional} of the {@link ParameterizedType} view of the given class. 422 */ 423 public static Optional<ParameterizedType> extractParameterizedType(final Class<?> typeToBeExtractedFrom) { 424 log.debug("Extracting ParameterizedType from {}", typeToBeExtractedFrom); 425 if (null == typeToBeExtractedFrom) { 426 return Optional.empty(); 427 } 428 if (Object.class.equals(typeToBeExtractedFrom)) { 429 log.debug("java.lang.Object is not a ParameterizedType"); 430 return Optional.empty(); 431 } 432 final Type genericSuperclass = typeToBeExtractedFrom.getGenericSuperclass(); 433 if (genericSuperclass instanceof ParameterizedType) { 434 return Optional.of((ParameterizedType) genericSuperclass); 435 } 436 // Check the tree 437 return extractParameterizedType(typeToBeExtractedFrom.getSuperclass()); 438 } 439 440 /** 441 * Returns a proxy instance that implements {@code interfaceType} by dispatching method 442 * invocations to {@code handler}. The class loader of {@code interfaceType} will be used to 443 * define the proxy class. To implement multiple 444 * interfaces or specify a class loader, use Proxy#newProxyInstance(Class, Constructor, 445 * InvocationHandler). 446 * 447 * @param interfaceType must not be null 448 * @param handler the invocation handler 449 * @param <T> the target type of the proxy 450 * @return the created Proxy-instance 451 * @throws IllegalArgumentException if {@code interfaceType} does not specify the type of Java 452 * interface 453 * @author https://github.com/google/guava/blob/master/guava/src/com/google/common/reflect/Reflection.java 454 */ 455 public static <T> T newProxy(final Class<T> interfaceType, final InvocationHandler handler) { 456 requireNonNull(handler); 457 Preconditions.checkArgument(interfaceType.isInterface(), "%s is not an interface", interfaceType); 458 final Object object = Proxy.newProxyInstance(interfaceType.getClassLoader(), new Class<?>[] {interfaceType}, handler); 459 return interfaceType.cast(object); 460 } 461 462 /** 463 * Try to detect class from call stack which was the previous, before the marker class name 464 * 465 * @param markerClasses class names which could be used as marker before the real caller name. 466 * Collection must not 467 * be {@code null}. 468 * @return option of detected caller class name 469 */ 470 public static Optional<String> findCaller(final Collection<String> markerClasses) { 471 final Optional<StackTraceElement> callerElement = findCallerElement(null, markerClasses); 472 return callerElement.map(StackTraceElement::getClassName); 473 } 474 475 /** 476 * Tries to detect class from call stack which was the previous, before the marker class name 477 * 478 * @param throwable is an optional parameter, will be used to access the stack 479 * @param markerClasses class names which could be used as marker before the real caller name. 480 * Collection must not be {@code null}. 481 * @return option of detected caller class name 482 */ 483 public static Optional<StackTraceElement> findCallerElement(final Throwable throwable, final Collection<String> markerClasses) { 484 Objects.requireNonNull(markerClasses, "Marker class names are missing"); 485 final StackTraceElement[] stackTraceElements; 486 if (null == throwable) { 487 stackTraceElements = Thread.currentThread().getStackTrace(); 488 } else { 489 stackTraceElements = throwable.getStackTrace(); 490 } 491 if (null == stackTraceElements || stackTraceElements.length < 5) { 492 return Optional.empty(); 493 } 494 for (int index = 2; index < stackTraceElements.length; index++) { 495 final StackTraceElement element = stackTraceElements[index]; 496 if (markerClasses.contains(element.getClassName())) { 497 if (stackTraceElements.length > index + 1) { 498 return Optional.of(stackTraceElements[index + 1]); 499 } else { 500 return Optional.empty(); 501 } 502 } 503 } 504 return Optional.empty(); 505 } 506 507 @java.lang.SuppressWarnings("all") 508 @lombok.Generated 509 private MoreReflection() { 510 throw new java.lang.UnsupportedOperationException("This is a utility class and cannot be instantiated"); 511 } 512}