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