package org.jruby.javasupport.binding;
import com.headius.backport9.modules.Modules;
import org.jruby.Ruby;
import org.jruby.RubyModule;
import org.jruby.internal.runtime.methods.JavaMethod;
import org.jruby.java.invokers.ConstructorInvoker;
import org.jruby.javasupport.Java;
import org.jruby.javasupport.JavaClass;
import org.jruby.javasupport.JavaSupport;
import org.jruby.runtime.Block;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.IdUtil;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import static org.jruby.runtime.Visibility.PUBLIC;
public class MethodGatherer {
private static final boolean DEBUG_SCALA = false;
private static final String METHOD_MANGLE = "__method";
private static final String CONSTRUCTOR_NAME = "__jcreate!";
private static final Method[] EMPTY_METHODS = new Method[0];
private static final int ACC_BRIDGE = 0x00000040;
private static final Map<String, String> SCALA_OPERATORS;
static {
HashMap<String, String> scalaOperators = new HashMap<>(24, 1);
scalaOperators.put("\\$plus", "+");
scalaOperators.put("\\$minus", "-");
scalaOperators.put("\\$colon", ":");
scalaOperators.put("\\$div", "/");
scalaOperators.put("\\$eq", "=");
scalaOperators.put("\\$less", "<");
scalaOperators.put("\\$greater", ">");
scalaOperators.put("\\$bslash", "\\\\");
scalaOperators.put("\\$hash", "#");
scalaOperators.put("\\$times", "*");
scalaOperators.put("\\$bang", "!");
scalaOperators.put("\\$at", "@");
scalaOperators.put("\\$percent", "%");
scalaOperators.put("\\$up", "^");
scalaOperators.put("\\$amp", "&");
scalaOperators.put("\\$tilde", "~");
scalaOperators.put("\\$qmark", "?");
scalaOperators.put("\\$bar", "|");
SCALA_OPERATORS = Collections.unmodifiableMap(scalaOperators);
}
private final Map<String, AssignedName> staticNames;
private final Map<String, AssignedName> instanceNames;
private static final Map<String, AssignedName> STATIC_RESERVED_NAMES;
private static final Map<String, AssignedName> INSTANCE_RESERVED_NAMES;
static {
STATIC_RESERVED_NAMES = newReservedNamesMap(1);
STATIC_RESERVED_NAMES.put("new", new AssignedName("new", Priority.RESERVED));
}
static {
INSTANCE_RESERVED_NAMES = newReservedNamesMap(2);
INSTANCE_RESERVED_NAMES.put("class", new AssignedName("class", Priority.RESERVED));
INSTANCE_RESERVED_NAMES.put("initialize", new AssignedName("initialize", Priority.RESERVED));
INSTANCE_RESERVED_NAMES.put("equal?", new AssignedName("equal?", Priority.RESERVED));
}
private static Map<String, AssignedName> newReservedNamesMap(final int size) {
HashMap<String, AssignedName> RESERVED_NAMES = new HashMap<>(size + 4, 1);
RESERVED_NAMES.put("__id__", new AssignedName("__id__", Priority.RESERVED));
RESERVED_NAMES.put("__send__", new AssignedName("__send__", Priority.RESERVED));
RESERVED_NAMES.put("instance_of?", new AssignedName("instance_of?", Priority.RESERVED));
return RESERVED_NAMES;
}
private Map<String, NamedInstaller> staticInstallers = Collections.EMPTY_MAP;
private Map<String, NamedInstaller> instanceInstallers = Collections.EMPTY_MAP;
private Map<String, ConstantField> constantFields = Collections.EMPTY_MAP;
final Ruby runtime;
MethodGatherer(final Ruby runtime, final Class superClass) {
this.runtime = runtime;
if (superClass == null) {
staticNames = new HashMap<>(STATIC_RESERVED_NAMES);
instanceNames = new HashMap<>(INSTANCE_RESERVED_NAMES);
} else {
JavaSupport javaSupport = runtime.getJavaSupport();
Map<String, AssignedName> staticAssignedNames = javaSupport.getStaticAssignedNames().get(superClass);
staticNames = new HashMap<>(staticAssignedNames.size() + STATIC_RESERVED_NAMES.size());
Map<String, AssignedName> instanceAssignedNames = javaSupport.getInstanceAssignedNames().get(superClass);
instanceNames = new HashMap<>(instanceAssignedNames.size() + INSTANCE_RESERVED_NAMES.size());
staticNames.putAll(STATIC_RESERVED_NAMES);
staticNames.putAll(staticAssignedNames);
instanceNames.putAll(INSTANCE_RESERVED_NAMES);
instanceNames.putAll(instanceAssignedNames);
}
}
void initialize(Class<?> javaClass, RubyModule proxy) {
setupFieldsAndConstants(javaClass);
setupMethods(javaClass);
setupScalaSingleton(javaClass);
assignStaticAliases();
JavaSupport javaSupport = runtime.getJavaSupport();
javaSupport.getStaticAssignedNames().get(javaClass).putAll(staticNames);
Map<String, AssignedName> instanceAssignedNames = javaSupport.getInstanceAssignedNames().get(javaClass);
if (javaClass.isInterface()) {
instanceAssignedNames.clear();
} else {
assignInstanceAliases();
instanceAssignedNames.putAll(instanceNames);
installInstanceMethods(proxy);
installConstructors(javaClass, proxy);
}
installConstants(proxy);
installClassMethods(proxy);
installInnerClasses(javaClass, proxy);
}
static Map<String, List<Method>> getMethods(final Class<?> javaClass) {
HashMap<String, List<Method>> nameMethods = new HashMap<>(32);
eachAccessibleMethod(
javaClass,
(classMethods) -> { addNewMethods(nameMethods, classMethods, true); return true; },
(interfaceMethods) -> { addNewMethods(nameMethods, interfaceMethods, false); return true; });
return nameMethods;
}
public static void eachAccessibleMethod(final Class<?> javaClass, Predicate<Method[]> classProcessor, Predicate<Method[]> interfaceProcessor) {
HashMap<String, List<Method>> nameMethods = new HashMap<>(32);
for ( Class<?> klass = javaClass; klass != null; klass = klass.getSuperclass() ) {
if (Modifier.isPublic(klass.getModifiers()) && Modules.isExported(klass, Java.class)) {
try {
PartitionedMethods filteredMethods = FILTERED_DECLARED_METHODS.get(klass);
if (!classProcessor.test(filteredMethods.instanceMethods)) return;
if (klass == javaClass) {
if (!classProcessor.test(filteredMethods.staticMethods)) return;
}
}
catch (SecurityException e) { }
}
for ( Class iface : INTERFACES.get(klass) ) {
try {
PartitionedMethods filteredMethods = FILTERED_METHODS.get(iface);
if (!interfaceProcessor.test(filteredMethods.instanceMethods)) return;
}
catch (SecurityException e) { }
}
}
}
private static boolean methodsAreEquivalent(Method child, Method parent) {
int childModifiers, parentModifiers;
return parent.getDeclaringClass().isAssignableFrom(child.getDeclaringClass())
&& child.getReturnType() == parent.getReturnType()
&& child.isVarArgs() == parent.isVarArgs()
&& Modifier.isPublic(childModifiers = child.getModifiers()) == Modifier.isPublic(parentModifiers = parent.getModifiers())
&& Modifier.isProtected(childModifiers) == Modifier.isProtected(parentModifiers)
&& Modifier.isStatic(childModifiers) == Modifier.isStatic(parentModifiers)
&& Arrays.equals(child.getParameterTypes(), parent.getParameterTypes());
}
private static void addNewMethods(
final HashMap<String, List<Method>> nameMethods,
final Method[] methods,
final boolean removeDuplicate) {
Methods: for (Method method : methods) {
List<Method> childMethods = nameMethods.get(method.getName());
if (childMethods == null) {
childMethods = new ArrayList<>(4);
childMethods.add(method);
nameMethods.put(method.getName(), childMethods);
}
else {
for (int i = 0; i < childMethods.size(); i++) {
final Method current = childMethods.get(i);
if ( methodsAreEquivalent(current, method) ) {
if (removeDuplicate) {
childMethods.set(i, method);
} else {
}
continue Methods;
}
}
childMethods.add(method);
}
}
}
public static final ClassValue<Method[]> DECLARED_METHODS = new ClassValue<Method[]>() {
@Override
public Method[] computeValue(Class cls) {
try {
return cls.getDeclaredMethods();
} catch (SecurityException se) {
return EMPTY_METHODS;
}
}
};
private static final ClassValue<PartitionedMethods> FILTERED_DECLARED_METHODS = new ClassValue<PartitionedMethods>() {
@Override
public PartitionedMethods computeValue(Class cls) {
return new PartitionedMethods(DECLARED_METHODS.get(cls));
}
};
private static final ClassValue<Method[]> METHODS = new ClassValue<Method[]>() {
@Override
public Method[] computeValue(Class cls) {
try {
return cls.getMethods();
} catch (SecurityException se) {
return EMPTY_METHODS;
}
}
};
private static final ClassValue<PartitionedMethods> FILTERED_METHODS = new ClassValue<PartitionedMethods>() {
@Override
public PartitionedMethods computeValue(Class cls) {
return new PartitionedMethods(METHODS.get(cls));
}
};
private static final ClassValue<Class<?>[]> INTERFACES = new ClassValue<Class<?>[]>() {
@Override
public Class<?>[] computeValue(Class cls) {
Class<?>[] baseInterfaces = cls.getInterfaces();
Set<Class<?>> interfaceSet = new HashSet<>();
addAllInterfaces(interfaceSet, cls);
return interfaceSet.toArray(new Class<?>[interfaceSet.size()]);
}
void addAllInterfaces(Set<Class<?>> set, Class<?> ifc) {
for (Class<?> i : ifc.getInterfaces()) {
set.add(i);
addAllInterfaces(set, i);
}
}
};
private static final ClassValue<Boolean> IS_SCALA = new ClassValue<Boolean>() {
@Override
protected Boolean computeValue(Class<?> type) {
if (type.isInterface()) return false;
boolean scalaAnno = false;
for (Annotation anno : type.getDeclaredAnnotations()) {
Package pkg = anno.annotationType().getPackage();
if (pkg != null && pkg.getName() != null && pkg.getName().startsWith("scala.")) {
scalaAnno = true;
break;
}
}
return scalaAnno;
}
};
protected void installInnerClasses(final Class<?> javaClass, final RubyModule proxy) {
Class<?>[] classes = JavaClass.getDeclaredClasses(javaClass);
final Ruby runtime = proxy.getRuntime();
for ( int i = classes.length; --i >= 0; ) {
final Class<?> clazz = classes[i];
if ( javaClass != clazz.getDeclaringClass() ) continue;
if ( ! Modifier.isPublic(clazz.getModifiers()) ) continue;
final String simpleName = JavaClass.getSimpleName(clazz);
if ( simpleName.length() == 0 ) continue;
if (constantFields.containsKey(simpleName)) {
runtime.getWarnings().warning("inner class \"" + javaClass.getName() + "::" + simpleName + "\" conflicts with field of same name");
continue;
}
final RubyModule innerProxy = Java.getProxyClass(runtime, JavaClass.get(runtime, clazz));
if ( IdUtil.isConstant(simpleName) ) {
if (proxy.getConstantAt(simpleName) == null) {
proxy.const_set(runtime.newString(simpleName), innerProxy);
}
}
else {
if ( ! proxy.respondsTo(simpleName) ) {
proxy.getSingletonClass().addMethod(simpleName, new JavaMethod.JavaMethodZero(proxy.getSingletonClass(), PUBLIC, simpleName) {
@Override
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule clazz, String name) {
return innerProxy;
}
});
}
}
}
}
protected void setupScalaSingleton(final Class<?> javaClass) {
if (javaClass.isInterface()) return;
try {
final ClassLoader loader = javaClass.getClassLoader();
if ( loader == null ) return;
if (!IS_SCALA.get(javaClass)) return;
Class<?> companionClass = loader.loadClass(javaClass.getName() + '$');
final Field field = companionClass.getField("MODULE$");
final Object singleton = field.get(null);
if ( singleton == null ) return;
getMethods(companionClass).forEach((name, methods) -> {
for (int j = 0; j < methods.size(); j++) {
final Method method = methods.get(j);
if (DEBUG_SCALA) Initializer.LOG.debug("Companion object method {} for {}", name, companionClass);
if (name.indexOf('$') >= 0) name = fixScalaNames(name);
if (!Modifier.isStatic(method.getModifiers())) {
AssignedName assignedName = staticNames.get(name);
if (INSTANCE_RESERVED_NAMES.containsKey(method.getName())) {
if (DEBUG_SCALA) Initializer.LOG.debug("in reserved " + name);
setupSingletonMethods(staticInstallers, javaClass, singleton, method, name + METHOD_MANGLE);
continue;
}
if (assignedName == null) {
staticNames.put(name, new AssignedName(name, Priority.METHOD));
if (DEBUG_SCALA) Initializer.LOG.debug("Assigned name is null");
} else {
if (Priority.METHOD.lessImportantThan(assignedName)) {
if (DEBUG_SCALA) Initializer.LOG.debug("Less important");
continue;
}
if (!Priority.METHOD.asImportantAs(assignedName)) {
staticInstallers.remove(name);
staticInstallers.remove(name + '=');
staticNames.put(name, new AssignedName(name, Priority.METHOD));
}
}
if (DEBUG_SCALA) Initializer.LOG.debug("Installing {} {} {}", name, method, singleton);
setupSingletonMethods(staticInstallers, javaClass, singleton, method, name);
} else {
if (DEBUG_SCALA) Initializer.LOG.debug("Method {} is sadly static", method);
}
}
});
}
catch (ClassNotFoundException e) { }
catch (NoSuchFieldException e) { }
catch (Exception e) {
if (DEBUG_SCALA) Initializer.LOG.debug("Failed with {}", e);
}
}
protected static String fixScalaNames(final String name) {
String s = name;
for (Map.Entry<String, String> entry : SCALA_OPERATORS.entrySet()) {
s = s.replaceAll(entry.getKey(), entry.getValue());
}
return s;
}
protected void installConstants(final RubyModule proxy) {
constantFields.forEach((name, field) -> field.install(proxy));
}
protected void installClassMethods(final RubyModule proxy) {
getStaticInstallers().forEach(($, value) -> value.install(proxy));
}
void installConstructors(Class<?> javaClass, final RubyModule proxy) {
Constructor[] constructors = JavaClass.getConstructors(javaClass);
boolean localConstructor = false;
for (Constructor constructor : constructors) {
localConstructor |= javaClass == constructor.getDeclaringClass();
}
if (localConstructor) {
proxy.addMethod(CONSTRUCTOR_NAME, new ConstructorInvoker(proxy, javaClass::getConstructors, CONSTRUCTOR_NAME));
} else {
proxy.addMethod(CONSTRUCTOR_NAME, new NoConstructorMethod(proxy, CONSTRUCTOR_NAME));
}
}
static class NoConstructorMethod extends JavaMethod {
NoConstructorMethod(RubyModule proxy, String name) {
super(proxy, PUBLIC, name);
}
@Override
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule clazz, String name, IRubyObject[] args, Block block) {
throw context.runtime.newTypeError("no public constructors for " + clazz);
}
}
protected void prepareStaticMethod(Class<?> javaClass, Method method, String name) {
prepareMethod(javaClass, method, name, getStaticInstallersForWrite(), STATIC_RESERVED_NAMES, staticNames, StaticMethodInvokerInstaller::new);
}
protected void prepareInstanceMethod(Class<?> javaClass, Method method, String name) {
prepareMethod(javaClass, method, name, getInstanceInstallersForWrite(), INSTANCE_RESERVED_NAMES, instanceNames, InstanceMethodInvokerInstaller::new);
}
protected void prepareMethod(Class<?> javaClass, Method method, String name, Map<String, NamedInstaller> installers, Map<String, AssignedName> reservedNames, Map<String, AssignedName> names, Function<String, NamedInstaller> constructor) {
if (reservedNames.containsKey(method.getName())) {
name = name + METHOD_MANGLE;
} else {
if (lowerPriority(name, installers, names)) return;
}
NamedInstaller invoker = installers.get(name);
if (invoker == null) {
invoker = constructor.apply(name);
installers.put(name, invoker);
}
((MethodInstaller) invoker).addMethod(method, javaClass);
}
private boolean lowerPriority(String name, Map<String, NamedInstaller> installers, Map<String, AssignedName> names) {
AssignedName assignedName = names.get(name);
if (assignedName == null) {
names.put(name, new AssignedName(name, Priority.METHOD));
} else {
if (Priority.METHOD.lessImportantThan(assignedName)) return true;
if (!Priority.METHOD.asImportantAs(assignedName)) {
installers.remove(name);
installers.remove(name + '=');
names.put(name, new AssignedName(name, Priority.METHOD));
}
}
return false;
}
Map<String, NamedInstaller> getStaticInstallers() {
return staticInstallers;
}
Map<String, NamedInstaller> getStaticInstallersForWrite() {
Map<String, NamedInstaller> staticInstallers = this.staticInstallers;
return staticInstallers == Collections.EMPTY_MAP ? this.staticInstallers = new HashMap() : staticInstallers;
}
Map<String, NamedInstaller> getInstanceInstallers() {
return instanceInstallers;
}
Map<String, NamedInstaller> getInstanceInstallersForWrite() {
Map<String, NamedInstaller> instanceInstallers = this.instanceInstallers;
return instanceInstallers == Collections.EMPTY_MAP ? this.instanceInstallers = new HashMap() : instanceInstallers;
}
void setupFieldsAndConstants(Class<?> javaClass) {
boolean isInterface = javaClass.isInterface();
Field[] fields = JavaClass.getDeclaredFields(javaClass);
for (Field field : fields) {
if (javaClass != field.getDeclaringClass()) continue;
int modifiers = field.getModifiers();
boolean isPublic = Modifier.isPublic(modifiers);
if (!isPublic) continue;
boolean isStatic = Modifier.isStatic(modifiers);
boolean isFinal = Modifier.isFinal(modifiers);
boolean constant = isPublic && isStatic && isFinal && Character.isUpperCase(field.getName().charAt(0));
if (constant) {
addConstantField(field);
if (!isInterface) continue;
}
if (isStatic) {
addField(getStaticInstallersForWrite(), staticNames, field, isFinal, true, constant);
} else {
addField(getInstanceInstallersForWrite(), instanceNames, field, isFinal, false, false);
}
}
}
private void addConstantField(Field field) {
Map<String, ConstantField> constantFields = this.constantFields;
if (constantFields == Collections.EMPTY_MAP) {
constantFields = this.constantFields = new HashMap<>();
}
constantFields.put(field.getName(), new ConstantField(field));
}
void setupMethods(Class<?> javaClass) {
boolean isInterface = javaClass.isInterface();
getMethods(javaClass).forEach((name, methods) -> {
for (int i = methods.size(); --i >= 0; ) {
Method method = methods.get(i);
if (Modifier.isStatic(method.getModifiers())) {
prepareStaticMethod(javaClass, method, name);
} else if (!isInterface) {
prepareInstanceMethod(javaClass, method, name);
}
}
});
}
private void setupSingletonMethods(Map<String, NamedInstaller> methodCallbacks, Class<?> javaClass, Object singleton, Method method, String name) {
NamedInstaller invoker = methodCallbacks.get(name);
if (invoker == null) {
invoker = new SingletonMethodInvokerInstaller(name, singleton);
methodCallbacks.put(name, invoker);
}
((MethodInstaller) invoker).addMethod(method, javaClass);
}
private void assignStaticAliases() {
getStaticInstallers().forEach((name, installer) -> {
if (installer.type == NamedInstaller.STATIC_METHOD && installer.hasLocalMethod()) {
if (name.endsWith(METHOD_MANGLE)) return;
MethodInstaller methodInstaller = (MethodInstaller) installer;
methodInstaller.assignAliases(staticNames);
}
});
}
private void assignInstanceAliases() {
getInstanceInstallers().forEach((name, installer) -> {
if (installer.type == NamedInstaller.INSTANCE_METHOD) {
if (name.endsWith(METHOD_MANGLE)) return;
MethodInstaller methodInstaller = (MethodInstaller) installer;
if (installer.hasLocalMethod()) {
methodInstaller.assignAliases(instanceNames);
}
if (name.equals("equals")) {
methodInstaller.setLocalMethod(true);
methodInstaller.addAlias("==");
}
}
});
}
protected static void addField(
final Map callbacks,
final Map<String, AssignedName> names,
final Field field,
final boolean isFinal,
final boolean isStatic,
final boolean isConstant) {
final String name = field.getName();
if ( Priority.FIELD.lessImportantThan( names.get(name) ) ) return;
names.put(name, new AssignedName(name, Priority.FIELD));
callbacks.put(name, isStatic ? new StaticFieldGetterInstaller(name, field, isConstant) :
new InstanceFieldGetterInstaller(name, field));
if (!isFinal) {
String setName = name + '=';
callbacks.put(setName, isStatic ? new StaticFieldSetterInstaller(setName, field) :
new InstanceFieldSetterInstaller(setName, field));
}
}
void installInstanceMethods(final RubyModule proxy) {
getInstanceInstallers().forEach(($, value) -> value.install(proxy));
}
private static class PartitionedMethods {
final Method[] instanceMethods;
final Method[] staticMethods;
PartitionedMethods(Method[] methods) {
List<Method> instanceMethods = Collections.EMPTY_LIST;
List<Method> staticMethods = Collections.EMPTY_LIST;
for (Method m : methods) {
int modifiers = m.getModifiers();
if (filterAccessible(m, modifiers)) {
if (Modifier.isStatic(modifiers)) {
if (staticMethods == Collections.EMPTY_LIST) staticMethods = new ArrayList<>();
staticMethods.add(m);
} else {
if (instanceMethods == Collections.EMPTY_LIST) instanceMethods = new ArrayList<>();
instanceMethods.add(m);
}
}
}
this.instanceMethods = instanceMethods.toArray(new Method[instanceMethods.size()]);
this.staticMethods = staticMethods.toArray(new Method[staticMethods.size()]);
}
private static boolean filterAccessible(Method method, int mod) {
if (Modifier.isPrivate(mod)) return false;
if (!Modifier.isPublic(mod) && !Java.trySetAccessible(method)) return false;
if ((mod & ACC_BRIDGE) != 0) return false;
return true;
}
}
}