Index: src/builtin/javasupport/proxy/base.rb =================================================================== --- src/builtin/javasupport/proxy/base.rb (revision 3514) +++ src/builtin/javasupport/proxy/base.rb (working copy) @@ -25,7 +25,11 @@ end end + + # leaving the setup_... methods in temporarily, issuing warnings + # if any are called. remove before 1.0 -BD def setup + puts "warning: deprecated setup called for #{self}" unless java_class.array? setup_attributes setup_class_methods @@ -36,6 +40,7 @@ end def setup_attributes + puts "warning: deprecated setup_attributes called for #{self}" instance_methods = java_class.java_instance_methods.collect! {|m| m.name} java_class.fields.select {|field| field.public? && !field.static? }.each do |attr| name = attr.name @@ -58,6 +63,7 @@ end def setup_class_methods + puts "warning: deprecated setup_class_methods called for #{self}" java_class.java_class_methods.select { |m| m.public? }.group_by { |m| m.name }.each do |name, methods| if methods.length == 1 @@ -79,6 +85,7 @@ end def setup_constants + puts "warning: deprecated setup_constants called for #{self}" fields = java_class.fields class_methods = java_class.java_class_methods.collect! { |m| m.name } @@ -99,6 +106,7 @@ end def setup_inner_classes + puts "warning: deprecated setup_inner_classes called for #{self}" # the select block filters out anonymous inner classes ($1 and friends) # these have empty simple names, which don't really work as constant names java_class.declared_classes.select{|c| !c.simple_name.empty?}.each do |clazz| @@ -108,6 +116,7 @@ end def setup_instance_methods + puts "warning: deprecated setup_instance_methods called for #{self}" java_class.define_instance_methods_for_proxy(self) end end Index: src/builtin/javasupport/utilities/base.rb =================================================================== --- src/builtin/javasupport/utilities/base.rb (revision 3514) +++ src/builtin/javasupport/utilities/base.rb (working copy) @@ -19,6 +19,7 @@ } subclass.send(:define_method, "setup_instance_methods") { + puts "subclass #{self} calling JPC.dimfp" # never called? self.java_proxy_class.define_instance_methods_for_proxy(subclass) } Index: src/org/jruby/javasupport/proxy/JavaProxyClass.java =================================================================== --- src/org/jruby/javasupport/proxy/JavaProxyClass.java (revision 3514) +++ src/org/jruby/javasupport/proxy/JavaProxyClass.java (working copy) @@ -611,9 +611,11 @@ return "[Proxy:" + getSuperclass().getName() + "]"; } + // this doesn't appear to get called anymore, leaving it in for the moment. public IRubyObject define_instance_methods_for_proxy(IRubyObject arg) { assert arg instanceof RubyClass; + System.out.println("JPC.define_instance_methods_for_proxy"); Map aliasesClump = getPropertysClumped(); Map methodsClump = getMethodsClumped(false); RubyClass proxy = (RubyClass) arg; Index: src/org/jruby/javasupport/JavaClass.java =================================================================== --- src/org/jruby/javasupport/JavaClass.java (revision 3514) +++ src/org/jruby/javasupport/JavaClass.java (working copy) @@ -19,6 +19,7 @@ * Copyright (C) 2005 Charles O Nutter * Copyright (C) 2006 Kresten Krab Thorup * Copyright (C) 2007 Miguel Covarrubias + * Copyright (C) 2007 William N Dortch * * Alternatively, the contents of this file may be used under the terms of * either of the GNU General Public License Version 2 or later (the "GPL"), @@ -45,9 +46,11 @@ import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -73,13 +76,668 @@ public class JavaClass extends JavaObject { + private static boolean DEBUG = false; + + private static class AssignedName { + // to override an assigned name, the type must be less than + // or equal to the assigned type. so a field name in a subclass + // will override an alias in a superclass, but not a method. + static final int RESERVED = 0; + static final int METHOD = 1; + static final int FIELD = 2; + static final int WEAKLY_RESERVED = 3; // we'll be peeved, but not devastated, if you override + static final int ALIAS = 4; + String name; + int type; + AssignedName () {} + AssignedName(String name, int type) { + this.name = name; + this.type = type; + } + } + + // TODO: other reserved names? + private static final Map RESERVED_NAMES = new HashMap(); + static { + RESERVED_NAMES.put("class", new AssignedName("class", AssignedName.RESERVED)); + RESERVED_NAMES.put("__id__", new AssignedName("__id__", AssignedName.RESERVED)); + RESERVED_NAMES.put("object_id", new AssignedName("object_id", AssignedName.RESERVED)); + RESERVED_NAMES.put("id", new AssignedName("id", AssignedName.WEAKLY_RESERVED)); + } + private static final Map STATIC_RESERVED_NAMES = new HashMap(RESERVED_NAMES); + static { + STATIC_RESERVED_NAMES.put("new", new AssignedName("new", AssignedName.RESERVED)); + } + private static final Map INSTANCE_RESERVED_NAMES = new HashMap(RESERVED_NAMES); + + private static abstract class NamedCallback implements Callback { + static final int STATIC_FIELD = 1; + static final int STATIC_METHOD = 2; + static final int INSTANCE_FIELD = 3; + static final int INSTANCE_METHOD = 4; + String name; + int type; + NamedCallback () {} + NamedCallback (String name, int type) { + this.name = name; + this.type = type; + } + abstract void install(RubyClass proxy); + // small hack to save a cast later on + boolean hasLocalMethod() { + return true; + } + void logMessage(IRubyObject self, IRubyObject[] args) { + if (!DEBUG) { + return; + } + String type; + switch (this.type) { + case STATIC_FIELD: type = "static field"; break; + case STATIC_METHOD: type = "static method"; break; + case INSTANCE_FIELD: type = "instance field"; break; + case INSTANCE_METHOD: type = "instance method"; break; + default: type = "?"; break; + } + StringBuffer b = new StringBuffer(type).append(" => '").append(name) + .append("'; args.length = ").append(args.length); + for (int i = 0; i < args.length; i++) { + b.append("\n arg[").append(i).append("] = ").append(args[i]); + } + System.out.println(b); + } + } + + private static abstract class FieldCallback extends NamedCallback { + Field field; + JavaField javaField; + FieldCallback(){} + FieldCallback(String name, int type, Field field) { + super(name,type); + this.field = field; + } + } + + private class StaticFieldGetter extends FieldCallback { + StaticFieldGetter(){} + StaticFieldGetter(String name, Field field) { + super(name,STATIC_FIELD,field); + } + void install(RubyClass proxy) { + proxy.getSingletonClass().defineFastMethod(this.name,this); + } + public IRubyObject execute(IRubyObject self, IRubyObject[] args, Block block) { + logMessage(self,args); + if (javaField == null) { + javaField = new JavaField(getRuntime(),field); + } + return Java.java_to_ruby(self,javaField.static_value(),Block.NULL_BLOCK); + } + public Arity getArity() { + return Arity.NO_ARGUMENTS; + } + } + + private class StaticFieldSetter extends FieldCallback { + StaticFieldSetter(){} + StaticFieldSetter(String name, Field field) { + super(name,STATIC_FIELD,field); + } + void install(RubyClass proxy) { + proxy.getSingletonClass().defineFastMethod(this.name,this); + } + public IRubyObject execute(IRubyObject self, IRubyObject[] args, Block block) { + logMessage(self,args); + if (javaField == null) { + javaField = new JavaField(getRuntime(),field); + } + return Java.java_to_ruby(self, + javaField.set_static_value(Java.ruby_to_java(self,args[0],Block.NULL_BLOCK)), + Block.NULL_BLOCK); + } + public Arity getArity() { + return Arity.ONE_ARGUMENT; + } + } + + private class InstanceFieldGetter extends FieldCallback { + InstanceFieldGetter(){} + InstanceFieldGetter(String name, Field field) { + super(name,INSTANCE_FIELD,field); + } + void install(RubyClass proxy) { + proxy.defineFastMethod(this.name,this); + } + public IRubyObject execute(IRubyObject self, IRubyObject[] args, Block block) { + logMessage(self,args); + if (javaField == null) { + javaField = new JavaField(getRuntime(),field); + } + return Java.java_to_ruby(self, + javaField.value(self.getInstanceVariable("@java_object")), + Block.NULL_BLOCK); + } + public Arity getArity() { + return Arity.NO_ARGUMENTS; + } + } + + private class InstanceFieldSetter extends FieldCallback { + InstanceFieldSetter(){} + InstanceFieldSetter(String name, Field field) { + super(name,INSTANCE_FIELD,field); + } + void install(RubyClass proxy) { + proxy.defineFastMethod(this.name,this); + } + public IRubyObject execute(IRubyObject self, IRubyObject[] args, Block block) { + logMessage(self,args); + if (javaField == null) { + javaField = new JavaField(getRuntime(),field); + } + return Java.java_to_ruby(self, + javaField.set_value(self.getInstanceVariable("@java_object"), + Java.ruby_to_java(self,args[0],Block.NULL_BLOCK)), + Block.NULL_BLOCK); + } + public Arity getArity() { + return Arity.ONE_ARGUMENT; + } + } + + private static abstract class MethodCallback extends NamedCallback { + boolean haveLocalMethod; + List methods; + List aliases; + IntHashMap javaMethods; + IntHashMap matchingMethods; + JavaMethod javaMethod; + MethodCallback(){} + MethodCallback(String name, int type) { + super(name,type); + } + void addMethod(Method method, Class javaClass) { + if (methods == null) { + methods = new ArrayList(); + } + methods.add(method); + haveLocalMethod = (haveLocalMethod || javaClass == method.getDeclaringClass()); + } + void addAlias(String alias) { + if (aliases == null) { + aliases = new ArrayList(); + } + if (!aliases.contains(alias)) + aliases.add(alias); + } + boolean hasLocalMethod () { + return haveLocalMethod; + } + // TODO: varargs? + // TODO: rework Java.matching_methods_internal and + // ProxyData.method_cache, since we really don't need to be passing + // around RubyArray objects anymore. + void createJavaMethods(Ruby runtime) { + if (methods != null) { + if (methods.size() == 1) { + javaMethod = JavaMethod.create(runtime,(Method)methods.get(0)); + } else { + javaMethods = new IntHashMap(); + matchingMethods = new IntHashMap(); + for (Iterator iter = methods.iterator(); iter.hasNext() ;) { + Method method = (Method)iter.next(); + // TODO: deal with varargs + //int arity = method.isVarArgs() ? -1 : method.getParameterTypes().length; + int arity = method.getParameterTypes().length; + RubyArray methodsForArity = (RubyArray)javaMethods.get(arity); + if (methodsForArity == null) { + methodsForArity = RubyArray.newArrayLight(runtime); + javaMethods.put(arity,methodsForArity); + } + methodsForArity.append(JavaMethod.create(runtime,method)); + } + } + methods = null; + } + } + void raiseNoMatchingMethodError(IRubyObject proxy, IRubyObject[] args, int start) { + int len = args.length; + List argTypes = new ArrayList(len - start); + for (int i = start ; i < len; i++) { + argTypes.add(((JavaClass)((JavaObject)args[i]).java_class()).getValue()); + } + throw proxy.getRuntime().newNameError("no " + this.name + " with arguments matching " + argTypes, null); + } + } + + private class StaticMethodInvoker extends MethodCallback { + StaticMethodInvoker(){} + StaticMethodInvoker(String name) { + super(name,STATIC_METHOD); + } + void install(RubyClass proxy) { + if (haveLocalMethod) { + RubyClass singleton = proxy.getSingletonClass(); + singleton.defineFastMethod(this.name,this); + if (aliases != null) { + for (Iterator iter = aliases.iterator(); iter.hasNext(); ) { + singleton.defineAlias((String)iter.next(), this.name); + } + aliases = null; + } + } + } + public IRubyObject execute(IRubyObject self, IRubyObject[] args, Block block) { + logMessage(self,args); + if (javaMethod == null && javaMethods == null) { + createJavaMethods(self.getRuntime()); + } + // TODO: ok to convert args in place, rather than new array? + int len = args.length; + IRubyObject[] convertedArgs = new IRubyObject[len]; + for (int i = len; --i >= 0; ) { + convertedArgs[i] = Java.ruby_to_java(self,args[i],Block.NULL_BLOCK); + } + if (javaMethods == null) { + return Java.java_to_ruby(self,javaMethod.invoke_static(convertedArgs),Block.NULL_BLOCK); + } else { + int argsTypeHash = 0; + for (int i = len; --i >= 0; ) { + argsTypeHash += 3*args[i].getMetaClass().id; + } + IRubyObject match = (IRubyObject)matchingMethods.get(argsTypeHash); + if (match == null) { + // TODO: varargs? + RubyArray methods = (RubyArray)javaMethods.get(len); + if (methods == null) { + raiseNoMatchingMethodError(self,convertedArgs,0); + } + match = Java.matching_method_internal(JAVA_UTILITIES, methods, convertedArgs, 0, len); + } + return Java.java_to_ruby(self, ((JavaMethod)match).invoke_static(convertedArgs), Block.NULL_BLOCK); + } + } + public Arity getArity() { + return Arity.OPTIONAL; + } + } + + private class InstanceMethodInvoker extends MethodCallback { + InstanceMethodInvoker(){} + InstanceMethodInvoker(String name) { + super(name,INSTANCE_METHOD); + } + void install(RubyClass proxy) { + if (haveLocalMethod) { + proxy.defineFastMethod(this.name,this); + if (aliases != null) { + for (Iterator iter = aliases.iterator(); iter.hasNext(); ) { + proxy.defineAlias((String)iter.next(), this.name); + } + aliases = null; + } + } + } + public IRubyObject execute(IRubyObject self, IRubyObject[] args, Block block) { + logMessage(self,args); + if (javaMethod == null && javaMethods == null) { + createJavaMethods(self.getRuntime()); + } + // TODO: ok to convert args in place, rather than new array? + int len = args.length; + IRubyObject[] convertedArgs = new IRubyObject[len+1]; + convertedArgs[0] = self.getInstanceVariable("@java_object"); + for (int i = len; --i >= 0; ) { + convertedArgs[i+1] = Java.ruby_to_java(self,args[i],Block.NULL_BLOCK); + } + if (javaMethods == null) { + return Java.java_to_ruby(self,javaMethod.invoke(convertedArgs),Block.NULL_BLOCK); + } else { + int argsTypeHash = 0; + for (int i = len; --i >= 0; ) { + argsTypeHash += 3*args[i].getMetaClass().id; + } + IRubyObject match = (IRubyObject)matchingMethods.get(argsTypeHash); + if (match == null) { + // TODO: varargs? + RubyArray methods = (RubyArray)javaMethods.get(len); + if (methods == null) { + raiseNoMatchingMethodError(self,convertedArgs,1); + } + match = Java.matching_method_internal(JAVA_UTILITIES, methods, convertedArgs, 1, len); + matchingMethods.put(argsTypeHash, match); + } + return Java.java_to_ruby(self,((JavaMethod)match).invoke(convertedArgs),Block.NULL_BLOCK); + } + } + public Arity getArity() { + return Arity.OPTIONAL; + } + } + + private static class ConstantField { + static final int CONSTANT = Modifier.FINAL | Modifier.PUBLIC | Modifier.STATIC; + Field field; + ConstantField(Field field) { + this.field = field; + } + void install(RubyClass proxy) { + if (proxy.getConstantAt(field.getName()) == null) { + JavaField javaField = new JavaField(proxy.getRuntime(),field); + RubyString name = javaField.name(); + proxy.const_set(name,Java.java_to_ruby(proxy,javaField.static_value(),Block.NULL_BLOCK)); + } + } + static boolean isConstant(Field field) { + return (field.getModifiers() & CONSTANT) == CONSTANT && + Character.isUpperCase(field.getName().charAt(0)); + } + } + + + private final RubyModule JAVA_UTILITIES = getRuntime().getModule("JavaUtilities"); + + private Map staticAssignedNames; + private Map instanceAssignedNames; + private Map staticCallbacks; + private Map instanceCallbacks; + private List constantFields; + + protected Map getStaticAssignedNames() { + return staticAssignedNames; + } + protected Map getInstanceAssignedNames() { + return instanceAssignedNames; + } + private JavaClass(Ruby runtime, Class javaClass) { super(runtime, (RubyClass) runtime.getModule("Java").getClass("JavaClass"), javaClass); + if (javaClass.isInterface()) { + initializeInterface(javaClass); + } else if (!(javaClass.isArray() || javaClass.isPrimitive())) { + // TODO: public only? + initializeClass(javaClass); + } } - public static synchronized JavaClass get(Ruby runtime, Class klass) { + private void initializeInterface(Class javaClass) { + Class superclass = javaClass.getSuperclass(); + Map staticNames; + if (superclass == null) { + staticNames = new HashMap(); + } else { + JavaClass superJavaClass = get(getRuntime(),superclass); + staticNames = new HashMap(superJavaClass.getStaticAssignedNames()); + } + staticNames.putAll(STATIC_RESERVED_NAMES); + List constantFields = new ArrayList(); + Field[] fields = javaClass.getFields(); + for (int i = fields.length; --i >= 0; ) { + Field field = fields[i]; + // treating constants specially until interface modules + // are implemented. we'll define any as-yet undefined + // constant, regardless of declaring class. this will + // slow us down a bit in setupProxy. + if (ConstantField.isConstant(field)) { + constantFields.add(new ConstantField(field)); + } + } + this.staticAssignedNames = staticNames; + this.constantFields = constantFields; + } + private void initializeClass(Class javaClass) { + Class superclass = javaClass.getSuperclass(); + Map staticNames; + Map instanceNames; + if (superclass == null) { + staticNames = new HashMap(); + instanceNames = new HashMap(); + } else { + JavaClass superJavaClass = get(getRuntime(),superclass); + staticNames = new HashMap(superJavaClass.getStaticAssignedNames()); + instanceNames = new HashMap(superJavaClass.getInstanceAssignedNames()); + } + staticNames.putAll(STATIC_RESERVED_NAMES); + instanceNames.putAll(INSTANCE_RESERVED_NAMES); + Map staticCallbacks = new HashMap(); + Map instanceCallbacks = new HashMap(); + List constantFields = new ArrayList(); + Field[] fields = javaClass.getFields(); + for (int i = fields.length; --i >= 0; ) { + Field field = fields[i]; + // treating constants specially until interface modules + // are implemented. we'll define any as-yet undefined + // constant, regardless of declaring class. this will + // slow us down a bit in setupProxy. + if (ConstantField.isConstant(field)) { + constantFields.add(new ConstantField(field)); + continue; + } + // for everything else, must be declared in this class + if (!javaClass.equals(field.getDeclaringClass())) + continue; + String name = field.getName(); + if (Modifier.isStatic(field.getModifiers())) { + AssignedName assignedName = (AssignedName)staticNames.get(name); + if (assignedName != null && assignedName.type < AssignedName.FIELD) + continue; + staticNames.put(name,new AssignedName(name,AssignedName.FIELD)); + staticCallbacks.put(name,new StaticFieldGetter(name,field)); + if (!Modifier.isFinal(field.getModifiers())) { + String setName = name + '='; + staticCallbacks.put(setName,new StaticFieldSetter(setName,field)); + } + } else { + AssignedName assignedName = (AssignedName)instanceNames.get(name); + if (assignedName != null && assignedName.type < AssignedName.FIELD) + continue; + instanceNames.put(name,new AssignedName(name,AssignedName.FIELD)); + instanceCallbacks.put(name,new InstanceFieldGetter(name,field)); + if (!Modifier.isFinal(field.getModifiers())) { + String setName = name + '='; + instanceCallbacks.put(setName,new InstanceFieldSetter(setName,field)); + } + } + } + Method[] methods = javaClass.getMethods(); + for (int i = methods.length; --i >= 0; ) { + // we need to collect all methods, though we'll only + // install the ones that are named in this class + Method method = methods[i]; + String name = method.getName(); + if (Modifier.isStatic(method.getModifiers())) { + AssignedName assignedName = (AssignedName)staticNames.get(name); + if (assignedName == null) { + staticNames.put(name,new AssignedName(name,AssignedName.METHOD)); + } else { + if (assignedName.type < AssignedName.METHOD) + continue; + if (assignedName.type != AssignedName.METHOD) { + staticCallbacks.remove(name); + staticCallbacks.remove(name+'='); + staticNames.put(name,new AssignedName(name,AssignedName.METHOD)); + } + } + StaticMethodInvoker invoker = (StaticMethodInvoker)staticCallbacks.get(name); + if (invoker == null) { + invoker = new StaticMethodInvoker(name); + staticCallbacks.put(name,invoker); + } + invoker.addMethod(method,javaClass); + } else { + AssignedName assignedName = (AssignedName)instanceNames.get(name); + if (assignedName == null) { + instanceNames.put(name,new AssignedName(name,AssignedName.METHOD)); + } else { + if (assignedName.type < AssignedName.METHOD) + continue; + if (assignedName.type != AssignedName.METHOD) { + instanceCallbacks.remove(name); + instanceCallbacks.remove(name+'='); + instanceNames.put(name,new AssignedName(name,AssignedName.METHOD)); + } + } + InstanceMethodInvoker invoker = (InstanceMethodInvoker)instanceCallbacks.get(name); + if (invoker == null) { + invoker = new InstanceMethodInvoker(name); + instanceCallbacks.put(name,invoker); + } + invoker.addMethod(method,javaClass); + } + } + this.staticAssignedNames = staticNames; + this.instanceAssignedNames = instanceNames; + this.staticCallbacks = staticCallbacks; + this.instanceCallbacks = instanceCallbacks; + this.constantFields = constantFields; + } + + public void setupProxy(RubyClass proxy) { + proxy.defineFastMethod("__jsend!", __jsend_method); + Class javaClass = javaClass(); + if (javaClass.isInterface()) { + setupInterfaceProxy(proxy); + return; + } else if (javaClass.isArray() || javaClass.isPrimitive()) { + return; + } + for (Iterator iter = constantFields.iterator(); iter.hasNext(); ) { + ((ConstantField)iter.next()).install(proxy); + } + for (Iterator iter = staticCallbacks.values().iterator(); iter.hasNext(); ) { + NamedCallback callback = (NamedCallback)iter.next(); + if (callback.type == NamedCallback.STATIC_METHOD && callback.hasLocalMethod()) { + assignAliases((MethodCallback)callback,staticAssignedNames); + } + callback.install(proxy); + } + for (Iterator iter = instanceCallbacks.values().iterator(); iter.hasNext(); ) { + NamedCallback callback = (NamedCallback)iter.next(); + if (callback.type == NamedCallback.INSTANCE_METHOD && callback.hasLocalMethod()) { + assignAliases((MethodCallback)callback,instanceAssignedNames); + } + callback.install(proxy); + } + // setup constants for public inner classes + Class[] classes = javaClass.getClasses(); + for (int i = classes.length; --i >= 0; ) { + if (javaClass == classes[i].getDeclaringClass()) { + Class clazz = classes[i]; + String simpleName = getSimpleName(clazz); + if (simpleName.length() == 0) + continue; + if (proxy.getConstantAt(simpleName) == null) { + proxy.const_set(getRuntime().newString(simpleName), + Java.get_proxy_class(JAVA_UTILITIES,get(getRuntime(),clazz))); + } + } + } + // TODO: we can probably release our references to the constantFields + // array and static/instance callback hashes at this point. I don't see + // a case where the proxy would be GC'd and we'd have to reinitialize. + // I suppose we could keep a reference to the proxy just to be sure... + } + + private static void assignAliases(MethodCallback callback, Map assignedNames) { + String name = callback.name; + addUnassignedAlias(getRubyCasedName(name),assignedNames,callback); + // logic adapted from java.beans.Introspector + if (!(name.length() > 3 || name.startsWith("is"))) + return; + + String javaPropertyName = getJavaPropertyName(name); + if (javaPropertyName == null) + return; // not a Java property name, done with this method + + for (Iterator iter = callback.methods.iterator(); iter.hasNext(); ) { + Method method = (Method)iter.next(); + Class[] argTypes = method.getParameterTypes(); + Class resultType = method.getReturnType(); + int argCount = argTypes.length; + if (argCount == 0) { + if (name.startsWith("get")) { + addUnassignedAlias(getRubyCasedName(javaPropertyName),assignedNames,callback); + addUnassignedAlias(javaPropertyName,assignedNames,callback); + } else if (resultType == boolean.class && name.startsWith("is")) { + String rubyName = getRubyCasedName(name.substring(2)); + if (rubyName != null) { + addUnassignedAlias(rubyName,assignedNames,callback); + addUnassignedAlias(rubyName+'?',assignedNames,callback); + } + if (!javaPropertyName.equals(rubyName)) { + addUnassignedAlias(javaPropertyName,assignedNames,callback); + addUnassignedAlias(javaPropertyName+'?',assignedNames,callback); + } + } + } else if (argCount == 1) { + // indexed get + if (argTypes[0] == int.class && name.startsWith("get")) { + addUnassignedAlias(getRubyCasedName(name.substring(3)),assignedNames,callback); + addUnassignedAlias(javaPropertyName,assignedNames,callback); + } else if (resultType == void.class && name.startsWith("set")) { + String rubyName = getRubyCasedName(name.substring(3)); + if (rubyName != null) { + addUnassignedAlias(rubyName + '=',assignedNames,callback); + } + if (!javaPropertyName.equals(rubyName)) { + addUnassignedAlias(javaPropertyName + '=',assignedNames,callback); + } + } + } + } + } + + private static void addUnassignedAlias(String name, Map assignedNames, MethodCallback callback ) { + if (name != null) { + AssignedName assignedName = (AssignedName)assignedNames.get(name); + if (assignedName == null || assignedName.type >= AssignedName.ALIAS) { + callback.addAlias(name); + if (assignedName == null || assignedName.type != AssignedName.ALIAS) { + assignedNames.put(name,new AssignedName(name,AssignedName.ALIAS)); + } + } + } + } + + private static final Pattern JAVA_PROPERTY_CHOPPER = Pattern.compile("(get|set|is)([A-Z0-9])(.*)"); + public static String getJavaPropertyName(String beanMethodName) { + Matcher m = JAVA_PROPERTY_CHOPPER.matcher(beanMethodName); + + if (!m.find()) return null; + String javaPropertyName = m.group(2).toLowerCase() + m.group(3); + return javaPropertyName; + } + + private static final Pattern CAMEL_CASE_SPLITTER = Pattern.compile("([a-z])([A-Z])"); + public static String getRubyCasedName(String javaCasedName) { + Matcher m = CAMEL_CASE_SPLITTER.matcher(javaCasedName); + String rubyCasedName = m.replaceAll("$1_$2").toLowerCase(); + if (rubyCasedName.equals(javaCasedName)) { + return null; + } + return rubyCasedName; + } + + + public void setupInterfaceProxy(RubyClass proxy) { + for (Iterator iter = constantFields.iterator(); iter.hasNext(); ){ + ((ConstantField)iter.next()).install(proxy); + } + } + + // unsynchronized, so create won't hold up get by other threads + public static JavaClass get(Ruby runtime, Class klass) { JavaClass javaClass = runtime.getJavaSupport().getJavaClassFromCache(klass); if (javaClass == null) { + javaClass = createJavaClass(runtime,klass); + } + return javaClass; + } + + private static synchronized JavaClass createJavaClass(Ruby runtime,Class klass) { + // double-check the cache now that we're synchronized + JavaClass javaClass = runtime.getJavaSupport().getJavaClassFromCache(klass); + if (javaClass == null) { javaClass = new JavaClass(runtime, klass); runtime.getJavaSupport().putJavaClassIntoCache(javaClass); } @@ -181,11 +839,31 @@ return JavaClass.get(recv.getRuntime(), klass); } + // TODO: part of interim solution, can be removed soon + private Set getPublicFieldNames(boolean isStatic) { + // we're not checking for final here; since the purpose is to prevent + // shortcut property names from being created that conflict with + // field names, it won't make sense to allow the property setter (name=) + // to be created but not the getter. + int mask = Modifier.PUBLIC | Modifier.STATIC; + int want = isStatic ? Modifier.PUBLIC | Modifier.STATIC : Modifier.PUBLIC; + Set names = new HashSet(); + names.add("class"); + Field[] fields = ((Class)getValue()).getFields(); + for (int i = fields.length; --i >= 0; ) { + if ((fields[i].getModifiers() & mask) == want) { + names.add(fields[i].getName()); + } + } + return names; + } + /** * Get all methods grouped by name (e.g. 'new => {new(), new(int), new(int, int)}, ...') * @param isStatic determines whether you want static or instance methods from the class */ - private Map getMethodsClumped(boolean isStatic) { + private Map getMethodsClumped(boolean isStatic, Set assignedNames) { + System.out.println("JC.gmc"); Map map = new HashMap(); if(((Class)getValue()).isInterface()) { return map; @@ -204,6 +882,7 @@ if (methodsWithName == null) { methodsWithName = RubyArray.newArrayLight(getRuntime()); map.put(key, methodsWithName); + assignedNames.add(key); } methodsWithName.append(JavaMethod.create(getRuntime(), methods[i])); @@ -212,7 +891,8 @@ return map; } - private Map getPropertysClumped() { + private Map getPropertysClumped(Set assignedNames) { + System.out.println("JC.gpc"); Map map = new HashMap(); BeanInfo info; @@ -223,7 +903,7 @@ } PropertyDescriptor[] descriptors = info.getPropertyDescriptors(); - + System.out.println("got bean info for class " + javaClass().getName() + ": " + descriptors.length); for (int i = 0; i < descriptors.length; i++) { Method readMethod = descriptors[i].getReadMethod(); @@ -241,7 +921,9 @@ readMethod.getReturnType() == boolean.class) { aliases.add(descriptors[i].getName() + "?"); } - aliases.add(descriptors[i].getName()); + if (!assignedNames.contains(descriptors[i].getName())) { + aliases.add(descriptors[i].getName()); + } } Method writeMethod = descriptors[i].getWriteMethod(); @@ -255,7 +937,9 @@ map.put(key, aliases); } - aliases.add(descriptors[i].getName() + "="); + if (!assignedNames.contains(descriptors[i].getName())) { + aliases.add(descriptors[i].getName() + "="); + } } } @@ -329,33 +1013,26 @@ } } - private static final Pattern CAMEL_CASE_SPLITTER = Pattern.compile("([a-z])([A-Z])"); - - public static String getRubyCasedName(String javaCasedName) { - Matcher m = CAMEL_CASE_SPLITTER.matcher(javaCasedName); - - String rubyCasedName = m.replaceAll("$1_$2").toLowerCase(); - - if (rubyCasedName.equals(javaCasedName)) { - return null; - } - - return rubyCasedName; - } - private static final Callback __jsend_method = new Callback() { public IRubyObject execute(IRubyObject self, IRubyObject[] args, Block block) { String methodSymbol = args[0].asSymbol(); RubyMethod method = (org.jruby.RubyMethod)self.getMetaClass().newMethod(self, methodSymbol, true); int v = RubyNumeric.fix2int(method.arity()); + // TODO: why twice? String name = args[0].asSymbol(); IRubyObject[] newArgs = new IRubyObject[args.length - 1]; System.arraycopy(args, 1, newArgs, 0, newArgs.length); - + if (DEBUG) + System.out.println("__jsend method => '" + name + "'; arity = " + v + ", args.length = " + newArgs.length); + if(v < 0 || v == (newArgs.length)) { + if (DEBUG) + System.out.println(" calling self"); return self.callMethod(self.getRuntime().getCurrentContext(), name, newArgs, CallType.FUNCTIONAL, block); } else { + if (DEBUG) + System.out.println(" calling super"); return self.callMethod(self.getRuntime().getCurrentContext(),self.getMetaClass().getSuperClass(), name, newArgs, CallType.SUPER, block); } } @@ -367,9 +1044,11 @@ public IRubyObject define_instance_methods_for_proxy(IRubyObject arg) { assert arg instanceof RubyClass; - - Map aliasesClump = getPropertysClumped(); - Map methodsClump = getMethodsClumped(false); + // shouldn't be getting called any more + System.out.println("JC.define_instance_methods_for_proxy"); + Set assignedNames = getPublicFieldNames(false); + Map methodsClump = getMethodsClumped(false,assignedNames); + Map aliasesClump = getPropertysClumped(assignedNames); RubyClass proxy = (RubyClass) arg; proxy.defineFastMethod("__jsend!", __jsend_method); @@ -524,6 +1203,8 @@ } public RubyArray declared_classes() { + // TODO: should be able to partially work around this by calling + // javaClass().getClasses, which will return any public inner classes. if (Ruby.isSecurityRestricted()) // Can't even get inner classes? return getRuntime().newArray(0); Class[] classes = javaClass().getDeclaredClasses(); Index: src/org/jruby/javasupport/Java.java =================================================================== --- src/org/jruby/javasupport/Java.java (revision 3514) +++ src/org/jruby/javasupport/Java.java (working copy) @@ -17,6 +17,7 @@ * Copyright (C) 2004 David Corbin * Copyright (C) 2004-2005 Thomas E Enebo * Copyright (C) 2006 Kresten Krab Thorup + * Copyright (C) 2007 William N Dortch * * Alternatively, the contents of this file may be used under the terms of * either of the GNU General Public License Version 2 or later (the "GPL"), @@ -38,7 +39,6 @@ import java.util.Iterator; import java.util.HashMap; import java.util.Map; - import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Modifier; @@ -49,6 +49,7 @@ import org.jruby.RubyClass; import org.jruby.RubyFixnum; import org.jruby.RubyFloat; +import org.jruby.RubyHash; import org.jruby.RubyModule; import org.jruby.RubyNumeric; import org.jruby.RubyProc; @@ -155,38 +156,90 @@ Class c = ((JavaClass)java_class).javaClass(); RubyClass base_type; boolean concrete = false; + boolean invokeInherited = true; if(c.isInterface()) { base_type = runtime.getClass("InterfaceJavaProxy"); } else if(c.isArray()) { base_type = runtime.getClass("ArrayJavaProxy"); } else { concrete = true; - base_type = runtime.getClass("ConcreteJavaProxy"); + if (Object.class.equals(c) || c.isPrimitive()) { + base_type = runtime.getClass("ConcreteJavaProxy"); + } else { + base_type = (RubyClass)get_proxy_class(recv,runtime.newString(c.getSuperclass().getName())); + invokeInherited = false; + } } - - RubyClass proxy_class = RubyClass.newClass(recv, new IRubyObject[]{base_type}, Block.NULL_BLOCK); + RubyClass proxy_class = RubyClass.newClass(recv, new IRubyObject[]{base_type}, Block.NULL_BLOCK,invokeInherited); proxy_class.callMethod(runtime.getCurrentContext(), "java_class=", java_class); - if(concrete) { + if(concrete && invokeInherited) { proxy_class.getMetaClass().defineFastMethod("inherited", pdata.callback); } proxy_classes.put(class_id, proxy_class); // We do not setup the proxy before we register it so that same-typed constants do // not try and create a fresh proxy class and go into an infinite loop - proxy_class.callMethod(runtime.getCurrentContext(), "setup"); - + ((JavaClass)java_class).setupProxy(proxy_class); for(Iterator iter = pdata.extenders.iterator(); iter.hasNext(); ) { ((IRubyObject)iter.next()).callMethod(runtime.getCurrentContext(), "extend_proxy", proxy_class); } + if (concrete && Modifier.isPublic(c.getModifiers()) && + !c.isPrimitive() && useJavaPackageModules(runtime)) { + addToJavaPackageModule(proxy_class,(JavaClass)java_class); + } } } return (IRubyObject)proxy_classes.get(class_id); } public static IRubyObject concrete_proxy_inherited(IRubyObject recv, IRubyObject subclass) { - ThreadContext tc = recv.getRuntime().getCurrentContext(); - recv.callMethod(tc,recv.getMetaClass().getSuperClass(), "inherited", new IRubyObject[]{subclass}, org.jruby.runtime.CallType.SUPER, Block.NULL_BLOCK); - return recv.getRuntime().getModule("JavaUtilities").callMethod(tc, "setup_java_subclass", new IRubyObject[]{subclass, recv.callMethod(tc,"java_class")}); + Ruby runtime = recv.getRuntime(); + ThreadContext tc = runtime.getCurrentContext(); + RubyClass javaProxyClass = runtime.getClass("JavaProxy").getMetaClass(); + recv.callMethod(tc,javaProxyClass, "inherited", new IRubyObject[]{subclass}, org.jruby.runtime.CallType.SUPER, Block.NULL_BLOCK); + // TODO: move to Java + return runtime.getModule("JavaUtilities").callMethod(tc, "setup_java_subclass", new IRubyObject[]{subclass, recv.callMethod(tc,"java_class")}); } + + public static boolean useJavaPackageModules (Ruby runtime) { + final RubyString javaModuleVar = runtime.newString("JRUBY_JAVA_MODULES"); + RubyHash h = ((RubyHash)runtime.getObject().getConstant("ENV")); + IRubyObject useMods = h.aref(javaModuleVar); + // right now, defaulting to 'true' unless explicity set to 'false'. + // might want to do this the other way + return (useMods == null || + !(useMods instanceof RubyString) || + ! "false".equals(useMods.toString())); + } + + private static void addToJavaPackageModule(RubyClass proxyClass, JavaClass javaClass) { + Class clazz = javaClass.javaClass(); + String fullName = clazz.getName(); + int endPackage; + // we'll only map conventional class names to modules + if (fullName == null || + (endPackage = fullName.lastIndexOf('.')) == -1 || + fullName.indexOf('$') != -1 || + !Character.isUpperCase(fullName.charAt(endPackage + 1))) { + return; + } + Ruby runtime = proxyClass.getRuntime(); + RubyModule parent = runtime.getModule("Java"); + for (int start = 0, offset = 0; start < endPackage; start = offset + 1) { + offset = fullName.indexOf('.',start); + String moduleName = fullName.substring(start,offset).toUpperCase(); + IRubyObject child = parent.getConstantAt(moduleName); + if (child == null) { + child = parent.defineModuleUnder(moduleName); + } else if (!(child instanceof RubyModule)) { + return; + } + parent = (RubyModule)child; + } + String className = fullName.substring(endPackage + 1); + if (parent.getConstantAt(className) == null) { + parent.const_set(runtime.newSymbol(className),proxyClass); + } + } public static IRubyObject matching_method(IRubyObject recv, IRubyObject methods, IRubyObject args) { Map matchCache = ((ProxyData)recv.dataGetStruct()).matchCache; Index: src/org/jruby/javasupport/JavaField.java =================================================================== --- src/org/jruby/javasupport/JavaField.java (revision 3514) +++ src/org/jruby/javasupport/JavaField.java (working copy) @@ -17,6 +17,7 @@ * Copyright (C) 2004 Stefan Matthias Aust * Copyright (C) 2004 David Corbin * Copyright (C) 2005 Charles O Nutter + * Copyright (C) 2007 William N Dortch * * Alternatively, the contents of this file may be used under the terms of * either of the GNU General Public License Version 2 or later (the "GPL"), @@ -60,6 +61,7 @@ result.defineFastMethod("static?", callbackFactory.getFastMethod("static_p")); result.defineFastMethod("value", callbackFactory.getFastMethod("value", IRubyObject.class)); result.defineFastMethod("set_value", callbackFactory.getFastMethod("set_value", IRubyObject.class, IRubyObject.class)); + result.defineFastMethod("set_static_value", callbackFactory.getFastMethod("set_static_value", IRubyObject.class)); result.defineFastMethod("final?", callbackFactory.getFastMethod("final_p")); result.defineFastMethod("static_value", callbackFactory.getFastMethod("static_value")); result.defineFastMethod("name", callbackFactory.getFastMethod("name")); @@ -152,6 +154,32 @@ } } + public JavaObject set_static_value(IRubyObject value) { + if (! (value instanceof JavaObject)) { + throw getRuntime().newTypeError("not a java object:" + value); + } + try { + Object convertedValue = JavaUtil.convertArgument(((JavaObject) value).getValue(), + field.getType()); + // TODO: Only setAccessible to account for pattern found by + // accessing constants included from a non-public interface. + // (aka java.util.zip.ZipConstants being implemented by many + // classes) + // TODO: not sure we need this at all, since we only expose + // public fields. + //field.setAccessible(true); + field.set(null, convertedValue); + } catch (IllegalAccessException iae) { + throw getRuntime().newTypeError( + "illegal access on setting static variable: " + iae.getMessage()); + } catch (IllegalArgumentException iae) { + throw getRuntime().newTypeError( + "wrong type for " + field.getType().getName() + ": " + + ((JavaObject) value).getValue().getClass().getName()); + } + return (JavaObject) value; + } + public RubyString name() { return getRuntime().newString(field.getName()); } Index: src/org/jruby/RubyClass.java =================================================================== --- src/org/jruby/RubyClass.java (revision 3514) +++ src/org/jruby/RubyClass.java (working copy) @@ -293,7 +293,7 @@ /** rb_class_s_new * */ - public static RubyClass newClass(IRubyObject recv, IRubyObject[] args, Block block) { + public static RubyClass newClass(IRubyObject recv, IRubyObject[] args, Block block, boolean invokeInherited) { final Ruby runtime = recv.getRuntime(); RubyClass superClass; @@ -310,19 +310,23 @@ ThreadContext tc = runtime.getCurrentContext(); // use allocator of superclass, since this will be a pure Ruby class - RubyClass newClass = superClass.newSubClass(null, superClass.getAllocator(),tc.peekCRef()); + RubyClass newClass = superClass.newSubClass(null, superClass.getAllocator(),tc.peekCRef(),invokeInherited); // call "initialize" method newClass.callInit(args, block); + // FIXME: inheritedBy called in superClass.newSubClass, so I + // assume this second call is a bug...? // call "inherited" method of the superclass - newClass.inheritedBy(superClass); + //newClass.inheritedBy(superClass); if (block.isGiven()) block.yield(tc, null, newClass, newClass, false); return newClass; } - + public static RubyClass newClass(IRubyObject recv, IRubyObject[] args, Block block) { + return newClass(recv,args,block,true); + } /** Return the real super class of this class. * * rb_class_superclass @@ -352,7 +356,8 @@ return (RubyClass) RubyModule.unmarshalFrom(output); } - public RubyClass newSubClass(String name, ObjectAllocator allocator, SinglyLinkedList parentCRef) { + public RubyClass newSubClass(String name, ObjectAllocator allocator, + SinglyLinkedList parentCRef, boolean invokeInherited) { RubyClass classClass = runtime.getClass("Class"); // Cannot subclass 'Class' or metaclasses @@ -365,7 +370,10 @@ RubyClass newClass = new RubyClass(runtime, classClass, this, allocator, parentCRef, name); newClass.makeMetaClass(getMetaClass(), newClass.getCRef()); - newClass.inheritedBy(this); + + if (invokeInherited) { + newClass.inheritedBy(this); + } if(null != name) { ((RubyModule)parentCRef.getValue()).setConstant(name, newClass); @@ -373,7 +381,11 @@ return newClass; } + public RubyClass newSubClass(String name, ObjectAllocator allocator, SinglyLinkedList parentCRef) { + return newSubClass(name,allocator,parentCRef,true); + } + protected IRubyObject doClone() { return RubyClass.cloneClass(getRuntime(), getMetaClass(), getSuperClass(), getAllocator(), null/*FIXME*/, null); }