org.bbottema.javareflection.MethodUtils Maven / Gradle / Ivy
Show all versions of java-reflection Show documentation
/*
* Copyright (C) ${project.inceptionYear} Benny Bottema ([email protected])
*
* 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
*
* http://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 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.model.MethodParameter;
import org.bbottema.javareflection.util.MiscUtil;
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.annotation.Annotation;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import static java.lang.String.format;
import static org.bbottema.javareflection.LookupCaches.METHOD_CACHE;
import static org.bbottema.javareflection.TypeUtils.containsAnnotation;
import static org.bbottema.javareflection.util.MiscUtil.trustedCast;
import static org.bbottema.javareflection.util.MiscUtil.trustedNullableCast;
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);
/**
* Delegates to {@link Method#invoke(Object, Object...)} while converting checked exceptions into runtime
* exceptions.
*/
@Nullable
@SuppressWarnings({"unchecked"})
public static T invokeMethodSimple(final Method method, final Object subject, final Object... args) {
try {
return (T) method.invoke(subject, args);
} catch (SecurityException e) {
throw new RuntimeException("unable to invoke method; security problem", e);
} catch (IllegalAccessException e) {
throw new RuntimeException("unable to access method", e);
} catch (InvocationTargetException e) {
throw new RuntimeException("unable to invoke method", e);
}
}
/**
* 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, Set, 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>[] parameterSignature = TypeUtils.collectTypes(args);
// setup lookup procedure starting with simple search mode
Set lookupMode = EnumSet.of(LookupMode.AUTOBOX, LookupMode.CAST_TO_SUPER);
Set> iMethods;
// try to find a compatible Java method using various lookup modes
try {
iMethods = findCompatibleMethod(datatype, identifier, lookupMode, parameterSignature);
} catch (final NoSuchMethodException e1) {
try {
// moderate search mode
lookupMode.add(LookupMode.CAST_TO_INTERFACE);
iMethods = findCompatibleMethod(datatype, identifier, lookupMode, parameterSignature);
} catch (final NoSuchMethodException e2) {
try {
// limited conversions searchmode
lookupMode.add(LookupMode.COMMON_CONVERT);
iMethods = findCompatibleMethod(datatype, identifier, lookupMode, parameterSignature);
} catch (NoSuchMethodException e3) {
// full searchmode
lookupMode.add(LookupMode.SMART_CONVERT);
iMethods = findCompatibleMethod(datatype, identifier, lookupMode, parameterSignature);
}
}
}
for (InvokableObject iMethod : iMethods) {
iMethod.getMethod().setAccessible(true);
try {
Object[] convertedArgs = ValueConversionHelper.convert(args, iMethod.getCompatibleSignature(), false);
return trustedNullableCast(iMethod.getMethod().invoke(context, convertedArgs));
} catch (IncompatibleTypeException e) {
// keep trying conversion candidates...
}
}
LOGGER.error(format("Was unable to find a suitable method on %s for the parameter signature %s", datatype, Arrays.toString(parameterSignature)));
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 parameterSignature 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, Set, Class...)}.
* @see java.lang.reflect.Constructor#newInstance(Object[])
*/
@SuppressWarnings("WeakerAccess")
@NotNull
public static T invokeConstructor(final Class datatype, final Class>[] parameterSignature, final Object[] args) throws NoSuchMethodException,
IllegalAccessException, InvocationTargetException, InstantiationException {
// setup lookup procedure
Set lookupMode = EnumSet.of(LookupMode.AUTOBOX, LookupMode.CAST_TO_SUPER);
Set> iConstructors;
// try to find a compatible Java constructor
try {
iConstructors = findCompatibleConstructor(datatype, lookupMode, parameterSignature);
} catch (final NoSuchMethodException e1) {
try {
lookupMode.add(LookupMode.CAST_TO_INTERFACE);
iConstructors = findCompatibleConstructor(datatype, lookupMode, parameterSignature);
} catch (final NoSuchMethodException e2) {
try {
lookupMode.add(LookupMode.COMMON_CONVERT);
iConstructors = findCompatibleConstructor(datatype, lookupMode, parameterSignature);
} catch (final NoSuchMethodException e3) {
lookupMode.add(LookupMode.SMART_CONVERT);
iConstructors = findCompatibleConstructor(datatype, lookupMode, parameterSignature);
}
}
}
for (InvokableObject iConstructor : iConstructors) {
try {
Object[] convertedArgs = ValueConversionHelper.convert(args, iConstructor.getCompatibleSignature(), false);
return trustedCast(iConstructor.getMethod().newInstance(convertedArgs));
} catch (IncompatibleTypeException e) {
// keep trying conversion candidates...
}
}
LOGGER.error(format("Was unable to find a suitable constructor on %s for the parameter signature %s", datatype, Arrays.toString(parameterSignature)));
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 Set> findCompatibleConstructor(final Class datatype, final Set lookupMode, final Class>... signature)
throws NoSuchMethodException {
// first try to find the constructor in the method cache
Set> iConstructors = getConstructorFromCache(datatype, datatype.getName(), signature);
if (iConstructors != null) {
return iConstructors;
} else {
iConstructors = new HashSet<>();
try {
// try standard call
iConstructors.add(new InvokableObject(datatype.getConstructor(signature), signature, signature));
} catch (final NoSuchMethodException e) {
for (final Class>[] compatibleSignature : TypeUtils.generateCompatibleTypeLists(lookupMode, signature)) {
try {
iConstructors.add(new InvokableObject(datatype.getConstructor(compatibleSignature), signature, compatibleSignature));
} catch (final NoSuchMethodException x) {
// do nothing
}
}
}
}
if (!iConstructors.isEmpty()) {
return addMethodToCache(datatype, datatype.getName(), iConstructors, signature);
} else {
throw new NoSuchMethodException();
}
}
/**
* Delegates to {@link #findCompatibleMethod(Class, String, Set, 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, Set, Class...)
*/
@NotNull
@SuppressWarnings("WeakerAccess")
public static Set> 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 new HashSet<>();
}
}
/**
* Delegates to {@link #findCompatibleMethod(Class, String, Set, Class[])}, with the types of the given arguments extracted using {@link TypeUtils#collectTypes(Object[])}.
*/
public static Set> findCompatibleMethod(final Class> datatype, final String methodName, final Set lookupMode,
final Object... args) throws NoSuchMethodException {
return findCompatibleMethod(datatype, methodName, lookupMode, TypeUtils.collectTypes(args));
}
/**
* 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 Set> findCompatibleMethod(final Class> datatype, final String methodName, final Set lookupMode,
final Class>... signature) throws NoSuchMethodException {
// first try to find the method in the method cache
Set> iMethods = getMethodFromCache(datatype, methodName, signature);
if (iMethods != null) {
return iMethods;
} else {
iMethods = new HashSet<>();
try {
// try standard call
iMethods.add(new InvokableObject<>(getMethod(datatype, methodName, signature), signature, signature));
} catch (final NoSuchMethodException e) {
for (final Class>[] compatibleSignature : TypeUtils.generateCompatibleTypeLists(lookupMode, signature)) {
try {
iMethods.add(new InvokableObject<>(getMethod(datatype, methodName, compatibleSignature), signature, compatibleSignature));
} catch (final NoSuchMethodException x) {
// do nothing
}
}
}
}
if (!iMethods.isEmpty()) {
return addMethodToCache(datatype, methodName, iMethods, signature);
} 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);
}
}
/**
* Tests if a list of classes is compatible with the signature of the given method, allowing for {@link LookupMode#SIMPLE} lookup mode.
*/
@SuppressWarnings({"unused"})
public static boolean isMethodCompatible(Method method, final Object... signature) {
return isMethodCompatible(method, LookupMode.SIMPLE, signature);
}
/**
* Tests if a list of classes is compatible with the signature of the given method, allowing for the given lookup modes.
*/
@SuppressWarnings({"unused"})
public static boolean isMethodCompatible(Method method, Set lookupMode, final Object... signature) {
return isMethodCompatible(method, lookupMode, TypeUtils.collectTypes(signature));
}
/**
* Tests if a list of arguments is compatible with the signature of the given method, allowing for {@link LookupMode#SIMPLE} lookup mode.
*/
@SuppressWarnings({"unused"})
public static boolean isMethodCompatible(Method method, final Class>... signature) {
return isMethodCompatible(method, LookupMode.SIMPLE, signature);
}
/**
* Tests if a list of arguments is compatible with the signature of the given method, allowing for the given lookup modes.
*/
@SuppressWarnings({"unused", "WeakerAccess"})
public static boolean isMethodCompatible(Method method, Set lookupMode, final Class>... signature) {
final Class>[] targetSignature = method.getParameterTypes();
if (signature.length != targetSignature.length) {
return false;
}
return TypeUtils.isTypeListCompatible(signature, targetSignature, lookupMode);
}
/**
* Given a method and a list of arguments, return a map of parameters matching their arguments.
* If the arguments are not compatible, return null instead.
*/
@SuppressWarnings({"unused"})
@Nullable
public static LinkedHashMap zipParametersAndArguments(Method method, Object... arguments) {
if (isMethodCompatible(method, arguments)) {
final LinkedHashMap result = new LinkedHashMap<>();
for (int i = 0; i < method.getParameterTypes().length; i++) {
result.put(new MethodParameter(i,
method.getParameterTypes()[i],
method.getGenericParameterTypes()[i],
Arrays.asList(method.getParameterAnnotations()[i])), arguments[i]);
}
return result;
}
return null;
}
/**
* 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 LookupCaches#METHOD_CACHE
* @see MethodUtils#addMethodToCache(Class, String, Set, Class[])
*/
@Nullable
private static Set getInvokableObjectFromCache(final Class datatype, final String method, final Class>... signature) {
final Map[], Set>> owner = METHOD_CACHE.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 && owner.containsKey(method)) {
return owner.get(method).get(signature);
}
}
// method not found or known not to be stored due to absent parameter list
return null;
}
@Nullable
private static Set> getMethodFromCache(final Class datatype, final String method, final Class>... signature) {
return trustedNullableCast(getInvokableObjectFromCache(datatype, method, signature));
}
@Nullable
private static Set> getConstructorFromCache(final Class datatype, final String method, final Class>... signature) {
return trustedNullableCast(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 methodInvocationCandidates The Method
reference that's actually being stored in the cache.
* @param signature The parameter list of the Method
being stored.
* @see LookupCaches#METHOD_CACHE
* @see MethodUtils#getMethodFromCache(Class, String, Class...)
*/
private static , T2 extends AccessibleObject> Set addMethodToCache(final Class> datatype, final String method,
final Set methodInvocationCandidates, final Class>... signature) {
// only store methods with a parameter list
if (signature.length > 0) {
// get or create owner entry
Map[], Set>> owner = METHOD_CACHE.get(datatype);
owner = owner != null ? owner : new LinkedHashMap[], Set>>();
// get or create list of methods with specified method name
Map[], Set> methods = owner.get(method);
methods = methods != null ? methods : new LinkedHashMap[], Set>();
// add or overwrite method entry
methods.put(signature, MiscUtil.>trustedCast(methodInvocationCandidates));
// finally shelve all the stuff back
methods.put(signature, MiscUtil.>trustedCast(methodInvocationCandidates));
owner.put(method, methods);
METHOD_CACHE.put(datatype, owner);
}
return methodInvocationCandidates;
}
/**
* Delegates to {@link #findMatchingMethods(Class, Class, String, String...)}
*/
@SuppressWarnings({ "unused", "WeakerAccess" })
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", "WeakerAccess" })
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}.
*/
@SuppressWarnings("WeakerAccess")
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;
}
public static Method onlyMethod(Set> methods) {
if (methods.size() == 0) {
return null;
} else if (methods.size() == 1) {
return methods.iterator().next().getMethod();
} else {
throw new AssertionError("Expected 1 or less methods, but found more than 1 methods: " + methods);
}
}
@SuppressWarnings("unchecked")
@Nullable
public static Target firstParameterArgumentByAnnotation(Method method, Object[] arguments, Class annotationClass) {
if (isMethodCompatible(method, arguments)) {
for (int i = 0; i < method.getParameterTypes().length; i++) {
if (containsAnnotation(method.getParameterAnnotations()[i], annotationClass)) {
return (Target) arguments[i];
}
}
}
return null;
}
public static int firstParameterIndexByAnnotation(Method method, Class annotationClass) {
for (int i = 0; i < method.getParameterTypes().length; i++) {
if (containsAnnotation(method.getParameterAnnotations()[i], annotationClass)) {
return i;
}
}
return -1;
}
}