All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.cuioss.tools.reflect.MoreReflection Maven / Gradle / Ivy

Go to download

Utility Library acting as a replacement for googles guava, certain apache-commons libraries and logging facades/frameworks.

There is a newer version: 2.1.1
Show newest version
/*
 * Copyright 2023 the original author or authors.
 * 

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

* https://www.apache.org/licenses/LICENSE-2.0 *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package de.cuioss.tools.reflect; import static de.cuioss.tools.collect.MoreCollections.requireNotEmpty; import static java.util.Objects.requireNonNull; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Proxy; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.WeakHashMap; import de.cuioss.tools.base.Preconditions; import de.cuioss.tools.collect.CollectionBuilder; import de.cuioss.tools.logging.CuiLogger; import lombok.Synchronized; import lombok.experimental.UtilityClass; /** * Provides a number of methods simplifying the usage of Reflection-based * access. *

Caution

*

* Use reflection only if there is no other way. Even if some of the problems * are minimized by using this type. It should be used either in test-code, what * is we actually do, and not production code. An other reason could be * framework code. as for that you should exactly know what you do. *

* * @author Oliver Wolff */ @UtilityClass public final class MoreReflection { private static final String IGNORING_METHOD_ON_CLASS = "Ignoring method '{}' on class '{}'"; private static final CuiLogger log = new CuiLogger(MoreReflection.class); /** * We use {@link WeakHashMap} in order to allow the garbage collector to do its * job */ private static final Map, List> publicObjectMethodCache = new WeakHashMap<>(); private static final Map, Map> fieldCache = new WeakHashMap<>(); /** * Tries to access a field on a given type. If none can be found it recursively * calls itself with the corresponding parent until {@link Object}. * Caution: *

* The field elements are shared between requests (cached), therefore you must * ensure that changes to the instance, like * {@link Field#setAccessible(boolean)} are reseted by the client. This can be * simplified by using {@link FieldWrapper} *

* * @param type to be checked, must not be null * @param fieldName to be checked, must not be null * @return an {@link Optional} {@link Field} if it can be found */ @Synchronized @SuppressWarnings("java:S3824") // owolff: computeIfAbsent is not an option because we add null // to the field public static Optional accessField(final Class type, final String fieldName) { requireNonNull(type); requireNonNull(fieldName); if (!fieldCache.containsKey(type)) { fieldCache.put(type, new HashMap<>()); } final Map typeMap = fieldCache.get(type); if (!typeMap.containsKey(fieldName)) { typeMap.put(fieldName, resolveField(type, fieldName).orElse(null)); } return Optional.ofNullable(typeMap.get(fieldName)); } private static Optional resolveField(final Class type, final String fieldName) { try { return Optional.of(type.getDeclaredField(fieldName)); } catch (final NoSuchFieldException | SecurityException e) { log.trace("Error while trying to read field {} on type {}", type, fieldName, e); if (Object.class.equals(type.getClass()) || null == type.getSuperclass()) { return Optional.empty(); } return resolveField(type.getSuperclass(), fieldName); } } /** * Determines the public not static methods of a given {@link Class}. * {@link Object#getClass()} will implicitly ignore * * @param clazz to be checked * @return the found public-methods. */ @Synchronized public static List retrievePublicObjectMethods(final Class clazz) { requireNonNull(clazz); if (!publicObjectMethodCache.containsKey(clazz)) { final List found = new ArrayList<>(); for (final Method method : clazz.getMethods()) { final int modifiers = method.getModifiers(); if (Modifier.isPublic(modifiers) && !Modifier.isStatic(modifiers) && !"getClass".equals(method.getName())) { found.add(method); } } publicObjectMethodCache.put(clazz, found); return found; } return publicObjectMethodCache.get(clazz); } /** * Determines the access methods of a given class. An access method is defined * as being a public not static zero-argument method that is prefixed with * either "get" or "is". The Method "getClass" is explicitly filtered * * @param clazz to be checked * @return the found access-methods. */ public static List retrieveAccessMethods(final Class clazz) { final List found = new ArrayList<>(); for (final Method method : retrievePublicObjectMethods(clazz)) { if (0 == method.getParameterCount()) { final var name = method.getName(); if (name.startsWith("get") || name.startsWith("is")) { log.debug("Adding found method '{}' on class '{}'", name, clazz); found.add(method); } } else { log.trace(IGNORING_METHOD_ON_CLASS, method.getName(), clazz); } } return found; } /** * Determines the access methods of a given class. An access method is defined * as being a public not static zero-argument method that is prefixed with * either "get" or "is". The Method "getClass" is explicitly filtered * * @param clazz to be checked * @param ignoreProperties identifies the property by name that must be filtered * from the result * @return the found access-methods. */ public static List retrieveAccessMethods(final Class clazz, final Collection ignoreProperties) { final List found = new ArrayList<>(); for (final Method method : retrieveAccessMethods(clazz)) { final var propertyName = computePropertyNameFromMethodName(method.getName()); if (!ignoreProperties.contains(propertyName)) { found.add(method); } } return found; } /** * Determines the modifier methods of a given class. A modifier method is * defined as being a public not static single-argument method that is prefixed * with either "set" or consists of the propertyName only. * * @param clazz to be checked * @param propertyName to be checked, must not be null * @param parameterType identifying the parameter to be passed to the given * method, must not be null * @return the found modifier-method or {@link Optional#empty()} if none could * be found */ public static Optional retrieveWriteMethod(final Class clazz, final String propertyName, final Class parameterType) { requireNonNull(parameterType); for (final Method method : retrieveWriteMethodCandidates(clazz, propertyName)) { if (checkWhetherParameterIsAssignable(method.getParameterTypes()[0], parameterType)) { return Optional.of(method); } log.trace(IGNORING_METHOD_ON_CLASS, method.getName(), clazz); } return Optional.empty(); } /** * @param assignableSource the type to be checked * @param queryType to be checked for * @return boolean indicating whether the given parameter, identified by their * class attributes match */ public static boolean checkWhetherParameterIsAssignable(final Class assignableSource, final Class queryType) { requireNonNull(assignableSource); requireNonNull(queryType); if (assignableSource.equals(queryType)) { log.trace("Parameter-type matches exactly '%s'", assignableSource); return true; } if (assignableSource.isAssignableFrom(queryType)) { log.trace("Parameter '%s' is assignable from '%s'", assignableSource, queryType); return true; } final Class boxedSource = resolveWrapperTypeForPrimitive(assignableSource); final Class boxedQuery = resolveWrapperTypeForPrimitive(queryType); if (boxedSource.equals(boxedQuery)) { log.trace("Parameter-type matches exactly after autoboxing '%s'", assignableSource); return true; } return boxedSource.isAssignableFrom(boxedQuery); } /** * Helper class for converting a primitive to a wrapper type. * * @param check must not be null * @return the wrapper type if the given type represents a primitive, the given * type otherwise. */ static Class resolveWrapperTypeForPrimitive(final Class check) { if (!check.isPrimitive()) { return check; } return switch (check.getName()) { case "boolean" -> Boolean.class; case "byte" -> Byte.class; case "char" -> Character.class; case "short" -> Short.class; case "int" -> Integer.class; case "long" -> Long.class; case "double" -> Double.class; case "float" -> Float.class; default -> { log.warn("Unable to determine wrapper type for '{}', ", check); yield check; } }; } /** * Determines the modifier methods of a given class for a property. A modifier * method is defined as being a public not static single-argument method that is * prefixed with either "set" or consists of the ropertyName only. This will * implicitly return all possible setter or builder methods, e.g. * {@code setPropertyName(String name)}, {@code propertyName(String name)} and * {@code setPropertyName(Collection name)} will all be part of the * result. * * @param clazz to be checked * @param propertyName to be checked, must not be null * @return the found modifier-methods or an empty {@link Collection} if none * could be found */ public static Collection retrieveWriteMethodCandidates(final Class clazz, final String propertyName) { requireNotEmpty(propertyName); final var builder = new CollectionBuilder(); for (final Method method : retrievePublicObjectMethods(clazz)) { if (1 == method.getParameterCount()) { final var name = method.getName(); if (propertyName.equals(name)) { log.debug("Returning found method '{}' on class '{}'", name, clazz); builder.add(method); } if (name.startsWith("set") && computePropertyNameFromMethodName(name).equalsIgnoreCase(propertyName)) { log.debug("Returning found method '{}' on class '{}'", name, clazz); builder.add(method); } } else { log.trace(IGNORING_METHOD_ON_CLASS, method.getName(), clazz); } } return builder.toImmutableList(); } /** * Retrieves the access-method for a given property Name. See * {@link #retrieveAccessMethods(Class)} for the definition of an access-method * * @param clazz must not be null * @param propertyName to be accessed * @return {@link Optional#empty()} in case no method could be found, an * {@link Optional} with the found method otherwise. */ public static Optional retrieveAccessMethod(final Class clazz, final String propertyName) { requireNotEmpty(propertyName); for (final Method method : retrieveAccessMethods(clazz)) { if (computePropertyNameFromMethodName(method.getName()).equalsIgnoreCase(propertyName)) { return Optional.of(method); } } return Optional.empty(); } /** * Helper method that extract the property-name from a given accessor-method * name. * * @param methodName must not be null nor empty * @return the possible attribute name of a given method-name, e.g. it return * 'name' for getName/setName/isName. If none of the prefixes 'get', * 'set', 'is' is found it returns the passed String. */ public static String computePropertyNameFromMethodName(final String methodName) { requireNotEmpty(methodName); if (methodName.startsWith("get") || methodName.startsWith("set")) { if (methodName.length() > 3) { return methodName.substring(3, 4).toLowerCase() + methodName.substring(4); } log.debug("Name to short for extracting attributeName '{}'", methodName); } if (methodName.startsWith("is")) { if (methodName.length() > 2) { return methodName.substring(2, 3).toLowerCase() + methodName.substring(3); } log.debug("Name to short for extracting attributeName '{}'", methodName); } return methodName; } /** * Helper class for extracting all annotations of a given class * including from their ancestors. * * @param the concrete annotation type * @param annotatedType the (possibly) annotated type. If it is null or * {@link Object#getClass()} it will return an empty list * @param annotation the annotation to be extracted, must not be null * @return an immutable List with all annotations found at the given object or * one of its ancestors. May be empty but never null */ public static List extractAllAnnotations(final Class annotatedType, final Class annotation) { if (null == annotatedType || Object.class.equals(annotatedType.getClass())) { return Collections.emptyList(); } final var builder = new CollectionBuilder(); builder.add(annotatedType.getAnnotationsByType(annotation)); builder.add(extractAllAnnotations(annotatedType.getSuperclass(), annotation)); return builder.toImmutableList(); } /** * Helper class for extracting an annotation of a given class including from * their ancestors. * * @param the concrete annotation type * @param annotatedType the (possibly) annotated type. If it is null or * {@link Object#getClass()} {@link Optional#empty()} * @param annotation the annotation to be extracted, must not be null * @return an {@link Optional} on the annotated Object if the annotation can be * found. In case the annotation is found multiple times the first * element will be returned. */ public static Optional extractAnnotation(final Class annotatedType, final Class annotation) { requireNonNull(annotation); final List extracted = extractAllAnnotations(annotatedType, annotation); if (extracted.isEmpty()) { return Optional.empty(); } return Optional.of(extracted.iterator().next()); } /** * Extracts the first generic type argument for the given type. * * @param identifying the type to be looked for * @param typeToBeExtractedFrom must not be null * @return an {@link Optional} of the KeyStoreType-Argument of the given class. * @throws IllegalArgumentException in case the given type does not represent a * generic. */ @SuppressWarnings("unchecked") // owolff: The unchecked casting is necessary public static Class extractFirstGenericTypeArgument(final Class typeToBeExtractedFrom) { final var parameterizedType = extractParameterizedType(typeToBeExtractedFrom) .orElseThrow(() -> new IllegalArgumentException( "Given type defines no generic KeyStoreType: " + typeToBeExtractedFrom)); requireNotEmpty(parameterizedType.getActualTypeArguments(), "No type argument found for " + typeToBeExtractedFrom.getName()); final Class firstType = extractGenericTypeCovariantly(parameterizedType.getActualTypeArguments()[0]) .orElseThrow(() -> new IllegalArgumentException( "Unable to determine genric type for " + typeToBeExtractedFrom)); try { return (Class) firstType; } catch (final ClassCastException e) { throw new IllegalArgumentException( "No type argument can be extracted from " + typeToBeExtractedFrom.getName(), e); } } /** * @param type to be extracted from * @return if applicable the actual type argument for the given type. If the * type represents already a {@link Class} it will be returned directly. * Otherwise, the super-type will be checked by calling the superclass */ public static Optional> extractGenericTypeCovariantly(final Type type) { if (null == type) { log.trace("No KeyStoreType given, returning empty"); return Optional.empty(); } if (type instanceof Class class1) { log.debug("Found actual class returning as result {}", type); return Optional.of(class1); } if (type instanceof ParameterizedType parameterizedType) { log.debug("found Parameterized type, for {}, calling recursively", type); return extractGenericTypeCovariantly(parameterizedType.getRawType()); } log.warn("Unable to determines generic-type for {}", type); return Optional.empty(); } /** * Extracts a {@link ParameterizedType} view for the given type * * @param typeToBeExtractedFrom must not be null * @return an {@link Optional} of the {@link ParameterizedType} view of the * given class. */ public static Optional extractParameterizedType(final Class typeToBeExtractedFrom) { log.debug("Extracting ParameterizedType from {}", typeToBeExtractedFrom); if (null == typeToBeExtractedFrom) { return Optional.empty(); } if (Object.class.equals(typeToBeExtractedFrom)) { log.debug("java.lang.Object is not a ParameterizedType"); return Optional.empty(); } final var genericSuperclass = typeToBeExtractedFrom.getGenericSuperclass(); if (genericSuperclass instanceof ParameterizedType type) { return Optional.of(type); } // Check the tree return extractParameterizedType(typeToBeExtractedFrom.getSuperclass()); } /** * Returns a proxy instance that implements {@code interfaceType} by dispatching * method invocations to {@code handler}. The class loader of * {@code interfaceType} will be used to define the proxy class. To implement * multiple interfaces or specify a class loader, use * Proxy#newProxyInstance(Class, Constructor, InvocationHandler). * * @param interfaceType must not be null * @param handler the invocation handler * @param the target type of the proxy * @return the created Proxy-instance * @throws IllegalArgumentException if {@code interfaceType} does not specify * the type of Java interface * @author https://github.com/google/guava/blob/master/guava/src/com/google/common/reflect/Reflection.java */ public static T newProxy(final Class interfaceType, final InvocationHandler handler) { requireNonNull(handler); Preconditions.checkArgument(interfaceType.isInterface(), "%s is not an interface", interfaceType); final var object = Proxy.newProxyInstance(interfaceType.getClassLoader(), new Class[] { interfaceType }, handler); return interfaceType.cast(object); } /** * Try to detect class from call stack which was the previous, before the marker * class name * * @param markerClasses class names which could be used as marker before the * real caller name. Collection must not be {@code null}. * @return option of detected caller class name */ public static Optional findCaller(final Collection markerClasses) { final var callerElement = findCallerElement(null, markerClasses); return callerElement.map(StackTraceElement::getClassName); } /** * Tries to detect class from call stack which was the previous, before the * marker class name * * @param throwable is an optional parameter, will be used to access the * stack * @param markerClasses class names which could be used as marker before the * real caller name. Collection must not be {@code null}. * @return option of detected caller class name */ public static Optional findCallerElement(final Throwable throwable, final Collection markerClasses) { Objects.requireNonNull(markerClasses, "Marker class names are missing"); final StackTraceElement[] stackTraceElements; if (null == throwable) { stackTraceElements = Thread.currentThread().getStackTrace(); } else { stackTraceElements = throwable.getStackTrace(); } if (null == stackTraceElements || stackTraceElements.length < 5) { return Optional.empty(); } for (var index = 2; index < stackTraceElements.length; index++) { final var element = stackTraceElements[index]; if (markerClasses.contains(element.getClassName())) { if (stackTraceElements.length > index + 1) { return Optional.of(stackTraceElements[index + 1]); } return Optional.empty(); } } return Optional.empty(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy