package tc.oc.commons.core.reflect;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Predicate;
import javax.annotation.Nullable;
import com.google.inject.TypeLiteral;
import tc.oc.commons.core.ListUtils;
import tc.oc.commons.core.util.Utils;
/**
* Search a type hiearchy for methods matching a given filter.
*
* If matching methods override each other, only the most derived
* method is included in the result.
*/
public class MethodScanner<D> {
private class Signature {
final @Nullable Object context;
final String name;
final List<Class<?>> parameters;
final int hashCode;
Signature(Method method) {
name = method.getName();
parameters = ListUtils.transformedCopyOf(decl.getParameterTypes(method), TypeLiteral::getRawType);
if(Members.isPrivate(method)) {
// Private methods are only visible within the same class (i.e. not overridable)
context = method.getDeclaringClass();
} else if(Members.isProtected(method) || Members.isPublic(method)) {
// Protected and public methods are visible throughout the entire hiearchy
context = null;
} else {
// Package-local methods can only override within the same package
context = method.getDeclaringClass().getPackage();
}
hashCode = Objects.hash(context, name, parameters);
}
@Override
public int hashCode() {
return hashCode;
}
@Override
public boolean equals(Object obj) {
return Utils.equals(Signature.class, this, obj, that ->
Objects.equals(this.context, that.context) &&
Objects.equals(this.name, that.name) &&
Objects.equals(this.parameters, that.parameters)
);
}
}
private final TypeLiteral<D> decl;
private final Map<Signature, Method> map = new HashMap<>();
private final Predicate<? super Method> filter;
public MethodScanner(Class<D> decl, Predicate<? super Method> filter) {
this(TypeLiteral.get(decl), filter);
}
public MethodScanner(TypeLiteral<D> decl, Predicate<? super Method> filter) {
this.decl = decl;
this.filter = filter;
visitClasses(decl.getRawType());
visitInterfaces(decl.getRawType());
}
public Collection<Method> methods() {
return map.values();
}
private void visitType(Class<?> type) {
if(!type.isInterface()) {
visitClasses(type);
}
visitInterfaces(type);
}
private void visitClasses(@Nullable Class<?> type) {
if(type != null) {
visitMethods(type);
visitClasses(type.getSuperclass());
}
}
private void visitInterfaces(Class<?> type) {
if(type.isInterface()) {
visitMethods(type);
}
for(Class<?> iface : type.getInterfaces()) {
visitInterfaces(iface);
}
}
private void visitMethods(Class<?> type) {
for(Method method : type.getDeclaredMethods()) {
if(!method.isSynthetic() && !method.isBridge() && filter.test(method)) {
map.merge(new Signature(method), method, (ma, mb) -> {
// Figure out which method overrides the other
final Class<?> ta = ma.getDeclaringClass();
final Class<?> tb = mb.getDeclaringClass();
// If one method is declared in a class and the other is declared
// in an interface, the class method always wins.
if(!ta.isInterface() && tb.isInterface()) return ma;
if(!tb.isInterface() && ta.isInterface()) return mb;
// If one method is a default (interface) method, and the other
// one isn't, keep the default one (the other method must be
// a normal interface method).
if(ma.isDefault() && !mb.isDefault()) return ma;
if(mb.isDefault() && !ma.isDefault()) return mb;
// If one method's owner is a subtype of the other method's owner,
// keep the subtype method.
if(tb.isAssignableFrom(ta)) return ma;
if(ta.isAssignableFrom(tb)) return mb;
// If all else fails, keep the method that was encountered first.
// I'm pretty sure this can only happen with two abstract methods
// in unrelated interfaces, in which case it probably doesn't
// matter which one is kept.
return ma;
});
}
}
}
}