diff --git a/jnius/jnius_conversion.pxi b/jnius/jnius_conversion.pxi index 711165c1..c3d3a30a 100644 --- a/jnius/jnius_conversion.pxi +++ b/jnius/jnius_conversion.pxi @@ -138,6 +138,7 @@ cdef convert_jobject_to_python(JNIEnv *j_env, definition, jobject j_object): r = definition[1:-1] cdef JavaObject ret_jobject cdef JavaClass ret_jc + cdef JavaClass c cdef jclass retclass cdef jmethodID retmeth @@ -200,18 +201,31 @@ cdef convert_jobject_to_python(JNIEnv *j_env, definition, jobject j_object): retmeth = j_env[0].GetMethodID(j_env, retclass, 'charValue', '()C') return ord(j_env[0].CallCharMethod(j_env, j_object, retmeth)) - if r not in jclass_register: + from .reflect import Object + if (r,(_DEFAULT_INCLUDE_PROTECTED, _DEFAULT_INCLUDE_PRIVATE)) not in jclass_register: if r.startswith('$Proxy'): # only for $Proxy on android, don't use autoclass. The dalvik vm is # not able to give us introspection on that one (FindClass return - # NULL). - from .reflect import Object + # NULL). ret_jc = Object(noinstance=True) else: - from .reflect import autoclass - ret_jc = autoclass(r.replace('/', '.'))(noinstance=True) + from .reflect import reflect_class, Class + # find_javaclass can raise an exception, but here that just means + # that we need to seek the Class instance from the instance, rather than JNI + c = find_javaclass(r, raise_error=False) + if c is None: + # The class may have come from another ClassLoader + # we need to get the Class from the instance itself + + #TODO: can we cache the following method id, or do we it already somewhere? + obj_class = j_env[0].FindClass(j_env, "java/lang/Object"); + get_class_method_id = j_env[0].GetMethodID(j_env, obj_class, "getClass", "()Ljava/lang/Class;") + retclass = j_env[0].CallObjectMethod(j_env, j_object, get_class_method_id) + c = Class(noinstance=True) + c.instanciate_from(create_local_ref(j_env, retclass)) + ret_jc = reflect_class(c, include_protected=_DEFAULT_INCLUDE_PROTECTED, include_private=_DEFAULT_INCLUDE_PRIVATE)(noinstance=True) else: - ret_jc = jclass_register[r](noinstance=True) + ret_jc = jclass_register[(r,(_DEFAULT_INCLUDE_PROTECTED, _DEFAULT_INCLUDE_PRIVATE))](noinstance=True) ret_jc.instanciate_from(create_local_ref(j_env, j_object)) return ret_jc diff --git a/jnius/jnius_export_class.pxi b/jnius/jnius_export_class.pxi index dc4fd111..d8f4f72c 100644 --- a/jnius/jnius_export_class.pxi +++ b/jnius/jnius_export_class.pxi @@ -111,11 +111,13 @@ class MetaJavaBase(type): cdef dict jclass_register = {} -# NOTE: The classparams default value in MetaJavaClass.__new__ and -# MetaJavaClass.get_javaclass need to be consistent with the include_protected -# and include_private default values in reflect.autoclass. +# these default params are parameterized so that MetaJavaClass.__new__, +# MetaJavaClass.get_javaclass, and reflect.autoclass can be consistent. +_DEFAULT_INCLUDE_PROTECTED=True +_DEFAULT_INCLUDE_PRIVATE=True + class MetaJavaClass(MetaJavaBase): - def __new__(meta, classname, bases, classDict, classparams=(True, True)): + def __new__(meta, classname, bases, classDict, classparams=(_DEFAULT_INCLUDE_PROTECTED, _DEFAULT_INCLUDE_PRIVATE)): meta.resolve_class(classDict) tp = type.__new__(meta, str(classname), bases, classDict) jclass_register[(classDict['__javaclass__'], classparams)] = tp @@ -160,7 +162,7 @@ class MetaJavaClass(MetaJavaBase): return super(MetaJavaClass, cls).__subclasscheck__(value) @staticmethod - def get_javaclass(name, classparams=(False, False)): + def get_javaclass(name, classparams=(_DEFAULT_INCLUDE_PROTECTED, _DEFAULT_INCLUDE_PRIVATE)): return jclass_register.get((name, classparams)) @classmethod @@ -207,17 +209,29 @@ class MetaJavaClass(MetaJavaBase): raise JavaException('Unable to create the class' ' {0}'.format(__javaclass__)) else: - class_name = str_for_c(__javaclass__) - jcs.j_cls = j_env[0].FindClass(j_env, - class_name) - if jcs.j_cls == NULL: - raise JavaException('Unable to find the class' + + if '_class' in classDict: + #we have a python copy of the class object, in classDict['_class']. lets use this instead of FindClass + + # classDict['_class'] is a jnius.reflect.Class, which extends JavaClass + # The jobject for that JavaClass is the jclass that we need to instantiate this object + JavaClass.copy_storage(classDict['_class'], jcs) + if jcs.j_cls == NULL: + raise JavaException('_class instance did not have a reference' + ' {0}'.format(__javaclass__)) + else: + class_name = str_for_c(__javaclass__) + jcs.j_cls = j_env[0].FindClass(j_env, + class_name) + if jcs.j_cls == NULL: + raise JavaException('Unable to find the class' ' {0}'.format(__javaclass__)) - # XXX do we need to grab a ref here? - # -> Yes, according to http://developer.android.com/training/articles/perf-jni.html - # in the section Local and Global References - jcs.j_cls = j_env[0].NewGlobalRef(j_env, jcs.j_cls) + # XXX do we need to grab a ref here? + # -> Yes, according to http://developer.android.com/training/articles/perf-jni.html + # in the section Local and Global References + jcs.j_cls = j_env[0].NewGlobalRef(j_env, jcs.j_cls) + # we only need this for a NEW FindClass call classDict['__cls_storage'] = jcs @@ -271,6 +285,9 @@ cdef class JavaClass(object): self.resolve_methods() self.resolve_fields() + cdef void copy_storage(self, JavaClassStorage jcs) except *: + jcs.j_cls = self.j_self.obj + cdef void instanciate_from(self, LocalRef j_self) except *: self.j_self = j_self self.resolve_methods() diff --git a/jnius/jnius_export_func.pxi b/jnius/jnius_export_func.pxi index 2bc6d8dc..8449e496 100644 --- a/jnius/jnius_export_func.pxi +++ b/jnius/jnius_export_func.pxi @@ -14,16 +14,23 @@ def cast(destclass, obj): return jc -def find_javaclass(namestr): +def find_javaclass(namestr, raise_error=True): namestr = namestr.replace('.', '/') cdef bytes name = str_for_c(namestr) from .reflect import Class cdef JavaClass cls cdef jclass jc cdef JNIEnv *j_env = get_jnienv() + cdef jthrowable jc = j_env[0].FindClass(j_env, name) - check_exception(j_env) + if raise_error: + check_exception(j_env) + else: + exc = j_env[0].ExceptionOccurred(j_env) + if exc: + j_env[0].ExceptionClear(j_env) + return None cls = Class(noinstance=True) cls.instanciate_from(create_local_ref(j_env, jc)) diff --git a/jnius/reflect.py b/jnius/reflect.py index 1d1db9aa..dac90413 100644 --- a/jnius/reflect.py +++ b/jnius/reflect.py @@ -9,10 +9,10 @@ from .jnius import ( JavaClass, MetaJavaClass, JavaMethod, JavaStaticMethod, JavaField, JavaStaticField, JavaMultipleMethod, find_javaclass, - JavaException + JavaException, _DEFAULT_INCLUDE_PROTECTED, _DEFAULT_INCLUDE_PRIVATE ) -__all__ = ('autoclass', 'ensureclass', 'protocol_map') +__all__ = ('autoclass', 'ensureclass', 'protocol_map', 'reflect_class') log = getLogger(__name__) @@ -25,6 +25,7 @@ class Class(with_metaclass(MetaJavaClass, JavaClass)): ('(Ljava/lang/String,Z,Ljava/lang/ClassLoader;)Ljava/langClass;', True, False), ('(Ljava/lang/String;)Ljava/lang/Class;', True, False), ]) getClassLoader = JavaMethod('()Ljava/lang/ClassLoader;') + getClass = JavaMethod('()Ljava/lang/Class;') getClasses = JavaMethod('()[Ljava/lang/Class;') getComponentType = JavaMethod('()Ljava/lang/Class;') getConstructor = JavaMethod('([Ljava/lang/Class;)Ljava/lang/reflect/Constructor;') @@ -55,6 +56,7 @@ class Class(with_metaclass(MetaJavaClass, JavaClass)): isInstance = JavaMethod('(Ljava/lang/Object;)Z') isInterface = JavaMethod('()Z') isPrimitive = JavaMethod('()Z') + hashCode = JavaMethod('()I') newInstance = JavaMethod('()Ljava/lang/Object;') toString = JavaMethod('()Ljava/lang/String;') @@ -213,36 +215,64 @@ def identify_hierarchy(cls, level, concrete=True): yield cls, level -# NOTE: if you change the include_protected or include_private default values, -# you also must change the classparams default value in MetaJavaClass.__new__ -# and MetaJavaClass.get_javaclass. -def autoclass(clsname, include_protected=True, include_private=True): +def autoclass(clsname, + include_protected=_DEFAULT_INCLUDE_PROTECTED, + include_private=_DEFAULT_INCLUDE_PRIVATE): + ''' + Auto-reflects a class based on its name. + + Parameters: + clsname (str): string name of the class, e.g. "java.util.HashMap" + include_protected (boolean): whether protected methods and fields should be included + include_private (boolean): whether protected methods and fields should be included + + Returns: + Returns a Python object representing the static class. + ''' jniname = clsname.replace('.', '/') cls = MetaJavaClass.get_javaclass(jniname, classparams=(include_protected, include_private)) if cls: return cls - classDict = {} - cls_start_packagename = '.'.join(clsname.split('.')[:-1]) - # c = Class.forName(clsname) c = find_javaclass(clsname) if c is None: raise Exception('Java class {0} not found'.format(c)) return None - classDict['_class'] = c + return reflect_class(c, include_protected, include_private) + + +# NOTE: See also comments on autoclass() on include_protected or include_private default values +def reflect_class(cls_object, include_protected=_DEFAULT_INCLUDE_PROTECTED, include_private=_DEFAULT_INCLUDE_PRIVATE): + ''' + Create a python wrapping class with the attributes and methods of the corresponding java class, from a python instance of the desired reflected java class. + + Parameters: + cls_object (Class): a Python instance of a java.lang.Class object. + include_protected (boolean): whether protected methods and fields should be included + include_private (boolean): whether protected methods and fields should be included + + Returns: + Returns a Python object representing the static class. + ''' + + clsname = cls_object.getName() + classDict = {} + cls_start_packagename = '.'.join(clsname.split('.')[:-1]) + + classDict['_class'] = cls_object constructors = [] - for constructor in c.getConstructors(): + for constructor in cls_object.getConstructors(): sig = '({0})V'.format( ''.join([get_signature(x) for x in constructor.getParameterTypes()])) constructors.append((sig, constructor.isVarArgs())) classDict['__javaconstructor__'] = constructors - class_hierachy = list(identify_hierarchy(c, 0, not c.isInterface())) + class_hierachy = list(identify_hierarchy(cls_object, 0, not cls_object.isInterface())) - log.debug("autoclass(%s) intf %r hierarchy is %s" % (clsname,c.isInterface(),str(class_hierachy))) + log.debug("autoclass(%s) intf %r hierarchy is %s" % (clsname,cls_object.isInterface(),str(class_hierachy))) cls_done=set() cls_methods = defaultdict(list) @@ -320,19 +350,11 @@ def autoclass(clsname, include_protected=True, include_private=True): get_signature(method.getReturnType())) if log.isEnabledFor(DEBUG): log_method(method, name, sig) - classDict[name] = (JavaStaticMethod if static else JavaMethod)(sig, varargs=varargs) - # methods that fit the characteristics of a JavaBean's methods get turned into properties. - # these added properties should not supercede any other methods or fields. - if name != 'getClass' and bean_getter(name) and len(method.getParameterTypes()) == 0: - lowername = lower_name(name[2 if name.startswith('is') else 3:]) - if lowername in classDict: - # don't add this to classDict if the property will replace a method or field. - continue - classDict[lowername] = (lambda n: property(lambda self: getattr(self, n)()))(name) + _add_single_method(classDict, name, static, sig, varargs) else: # multiple signatures signatures = [] - log.debug("method %s has %d multiple signatures in hierarchy of cls %s" % (name, len(cls_methods[name]), c)) + log.debug("method %s has %d multiple signatures in hierarchy of cls %s" % (name, len(cls_methods[name]), clsname)) paramsig_to_level=defaultdict(lambda: float('inf')) # we now identify if any have the same signature, as we will call the _lowest_ in the hierarchy, @@ -357,8 +379,14 @@ def autoclass(clsname, include_protected=True, include_private=True): log_method(method, name, sig) signatures.append((sig, Modifier.isStatic(method.getModifiers()), method.isVarArgs())) - log.debug("method selected %d multiple signatures of %s" % (len(signatures), str(signatures))) - classDict[name] = JavaMultipleMethod(signatures) + if len(signatures) > 1: + log.debug("method selected %d multiple signatures of %s" % (len(signatures), str(signatures))) + classDict[name] = JavaMultipleMethod(signatures) + elif len(signatures) == 1: + (sig, static, varargs) = signatures[0] + if log.isEnabledFor(DEBUG): + log_method(method, name, sig) + _add_single_method(classDict, name, static, sig, varargs) # check whether any classes in the hierarchy appear in the protocol_map for cls, _ in class_hierachy: @@ -375,6 +403,16 @@ def autoclass(clsname, include_protected=True, include_private=True): classDict, classparams=(include_protected, include_private)) +def _add_single_method(classDict, name, static, sig, varargs): + classDict[name] = (JavaStaticMethod if static else JavaMethod)(sig, varargs=varargs) + # methods that fit the characteristics of a JavaBean's methods get turned into properties. + # these added properties should not supercede any other methods or fields. + if name != 'getClass' and bean_getter(name) and sig.startswith("()"): + lowername = lower_name(name[2 if name.startswith('is') else 3:]) + if lowername in classDict: + # don't add this to classDict if the property will replace a method or field. + return + classDict[lowername] = (lambda n: property(lambda self: getattr(self, n)()))(name) def _getitem(self, index): ''' dunder method for List ''' diff --git a/tests/test_reflect.py b/tests/test_reflect.py index a1f1a425..72feb98b 100644 --- a/tests/test_reflect.py +++ b/tests/test_reflect.py @@ -5,7 +5,7 @@ from jnius.reflect import autoclass from jnius import cast from jnius.reflect import identify_hierarchy -from jnius import find_javaclass +from jnius import find_javaclass, MetaJavaClass def identify_hierarchy_dict(cls, level, concrete=True): return({ cls.getName() : level for cls,level in identify_hierarchy(cls, level, concrete) }) @@ -102,3 +102,10 @@ def test_list_iteration(self): words.add('world') self.assertEqual(['hello', 'world'], [word for word in words]) + def test_autoclass_default_params(self): + cls_name = 'javax.crypto.Cipher' + jni_name = cls_name.replace('.', '/') + if MetaJavaClass.get_javaclass(jni_name) is not None: + self.skipTest("%s already loaded - has this test run more than once?" % cls_name) + self.assertIsNotNone(autoclass(cls_name)) + self.assertIsNotNone(MetaJavaClass.get_javaclass(jni_name)) diff --git a/tests/test_reflect_class.py b/tests/test_reflect_class.py new file mode 100644 index 00000000..1ea10b36 --- /dev/null +++ b/tests/test_reflect_class.py @@ -0,0 +1,57 @@ + + +import unittest +from jnius.reflect import autoclass, reflect_class + +class ReflectTest(unittest.TestCase): + + def test_reflect_class(self): + cls_loader = autoclass("java.lang.ClassLoader").getSystemClassLoader() + + # choose an obscure class that jnius hasnt seen before during unit tests + cls_name = "java.util.zip.CRC32" + from jnius import MetaJavaClass + if MetaJavaClass.get_javaclass(cls_name) is not None: + self.skipTest("%s already loaded - has this test run more than once?" % cls_name) + + cls = cls_loader.loadClass(cls_name) + # we get a Class object + self.assertEqual("java.lang.Class", cls.getClass().getName()) + # which represents CRC32 + self.assertEqual(cls_name, cls.getName()) + # lets make that into a python obj representing the class + pyclass = reflect_class(cls) + # check it refers to the same thing + self.assertEqual(cls_name.replace('.', '/'), pyclass.__javaclass__) + # check we can instantiate it + instance = pyclass() + self.assertIsNotNone(instance) + + + def test_dynamic_jar(self): + # the idea behind this test is to: + # 1. load an external jar file using an additional ClassLoader + # 2. check we can instantate the Class instance + # 3. check we can reflect the Class instance + # 4. check we can call a method that returns an object that can only be accessed using the additional ClassLoader + jar_url = "https://repo1.maven.org/maven2/commons-io/commons-io/2.6/commons-io-2.6.jar" + url = autoclass("java.net.URL")(jar_url) + sys_cls_loader = autoclass("java.lang.ClassLoader").getSystemClassLoader() + new_cls_loader = autoclass("java.net.URLClassLoader").newInstance([url], sys_cls_loader) + cls_object = new_cls_loader.loadClass("org.apache.commons.io.IOUtils") + self.assertIsNotNone(cls_object) + io_utils = reflect_class(cls_object) + self.assertIsNotNone(io_utils) + + stringreader = autoclass("java.io.StringReader")("test1\ntest2") + #lineIterator returns an object of class LineIterator - here we check that jnius can reflect that, despite not being in the boot classpath + lineiter = io_utils.lineIterator(stringreader) + self.assertEqual("test1", lineiter.next()) + self.assertEqual("test2", lineiter.next()) + + # Equivalent Java code: + # var new_cls_loader = java.net.URLClassLoader.newInstance(new java.net.URL[] {new java.net.URL("https://repo1.maven.org/maven2/commons-io/commons-io/2.6/commons-io-2.6.jar")}, ClassLoader.getSystemClassLoader()) + # var cls = new_cls_loader.loadClass("org.apache.commons.io.IOUtils") + # var sr = new java.io.StringReader("test1\ntest2") + # var m = $2.getMethod("lineIterator", Reader.class) + # m.invoke(null, (Object) sr) \ No newline at end of file diff --git a/tests/test_visibility_all.py b/tests/test_visibility_all.py index 85b34e6e..413c6cbc 100644 --- a/tests/test_visibility_all.py +++ b/tests/test_visibility_all.py @@ -216,21 +216,21 @@ def assert_is_method(obj, name): def test_check_method_vs_property(self): """check that "bean" properties don't replace methods. - The ExecutorService Interface has methods `shutdown()`, `isShutdown()`, - `isTerminated()`. The `autoclass` function will create a Python + The ExecutorService Interface has methods `shutdown()`, `isShutdown()`, + `isTerminating()`. The `autoclass` function will create a Python `property` if a function name matches a JavaBean name pattern. Those properties are important but they should not take priority over a method. For this Interface it wants to create properties called `shutdown` and - `terminated` because of `isShutdown` and `isTerminated`. A `shutdown` + `terminating` because of `isShutdown` and `isTerminating`. A `shutdown` property would conflict with the `shutdown()` method so it should be - skipped. The `terminated` property is OK though. + skipped. The `terminating` property is OK though. """ executor = autoclass("java.util.concurrent.Executors") pool = executor.newFixedThreadPool(1) self.assertTrue(isinstance(pool.__class__.__dict__['shutdown'], JavaMethod)) - self.assertTrue(isinstance(pool.__class__.__dict__['terminated'], property)) + self.assertTrue(isinstance(pool.__class__.__dict__['terminating'], property)) self.assertTrue(isinstance(pool.__class__.__dict__['isShutdown'], JavaMethod)) self.assertTrue(isinstance(pool.__class__.__dict__['isTerminated'], JavaMethod))