package org.jruby.java.invokers;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Array;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.function.Supplier;
import org.jruby.Ruby;
import org.jruby.RubyModule;
import org.jruby.exceptions.RaiseException;
import org.jruby.internal.runtime.methods.JavaMethod;
import org.jruby.java.dispatch.CallableSelector;
import org.jruby.java.proxies.ArrayJavaProxy;
import org.jruby.java.proxies.ConcreteJavaProxy;
import org.jruby.java.proxies.JavaProxy;
import org.jruby.javasupport.Java;
import org.jruby.javasupport.JavaCallable;
import org.jruby.javasupport.JavaConstructor;
import org.jruby.javasupport.ParameterTypes;
import org.jruby.runtime.Arity;
import org.jruby.runtime.Visibility;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.cli.Options;
import org.jruby.util.collections.IntHashMap;
import org.jruby.util.collections.NonBlockingHashMapLong;
import static org.jruby.util.CodegenUtils.prettyParams;
public abstract class RubyToJavaInvoker<T extends JavaCallable> extends JavaMethod {
static final NonBlockingHashMapLong NULL_CACHE = new NullHashMapLong();
protected T javaCallable;
protected T[][] javaCallables;
protected T[] javaVarargsCallables;
NonBlockingHashMapLong<T> cache;
volatile boolean initialized;
private final Ruby runtime;
private final Supplier<Member[]> members;
@SuppressWarnings("unchecked")
RubyToJavaInvoker(RubyModule host, Supplier<Member[]> members, String name) {
super(host, Visibility.PUBLIC, name);
this.runtime = host.getRuntime();
this.members = members;
initialize();
}
void initialize() {
if (initialized) return;
synchronized (this) {
if (initialized) return;
final T callable;
final T[][] callables;
T[] varargsCallables = null;
int minVarArgsArity = -1;
int maxArity, minArity;
Member[] members = this.members.get();
final int length = members.length;
if (length == 1) {
callable = createCallable(runtime, members[0]);
maxArity = minArity = callable.getArity();
if (callable.isVarArgs()) {
varargsCallables = createCallableArray(callable);
minVarArgsArity = getMemberArity(members[0]) - 1;
}
callables = null;
cache = NULL_CACHE;
} else {
callable = null;
maxArity = -1;
minArity = Integer.MAX_VALUE;
IntHashMap<ArrayList<T>> arityMap = new IntHashMap<>(length, 1);
ArrayList<T> varArgs = null;
for (int i = 0; i < length; i++) {
final Member method = members[i];
final int currentArity = getMemberArity(method);
maxArity = Math.max(currentArity, maxArity);
minArity = Math.min(currentArity, minArity);
final T javaMethod = createCallable(runtime, method);
ArrayList<T> methodsForArity = arityMap.get(currentArity);
if (methodsForArity == null) {
methodsForArity = new ArrayList<T>(length);
arityMap.put(currentArity, methodsForArity);
}
methodsForArity.add(javaMethod);
if (javaMethod.isVarArgs()) {
final int usableArity = currentArity - 1;
if ((methodsForArity = arityMap.get(usableArity)) == null) {
methodsForArity = new ArrayList<T>(length);
arityMap.put(usableArity, methodsForArity);
}
methodsForArity.add(javaMethod);
if (varArgs == null) varArgs = new ArrayList<T>(length);
varArgs.add(javaMethod);
if (minVarArgsArity == -1) minVarArgsArity = Integer.MAX_VALUE;
minVarArgsArity = Math.min(usableArity, minVarArgsArity);
}
}
callables = createCallableArrayArray(maxArity + 1);
for (IntHashMap.Entry<ArrayList<T>> entry : arityMap.entrySet()) {
ArrayList<T> methodsForArity = entry.getValue();
T[] methodsArray = methodsForArity.toArray(createCallableArray(methodsForArity.size()));
callables[entry.getKey() ] = methodsArray;
}
if (varArgs != null ) {
varargsCallables = (T[]) varArgs.toArray(createCallableArray(varArgs.size()));
}
cache = new NonBlockingHashMapLong<>(4, true);
}
this.javaCallable = callable;
this.javaCallables = callables;
this.javaVarargsCallables = varargsCallables;
setArity(minArity, maxArity, minVarArgsArity);
setupNativeCall();
initialized = true;
}
}
private void setArity(final int minArity, final int maxArity, final int minVarArgsArity) {
if ( minVarArgsArity == -1 ) {
if ( minArity == maxArity ) {
setArity( Arity.fixed(minArity) );
}
else {
setArity(Arity.required(minArity));
}
}
else {
setArity( Arity.required(minVarArgsArity < minArity ? minVarArgsArity : minArity) );
}
}
final void setupNativeCall() {
if (javaCallable != null) {
if (javaCallable instanceof org.jruby.javasupport.JavaMethod) {
setNativeCallIfPublic(((org.jruby.javasupport.JavaMethod) javaCallable).getValue());
}
} else {
for ( int i = 0; i< javaCallables.length; i++ ) {
final JavaCallable[] callablesForArity = javaCallables[i];
if ( callablesForArity == null || callablesForArity.length != 1 ) continue;
if ( callablesForArity[0] instanceof org.jruby.javasupport.JavaMethod ) {
final Method method = ((org.jruby.javasupport.JavaMethod) callablesForArity[0]).getValue();
if ( setNativeCallIfPublic( method ) ) break;
}
}
}
}
private boolean setNativeCallIfPublic(final Method method) {
final int mod = method.getModifiers();
if ( Modifier.isPublic(mod) && Modifier.isPublic( method.getDeclaringClass().getModifiers() ) ) {
setNativeCall(method.getDeclaringClass(), method.getName(), method.getReturnType(), method.getParameterTypes(), Modifier.isStatic(mod), true);
return true;
}
return false;
}
public final T getSignature(int signatureCode) {
return cache.get(signatureCode);
}
public final void putSignature(int signatureCode, T callable) {
cache.put(signatureCode, callable);
}
protected abstract T createCallable(Ruby runtime, Member member);
protected abstract T[] createCallableArray(T callable);
protected abstract T[] createCallableArray(int size);
protected abstract T[][] createCallableArrayArray(int size);
protected abstract Class[] getMemberParameterTypes(Member member);
@Deprecated
protected abstract boolean isMemberVarArgs(Member member);
final int getMemberArity(Member member) {
return getMemberParameterTypes(member).length;
}
public static Object[] convertArguments(final ParameterTypes method, final IRubyObject[] args) {
return convertArguments(method, args, 0);
}
public static Object[] convertArguments(final ParameterTypes method, final IRubyObject[] args, final int addSpace) {
final Class<?>[] paramTypes = method.getParameterTypes();
final Object[] javaArgs; final int len = args.length;
if ( method.isVarArgs() ) {
final int last = paramTypes.length - 1;
javaArgs = new Object[ last + 1 + addSpace ];
for ( int i = 0; i < last; i++ ) {
javaArgs[i] = args[i].toJava(paramTypes[i]);
}
javaArgs[ last ] = convertVarArgumentsOnly(paramTypes[ last ], last, args);
}
else {
javaArgs = new Object[ len + addSpace ];
for ( int i = 0; i < len; i++ ) {
javaArgs[i] = args[i].toJava(paramTypes[i]);
}
}
return javaArgs;
}
public static Object[] convertArguments(final ParameterTypes method, final IRubyObject arg0, final int addSpace) {
final Class<?>[] paramTypes = method.getParameterTypes();
final Object[] javaArgs;
if ( method.isVarArgs() ) {
javaArgs = new Object[ 1 + addSpace ];
javaArgs[0] = convertVarArgumentsOnly(paramTypes[0], arg0);
}
else {
javaArgs = new Object[ 1 + addSpace ];
javaArgs[0] = arg0.toJava(paramTypes[0]);
}
return javaArgs;
}
private static Object convertVarArgumentsOnly(final Class<?> varArrayType,
final int varStart, final IRubyObject[] args) {
final int varCount = args.length - varStart;
if ( args.length == 0 || varCount <= 0 ) {
return Array.newInstance(varArrayType.getComponentType(), 0);
}
if ( varCount == 1 && args[varStart] instanceof ArrayJavaProxy ) {
return args[varStart].toJava(varArrayType);
}
final Class<?> compType = varArrayType.getComponentType();
final Object varArgs = Array.newInstance(compType, varCount);
for ( int i = 0; i < varCount; i++ ) {
Array.set(varArgs, i, args[varStart + i].toJava(compType));
}
return varArgs;
}
private static Object convertVarArgumentsOnly(final Class<?> varArrayType,
final IRubyObject arg0) {
if ( arg0 instanceof ArrayJavaProxy ) {
return arg0.toJava(varArrayType);
}
final Class<?> compType = varArrayType.getComponentType();
final Object varArgs = Array.newInstance(compType, 1);
Array.set(varArgs, 0, arg0.toJava(compType));
return varArgs;
}
static JavaProxy castJavaProxy(final IRubyObject self) {
assert self instanceof JavaProxy : "Java methods can only be invoked on Java objects";
return (JavaProxy) self;
}
static <T extends AccessibleObject & Member> T setAccessible(T accessible) {
if (!accessible.isAccessible() &&
!Ruby.isSecurityRestricted() &&
Options.JI_SETACCESSIBLE.load() &&
accessible instanceof Member) {
try { Java.trySetAccessible(accessible); }
catch (SecurityException e) {}
catch (RuntimeException re) {
rethrowIfNotInaccessibleObject(re);
}
}
return accessible;
}
static <T extends AccessibleObject & Member> T[] setAccessible(T[] accessibles) {
if (!Ruby.isSecurityRestricted() &&
Options.JI_SETACCESSIBLE.load()) {
try {
for (T accessible : accessibles) {
if (accessible.isAccessible()) continue;
if (!(accessible instanceof Member)) continue;
Java.trySetAccessible(accessible);
}
}
catch (SecurityException e) {}
catch (RuntimeException re) {
rethrowIfNotInaccessibleObject(re);
}
}
return accessibles;
}
private static void rethrowIfNotInaccessibleObject(RuntimeException re) {
if (re.getClass().getName().equals("java.lang.reflect.InaccessibleObjectException")) {
} else {
throw re;
}
}
private static <T extends AccessibleObject> boolean allAreAccessible(T[] accessibles) {
for (T accessible : accessibles) if (!accessible.isAccessible()) return false;
return true;
}
private static <T extends AccessibleObject> boolean allAreMember(T[] accessibles) {
for (T accessible : accessibles) if (!(accessible instanceof Member)) return false;
return true;
}
protected T findCallable(IRubyObject self, String name, IRubyObject[] args, final int arity) {
switch (arity) {
case 0:
return findCallableArityZero(self, name);
case 1:
return findCallableArityOne(self, name, args[0]);
case 2:
return findCallableArityTwo(self, name, args[0], args[1]);
case 3:
return findCallableArityThree(self, name, args[0], args[1], args[2]);
case 4:
return findCallableArityFour(self, name, args[0], args[1], args[2], args[3]);
}
return findCallableArityN(self, name, args, arity);
}
protected final T findCallableArityZero(IRubyObject self, String name) {
T callable = this.javaCallable;
if ( callable == null ) {
final T[] callablesForArity;
if ( javaCallables.length == 0 || (callablesForArity = javaCallables[0]) == null ) {
if ( ( callable = matchVarArgsCallableArityZero(self) ) == null ) {
throw newErrorDueNoMatchingCallable(self, name);
}
return callable;
}
callable = CallableSelector.matchingCallableArityZero(runtime, this, callablesForArity);
if ( callable == null ) {
if ((callable = matchVarArgsCallableArityZero(self)) == null ) {
throw newErrorDueArgumentTypeMismatch(self, callablesForArity);
}
}
}
else {
if (!callable.isVarArgs()) checkCallableArity(callable, 0);
}
return callable;
}
protected final T findCallableArityOne(IRubyObject self, String name, IRubyObject arg0) {
T callable = this.javaCallable;
if ( callable == null ) {
final T[] callablesForArity;
if ( javaCallables.length <= 1 || (callablesForArity = javaCallables[1]) == null ) {
if ((callable = matchVarArgsCallableArityOne(self, arg0)) == null) {
throw runtime.newArgumentError(1, javaCallables.length - 1);
}
return callable;
}
callable = CallableSelector.matchingCallableArityOne(runtime, this, callablesForArity, arg0);
if ( callable == null ) {
if ((callable = matchVarArgsCallableArityOne(self, arg0)) == null ) {
throw newErrorDueArgumentTypeMismatch(self, callablesForArity, arg0);
}
}
} else {
if (!callable.isVarArgs()) checkCallableArity(callable, 1);
}
return callable;
}
protected final T findCallableArityTwo(IRubyObject self, String name, IRubyObject arg0, IRubyObject arg1) {
T callable = this.javaCallable;
if ( callable == null ) {
final T[] callablesForArity;
if ( javaCallables.length <= 2 || (callablesForArity = javaCallables[2]) == null ) {
if ((callable = matchVarArgsCallableArityTwo(self, arg0, arg1)) == null ) {
throw runtime.newArgumentError(2, javaCallables.length - 1);
}
return callable;
}
callable = CallableSelector.matchingCallableArityTwo(runtime, this, callablesForArity, arg0, arg1);
if ( callable == null ) {
if ((callable = matchVarArgsCallableArityTwo(self, arg0, arg1)) == null ) {
throw newErrorDueArgumentTypeMismatch(self, callablesForArity, arg0, arg1);
}
}
} else {
if (!callable.isVarArgs()) checkCallableArity(callable, 2);
}
return callable;
}
protected final T findCallableArityThree(IRubyObject self, String name, IRubyObject arg0, IRubyObject arg1, IRubyObject arg2) {
T callable = this.javaCallable;
if ( callable == null ) {
final T[] callablesForArity;
if ( javaCallables.length <= 3 || (callablesForArity = javaCallables[3]) == null ) {
if ( ( callable = matchVarArgsCallableArityThree(self, arg0, arg1, arg2) ) == null ) {
throw runtime.newArgumentError(3, javaCallables.length - 1);
}
return callable;
}
callable = CallableSelector.matchingCallableArityThree(runtime, this, callablesForArity, arg0, arg1, arg2);
if ( callable == null ) {
if ( ( callable = matchVarArgsCallableArityThree(self, arg0, arg1, arg2) ) == null ) {
throw newErrorDueArgumentTypeMismatch(self, callablesForArity, arg0, arg1, arg2);
}
}
} else {
if (!callable.isVarArgs()) checkCallableArity(callable, 3);
}
return callable;
}
protected final T findCallableArityFour(IRubyObject self, String name, IRubyObject arg0, IRubyObject arg1, IRubyObject arg2, IRubyObject arg3) {
T callable = this.javaCallable;
if ( callable == null ) {
final T[] callablesForArity;
if ( javaCallables.length <= 4 || (callablesForArity = javaCallables[4]) == null ) {
if ( ( callable = matchVarArgsCallableArityFour(self, arg0, arg1, arg2, arg3) ) == null ) {
throw runtime.newArgumentError(4, javaCallables.length - 1);
}
return callable;
}
callable = CallableSelector.matchingCallableArityFour(runtime, this, callablesForArity, arg0, arg1, arg2, arg3);
if ( callable == null ) {
if ( ( callable = matchVarArgsCallableArityFour(self, arg0, arg1, arg2, arg3) ) == null ) {
throw newErrorDueArgumentTypeMismatch(self, callablesForArity, arg0, arg1, arg2, arg3);
}
}
} else {
if (!callable.isVarArgs()) checkCallableArity(callable, 4);
}
return callable;
}
private T findCallableArityN(IRubyObject self, String name, IRubyObject[] args, int arity) {
T callable = this.javaCallable;
if ( callable == null ) {
final T[] callablesForArity;
if ( arity >= javaCallables.length || (callablesForArity = javaCallables[arity]) == null ) {
if ( ( callable = matchVarArgsCallableArityN(self, args) ) == null ) {
throw runtime.newArgumentError(args.length, javaCallables.length - 1);
}
return callable;
}
callable = CallableSelector.matchingCallableArityN(runtime, this, callablesForArity, args);
if ( callable == null ) {
if ( ( callable = matchVarArgsCallableArityN(self, args) ) == null ) {
throw newErrorDueArgumentTypeMismatch(self, callablesForArity, args);
}
}
}
else {
if (!callable.isVarArgs()) checkCallableArity(callable, args.length);
}
return callable;
}
private T matchVarArgsCallableArityZero(IRubyObject self) {
final T[] varArgsCallables = this.javaVarargsCallables;
if ( varArgsCallables != null ) {
T callable = CallableSelector.matchingCallableArityZero(runtime, this, varArgsCallables);
if ( callable == null ) {
throw newErrorDueArgumentTypeMismatch(self, varArgsCallables);
}
return callable;
}
return null;
}
private T matchVarArgsCallableArityOne(IRubyObject self, IRubyObject arg0) {
final T[] varArgsCallables = this.javaVarargsCallables;
if ( varArgsCallables != null ) {
T callable = CallableSelector.matchingCallableArityOne(runtime, this, varArgsCallables, arg0);
if ( callable == null ) {
throw newErrorDueArgumentTypeMismatch(self, varArgsCallables, arg0);
}
return callable;
}
return null;
}
private T matchVarArgsCallableArityTwo(IRubyObject self, IRubyObject arg0, IRubyObject arg1) {
final T[] varArgsCallables = this.javaVarargsCallables;
if ( varArgsCallables != null ) {
T callable = CallableSelector.matchingCallableArityTwo(runtime, this, varArgsCallables, arg0, arg1);
if ( callable == null ) {
throw newErrorDueArgumentTypeMismatch(self, varArgsCallables, arg0, arg1);
}
return callable;
}
return null;
}
private T matchVarArgsCallableArityThree(IRubyObject self, IRubyObject arg0, IRubyObject arg1, IRubyObject arg2) {
final T[] varArgsCallables = this.javaVarargsCallables;
if ( varArgsCallables != null ) {
T callable = CallableSelector.matchingCallableArityThree(runtime, this, varArgsCallables, arg0, arg1, arg2);
if ( callable == null ) {
throw newErrorDueArgumentTypeMismatch(self, varArgsCallables, arg0, arg1, arg2);
}
return callable;
}
return null;
}
private T matchVarArgsCallableArityFour(IRubyObject self, IRubyObject arg0, IRubyObject arg1, IRubyObject arg2, IRubyObject arg3) {
final T[] varArgsCallables = this.javaVarargsCallables;
if ( varArgsCallables != null ) {
T callable = CallableSelector.matchingCallableArityFour(runtime, this, varArgsCallables, arg0, arg1, arg2, arg3);
if ( callable == null ) {
throw newErrorDueArgumentTypeMismatch(self, varArgsCallables, arg0, arg1, arg2, arg3);
}
return callable;
}
return null;
}
private T matchVarArgsCallableArityN(IRubyObject self, IRubyObject[] args) {
final T[] varArgsCallables = this.javaVarargsCallables;
if ( varArgsCallables != null ) {
T callable = CallableSelector.matchingCallableArityN(runtime, this, varArgsCallables, args);
if ( callable == null ) {
throw newErrorDueArgumentTypeMismatch(self, varArgsCallables, args);
}
return callable;
}
return null;
}
private void checkCallableArity(final T callable, final int expected) {
final int arity = callable.getArity();
if ( arity != expected ) throw runtime.newArgumentError(expected, arity);
}
private T someCallable() {
if ( javaCallable == null ) {
for ( int i = 0; i < javaCallables.length; i++ ) {
T[] callables = javaCallables[i];
if ( callables != null && callables.length > 0 ) {
for ( int j = 0; j < callables.length; j++ ) {
if ( callables[j] != null ) return callables[j];
}
}
}
return null;
}
return javaCallable;
}
private boolean isConstructor() {
return someCallable() instanceof JavaConstructor;
}
RaiseException newErrorDueArgumentTypeMismatch(final IRubyObject receiver,
final T[] methods, IRubyObject... args) {
final Class[] argTypes = new Class[args.length];
for (int i = 0; i < args.length; i++) {
argTypes[i] = getClass( args[i] );
}
final StringBuilder error = new StringBuilder(64);
error.append("no ");
if ( isConstructor() ) error.append("constructor");
else {
org.jruby.javasupport.JavaMethod method = (org.jruby.javasupport.JavaMethod) methods[0];
error.append("method '").append( method.getValue().getName() ).append("'");
}
error.append(" for arguments ");
prettyParams(error, argTypes);
error.append(" on ").append( formatReceiver(receiver) );
if ( methods.length > 1 ) {
error.append("\n available overloads:");
for (ParameterTypes method : methods) {
Class<?>[] paramTypes = method.getParameterTypes();
error.append("\n "); prettyParams( error, paramTypes );
}
}
return runtime.newNameError(error.toString(), null);
}
private RaiseException newErrorDueNoMatchingCallable(final IRubyObject receiver, final String name) {
final StringBuilder error = new StringBuilder(48);
error.append("no ");
if ( isConstructor() ) error.append("constructor");
else {
error.append("method '").append( name ).append("'");
}
error.append(" (for zero arguments) on ").append( formatReceiver(receiver) );
return runtime.newArgumentError( error.toString() );
}
private static Class<?> getClass(final IRubyObject object) {
if (object == null) return void.class;
if (object instanceof ConcreteJavaProxy) {
return ((ConcreteJavaProxy) object).getJavaClass();
}
return object.getClass();
}
private static String formatReceiver(final IRubyObject object) {
if ( object instanceof RubyModule ) {
return ((RubyModule) object).getName();
}
return object.getMetaClass().getRealClass().getName();
}
private static class NullHashMapLong<V> extends NonBlockingHashMapLong<V> {
NullHashMapLong() { super(0, false); }
@Override
public V put( long key, V val) { return null; }
@Override
public V putIfAbsent( long key, V val ) { return null; }
@Override
public V get(Object key) { return null; }
}
}