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

org.bbottema.javareflection.MethodUtils Maven / Gradle / Ivy

package org.bbottema.javareflection;

import lombok.experimental.UtilityClass;
import org.bbottema.javareflection.model.InvokableObject;
import org.bbottema.javareflection.model.LookupMode;
import org.bbottema.javareflection.model.MethodModifier;
import org.bbottema.javareflection.valueconverter.IncompatibleTypeException;
import org.bbottema.javareflection.valueconverter.ValueConversionHelper;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static org.bbottema.javareflection.util.MiscUtil.trustedCast;
import static org.slf4j.LoggerFactory.getLogger;

/**
 * This reflection tool is designed to perform advanced method or constructor lookups,
 * using a combination of {@link LookupMode} strategies.
 * 

* It tries to find a constructor of a given datatype, with a given argument * datatypelist, where types do not have to match formal types (auto-boxing, supertypes, implemented interfaces and type conversions are allowed as * they are included in the lookup cycles). This expanded version tries a simple call first (exact match, which is provided natively by the Java) and * when this fails, it generates a list of datatype arrays (signatures) with all possible versions of any type in the original list possible, and * combinations thereof. *

* Observe the following (trivial) example: * *

 * 	interface Foo {
 * 		void foo(Double value, Fruit fruit, char c);
 *        }
 * 	abstract class A implements Foo {
 *    }
 * 	abstract class B extends A {
 *    }
 *
 * ClassUtils.findCompatibleJavaMethod(B.class, "foo", EnumSet.allOf(LookupMode.class), double.class, Pear.class, String.class)}
 * 
*

* In the above example, the method foo will be found by finding all methods named "Foo" on the interfaces implemented by supertype A, * and then foo's method signature will be matched using autoboxing on the double type, a cast to the Fruit supertype for * the Pear type and finally by attempting a common conversion from String to char. This will give you a Java * {@link Method}, but you won't be able to invoke it if it was found using a less strict lookup than one with a simple exact match. There are two * ways to do this: use {@link #invokeCompatibleMethod(Object, Class, String, Object...)} instead or perform the conversion yourself using {@link * ValueConversionHelper#convert(Object[], Class[], boolean)} prior to invoking the method. ValueConverter.convert(args, * method.getParameterTypes()). *

* A reverse lookup is also possible: given an ordered list of possible types, is a given Method compatible? *

* Because this lookup is potentially very expensive, a cache is present to store lookup results. */ @UtilityClass public final class MethodUtils { private static final Logger LOGGER = getLogger(MethodUtils.class); /** * {@link Method} cache categorized by owning Classes (since several owners can have a method with the same name and signature). * Methods are stored based on Method reference along with their unique signature (per owner), so multiple methods on one owner with * the same name can coexist.
*
* This cache is being maintained to reduce lookup times when trying to find signature compatible Java methods. The possible signature * combinations using autoboxing and/or automatic common conversions can become very large (7000+ with only three parameters) and can become a * real problem. The more frequently a method is being called the larger the performance gain, especially for methods with long parameter lists * * @see MethodUtils#addMethodToCache(Class, String, InvokableObject, Class[]) * @see MethodUtils#getMethodFromCache(Class, String, Class[]) */ private final static Map, Map[]>>> methodCache = new LinkedHashMap<>(); @SuppressWarnings({"WeakerAccess", "unused"}) public static void resetCache() { methodCache.clear(); } /** * Locates a method on an Object using serveral searchmodes for optimization. First of all a {@link Method} cache is being maintained to quickly * fetch heavily used methods. If not cached before and if a simple search (autoboxing and supertype casts) fails a more complex search is done * where all interfaces are searched for the method as well. If this fails as well, this method will try to autoconvert the types of the arguments * and find a matching signature that way. * * @param context The object to call the method from (can be null). * @param datatype The class to find the method on. * @param identifier The name of the method to locate. * @param args A list of [non-formal] arguments. * @return The return value of the invoke method, if successful. * @throws NoSuchMethodException Thrown by {@link #findCompatibleMethod(Class, String, EnumSet, Class...)}. * @throws IllegalArgumentException Thrown by {@link Method#invoke(Object, Object...)}. * @throws IllegalAccessException Thrown by {@link Method#invoke(Object, Object...)}. * @throws InvocationTargetException Thrown by {@link Method#invoke(Object, Object...)}. */ @SuppressWarnings({"WeakerAccess"}) @Nullable public static T invokeCompatibleMethod(@Nullable final Object context, final Class datatype, final String identifier, final Object... args) throws NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException { // determine the signature we want to find a compatible java method for final Class[] parameterList = TypeUtils.collectTypes(args); // setup lookup procedure starting with simple search mode EnumSet lookupMode = EnumSet.of(LookupMode.AUTOBOX, LookupMode.CAST_TO_SUPER); InvokableObject iMethod; // try to find a compatible Java method using various lookup modes try { iMethod = findCompatibleMethod(datatype, identifier, lookupMode, parameterList); } catch (final NoSuchMethodException e1) { try { // moderate search mode lookupMode.add(LookupMode.CAST_TO_INTERFACE); iMethod = findCompatibleMethod(datatype, identifier, lookupMode, parameterList); } catch (final NoSuchMethodException e2) { try { // limited conversions searchmode lookupMode.add(LookupMode.COMMON_CONVERT); iMethod = findCompatibleMethod(datatype, identifier, lookupMode, parameterList); } catch (NoSuchMethodException e3) { // full searchmode lookupMode.add(LookupMode.SMART_CONVERT); iMethod = findCompatibleMethod(datatype, identifier, lookupMode, parameterList); } } } Method method = (Method) iMethod.getMethod(); method.setAccessible(true); try { Object[] convertedArgs = ValueConversionHelper.convert(args, iMethod.getCompatibleSignature(), false); return trustedCast(method.invoke(context, convertedArgs)); } catch (IncompatibleTypeException e) { LOGGER.error("Found a method with compatible parameter list, but not all parameters could be converted", e); throw new NoSuchMethodException(); } } /** * Locates and invokes a {@link Constructor}using {@link #invokeConstructor(Class, Class[], Object[])} * * @param Used to parameterize the returned object so that the caller doesn't need to cast. * @param datatype The class to find the constructor for. * @param args A list of [non-formal] arguments. * @return The instantiated object of class datatype. * @throws IllegalAccessException Thrown by {@link #invokeConstructor(Class, Class[], Object[])}. * @throws InvocationTargetException Thrown by {@link #invokeConstructor(Class, Class[], Object[])}. * @throws InstantiationException Thrown by {@link #invokeConstructor(Class, Class[], Object[])}. * @throws NoSuchMethodException Thrown by {@link #invokeConstructor(Class, Class[], Object[])}. * @see java.lang.reflect.Constructor#newInstance(Object[]) */ @SuppressWarnings({"UnusedReturnValue", "WeakerAccess", "unused"}) @NotNull public static T invokeCompatibleConstructor(final Class datatype, final Object... args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { final Class[] parameterList = TypeUtils.collectTypes(args); return invokeConstructor(datatype, parameterList, args); } /** * Locates and invokes a {@link Constructor}, using a customized typelist. Avoids dynamically trying to find correct parameter type list. Can also * be used to force up/down casting (ie. passing a specific type of List into a generic type) * * @param Used to parameterize the returned object so that the caller doesn't need to cast. * @param datatype The class to find the constructor for. * @param signature The typelist used to find correct constructor. * @param args A list of [non-formal] arguments. * @return The instantiated object of class datatype. * @throws IllegalAccessException Thrown by {@link Constructor#newInstance(Object...)}. * @throws InvocationTargetException Thrown by {@link Constructor#newInstance(Object...)}. * @throws InstantiationException Thrown by {@link Constructor#newInstance(Object...)}. * @throws NoSuchMethodException Thrown by {@link #findCompatibleConstructor(Class, EnumSet, Class...)}. * @see java.lang.reflect.Constructor#newInstance(Object[]) */ @SuppressWarnings("WeakerAccess") @NotNull public static T invokeConstructor(final Class datatype, final Class[] signature, final Object[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { // setup lookup procedure EnumSet lookupMode = EnumSet.of(LookupMode.AUTOBOX, LookupMode.CAST_TO_SUPER); InvokableObject iConstructor; // try to find a compatible Java constructor try { iConstructor = findCompatibleConstructor(datatype, lookupMode, signature); } catch (final NoSuchMethodException e1) { try { lookupMode.add(LookupMode.CAST_TO_INTERFACE); iConstructor = findCompatibleConstructor(datatype, lookupMode, signature); } catch (final NoSuchMethodException e2) { try { lookupMode.add(LookupMode.COMMON_CONVERT); iConstructor = findCompatibleConstructor(datatype, lookupMode, signature); } catch (final NoSuchMethodException e3) { lookupMode.add(LookupMode.SMART_CONVERT); iConstructor = findCompatibleConstructor(datatype, lookupMode, signature); } } } try { Object[] convertedArgs = ValueConversionHelper.convert(args, iConstructor.getCompatibleSignature(), false); return trustedCast(iConstructor.getMethod().newInstance(convertedArgs)); } catch (IncompatibleTypeException e) { LOGGER.error("Found a constructor with compatible parameter list, but not all parameters could be converted", e); throw new NoSuchMethodException(); } } /** * Tries to find a {@link Constructor} of a given type, with a given typelist, where types do not match due to formal types simple types. * This expanded version tries a simple call first and when it fails, it generates a list of type arrays with all possible (un)wraps of any type * in the original list possible, and combinations thereof. * * @param Used to parameterize the returned constructor. * @param datatype The class to get the constructor from. * @param lookupMode Flag indicating the search steps that need to be done. * @param signature The list of types as specified by the user. * @return The constructor if found, otherwise exception is thrown. * @exception NoSuchMethodException Thrown when the {@link Constructor} could not be found on the data type, even after performing optional * conversions. */ @SuppressWarnings({"WeakerAccess"}) public static InvokableObject findCompatibleConstructor(final Class datatype, final EnumSet lookupMode, final Class... signature) throws NoSuchMethodException { // first try to find the constructor in the method cache InvokableObject constructor = getConstructorFromCache(datatype, datatype.getName(), signature); if (constructor != null) { return constructor; } else { try { // try standard call constructor = new InvokableObject(datatype.getConstructor(signature), signature, signature); } catch (final NoSuchMethodException e) { // failed, try all possible wraps/unwraps final List[]> compatibleSignatures = TypeUtils.generateCompatibleTypeLists(lookupMode, signature); for (final Class[] compatibleSignature : compatibleSignatures) { try { constructor = new InvokableObject(datatype.getConstructor(compatibleSignature), signature, compatibleSignature); break; } catch (final NoSuchMethodException x) { // do nothing } } } } // if a constructor was found (and it wasn't in the cache, because method would've returned already) if (constructor != null) { addMethodToCache(datatype, datatype.getName(), constructor, signature); return constructor; } else { throw new NoSuchMethodException(); } } /** * Delegates to {@link #findCompatibleMethod(Class, String, EnumSet, Class...)}, using strict lookupmode (no autoboxing, casting etc.) and * optional signature parameters. * * @param datatype The class to get the constructor from. * @param methodName The name of the method to retrieve from the class. * @param signature The list of types as specified by the user. * @return null in case of a NoSuchMethodException exception. * @see #findCompatibleMethod(Class, String, EnumSet, Class...) */ @Nullable @SuppressWarnings("WeakerAccess") public static InvokableObject findSimpleCompatibleMethod(final Class datatype, final String methodName, final Class... signature) { try { return findCompatibleMethod(datatype, methodName, EnumSet.noneOf(LookupMode.class), signature); } catch (final NoSuchMethodException e) { return null; } } /** * Same as getConstructor(), except for getting a {@link Method} of a classtype, using the name to indicate which method should be * located. * * @param datatype The class to get the constructor from. * @param methodName The name of the method to retrieve from the class. * @param lookupMode Flag indicating the search steps that need to be done. * @param signature The list of types as specified by the user. * @return The method if found, otherwise exception is thrown. * @exception NoSuchMethodException Thrown when the {@link Method} could not be found on the data type, even after performing optional * conversions. */ @NotNull @SuppressWarnings("WeakerAccess") public static InvokableObject findCompatibleMethod(final Class datatype, final String methodName, final EnumSet lookupMode, final Class... signature) throws NoSuchMethodException { // first try to find the method in the method cache InvokableObject iMethod = getMethodFromCache(datatype, methodName, signature); if (iMethod != null) { return iMethod; } else { try { // try standard call iMethod = new InvokableObject<>(getMethod(datatype, methodName, signature), signature, signature); } catch (final NoSuchMethodException e) { // failed, try all possible wraps/unwraps final List[]> signatures = TypeUtils.generateCompatibleTypeLists(lookupMode, signature); for (final Class[] compatibleSignature : signatures) { try { iMethod = new InvokableObject<>(getMethod(datatype, methodName, compatibleSignature), signature, compatibleSignature); break; } catch (final NoSuchMethodException x) { // do nothing } } } } // if a method was found (and it wasn't in the cache, because method would've returned already) if (iMethod != null) { addMethodToCache(datatype, methodName, iMethod, signature); return iMethod; } else { throw new NoSuchMethodException(); } } /** * Searches a specific class object for a {@link Method} using java reflect using a specific signature. This method will first search all * implemented interfaces for the method to avoid visibility problems.
*
* An example of such a problem is the Iterator as implemented by the ArrayList. The Iterator is implemented as a * private innerclass and as such not accessible by java reflect (even though the implemented methods are declared public), unlike the * interface's definition. * * @param datatype The class reference to locate the method on. * @param name The name of the method to find. * @param signature The signature the method should match. * @return The Method found on the data type that matched the specified signature. * @exception NoSuchMethodException Thrown when the {@link Method} could not be found on the interfaces implemented by the given data type. * @see java.lang.Class#getMethod(String, Class[]) */ @SuppressWarnings("WeakerAccess") @NotNull public static Method getMethod(final Class datatype, final String name, final Class... signature) throws NoSuchMethodException { for (final Class iface : datatype.getInterfaces()) { try { return iface.getMethod(name, signature); } catch (final NoSuchMethodException e) { // do nothing } } try { return datatype.getMethod(name, signature); } catch (final NoSuchMethodException e) { return datatype.getDeclaredMethod(name, signature); } } @SuppressWarnings({"unused", "WeakerAccess"}) public static boolean isMethodCompatible(Method method, EnumSet lookupMode, final Class... signature) { final Class[] targetSignature = method.getParameterTypes(); if (signature.length != targetSignature.length) { return false; } return TypeUtils.isTypeListCompatible(signature, targetSignature, lookupMode); } /** * Retrieves a {@link Method} from a cache. * * @param datatype The owning {@link Class} of the Method being searched for. * @param method The name of the method that is being searched for. * @param signature The parameter list of the method we need to match if a method was found by name. * @return The Method found on the specified owner with matching name and signature. * @see MethodUtils#methodCache * @see MethodUtils#addMethodToCache(Class, String, InvokableObject, Class[]) */ @Nullable private static InvokableObject getInvokableObjectFromCache(final Class datatype, final String method, final Class... signature) { final Map[]>> owner = methodCache.get(datatype); // we know only methods with parameter list are stored in the cache if (signature.length > 0) { // get owner, its methods matching specified name and match their signatures if (owner != null) { final Map[]> signatures = owner.get(method); if (signatures != null) { for (final Map.Entry[]> entry : signatures.entrySet()) { if (Arrays.equals(entry.getValue(), signature)) { return entry.getKey(); } } } } } // method not found or known not to be stored due to absent parameter list return null; } @Nullable private static InvokableObject getMethodFromCache(final Class datatype, final String method, final Class... signature) { return trustedCast(getInvokableObjectFromCache(datatype, method, signature)); } @Nullable private static InvokableObject getConstructorFromCache(final Class datatype, final String method, final Class... signature) { return trustedCast(getInvokableObjectFromCache(datatype, method, signature)); } /** * Adds a specific Method to the cache. * * @param datatype The Class that owns the Method. * @param method The Method's name by which methods can be found on the specified owner. * @param methodRef The Method reference that's actually being stored in the cache. * @param signature The parameter list of the Method being stored. * @see MethodUtils#methodCache * @see MethodUtils#getMethodFromCache(Class, String, Class...) */ private static void addMethodToCache(final Class datatype, final String method, final InvokableObject methodRef, final Class... signature) { // only store methods with a parameter list if (signature.length > 0) { // get or create owner entry Map[]>> owner = methodCache.get(datatype); owner = owner != null ? owner : new LinkedHashMap[]>>(); // get or create list of methods with specified method name Map[]> methods = owner.get(method); methods = methods != null ? methods : new LinkedHashMap[]>(); // add or overwrite method entry methods.put(methodRef, signature); // finally shelve all the stuff back methods.put(methodRef, signature); owner.put(method, methods); methodCache.put(datatype, owner); } } /** * Delegates to {@link #findMatchingMethods(Class, Class, String, String...)} */ @SuppressWarnings("unused") public static Set findMatchingMethods(final Class datatype, @Nullable Class boundaryMarker, String methodName, List paramTypeNames) { return findMatchingMethods(datatype, boundaryMarker, methodName, paramTypeNames.toArray(new String[0])); } /** * @return Methods found using {@link ClassUtils#collectMethods(Class, Class, EnumSet)} * and then filters based on the parameter type names. */ @SuppressWarnings("unused") public static Set findMatchingMethods(final Class datatype, @Nullable Class boundaryMarker, String methodName, String... paramTypeNames) { Set matchingMethods = new HashSet<>(); for (Method method : ClassUtils.collectMethods(datatype, boundaryMarker, MethodModifier.MATCH_ANY)) { Class[] methodParameterTypes = method.getParameterTypes(); if (method.getName().equals(methodName) && methodParameterTypes.length == paramTypeNames.length && typeNamesMatch(methodParameterTypes, paramTypeNames)) { matchingMethods.add(method); } } return matchingMethods; } private static boolean typeNamesMatch(Class[] parameterTypes, String[] typeNamesToMatch) { for (int i = 0; i < parameterTypes.length; i++) { final Class parameterType = parameterTypes[i]; final String typeNameToMatch = typeNamesToMatch[i]; if (parameterType.isArray()) { final String arrayTypeNameToMatch = typeNameToMatch.endsWith("...") ? typeNameToMatch.substring(0, typeNameToMatch.indexOf("...")) : typeNameToMatch; if (typeNamesDontMatch(parameterType.getComponentType(), arrayTypeNameToMatch)) { return false; } } else if (typeNamesDontMatch(parameterType, typeNameToMatch)) { return false; } } return true; } private static boolean typeNamesDontMatch(Class parameterType, String typeNameToMatch) { return !parameterType.getName().equals(typeNameToMatch) && !parameterType.getSimpleName().equals(typeNameToMatch); } /** * @return True if the given method contains a parameter that is an array of an {@link Iterable}. */ public static boolean methodHasCollectionParameter(final Method m) { for (Class parameterType : m.getParameterTypes()) { if (parameterType.isArray() || Iterable.class.isAssignableFrom(parameterType) || Map.class.isAssignableFrom(parameterType)) { return true; } } return false; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy