From 6328de8142ae504c8a6a04a481a2316c463fc7ef Mon Sep 17 00:00:00 2001 From: Len Trigg Date: Fri, 16 May 2014 12:12:12 +1200 Subject: [PATCH 1/2] Incorporate patch from #96 and a unit test --- jnius/jnius_utils.pxi | 11 ++++++++++- tests/test_bad_declaration.py | 9 +++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/jnius/jnius_utils.pxi b/jnius/jnius_utils.pxi index c2515282..7f473dc7 100644 --- a/jnius/jnius_utils.pxi +++ b/jnius/jnius_utils.pxi @@ -36,11 +36,20 @@ cdef parse_definition(definition): cdef void check_exception(JNIEnv *j_env) except *: + cdef jmethodID toString = NULL + cdef jstring e_string + cdef jboolean isCopy cdef jthrowable exc = j_env[0].ExceptionOccurred(j_env) if exc: j_env[0].ExceptionDescribe(j_env) j_env[0].ExceptionClear(j_env) - raise JavaException('JVM exception occured') + + toString = j_env[0].GetMethodID(j_env, j_env[0].FindClass(j_env, "java/lang/Object"), "toString", "()Ljava/lang/String;"); + e_string = j_env[0].CallObjectMethod(j_env, exc, toString); + + pystr = convert_jobject_to_python(j_env, 'Ljava/lang/String;', e_string) + + raise JavaException('JVM exception occurred: ' + pystr) cdef dict assignable_from = {} diff --git a/tests/test_bad_declaration.py b/tests/test_bad_declaration.py index a7b46a71..c75b7867 100644 --- a/tests/test_bad_declaration.py +++ b/tests/test_bad_declaration.py @@ -21,3 +21,12 @@ def test_with_too_much_arguments(self): Stack = autoclass('java.util.Stack') stack = Stack() self.assertRaises(JavaException, stack.push, 'hello', 'world', 123) + + def test_java_exception_handling(self): + Stack = autoclass('java.util.Stack') + stack = Stack() + try: + stack.pop() + self.fail("Expected exception to be thrown") + except JavaException as je: + self.assertIn("EmptyStackException", str(je)) From d93c911d9e89c9d82febb3b9d9e9f8d1486ba28d Mon Sep 17 00:00:00 2001 From: Lenbok Date: Mon, 19 May 2014 10:22:36 +1200 Subject: [PATCH 2/2] Add improved exception handling: - Disable calling ExceptionDescribe, as this pollutes stderr, making it harder to implement clean exception handling. JNI documentation describes it as being for debugging purposes. - Extract separate fields for the exception class name, exception message, and exception stack trace, and add these as attributes to JavaException. Calling code can now decide for itself whether and how to present this information. --- jnius/jnius_export_class.pxi | 10 ++++- jnius/jnius_utils.pxi | 74 +++++++++++++++++++++++++++++---- tests/org/jnius/BasicsTest.java | 11 +++++ tests/test_bad_declaration.py | 21 +++++++++- 4 files changed, 107 insertions(+), 9 deletions(-) diff --git a/jnius/jnius_export_class.pxi b/jnius/jnius_export_class.pxi index 0e321d9f..e6fe76b9 100644 --- a/jnius/jnius_export_class.pxi +++ b/jnius/jnius_export_class.pxi @@ -2,7 +2,15 @@ class JavaException(Exception): '''Can be a real java exception, or just an exception from the wrapper. ''' - pass + classname = None # The classname of the exception + innermessage = None # The message of the inner exception + stacktrace = None # The stack trace of the inner exception + + def __init__(self, message, classname=None, innermessage=None, stacktrace=None): + self.classname = classname + self.innermessage = innermessage + self.stacktrace = stacktrace + Exception.__init__(self, message) cdef class JavaObject(object): diff --git a/jnius/jnius_utils.pxi b/jnius/jnius_utils.pxi index 7f473dc7..edbbd8cb 100644 --- a/jnius/jnius_utils.pxi +++ b/jnius/jnius_utils.pxi @@ -37,19 +37,79 @@ cdef parse_definition(definition): cdef void check_exception(JNIEnv *j_env) except *: cdef jmethodID toString = NULL - cdef jstring e_string + cdef jmethodID getCause = NULL + cdef jmethodID getStackTrace = NULL + cdef jmethodID getMessage = NULL + cdef jstring e_msg cdef jboolean isCopy cdef jthrowable exc = j_env[0].ExceptionOccurred(j_env) if exc: - j_env[0].ExceptionDescribe(j_env) + # ExceptionDescribe always writes to stderr, preventing tidy exception + # handling, so should only be for debugging + # j_env[0].ExceptionDescribe(j_env) j_env[0].ExceptionClear(j_env) toString = j_env[0].GetMethodID(j_env, j_env[0].FindClass(j_env, "java/lang/Object"), "toString", "()Ljava/lang/String;"); - e_string = j_env[0].CallObjectMethod(j_env, exc, toString); - - pystr = convert_jobject_to_python(j_env, 'Ljava/lang/String;', e_string) - - raise JavaException('JVM exception occurred: ' + pystr) + getMessage = j_env[0].GetMethodID(j_env, j_env[0].FindClass(j_env, "java/lang/Throwable"), "getMessage", "()Ljava/lang/String;"); + getCause = j_env[0].GetMethodID(j_env, j_env[0].FindClass(j_env, "java/lang/Throwable"), "getCause", "()Ljava/lang/Throwable;"); + getStackTrace = j_env[0].GetMethodID(j_env, j_env[0].FindClass(j_env, "java/lang/Throwable"), "getStackTrace", "()[Ljava/lang/StackTraceElement;"); + + e_msg = j_env[0].CallObjectMethod(j_env, exc, getMessage); + pymsg = None if e_msg == NULL else convert_jobject_to_python(j_env, 'Ljava/lang/String;', e_msg) + + pystack = [] + _append_exception_trace_messages(j_env, pystack, exc, getCause, getStackTrace, toString) + + pyexcclass = lookup_java_object_name(j_env, exc).replace('/', '.') + + raise JavaException('JVM exception occurred: %s' % (pymsg if pymsg is not None else pyexcclass), pyexcclass, pymsg, pystack) + + +cdef void _append_exception_trace_messages( + JNIEnv* j_env, + list pystack, + jthrowable exc, + jmethodID mid_getCause, + jmethodID mid_getStackTrace, + jmethodID mid_toString): + + # Get the array of StackTraceElements. + cdef jobjectArray frames = j_env[0].CallObjectMethod(j_env, exc, mid_getStackTrace) + cdef jsize frames_length = j_env[0].GetArrayLength(j_env, frames) + cdef jstring msg_obj + cdef jobject frame + cdef jthrowable cause + + # Add Throwable.toString() before descending stack trace messages. + if frames != NULL: + msg_obj = j_env[0].CallObjectMethod(j_env, exc, mid_toString) + pystr = None if msg_obj == NULL else convert_jobject_to_python(j_env, 'Ljava/lang/String;', msg_obj) + # If this is not the top-of-the-trace then this is a cause. + if len(pystack) > 0: + pystack.append("Caused by:") + pystack.append(pystr) + j_env[0].DeleteLocalRef(j_env, msg_obj) + + # Append stack trace messages if there are any. + if frames_length > 0: + for i in range(frames_length): + # Get the string returned from the 'toString()' method of the next frame and append it to the error message. + frame = j_env[0].GetObjectArrayElement(j_env, frames, i) + msg_obj = j_env[0].CallObjectMethod(j_env, frame, mid_toString) + pystr = None if msg_obj == NULL else convert_jobject_to_python(j_env, 'Ljava/lang/String;', msg_obj) + pystack.append(pystr) + j_env[0].DeleteLocalRef(j_env, msg_obj) + j_env[0].DeleteLocalRef(j_env, frame) + + # If 'exc' has a cause then append the stack trace messages from the cause. + if frames != NULL: + cause = j_env[0].CallObjectMethod(j_env, exc, mid_getCause) + if cause != NULL: + _append_exception_trace_messages(j_env, pystack, cause, + mid_getCause, mid_getStackTrace, mid_toString) + j_env[0].DeleteLocalRef(j_env, cause) + + j_env[0].DeleteLocalRef(j_env, frames) cdef dict assignable_from = {} diff --git a/tests/org/jnius/BasicsTest.java b/tests/org/jnius/BasicsTest.java index c2578229..feeceb0a 100644 --- a/tests/org/jnius/BasicsTest.java +++ b/tests/org/jnius/BasicsTest.java @@ -22,6 +22,17 @@ public class BasicsTest { public float methodF() { return 1.23456789f; }; public double methodD() { return 1.23456789; }; public String methodString() { return new String("helloworld"); } + public void methodException(int depth) throws IllegalArgumentException { + if (depth == 0) throw new IllegalArgumentException("helloworld"); + else methodException(depth -1); + } + public void methodExceptionChained() throws IllegalArgumentException { + try { + methodException(5); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("helloworld2", e); + } + } static public boolean fieldStaticZ = true; static public byte fieldStaticB = 127; diff --git a/tests/test_bad_declaration.py b/tests/test_bad_declaration.py index c75b7867..a1f66f4d 100644 --- a/tests/test_bad_declaration.py +++ b/tests/test_bad_declaration.py @@ -29,4 +29,23 @@ def test_java_exception_handling(self): stack.pop() self.fail("Expected exception to be thrown") except JavaException as je: - self.assertIn("EmptyStackException", str(je)) + # print "Got JavaException: " + str(je) + # print "Got Exception Class: " + je.classname + # print "Got stacktrace: \n" + '\n'.join(je.stacktrace) + self.assertEquals("java.util.EmptyStackException", je.classname) + + def test_java_exception_chaining(self): + BasicsTest = autoclass('org.jnius.BasicsTest') + basics = BasicsTest() + try: + basics.methodExceptionChained() + self.fail("Expected exception to be thrown") + except JavaException as je: + # print "Got JavaException: " + str(je) + # print "Got Exception Class: " + je.classname + # print "Got Exception Message: " + je.innermessage + # print "Got stacktrace: \n" + '\n'.join(je.stacktrace) + self.assertEquals("java.lang.IllegalArgumentException", je.classname) + self.assertEquals("helloworld2", je.innermessage) + self.assertIn("Caused by:", je.stacktrace) + self.assertEquals(11, len(je.stacktrace)) \ No newline at end of file