All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jruby.javasupport.binding.Initializer Maven / Gradle / Ivy

package org.jruby.javasupport.binding;

import org.jruby.Ruby;
import org.jruby.RubyClass;
import org.jruby.RubyModule;
import org.jruby.internal.runtime.methods.DynamicMethod;
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.Block;
import org.jruby.runtime.Helpers;
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.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.ListIterator;
import java.util.Map;

import static org.jruby.runtime.Visibility.PUBLIC;

/**
* Created by headius on 2/26/15.
*/
public abstract class Initializer {
    protected final Ruby runtime;
    protected final JavaSupport javaSupport;
    protected final Class javaClass;

    private static final Logger LOG = LoggerFactory.getLogger("Initializer");

    private static final int ACC_BRIDGE    = 0x00000040;

    public static final boolean DEBUG_SCALA = false;

    public static final String METHOD_MANGLE = "__method";

    private static final Map SCALA_OPERATORS;

    // TODO: other reserved names?
    private static final Map RESERVED_NAMES = new HashMap();
    static {
        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));
    }
    protected static final Map STATIC_RESERVED_NAMES = new HashMap(RESERVED_NAMES);
    static {
        STATIC_RESERVED_NAMES.put("new", new AssignedName("new", Priority.RESERVED));
    }
    protected static final Map INSTANCE_RESERVED_NAMES = new HashMap(RESERVED_NAMES);
    static {
        // 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, final RubyClass proxy) {
        setJavaClassFor(javaClass, proxy);

        return new ClassInitializer(runtime, javaClass).initialize(proxy);
    }

    public static RubyModule setupProxyModule(Ruby runtime, final Class javaClass, final RubyModule proxy) {
        setJavaClassFor(javaClass, proxy);

        assert javaClass.isInterface();

        return new InterfaceInitializer(runtime, javaClass).initialize(proxy);
    }

    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) {
        for (Map.Entry entry : state.staticInstallers.entrySet()) {
            // no aliases for __method methods
            if (entry.getKey().endsWith("__method")) continue;

            if (entry.getValue().type == NamedInstaller.STATIC_METHOD && entry.getValue().hasLocalMethod()) {
                assignAliases((MethodInstaller) entry.getValue(), state.staticNames);
            }
        }
    }

    protected static void assignAliases(MethodInstaller installer, Map assignedNames) {
        String name = installer.name;
        String rubyCasedName = JavaUtil.getRubyCasedName(name);
        addUnassignedAlias(rubyCasedName, assignedNames, installer);

        String javaPropertyName = JavaUtil.getJavaPropertyName(name);
        String rubyPropertyName = null;

        for (Method method: installer.methods) {
            Class[] argTypes = method.getParameterTypes();
            Class resultType = method.getReturnType();
            int argCount = argTypes.length;

            // Add scala aliases for apply/update to roughly equivalent Ruby names
            if (rubyCasedName.equals("apply")) {
                addUnassignedAlias("[]", assignedNames, installer);
            }
            if (rubyCasedName.equals("update") && argCount == 2) {
                addUnassignedAlias("[]=", assignedNames, installer);
            }

            // Scala aliases for $ method names
            if (name.startsWith("$")) {
                addUnassignedAlias(ClassInitializer.fixScalaNames(name), assignedNames, installer);
            }

            // Add property name aliases
            if (javaPropertyName != null) {
                if (rubyCasedName.startsWith("get_")) {
                    rubyPropertyName = rubyCasedName.substring(4);
                    if (argCount == 0 ||                                // getFoo      => foo
                            argCount == 1 && argTypes[0] == int.class) {    // getFoo(int) => foo(int)

                        addUnassignedAlias(javaPropertyName, assignedNames, installer);
                        addUnassignedAlias(rubyPropertyName, assignedNames, installer);
                    }
                } else if (rubyCasedName.startsWith("set_")) {
                    rubyPropertyName = rubyCasedName.substring(4);
                    if (argCount == 1 && resultType == void.class) {    // setFoo(Foo) => foo=(Foo)
                        addUnassignedAlias(javaPropertyName + '=', assignedNames, installer);
                        addUnassignedAlias(rubyPropertyName + '=', assignedNames, installer);
                    }
                } else if (rubyCasedName.startsWith("is_")) {
                    rubyPropertyName = rubyCasedName.substring(3);
                    if (resultType == boolean.class) {                  // isFoo() => foo, isFoo(*) => foo(*)
                        addUnassignedAlias(javaPropertyName, assignedNames, installer);
                        addUnassignedAlias(rubyPropertyName, assignedNames, installer);
                    }
                }
            }

            // Additionally add ?-postfixed aliases to any boolean methods and properties.
            if (resultType == boolean.class) {
                // is_something?, contains_thing?
                addUnassignedAlias(rubyCasedName + '?', assignedNames, installer);
                if (rubyPropertyName != null) {
                    // something?
                    addUnassignedAlias(rubyPropertyName + '?', assignedNames, installer);
                }
            }
        }
    }

    private static void addUnassignedAlias(String name, Map assignedNames,
                                           MethodInstaller installer) {
        if (name == null) return;

        AssignedName assignedName = assignedNames.get(name);
        // TODO: missing additional logic for dealing with conflicting protected fields.
        if (Priority.ALIAS.moreImportantThan(assignedName)) {
            installer.addAlias(name);
            assignedNames.put(name, new AssignedName(name, Priority.ALIAS));
        } else if (Priority.ALIAS.asImportantAs(assignedName)) {
            installer.addAlias(name);
        }
    }

    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();
        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

            Class companionClass = loader.loadClass(javaClass.getName() + '$');
            final Field field = companionClass.getField("MODULE$");
            final Object singleton = field.get(null);
            if ( singleton == null ) return;

            final List scalaMethods = getMethods(companionClass);
            for ( int j = scalaMethods.size() - 1; j >= 0; j-- ) {
                final Method method = scalaMethods.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) {
                        @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 void initializeBase(RubyModule proxy) {
        proxy.addMethod("__jsend!", new org.jruby.internal.runtime.methods.JavaMethod.JavaMethodNBlock(proxy, PUBLIC) {
            @Override
            public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule clazz, String name, IRubyObject[] args, Block block) {
                String callName = args[0].asJavaString();

                DynamicMethod method = self.getMetaClass().searchMethod(callName);
                int v = method.getArity().getValue();

                IRubyObject[] newArgs = new IRubyObject[args.length - 1];
                System.arraycopy(args, 1, newArgs, 0, newArgs.length);

                context.runtime.getWarnings().warn("#__jsend! is deprecated (and will no longer work in 9K); use #java_send instead");

                if (v < 0 || v == (newArgs.length)) {
                    return Helpers.invoke(context, self, callName, newArgs, Block.NULL_BLOCK);
                }
                RubyClass superClass = self.getMetaClass().getSuperClass();
                return Helpers.invokeAs(context, superClass, self, callName, newArgs, Block.NULL_BLOCK);
            }
        });
    }

    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();
                instanceNames = new HashMap();
            } 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);
        }

    }

    static List getMethods(final Class javaClass) {
        HashMap> nameMethods = new HashMap>(32);

        // to better size the final ArrayList below
        int totalMethods = 0;

        // 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
                    totalMethods += addNewMethods(nameMethods, klass.getDeclaredMethods(), klass == javaClass, true);
                }
                catch (SecurityException e) { /* ignored */ }
            }

            // then do the same for each interface
            for ( Class iface : klass.getInterfaces() ) {
                try {
                    // add methods, not including static (should be none on
                    // interfaces anyway) and not replacing child methods with
                    // parent methods
                    totalMethods += addNewMethods(nameMethods, iface.getMethods(), false, false);
                }
                catch (SecurityException e) { /* ignored */ }
            }
        }

        // now only bind the ones that remain
        ArrayList finalList = new ArrayList(totalMethods);

        for ( Map.Entry> entry : nameMethods.entrySet() ) {
            finalList.addAll( entry.getValue() );
        }

        return finalList;
    }

    private static boolean methodsAreEquivalent(Method child, Method parent) {
        return parent.getDeclaringClass().isAssignableFrom(child.getDeclaringClass())
                && child.getReturnType() == parent.getReturnType()
                && child.isVarArgs() == parent.isVarArgs()
                && Modifier.isPublic(child.getModifiers()) == Modifier.isPublic(parent.getModifiers())
                && Modifier.isProtected(child.getModifiers()) == Modifier.isProtected(parent.getModifiers())
                && Modifier.isStatic(child.getModifiers()) == Modifier.isStatic(parent.getModifiers())
                && 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;

            // 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
                final ListIterator childMethodsIterator = childMethods.listIterator();
                while ( childMethodsIterator.hasNext() ) {
                    final Method current = childMethodsIterator.next();
                    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.
                            childMethodsIterator.set(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