package tc.oc.commons.core.reflect;
import java.lang.annotation.Annotation;
import java.lang.invoke.LambdaMetafactory;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import com.google.common.base.Preconditions;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableSet;
import com.google.common.reflect.Invokable;
import com.google.common.reflect.Parameter;
import com.google.common.reflect.TypeToken;
import static com.google.common.base.Preconditions.checkArgument;
public final class Methods {
private Methods() {}
public static String describeParameters(Stream<Class<?>> parameterTypes) {
return "(" + parameterTypes.map(Class::getSimpleName).collect(Collectors.joining(", ")) + ")";
}
public static String describeParameters(Class<?>... parameterTypes) {
return describeParameters(Stream.of(parameterTypes));
}
public static String describeParameters(MethodType methodType) {
return describeParameters(methodType.parameterList().stream());
}
public static String describe(@Nullable Class<?> decl, @Nullable Class<?> returnType, @Nullable String name, @Nullable Stream<Class<?>> parameterTypes) {
String text = "";
if(returnType != null) {
text += returnType.getSimpleName() + " ";
}
if(name != null) {
if(decl != null) {
text += decl.getSimpleName() + "#" + name;
} else {
text += name;
}
}
if(parameterTypes != null) {
text += describeParameters(parameterTypes);
}
return text;
}
public static String describe(Class<?> decl, Class<?> returnType, String name, Class<?>... parameterTypes) {
return describe(decl, returnType, name, Stream.of(parameterTypes));
}
public static String describe(Class<?> decl, String name, Class<?>... parameterTypes) {
return describe(decl, null, name, Stream.of(parameterTypes));
}
public static String describe(Class<?> decl, String name) {
return describe(decl, null, name, (Stream) null);
}
public static String describe(Method method) {
return describe(method.getDeclaringClass(), method.getReturnType(), method.getName(), method.getParameterTypes());
}
public static String describe(Class<?> decl, MethodType methodType, String name) {
return describe(decl, methodType.returnType(), name, methodType.parameterArray());
}
public static String describe(MethodType methodType, String name) {
return describe(null, methodType, name);
}
public static String removeBeanPrefix(String name, String prefix) {
if(name.startsWith(prefix) && name.length() > prefix.length()) {
final char first = name.charAt(prefix.length());
if(Character.isUpperCase(first)) {
return Character.toLowerCase(first) + name.substring(prefix.length() + 1);
}
}
return name;
}
public static String removeBeanPrefix(String name) {
name = removeBeanPrefix(name, "get");
name = removeBeanPrefix(name, "set");
name = removeBeanPrefix(name, "is");
return name;
}
public static @Nullable Method tryMethod(Class<?> decl, Method method) {
return tryMethod(decl, method.getName(), method.getParameterTypes());
}
public static @Nullable Method tryMethod(Class<?> decl, String name, Class<?>... params) {
try {
return decl.getMethod(name, params);
} catch(NoSuchMethodException e) {
return null;
}
}
public static @Nullable Method tryDeclaredMethod(Class<?> decl, String name, Class<?>... params) {
try {
return decl.getDeclaredMethod(name, params);
} catch(NoSuchMethodException e) {
return null;
}
}
public static boolean hasMethod(Class<?> decl, String name, Class<?>... params) {
return tryMethod(decl, name, params) != null;
}
public static Method method(Class<?> decl, Method method) {
return method(decl, method.getName(), method.getParameterTypes());
}
public static Method method(Class<?> decl, String name, Class<?>... params) {
try {
return decl.getMethod(name, params);
} catch(NoSuchMethodException e) {
throw new NoSuchMethodError(describe(decl, name, params));
}
}
public static Method declaredMethod(Class<?> decl, String name, Class<?>... params) {
try {
return decl.getDeclaredMethod(name, params);
} catch(NoSuchMethodException e) {
throw new NoSuchMethodError(describe(decl, name, params));
}
}
public static void assertPublicThrows(Executable method, Class<?>... exceptions) {
Members.assertPublic(method);
for(Class<?> ex : method.getExceptionTypes()) {
if(!RuntimeException.class.isAssignableFrom(ex)) {
boolean found = false;
for(Class<?> allowed : exceptions) {
if(allowed.isAssignableFrom(ex)) {
found = true;
break;
}
}
if(!found) {
Members.error(method, "throws unhandled exception " + ex.getName());
}
}
}
}
public static String descriptor(Class<?>[] parameterTypes, Class<?> returnType) {
String desc = "(";
for(Class param : parameterTypes) {
desc += Types.descriptor(param);
}
return desc + ')' + Types.descriptor(returnType);
}
public static String descriptor(Method method) {
return descriptor(method.getParameterTypes(), method.getReturnType());
}
public static String descriptor(Constructor<?> ctor) {
return descriptor(ctor.getParameterTypes(), void.class);
}
public static boolean isCallable(Method method) {
return !Members.isAbstract(method);
}
public static boolean respondsTo(Object obj, Method call) {
return call.getDeclaringClass().isInstance(obj) || respondsTo(obj.getClass(), call);
}
public static boolean respondsTo(Class<?> cls, Method call) {
return callableMethod(cls, call) != null;
}
public static @Nullable Method accessibleMethod(Class<?> cls, Method call) {
Method method = findClassMethod(cls, call);
if(method != null) return method;
return findInterfaceMethod(cls, call, false);
}
public static @Nullable Method callableMethod(Class<?> cls, Method call) {
Method method = findClassMethod(cls, call);
if(method != null) {
// If any superclass declares the method, defaults will not
// be called, even if the override is abstract.
return isCallable(method) ? method : null;
}
method = findInterfaceMethod(cls, call, true);
if(method != null) return method;
return null;
}
private static @Nullable Method findClassMethod(@Nullable Class<?> cls, Method call) {
if(cls == null) return null;
if(cls.isInterface()) return null;
try {
final Method method = cls.getDeclaredMethod(call.getName(), call.getParameterTypes());
if(!Members.isPrivate(method)) return method; // may be abstract
} catch(NoSuchMethodException ignored) {}
return findClassMethod(cls.getSuperclass(), call);
}
private static @Nullable Method findInterfaceMethod(@Nullable Class<?> cls, Method call, boolean callable) {
if(cls == null) return null;
if(callable && call.getDeclaringClass().isAssignableFrom(Object.class)) return null; // Object method defaults are not allowed
if(cls.isInterface()) {
try {
final Method method = cls.getDeclaredMethod(call.getName(), call.getParameterTypes());
if(!callable || isCallable(method)) return method;
} catch(NoSuchMethodException ignored) {}
}
for(Class<?> iface : cls.getInterfaces()) {
final Method method = findInterfaceMethod(iface, call, callable);
if(method != null) return method;
}
return null;
}
/**
* Would the given method be overridden/implemented by a method with the given name and parameter types?
*
* This is true if all of the following are true:
*
* - The parent method is overridable (i.e. non-private, and non-static)
* - Both methods have the same name
* - Both methods have identical parameter types
* - The parent method's return type is assignable from the child method's return type
*
* @param parent Parent method
* @param returnType Return type of the child method
* @param name Name of the child method
* @param parameterTypes Parameter types of the child method
*/
//public static boolean isOverride(Invokable parent, TypeToken<?> returnType, String name, List<TypeToken<?>> parameterTypes) {
// return name.equals(parent.getName()) &&
// isSignatureOverride(parent, returnType, parameterTypes);
//}
//
//public static boolean isSignatureOverride(Invokable parent, TypeToken<?> returnType, List<TypeToken<?>> parameterTypes) {
// return parent.isOverridable() &&
// parent.getParameters()
// parameterTypes.equals(context.getParameterTypes(parent)) &&
// Types.isAssignable(context.getReturnType(parent), returnType);
//}
//
//public static boolean isOverride(Method parent, Invokable child, TypeLiteral<?> context) {
// return child.getName().equals(parent.getName()) &&
// isSignatureOverride(parent, child, context);
//}
//
//public static boolean isSignatureOverride(Invokable parent, Invokable child) {
// return parent.isOverridable() &&
// parent.getParameters()
// return isSignatureOverride(parent,
// context.getReturnType(child),
// context.getParameterTypes(child),
// context);
//}
/**
* Find a method declared on the given class that would override, or be overridden by,
* a method with the given name and parameter types. Return null if no such method can be found.
*/
public static @Nullable Method overrideIn(Class<?> cls, String name, Class<?>... parameterTypes) {
try {
final Method method = cls.getDeclaredMethod(name, parameterTypes);
if(Members.isInheritable(method)) return method;
} catch(NoSuchMethodException ignored) {}
return null;
}
public static @Nullable Method overrideIn(Class<?> cls, Method child) {
return overrideIn(cls, child.getName(), child.getParameterTypes());
}
public static boolean hasOverrideIn(Class<?> cls, Method method) {
return overrideIn(cls, method) != null;
}
public static Set<Method> ancestors(Method method) {
if(Members.isPrivate(method)) return Collections.emptySet();
final ImmutableSet.Builder<Method> builder = ImmutableSet.builder();
for(Class<?> ancestor : Types.ancestors(method.getDeclaringClass())) {
final Method sup = overrideIn(ancestor, method);
if(sup != null) builder.add(sup);
}
return builder.build();
}
public static Stream<Method> accessibleMethods(Class<?> klass) {
return declaredMethodsInAncestors(klass).filter(method -> !Members.isPrivate(method));
}
public static Stream<Method> declaredMethodsInAncestors(Class<?> klass) {
return Types.ancestors(klass)
.stream()
.flatMap(ancestor -> Stream.of(ancestor.getDeclaredMethods()))
.distinct();
}
public static <T> Stream<Invokable<T, Object>> declaredMethodsInAncestors(TypeToken<T> klass) {
return declaredMethodsInAncestors(klass.getRawType()).map(klass::method);
}
/**
* Return a collection of all methods in the given class that have the given annotation
*/
public static <T extends Annotation> Collection<Method> annotatedMethods(Class<?> klass, final Class<T> annotation) {
return Collections2.filter(Arrays.asList(klass.getMethods()),
method -> method.getAnnotation(annotation) != null);
}
/**
* Get the first annotation of the given type on the given method or any of its supermethods.
* Supermethods are searched in the same order as {@link Types#ancestors(Class, boolean)}.
*/
public static @Nullable <T extends Annotation> T inheritableAnnotation(Method method, Class<T> annotationType) {
{
final T annotation = method.getAnnotation(annotationType);
if(annotation != null) return annotation;
}
if(!Members.isInheritable(method)) return null;
return Types.findForAncestor(method.getDeclaringClass(), t -> {
final Method parent = overrideIn(t, method);
return parent == null ? null : parent.getAnnotation(annotationType);
});
}
public static MethodType methodType(Method method) {
return MethodType.methodType(method.getReturnType(), method.getParameterTypes());
}
public static MethodType methodType(TypeToken<?> returnType, Collection<Parameter> parameters) {
return MethodType.methodType(returnType.getRawType(),
parameters.stream()
.map(p -> p.getType().getRawType())
.collect(Collectors.toList()));
}
public static MethodType methodType(Invokable method) {
return methodType(method.getReturnType(), method.getParameters());
}
private static @Nullable String invocationFailureReason(MethodType to, MethodType from) {
if(!Types.isConvertibleForInvocation(to.returnType(), from.returnType())) {
return "cannot convert return value from " + from.returnType().getName() + " to " + to.returnType().getName();
}
if(to.parameterCount() != from.parameterCount()) {
return "required " + to.parameterCount() + " parameters, but " + from.parameterCount() + " provided";
}
for(int i = 0; i < to.parameterCount(); i++) {
if(!Types.isConvertibleForInvocation(from.parameterType(i), to.parameterType(i))) {
return "cannot convert parameter " + i + " from " + to.parameterType(i).getName() + " to " + from.parameterType(i).getName();
}
}
return null;
}
private static @Nullable String invocationFailureReason(Invokable<?, ?> to, Invokable<?, ?> from) {
final String reason = invocationFailureReason(methodType(to), methodType(from));
if(reason != null) return reason;
thrownLoop: for(TypeToken<? extends Throwable> thrown : from.getExceptionTypes()) {
final Class<?> thrownRaw = thrown.getRawType();
if(Error.class.isAssignableFrom(thrownRaw)) continue;
if(RuntimeException.class.isAssignableFrom(thrownRaw)) continue ;
for(TypeToken<? extends Throwable> caught : to.getExceptionTypes()) {
if(caught.getRawType().isAssignableFrom(thrownRaw)) continue thrownLoop;
}
return "unhandled exception " + thrown.getRawType().getName();
}
return null;
}
public static @Nullable Method trySamMethod(Class<?> iface) {
if(!iface.isInterface()) return null;
final Method[] methods = iface.getMethods();
if(methods.length == 1) {
return methods[0];
}
Method sam = null;
boolean first = true;
for(Method method : methods) {
if(Members.isStatic(method)) continue;
if(first) {
// If we've only seen one method, assume it's the SAM
first = false;
sam = method;
} else {
// If there are multiple methods, and we initially assumed that a default method was the
// SAM, reverse that assumption.
if(sam != null && sam.isDefault()) {
sam = null;
}
// If we find an abstract method, and we already have a SAM (which must also be abstract),
// then the interface is non-functional. Otherwise, assume this method is the SAM and keep
// going (to make sure there are no more abstract methods).
if(!method.isDefault()) {
if(sam != null) {
return null;
}
sam = method;
}
}
}
return sam;
}
public static boolean isFunctionalInterface(Class<?> iface) {
return trySamMethod(iface) != null;
}
public static Method samMethod(Class<?> iface) {
final Method method = trySamMethod(iface);
if(method == null) {
throw new ClassFormException(iface, "not a functional interface");
}
return method;
}
public static @Nullable <T> Invokable<T, Object> trySamInvokable(TypeToken<T> iface) {
final Method method = trySamMethod(iface.getRawType());
return method == null ? null : iface.method(method);
}
public static <T> Invokable<T, Object> samInvokable(TypeToken<T> iface) {
return iface.method(samMethod(iface.getRawType()));
}
//public static boolean implementsFunctionalInterface(Class<?> iface, Invokable method) {
// return isSignatureOverride(samMethod(iface), method);
//}
public static <T, U> T lambda(Class<T> samType, Method method, @Nullable U target) {
return lambda(TypeToken.of(samType), method, target);
}
/**
* Implement the given functional interface by delegating to the given
* method and target object. The signature of the method is verified to
* be compatible with the interface, using the same rules as lexical
* method references.
*/
public static <T, U> T lambda(TypeToken<T> samType, Method implMethod, @Nullable U target) {
final boolean instance = !Members.isStatic(implMethod);
if(instance) {
Preconditions.checkNotNull(target);
} else {
checkArgument(target == null);
}
final MethodHandles.Lookup lookup;
final MethodType callSiteType;
final Invokable<U, Object> implInvokable;
if(instance) {
lookup = MethodHandleUtils.privateLookup(target.getClass());
callSiteType = MethodType.methodType(samType.getRawType(), target.getClass());
implInvokable = (Invokable<U, Object>) TypeToken.of(target.getClass()).method(implMethod);
} else {
lookup = MethodHandleUtils.privateLookup(implMethod.getDeclaringClass());
callSiteType = MethodType.methodType(samType.getRawType());
implInvokable = (Invokable<U, Object>) Invokable.from(implMethod);
}
final Method samMethod = samMethod(samType.getRawType());
final MethodType samMethodType = methodType(samMethod);
final Invokable<T, Object> samInvokable = samType.method(samMethod);
final MethodType samInvokableType = methodType(samInvokable);
final String error = invocationFailureReason(samInvokable, implInvokable);
if(error != null) {
throw new MethodFormException(implMethod, "could not be adapted to functional interface " + samType + ": " + error);
}
try {
final MethodHandle factory = LambdaMetafactory.metafactory(
lookup,
samMethod.getName(),
callSiteType,
samMethodType,
lookup.unreflect(implMethod),
samInvokableType
).getTarget();
return (T) (instance ? factory.invoke(target) : factory.invoke());
} catch(Throwable e) {
throw new MethodFormException(implMethod, "could not be adapted to functional interface " + samType, e);
}
}
}