package org.jruby.java.proxies;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyClass;
import org.jruby.RubyInstanceConfig;
import org.jruby.RubyModule;
import org.jruby.anno.JRubyMethod;
import org.jruby.exceptions.RaiseException;
import org.jruby.internal.runtime.methods.DynamicMethod;
import org.jruby.internal.runtime.methods.JavaMethod.JavaMethodN;
import org.jruby.internal.runtime.methods.JavaMethod.JavaMethodOne;
import org.jruby.internal.runtime.methods.JavaMethod.JavaMethodOneBlock;
import org.jruby.internal.runtime.methods.JavaMethod.JavaMethodZero;
import org.jruby.internal.runtime.methods.UndefinedMethod;
import org.jruby.javasupport.Java;
import org.jruby.javasupport.JavaClass;
import org.jruby.javasupport.JavaObject;
import org.jruby.javasupport.JavaUtilities;
import org.jruby.javasupport.util.RuntimeHelpers;
import org.jruby.runtime.Arity;
import org.jruby.runtime.Block;
import org.jruby.runtime.ObjectAllocator;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.Visibility;
import org.jruby.runtime.builtin.IRubyObject;
public class JavaInterfaceTemplate {
public static RubyModule createJavaInterfaceTemplateModule(ThreadContext context) {
Ruby runtime = context.runtime;
RubyModule javaInterfaceTemplate = runtime.defineModule("JavaInterfaceTemplate");
RubyClass singleton = javaInterfaceTemplate.getSingletonClass();
singleton.addReadAttribute(context, "java_class");
singleton.defineAnnotatedMethods(JavaInterfaceTemplate.class);
return javaInterfaceTemplate;
}
// not intended to be called directly by users (private)
// OLD TODO from Ruby code:
// This should be implemented in JavaClass.java, where we can
// check for reserved Ruby names, conflicting methods, etc.
@JRubyMethod(visibility = Visibility.PRIVATE)
public static IRubyObject implement(ThreadContext context, IRubyObject self, IRubyObject clazz) {
Ruby runtime = context.runtime;
if (!(clazz instanceof RubyModule)) {
throw runtime.newTypeError(clazz, runtime.getModule());
}
RubyModule targetModule = (RubyModule)clazz;
JavaClass javaClass = (JavaClass)self.getInstanceVariables().getInstanceVariable("@java_class");
Method[] javaInstanceMethods = javaClass.javaClass().getMethods();
DynamicMethod dummyMethod = new org.jruby.internal.runtime.methods.JavaMethod(targetModule, Visibility.PUBLIC) {
@Override
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule clazz, String name, IRubyObject[] args, Block block) {
// dummy bodies for default impls
return context.runtime.getNil();
}
};
for (int i = 0; i < javaInstanceMethods.length; i++) {
Method method = javaInstanceMethods[i];
String name = method.getName();
if (targetModule.searchMethod(name) != UndefinedMethod.INSTANCE) continue;
targetModule.addMethod(name, dummyMethod);
}
return runtime.getNil();
}
// framed for invokeSuper
@JRubyMethod(frame = true)
public static IRubyObject append_features(ThreadContext context, IRubyObject self, IRubyObject clazz, Block block) {
if (clazz instanceof RubyClass) {
appendFeaturesToClass(context, self, (RubyClass)clazz);
} else if (clazz instanceof RubyModule) {
appendFeaturesToModule(context, self, (RubyModule)clazz);
} else {
throw context.runtime.newTypeError("received " + clazz + ", expected Class/Module");
}
return RuntimeHelpers.invokeSuper(context, self, clazz, block);
}
private static void appendFeaturesToClass(ThreadContext context, IRubyObject self, final RubyClass clazz) {
Ruby runtime = context.runtime;
checkAlreadyReified(clazz, runtime);
IRubyObject javaClassObj = RuntimeHelpers.getInstanceVariable(self, runtime, "@java_class");
IRubyObject javaInterfaces;
if (!clazz.hasInstanceVariable("@java_interfaces")) {
javaInterfaces = RubyArray.newArray(runtime, javaClassObj);
RuntimeHelpers.setInstanceVariable(javaInterfaces, clazz, "@java_interfaces");
initInterfaceImplMethods(context, clazz);
} else {
javaInterfaces = RuntimeHelpers.getInstanceVariable(clazz, runtime, "@java_interfaces");
// we've already done the above priming logic, just add another interface
// to the list of intentions unless we're past the point of no return or
// already intend to implement the given interface
if (!(javaInterfaces.isFrozen() || ((RubyArray)javaInterfaces).includes(context, javaClassObj))) {
((RubyArray)javaInterfaces).append(javaClassObj);
}
}
}
private static void checkAlreadyReified(final RubyClass clazz, Ruby runtime) throws RaiseException {
// not allowed for original (non-generated) Java classes
// note: not allowing for any previously created class right now;
// this restriction might be loosened later for generated classes
if ((Java.NEW_STYLE_EXTENSION && clazz.getReifiedClass() != null)
||
(clazz.hasInstanceVariable("@java_class")
&& clazz.getInstanceVariable("@java_class").isTrue()
&& !clazz.getSingletonClass().isMethodBound("java_proxy_class", false))
||
(clazz.hasInstanceVariable("@java_proxy_class")
&& clazz.getInstanceVariable("@java_proxy_class").isTrue())) {
throw runtime.newArgumentError("can not add Java interface to existing Java class");
}
}
private static void initInterfaceImplMethods(ThreadContext context, RubyClass clazz) {
// setup new, etc unless this is a ConcreteJavaProxy subclass
// For JRUBY-4571, check both these, since JavaProxy extension stuff adds the former and this code adds the latter
if (!(clazz.isMethodBound("__jcreate!", false) || clazz.isMethodBound("__jcreate_meta!", false))) {
// First we make modifications to the class, to adapt it to being
// both a Ruby class and a proxy for a Java type
RubyClass singleton = clazz.getSingletonClass();
// list of interfaces we implement
singleton.addReadAttribute(context, "java_interfaces");
if (
(!Java.NEW_STYLE_EXTENSION && clazz.getSuperClass().getRealClass().hasInstanceVariable("@java_class"))
|| RubyInstanceConfig.INTERFACES_USE_PROXY) {
// superclass is a Java class...use old style impl for now
// The replacement "new" allocates and inits the Ruby object as before, but
// also instantiates our proxified Java object by calling __jcreate!
final ObjectAllocator proxyAllocator = clazz.getAllocator();
clazz.setAllocator(new ObjectAllocator() {
public IRubyObject allocate(Ruby runtime, RubyClass klazz) {
IRubyObject newObj = proxyAllocator.allocate(runtime, klazz);
RuntimeHelpers.invoke(runtime.getCurrentContext(), newObj, "__jcreate!");
return newObj;
}
});
// jcreate instantiates the proxy object which implements all interfaces
// and which is wrapped and implemented by this object
clazz.addMethod("__jcreate!", new JavaMethodN(clazz, Visibility.PRIVATE) {
@Override
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule clazz, String name, IRubyObject[] args) {
return jcreateProxy(self);
}
});
} else {
// The new "new" actually generates a real Java class to use for the Ruby class's
// backing store, instantiates that, and then calls initialize on it.
addRealImplClassNew(clazz);
}
// Next, we define a few private methods that we'll use to manipulate
// the Java object contained within this Ruby object
// Used by our duck-typification of Proc into interface types, to allow
// coercing a simple proc into an interface parameter.
clazz.addMethod("__jcreate_meta!", new JavaMethodN(clazz, Visibility.PRIVATE) {
@Override
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule clazz, String name, IRubyObject[] args) {
IRubyObject result = jcreateProxy(self);
return result;
}
});
// If we hold a Java object, we need a java_class accessor
clazz.addMethod("java_class", new JavaMethodZero(clazz, Visibility.PUBLIC) {
@Override
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule clazz, String name) {
return ((JavaObject) self.dataGetStruct()).java_class();
}
});
// Because we implement Java interfaces now, we need a new === that's
// aware of those additional "virtual" supertypes
if (!clazz.searchMethod("===").isUndefined()) {
clazz.defineAlias("old_eqq", "===");
clazz.addMethod("===", new JavaMethodOne(clazz, Visibility.PUBLIC) {
@Override
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule clazz, String name, IRubyObject arg) {
// TODO: WRONG - get interfaces from class
if (arg.respondsTo("java_object")) {
IRubyObject interfaces = self.getMetaClass().getInstanceVariables().getInstanceVariable("@java_interfaces");
assert interfaces instanceof RubyArray : "interface list was not an array";
return context.runtime.newBoolean(((RubyArray) interfaces).op_diff(
((JavaClass) ((JavaObject) arg.dataGetStruct()).java_class()).interfaces()).equals(RubyArray.newArray(context.runtime)));
} else {
return RuntimeHelpers.invoke(context, self, "old_eqq", arg);
}
}
});
}
}
// Now we add an "implement" and "implement_all" methods to the class
if (!clazz.isMethodBound("implement", false)) {
RubyClass singleton = clazz.getSingletonClass();
// implement is called to force this class to create stubs for all
// methods in the given interface, so they'll show up in the list
// of methods and be invocable without passing through method_missing
singleton.addMethod("implement", new JavaMethodOne(clazz, Visibility.PRIVATE) {
@Override
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule clazz, String name, IRubyObject arg) {
IRubyObject javaInterfaces = self.getInstanceVariables().getInstanceVariable("@java_interfaces");
if (javaInterfaces != null && ((RubyArray) javaInterfaces).includes(context, arg)) {
return RuntimeHelpers.invoke(context, arg, "implement", self);
}
return context.runtime.getNil();
}
});
// implement all forces implementation of all interfaces we intend
// for this class to implement
singleton.addMethod("implement_all", new JavaMethodOne(clazz, Visibility.PRIVATE) {
@Override
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule clazz, String name, IRubyObject arg) {
RubyArray javaInterfaces = (RubyArray) self.getInstanceVariables().getInstanceVariable("@java_interfaces");
for (int i = 0; i < javaInterfaces.size(); i++) {
RuntimeHelpers.invoke(context, JavaUtilities.get_interface_module(self, javaInterfaces.eltInternal(i)), "implement", self);
}
return javaInterfaces;
}
});
}
}
public static void addRealImplClassNew(RubyClass clazz) {
clazz.setAllocator(new ObjectAllocator() {
private Constructor proxyConstructor;
public IRubyObject allocate(Ruby runtime, RubyClass klazz) {
// if we haven't been here before, reify the class
Class reifiedClass = klazz.getReifiedClass();
if (proxyConstructor == null || proxyConstructor.getDeclaringClass() != reifiedClass) {
if (reifiedClass == null) {
reifiedClass = Java.generateRealClass(klazz);
}
proxyConstructor = Java.getRealClassConstructor(runtime, reifiedClass);
}
IRubyObject newObj = Java.constructProxy(runtime, proxyConstructor, klazz);
return newObj;
}
});
}
private static IRubyObject jcreateProxy(IRubyObject self) {
RubyClass current = self.getMetaClass();
// construct the new interface impl and set it into the object
IRubyObject newObject = Java.newInterfaceImpl(self, Java.getInterfacesFromRubyClass(current));
return JavaUtilities.set_java_object(self, self, newObject);
}
private static void appendFeaturesToModule(ThreadContext context, IRubyObject self, RubyModule module) {
// assuming the user wants a collection of interfaces that can be
// included together. make it so.
Ruby runtime = context.runtime;
// not allowed for existing Java interface modules
if (module.getInstanceVariables().hasInstanceVariable("@java_class") &&
module.getInstanceVariables().getInstanceVariable("@java_class").isTrue()) {
throw runtime.newTypeError("can not add Java interface to existing Java interface");
}
// To turn a module into an "interface collection" we add a class instance
// variable to hold the list of interfaces, and modify append_features
// for this module to call append_features on each of those interfaces as
// well
synchronized (module) {
if (!module.getInstanceVariables().hasInstanceVariable("@java_interface_mods")) {
RubyArray javaInterfaceMods = RubyArray.newArray(runtime, self);
module.getInstanceVariables().setInstanceVariable("@java_interface_mods", javaInterfaceMods);
RubyClass singleton = module.getSingletonClass();
singleton.addMethod("append_features", new JavaMethodOneBlock(singleton, Visibility.PUBLIC) {
@Override
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule clazz, String name, IRubyObject arg, Block block) {
if (!(arg instanceof RubyClass)) {
throw context.runtime.newTypeError("append_features called with non-class");
}
RubyClass target = (RubyClass)arg;
RubyArray javaInterfaceMods = (RubyArray)self.getInstanceVariables().getInstanceVariable("@java_interface_mods");
target.include(javaInterfaceMods.toJavaArray());
return RuntimeHelpers.invokeAs(context, clazz.getSuperClass(), self, name, arg, block);
}
});
} else {
// already set up append_features, just add the interface if we haven't already
RubyArray javaInterfaceMods =(RubyArray)module.getInstanceVariables().getInstanceVariable("@java_interface_mods");
if (!javaInterfaceMods.includes(context, self)) {
javaInterfaceMods.append(self);
}
}
}
}
@JRubyMethod
public static IRubyObject extended(ThreadContext context, IRubyObject self, IRubyObject object) {
if (!(self instanceof RubyModule)) {
throw context.runtime.newTypeError(self, context.runtime.getModule());
}
RubyClass singleton = object.getSingletonClass();
singleton.include(new IRubyObject[] {self});
return singleton;
}
@JRubyMethod(name = "[]", rest = true)
public static IRubyObject op_aref(ThreadContext context, IRubyObject self, IRubyObject[] args) {
return JavaProxy.op_aref(context, self, args);
}
@JRubyMethod(rest = true)
public static IRubyObject impl(ThreadContext context, IRubyObject self, IRubyObject[] args, final Block implBlock) {
Ruby runtime = context.runtime;
if (!implBlock.isGiven()) throw runtime.newArgumentError("block required to call #impl on a Java interface");
final RubyArray methodNames = (args.length > 0) ? runtime.newArray(args) : null;
RubyClass implClass = RubyClass.newClass(runtime, runtime.getObject());
implClass.include(new IRubyObject[] {self});
IRubyObject implObject = implClass.callMethod(context, "new");
implClass.addMethod("method_missing",
new org.jruby.internal.runtime.methods.JavaMethod(implClass, Visibility.PUBLIC) {
@Override
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule clazz, String name, IRubyObject[] args, Block block) {
Arity.checkArgumentCount(context.runtime, name, args.length, 1, -1);
if (methodNames == null || methodNames.include_p(context, args[0]).isTrue()) {
return implBlock.call(context, args);
} else {
return clazz.getSuperClass().callMethod(context, "method_missing", args, block);
}
}
});
return implObject;
}
@JRubyMethod(name = "new", rest = true)
public static IRubyObject rbNew(ThreadContext context, IRubyObject self, IRubyObject[] args, Block block) {
Ruby runtime = context.runtime;
RubyClass implClass = (RubyClass)self.getInstanceVariables().getInstanceVariable("@__implementation");
if (implClass == null) {
implClass = RubyClass.newClass(runtime, (RubyClass)runtime.getClass("InterfaceJavaProxy"));
implClass.include(new IRubyObject[] {self});
RuntimeHelpers.setInstanceVariable(implClass, self, "@__implementation");
}
return RuntimeHelpers.invoke(context, implClass, "new", args, block);
}
}