org.jruby.javasupport.binding.Initializer Maven / Gradle / Ivy
package org.jruby.javasupport.binding;
import com.headius.modulator.Modulator;
import org.jruby.Ruby;
import org.jruby.RubyClass;
import org.jruby.RubyModule;
import org.jruby.internal.runtime.methods.JavaMethod;
import org.jruby.javasupport.Java;
import org.jruby.javasupport.JavaClass;
import org.jruby.javasupport.JavaSupport;
import org.jruby.javasupport.JavaUtil;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.IdUtil;
import org.jruby.util.log.Logger;
import org.jruby.util.log.LoggerFactory;
import java.lang.annotation.Annotation;
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.List;
import java.util.Map;
import static org.jruby.runtime.Visibility.PUBLIC;
import static org.jruby.util.StringSupport.startsWith;
/**
* Created by headius on 2/26/15.
*/
public abstract class Initializer {
static final Logger LOG = LoggerFactory.getLogger(Initializer.class);
public static final boolean DEBUG_SCALA = false;
protected final Ruby runtime;
protected final JavaSupport javaSupport;
protected final Class javaClass;
private static final int ACC_BRIDGE = 0x00000040;
public static final String METHOD_MANGLE = "__method";
private static final Map SCALA_OPERATORS;
// TODO: other reserved names?
private static Map newReservedNamesMap(final int size) {
HashMap 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));
// JRUBY-5132: java.awt.Component.instance_of?() expects 2 args
RESERVED_NAMES.put("instance_of?", new AssignedName("instance_of?", Priority.RESERVED));
return RESERVED_NAMES;
}
protected static final Map STATIC_RESERVED_NAMES;
static {
STATIC_RESERVED_NAMES = newReservedNamesMap(1);
STATIC_RESERVED_NAMES.put("new", new AssignedName("new", Priority.RESERVED));
}
protected static final Map INSTANCE_RESERVED_NAMES;
static {
INSTANCE_RESERVED_NAMES = newReservedNamesMap(2);
// only possible for "getClass" to be an instance method in Java
INSTANCE_RESERVED_NAMES.put("class", new AssignedName("class", Priority.RESERVED));
// "initialize" has meaning only for an instance (as opposed to a class)
INSTANCE_RESERVED_NAMES.put("initialize", new AssignedName("initialize", Priority.RESERVED));
}
public Initializer(Ruby runtime, Class javaClass) {
this.runtime = runtime;
this.javaSupport = runtime.getJavaSupport();
this.javaClass = javaClass;
}
public static RubyModule setupProxyClass(Ruby runtime, final Class> javaClass, RubyClass proxy) {
setJavaClassFor(javaClass, proxy);
proxy.setReifiedClass((Class) javaClass);
if ( javaClass.isArray() ) {
flagAsJavaProxy(proxy); return proxy;
}
if ( javaClass.isPrimitive() ) {
final RubyClass proxySingleton = proxy.getSingletonClass();
proxySingleton.undefineMethod("new"); // remove ConcreteJavaProxy class method 'new'
if ( javaClass == Void.TYPE ) {
// special treatment ... while Java::int[4] is OK Java::void[2] is NOT!
proxySingleton.undefineMethod("[]"); // from JavaProxy
proxySingleton.undefineMethod("new_array"); // from JavaProxy
}
flagAsJavaProxy(proxy); return proxy;
}
proxy = new ClassInitializer(runtime, javaClass).initialize(proxy);
flagAsJavaProxy(proxy); return proxy;
}
public static RubyModule setupProxyModule(Ruby runtime, final Class> javaClass, RubyModule proxy) {
setJavaClassFor(javaClass, proxy);
assert javaClass.isInterface();
proxy = new InterfaceInitializer(runtime, javaClass).initialize(proxy);
flagAsJavaProxy(proxy); return proxy;
}
private static void flagAsJavaProxy(final RubyModule proxy) {
// flag the class as a Java class proxy.
proxy.setJavaProxy(true);
proxy.getSingletonClass().setJavaProxy(true);
}
protected static void addField(
final Map callbacks,
final Map names,
final Field field,
final boolean isFinal,
final boolean isStatic) {
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) :
new InstanceFieldGetterInstaller(name, field));
if (!isFinal) {
String setName = name + '=';
callbacks.put(setName, isStatic ? new StaticFieldSetterInstaller(setName, field) :
new InstanceFieldSetterInstaller(setName, field));
}
}
protected static void prepareStaticMethod(Class> javaClass, State state, Method method, String name) {
AssignedName assignedName = state.staticNames.get(name);
// For JRUBY-4505, restore __method methods for reserved names
if (STATIC_RESERVED_NAMES.containsKey(method.getName())) {
setupStaticMethods(state.staticInstallers, javaClass, method, name + METHOD_MANGLE);
return;
}
if (assignedName == null) {
state.staticNames.put(name, new AssignedName(name, Priority.METHOD));
} else {
if (Priority.METHOD.lessImportantThan(assignedName)) return;
if (!Priority.METHOD.asImportantAs(assignedName)) {
state.staticInstallers.remove(name);
state.staticInstallers.remove(name + '=');
state.staticNames.put(name, new AssignedName(name, Priority.METHOD));
}
}
setupStaticMethods(state.staticInstallers, javaClass, method, name);
}
private static void setupStaticMethods(Map methodCallbacks, Class> javaClass, Method method, String name) {
MethodInstaller invoker = (MethodInstaller) methodCallbacks.get(name);
if (invoker == null) {
invoker = new StaticMethodInvokerInstaller(name);
methodCallbacks.put(name, invoker);
}
invoker.addMethod(method, javaClass);
}
protected static void assignStaticAliases(final State state) {
final Map installers = state.staticInstallers;
for (Map.Entry entry : installers.entrySet()) {
// no aliases for __method methods
if (entry.getKey().endsWith(METHOD_MANGLE)) continue;
if (entry.getValue().type == NamedInstaller.STATIC_METHOD && entry.getValue().hasLocalMethod()) {
assignAliases((MethodInstaller) entry.getValue(), state.staticNames, installers);
}
}
}
static void assignAliases(final MethodInstaller installer,
final Map assignedNames, final Map installers) {
final String name = installer.name;
String rubyCasedName = JavaUtil.getRubyCasedName(name);
addUnassignedAlias(rubyCasedName, assignedNames, installer, Priority.ALIAS);
String javaPropertyName = JavaUtil.getJavaPropertyName(name);
final List methods = installer.methods;
for ( int i = 0; i < methods.size(); i++ ) {
final Method method = methods.get(i);
Class>[] argTypes = method.getParameterTypes();
Class> resultType = method.getReturnType();
int argCount = argTypes.length;
// Add scala aliases for apply/update to roughly equivalent Ruby names
if (name.equals("apply")) {
addUnassignedAlias("[]", assignedNames, installer, Priority.ALIAS);
} else if (argCount == 2 && name.equals("update")) {
addUnassignedAlias("[]=", assignedNames, installer, Priority.ALIAS);
} else if (startsWith(name, '$')) { // Scala aliases for $ method names
addUnassignedAlias(ClassInitializer.fixScalaNames(name), assignedNames, installer, Priority.ALIAS);
}
String rubyPropertyName = null;
// Add property name aliases
if (javaPropertyName != null) {
if (rubyCasedName.startsWith("get_")) {
rubyPropertyName = rubyCasedName.substring(4);
if (argCount == 0) { // getFoo => foo
addUnassignedAlias(javaPropertyName, assignedNames, installer, Priority.GET_ALIAS);
addUnassignedAlias(rubyPropertyName, assignedNames, installer, Priority.GET_ALIAS);
}
} else if (rubyCasedName.startsWith("set_")) {
rubyPropertyName = rubyCasedName.substring(4); // TODO do not add foo? for setFoo (returning boolean)
if (argCount == 1 && resultType == void.class) { // setFoo(Foo) => foo=(Foo)
addUnassignedAlias(javaPropertyName + '=', assignedNames, installer, Priority.ALIAS);
addUnassignedAlias(rubyPropertyName + '=', assignedNames, installer, Priority.ALIAS);
}
} else if (rubyCasedName.startsWith("is_")) {
rubyPropertyName = rubyCasedName.substring(3);
// TODO (9.2) should be another check here to make sure these are only for getters
// ... e.g. isFoo() and not arbitrary isFoo(param) see GH-4432
if (resultType == boolean.class) { // isFoo() => foo, isFoo(*) => foo(*)
addUnassignedAlias(javaPropertyName, assignedNames, installer, Priority.IS_ALIAS);
addUnassignedAlias(rubyPropertyName, assignedNames, installer, Priority.IS_ALIAS);
// foo? is added bellow
}
}
}
// Additionally add ?-postfixed aliases to any boolean methods and properties.
if (resultType == boolean.class) {
// isFoo -> isFoo?, contains -> contains?
addUnassignedAlias(rubyCasedName + '?', assignedNames, installer, Priority.ALIAS);
if (rubyPropertyName != null) { // isFoo -> foo?
addUnassignedAlias(rubyPropertyName + '?', assignedNames, installer, Priority.ALIAS);
}
}
}
}
private static boolean addUnassignedAlias(final String name,
final Map assignedNames, final MethodInstaller installer,
final Priority aliasType) {
AssignedName assignedName = assignedNames.get(name);
if (aliasType.moreImportantThan(assignedName)) {
installer.addAlias(name);
assignedNames.put(name, new AssignedName(name, aliasType));
return true;
}
if (aliasType.asImportantAs(assignedName)) {
installer.addAlias(name);
return true;
}
// TODO: missing additional logic for dealing with conflicting protected fields.
return false;
}
protected static String fixScalaNames(final String name) {
String s = name;
for (Map.Entry entry : SCALA_OPERATORS.entrySet()) {
s = s.replaceAll(entry.getKey(), entry.getValue());
}
return s;
}
static {
HashMap 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);
}
protected static void handleScalaSingletons(final Class> javaClass, final State state) {
// check for Scala companion object
try {
final ClassLoader loader = javaClass.getClassLoader();
if ( loader == null ) return; //this is a core class, bail
// scan annotations for "scala" packages; if none present, it's not scala
boolean scalaAnno = false;
for ( Annotation anno : javaClass.getAnnotations() ) {
Package pkg = anno.annotationType().getPackage();
if ( pkg != null && pkg.getName() != null && pkg.getName().startsWith("scala.") ) {
scalaAnno = true; break;
}
}
if ( ! scalaAnno ) return;
Class> companionClass = loader.loadClass(javaClass.getName() + '$');
final Field field = companionClass.getField("MODULE$");
final Object singleton = field.get(null);
if ( singleton == null ) return;
final Map> scalaMethods = getMethods(companionClass);
for (Map.Entry> entry : scalaMethods.entrySet()) {
final List methods = entry.getValue();
for (int j = 0; j < methods.size(); j++) {
final Method method = methods.get(j);
String name = method.getName();
if (DEBUG_SCALA) LOG.debug("Companion object method {} for {}", name, companionClass);
if (name.indexOf('$') >= 0) name = fixScalaNames(name);
if (!Modifier.isStatic(method.getModifiers())) {
AssignedName assignedName = state.staticNames.get(name);
// For JRUBY-4505, restore __method methods for reserved names
if (INSTANCE_RESERVED_NAMES.containsKey(method.getName())) {
if (DEBUG_SCALA) LOG.debug("in reserved " + name);
setupSingletonMethods(state.staticInstallers, javaClass, singleton, method, name + METHOD_MANGLE);
continue;
}
if (assignedName == null) {
state.staticNames.put(name, new AssignedName(name, Priority.METHOD));
if (DEBUG_SCALA) LOG.debug("Assigned name is null");
} else {
if (Priority.METHOD.lessImportantThan(assignedName)) {
if (DEBUG_SCALA) LOG.debug("Less important");
continue;
}
if (!Priority.METHOD.asImportantAs(assignedName)) {
state.staticInstallers.remove(name);
state.staticInstallers.remove(name + '=');
state.staticNames.put(name, new AssignedName(name, Priority.METHOD));
}
}
if (DEBUG_SCALA) LOG.debug("Installing {} {} {}", name, method, singleton);
setupSingletonMethods(state.staticInstallers, javaClass, singleton, method, name);
} else {
if (DEBUG_SCALA) LOG.debug("Method {} is sadly static", method);
}
}
}
}
catch (ClassNotFoundException e) { /* there's no companion object */ }
catch (NoSuchFieldException e) { /* no MODULE$ field in companion */ }
catch (Exception e) {
if (DEBUG_SCALA) LOG.debug("Failed with {}", e);
}
}
private static void setupSingletonMethods(Map methodCallbacks, Class> javaClass, Object singleton, Method method, String name) {
MethodInstaller invoker = (MethodInstaller) methodCallbacks.get(name);
if (invoker == null) {
invoker = new SingletonMethodInvokerInstaller(name, singleton);
methodCallbacks.put(name, invoker);
}
invoker.addMethod(method, javaClass);
}
protected static void installClassFields(final RubyModule proxy, final State state) {
//assert state.constantFields != null;
for (ConstantField field : state.constantFields) {
field.install(proxy);
}
}
protected static void installClassStaticMethods(final RubyModule proxy, final State state) {
//assert state.staticInstallers != null;
for ( Map.Entry entry : state.staticInstallers.entrySet() ) {
entry.getValue().install(proxy);
}
}
protected static void installClassClasses(final Class> javaClass, final RubyModule proxy) {
// setup constants for public inner classes
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;
// no non-public inner classes
if ( ! Modifier.isPublic(clazz.getModifiers()) ) continue;
final String simpleName = JavaClass.getSimpleName(clazz);
if ( simpleName.length() == 0 ) 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 { // lower-case name
if ( ! proxy.respondsTo(simpleName) ) {
// define a class method
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;
}
});
}
}
}
}
private static void setJavaClassFor(final Class> javaClass, final RubyModule proxy) {
proxy.setInstanceVariable("@java_class", proxy.getRuntime().getJavaSupport().getJavaClassFromCache(javaClass));
proxy.dataWrapStruct(javaClass);
}
public abstract RubyModule initialize(RubyModule proxy);
public static class State {
final Map staticNames;
final Map instanceNames;
final Map staticInstallers = new HashMap<>();
final Map instanceInstallers = new HashMap<>();
final List constantFields = new ArrayList<>();
ConstructorInvokerInstaller constructorInstaller;
State(final Ruby runtime, final Class superClass) {
if (superClass == null) {
staticNames = new HashMap<>(8);
instanceNames = new HashMap<>(26);
} else {
staticNames = new HashMap<>(runtime.getJavaSupport().getStaticAssignedNames().get(superClass));
instanceNames = new HashMap<>(runtime.getJavaSupport().getInstanceAssignedNames().get(superClass));
}
staticNames.putAll(STATIC_RESERVED_NAMES);
instanceNames.putAll(INSTANCE_RESERVED_NAMES);
}
}
public static final java.lang.ClassValue DECLARED_METHODS = new java.lang.ClassValue() {
@Override
public Method[] computeValue(Class cls) {
return cls.getDeclaredMethods();
}
};
public static final java.lang.ClassValue METHODS = new java.lang.ClassValue() {
@Override
public Method[] computeValue(Class cls) {
return cls.getMethods();
}
};
public static final java.lang.ClassValue[]> INTERFACES = new java.lang.ClassValue[]>() {
@Override
public Class>[] computeValue(Class cls) {
return cls.getInterfaces();
}
};
static Map> getMethods(final Class> javaClass) {
HashMap> nameMethods = new HashMap<>(32);
// we scan all superclasses, but avoid adding superclass methods with
// same name+signature as subclass methods (see JRUBY-3130)
for ( Class> klass = javaClass; klass != null; klass = klass.getSuperclass() ) {
// only add class's methods if it's public or we can set accessible
// (see JRUBY-4799)
if (Modifier.isPublic(klass.getModifiers()) || JavaUtil.CAN_SET_ACCESSIBLE) {
// for each class, scan declared methods for new signatures
try {
// add methods, including static if this is the actual class,
// and replacing child methods with equivalent parent methods
addNewMethods(nameMethods, DECLARED_METHODS.get(klass), klass == javaClass, true);
}
catch (SecurityException e) { /* ignored */ }
}
// then do the same for each interface
for ( Class iface : INTERFACES.get(klass) ) {
try {
// add methods, not including static (should be none on
// interfaces anyway) and not replacing child methods with
// parent methods
addNewMethods(nameMethods, METHODS.get(iface), false, false);
}
catch (SecurityException e) { /* ignored */ }
}
}
return nameMethods;
}
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 int addNewMethods(
final HashMap> nameMethods,
final Method[] methods,
final boolean includeStatic,
final boolean removeDuplicate) {
int added = 0;
Methods: for (final Method method : methods) {
final int mod = method.getModifiers();
// Skip private methods, since they may mess with dispatch
if ( Modifier.isPrivate(mod) ) continue;
// Skip protected methods if we can't set accessible
if ( !Modifier.isPublic(mod) && !Modulator.trySetAccessible(method)) continue;
// ignore bridge methods because we'd rather directly call methods that this method
// is bridging (and such methods are by definition always available.)
if ( ( mod & ACC_BRIDGE ) != 0 ) continue;
if ( ! includeStatic && Modifier.isStatic(mod) ) {
// Skip static methods if we're not suppose to include them.
// Generally for superclasses; we only bind statics from the actual
// class.
continue;
}
List childMethods = nameMethods.get(method.getName());
if (childMethods == null) {
// first method of this name, add a collection for it
childMethods = new ArrayList<>(4);
childMethods.add(method); added++;
nameMethods.put(method.getName(), childMethods);
}
else {
// we have seen other methods; check if we already have an equivalent one
for (int i = 0; i < childMethods.size(); i++) {
final Method current = childMethods.get(i);
if ( methodsAreEquivalent(current, method) ) {
if (removeDuplicate) {
// Replace the existing method, since the super call is more general
// and virtual dispatch will call the subclass impl anyway.
// Used for instance methods, for which we usually want to use the highest-up
// callable implementation.
childMethods.set(i, method);
} else {
// just skip the new method, since we don't need it (already found one)
// used for interface methods, which we want to add unconditionally
// but only if we need them
}
continue Methods;
}
}
// no equivalent; add it
childMethods.add(method); added++;
}
}
return added;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy