diff --git a/jnius/jnius_export_class.pxi b/jnius/jnius_export_class.pxi index 0e321d9..e6fe76b 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 7f473dc..edbbd8c 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); + 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;"); - pystr = convert_jobject_to_python(j_env, 'Ljava/lang/String;', e_string) + 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) - raise JavaException('JVM exception occurred: ' + pystr) + 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 c257822..feeceb0 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 c75b786..a1f66f4 100644 --- a/tests/test_bad_declaration.py +++ b/tests/test_bad_declaration.py @@ -29,4 +29,23 @@ class BadDeclarationTest(unittest.TestCase): 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