Methods.java
/*-
* #%L
* io.earcam.instrumental.reflect
* %%
* Copyright (C) 2018 earcam
* %%
* SPDX-License-Identifier: (BSD-3-Clause OR EPL-1.0 OR Apache-2.0 OR MIT)
*
* You <b>must</b> choose to accept, in full - any individual or combination of
* the following licenses:
* <ul>
* <li><a href="https://opensource.org/licenses/BSD-3-Clause">BSD-3-Clause</a></li>
* <li><a href="https://www.eclipse.org/legal/epl-v10.html">EPL-1.0</a></li>
* <li><a href="https://www.apache.org/licenses/LICENSE-2.0">Apache-2.0</a></li>
* <li><a href="https://opensource.org/licenses/MIT">MIT</a></li>
* </ul>
* #L%
*/
package io.earcam.instrumental.reflect;
import static java.util.stream.Collectors.toList;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Stream;
import javax.annotation.ParametersAreNonnullByDefault;
import io.earcam.unexceptional.Exceptional;
/**
* <p>
* Methods class.
* </p>
*
*/
@ParametersAreNonnullByDefault
public final class Methods {
/** Constant <code>IS_BRIDGE</code> */
public static final Predicate<Method> IS_BRIDGE = Method::isBridge;
/** Constant <code>IS_SYNTHETIC</code> */
public static final Predicate<Method> IS_SYNTHETIC = Method::isSynthetic;
/** Constant <code>IS_STATIC</code> */
public static final Predicate<Method> IS_STATIC = hasModifiers(Modifier.STATIC);
static final Predicate<Method> IS_NOT_BRIDGE = IS_BRIDGE.negate();
static final Predicate<Method> IS_NOT_SYNTHETIC = IS_SYNTHETIC.negate();
static final Predicate<Method> IS_NOT_STATIC = IS_STATIC.negate();
private Methods()
{}
/**
* <p>
* hasModifiers.
* </p>
*
* @param modifiers a int.
* @return a {@link java.util.function.Predicate} object.
*/
public static Predicate<Method> hasModifiers(int... modifiers)
{
int mods = Arrays.stream(modifiers).reduce(0, (a, b) -> a | b);
return m -> (m.getModifiers() & mods) == mods;
}
/**
* <p>
* getMethod.
* </p>
*
* @param type a {@link java.lang.Class} object.
* @param name a {@link java.lang.String} object.
* @param parameterTypes a {@link java.lang.Class} object.
* @return a {@link java.util.Optional} object.
*/
public static Optional<Method> getMethod(Class<?> type, String name, Class<?>... parameterTypes)
{
return allMethodsOf(type)
.filter(m -> m.getName().equals(name) && Arrays.equals(m.getParameterTypes(), parameterTypes))
.findFirst();
}
/**
* Note: To find a <b>var-args</b> method, the <code>args</code> element must be an actual array
*
* @param instance a {@link java.lang.Object} object.
* @param name the method's name
* @param args instances from which the parameter type can be deduced
* @return the {@link java.lang.reflect.Method} if found
*/
public static Optional<Method> getMethod(Object instance, String name, Object... args)
{
return getMethod(instance.getClass(), name, parameterTypesOf(args));
}
private static Class<?>[] parameterTypesOf(Object[] args)
{
return Arrays.stream(args)
.map(Object::getClass)
.toArray(s -> new Class<?>[s]);
}
/**
* <p>
* allMethodsOf.
* </p>
*
* @param type the class
* @return absolutely <b>all</b> methods, including those inherited <b>and</b> those overridden
* @see #methodsOf(Class)
*/
public static Stream<Method> allMethodsOf(Class<?> type)
{
Objects.requireNonNull(type, "type cannot be null");
Stream<Method> methods = Stream.empty();
Class<?> t = type;
while(t != null) {
methods = Stream.concat(methods, Arrays.stream(t.getDeclaredMethods())).sequential();
t = t.getSuperclass();
}
t = type;
while(t != null) {
for(Class<?> iface : t.getInterfaces()) {
methods = Stream.concat(methods, Arrays.stream(iface.getDeclaredMethods()).filter(Method::isDefault)).sequential();
}
t = t.getSuperclass();
}
return methods;
}
/**
* <p>
* Finds <i>all</i> methods, including inherited but excluding overridden, bridge
* and synthetic - and excluding those inherited from {@link Object}.
* </p>
*
* @param type the class
* @return all methods, including inherited but excluding overridden, bridge and synthetic
* @see #allMethodsOf(Class)
*/
public static Stream<Method> methodsOf(Class<?> type)
{
List<Method> methods = allMethodsOf(type)
.filter(IS_NOT_BRIDGE.and(IS_NOT_SYNTHETIC))
.filter(Methods::isNotDeclaredOnObject)
.collect(toList());
List<Method> overridenRidden = removeOverridden(methods);
return overridenRidden.stream().sequential();
}
/**
* <p>
* removeOverridden.
* </p>
*
* @param methods a {@link java.util.List} object.
* @return a {@link java.util.List} object.
*/
protected static List<Method> removeOverridden(List<Method> methods)
{
List<Method> overridenRidden = new ArrayList<>(methods.size());
for(Method method : methods) {
if(!isOverridden(method, overridenRidden)) {
overridenRidden.add(method);
}
}
return overridenRidden;
}
private static boolean isNotDeclaredOnObject(Method m)
{
return m.getDeclaringClass() != Object.class;
}
private static boolean isOverridden(Method method, List<Method> overridenRidden)
{
Predicate<Method> bySameName = m -> m.getName().equals(method.getName());
Predicate<Method> parameterTypes = sameParameters(method);
Predicate<Method> returnTypes = m -> m.getReturnType().equals(method.getReturnType());
return overridenRidden.stream()
.anyMatch(bySameName.and(parameterTypes).and(returnTypes));
}
private static Predicate<Method> sameParameters(Method method)
{
Predicate<Method> numberOfParameters = m -> m.getParameterCount() == method.getParameterCount();
Predicate<Method> parameterTypes = m -> Arrays.equals(m.getParameterTypes(), method.getParameterTypes());
Predicate<Method> genericParameterTypes = m -> {
Type[] genericTypes = m.getGenericParameterTypes();
for(int i = 0; i < m.getParameterCount(); i++) {
Class<?> type = getClassFromType(genericTypes[i], m.getParameterTypes()[i], m);
if(!method.getParameterTypes()[i].isAssignableFrom(type)) {
return false;
}
}
return true;
};
return numberOfParameters.and(parameterTypes.or(genericParameterTypes));
}
private static Class<?> getClassFromType(Type type, Class<?> clazz, Method m)
{
if(type instanceof ParameterizedType) {
ParameterizedType parameterized = (ParameterizedType) type;
return Exceptional.apply(classLoaderOf(m)::loadClass, parameterized.getRawType().getTypeName());
}
if(type.getTypeName().equals(clazz.getCanonicalName())) {
return clazz;
}
return Exceptional.apply(classLoaderOf(m)::loadClass, type.getTypeName());
}
private static ClassLoader classLoaderOf(Method m)
{
ClassLoader classLoader = m.getDeclaringClass().getClassLoader();
return (classLoader == null) ? ClassLoader.getSystemClassLoader() : classLoader;
}
/**
* MethodHandle for normal class, or default interface, method.
*
* @param method a normal class instance or interface default method
* @return a handle
*/
public static MethodHandle handleFor(Method method)
{
return Exceptional.get(() -> isJava8() ? java8HandleFor(method) : java9PlusHandleFor(method));
}
private static boolean isJava8()
{
return System.getProperty("java.version").startsWith("1.8");
}
private static MethodHandle java9PlusHandleFor(Method method) throws ReflectiveOperationException
{
Class<?> targetClass = method.getDeclaringClass();
String descriptor = Names.descriptorFor(method).toString();
MethodType methodType = MethodType.fromMethodDescriptorString(descriptor, targetClass.getClassLoader());
if(IS_STATIC.test(method)) {
return MethodHandles.lookup()
.in(Methods.class)
.findStatic(targetClass, method.getName(), methodType);
} else if(method.isDefault()) {
return MethodHandles.lookup()
.in(Methods.class)
.findSpecial(targetClass, method.getName(), methodType, targetClass);
} else {
return MethodHandles.lookup()
.in(Methods.class)
.findVirtual(targetClass, method.getName(), methodType);
}
}
private static MethodHandle java8HandleFor(Method method) throws ReflectiveOperationException
{
Class<?> targetClass = method.getDeclaringClass();
Field field = Lookup.class.getDeclaredField("IMPL_LOOKUP");
field.setAccessible(true);
Lookup lookup = (Lookup) field.get(null);
return (IS_STATIC.test(method)) ? lookup.unreflect(method) : lookup.unreflectSpecial(method, targetClass);
}
}