Better BeanShell Debugging in IdentityIQ

Audience

This blog post is intended for IdentityIQ developers—engineers who are writing BeanShell and Java code to customize IdentityIQ.
You should know how to compile and decompile Java and create jar files. If you don’t know how to do that, there are resources online that can help you.

Introduction

BeanShell is extensively used throughout IdentityIQ. At the same time, often, the business logic is primarily implemented in Java, with BeanShell serving as the initial invocation point. This approach is advantageous as it allows developers to leverage the rich set of tools available for Java development.

However, when an exception is thrown within the Java code initiated by BeanShell—whether it’s a NullPointerException (NPE) or any other error—you may find a significant portion of the stack trace missing in the logs. This missing section usually contains the method invocations within your Java code, which is often the most crucial part for debugging.

Fortunately, there is a straightforward, albeit somewhat hacky, solution to this problem that can greatly assist in diagnosing issues.

Looking for a video demonstration of this? Check it out here:

The Default Behavior of BeanShell

To illustrate the problem, let’s consider a simple example. Imagine you have a workflow step with the following BeanShell code:

<Step name="Step with beanshell code">
        <Script>
            <Source><![CDATA[
                import sailpoint.workflow.CustomWflLibrary;

                CustomWflLibrary lib = new CustomWflLibrary();
                lib.methodThatThrowsException();
            ]]>
            </Source>
        </Script>
        <Transition to="End" />
    </Step>

And the corresponding Java implementation:

public class CustomWflLibrary {

	public void methodThatThrowsException() throws GeneralException {
		methodThatActuallyThrows(null);
	}

	private void methodThatActuallyThrows(String inputString) throws GeneralException {
		String s = "This is a test string";
		String uninitializedString = inputString;
		if (uninitializedString.equals(s)) {
			throw new GeneralException("This is a test exception and there could be some important data here");
		}
	}
}

When you run this workflow, the stack trace appears as follows:

2024-08-06T20:56:38,689 ERROR TestCaseImmediateRunnerThread Thread-7 sailpoint.api.Workflower:4605 - An unexpected error occurred: sailpoint.tools.GeneralException: The application script threw an exception: java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because "uninitializedString" is null BSF info: script at line: 0 column: columnNo
sailpoint.tools.GeneralException: sailpoint.tools.GeneralException: The application script threw an exception: java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because "uninitializedString" is null BSF info: script at line: 0 column: columnNo
        at sailpoint.server.ScriptletEvaluator.doScript(ScriptletEvaluator.java:268) ~[identityiq.jar:8.4 Build bdd0ed4de58-20230919-192552]
        at sailpoint.server.ScriptletEvaluator.evalSource(ScriptletEvaluator.java:71) ~[identityiq.jar:8.4 Build bdd0ed4de58-20230919-192552]
        at sailpoint.api.Workflower.evalSource(Workflower.java:5937) ~[identityiq.jar:8.4 Build bdd0ed4de58-20230919-192552]
        at sailpoint.api.Workflower.advanceStep(Workflower.java:5176) ~[identityiq.jar:8.4 Build bdd0ed4de58-20230919-192552]
        at sailpoint.api.Workflower.advance(Workflower.java:4563) [identityiq.jar:8.4 Build bdd0ed4de58-20230919-192552]
        at sailpoint.api.Workflower.startCase(Workflower.java:3149) [identityiq.jar:8.4 Build bdd0ed4de58-20230919-192552]
        at sailpoint.api.Workflower.launchInner(Workflower.java:2818) [identityiq.jar:8.4 Build bdd0ed4de58-20230919-192552]
        at sailpoint.api.Workflower.launch(Workflower.java:2668) [identityiq.jar:8.4 Build bdd0ed4de58-20230919-192552]
        at sailpoint.api.Workflower.launch(Workflower.java:2502) [identityiq.jar:8.4 Build bdd0ed4de58-20230919-192552]
        at com.itf.handler.WflHandler.runWorkflow(WflHandler.java:339) [?:?]
        at com.itf.handler.WflHandler.execute(WflHandler.java:411) [?:?]
        at com.itf.engine.TestCaseExecutionEnvironment.runTest(TestCaseExecutionEnvironment.java:281) [?:?]
        at com.itf.remote.TestCaseRunnable.run(TestCaseRunnable.java:73) [?:?]
        at java.lang.Thread.run(Thread.java:833) [?:?]
Caused by: sailpoint.tools.GeneralException: The application script threw an exception: java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because "uninitializedString" is null BSF info: script at line: 0 column: columnNo
        at sailpoint.server.BSFRuleRunner.runScript(BSFRuleRunner.java:349) ~[identityiq.jar:8.4 Build bdd0ed4de58-20230919-192552]
        at sailpoint.server.InternalContext.runScript(InternalContext.java:1350) ~[identityiq.jar:8.4 Build bdd0ed4de58-20230919-192552]
        at sailpoint.server.ScriptletEvaluator.doScript(ScriptletEvaluator.java:263) ~[identityiq.jar:8.4 Build bdd0ed4de58-20230919-192552]
        ... 13 more
Caused by: org.apache.bsf.BSFException: The application script threw an exception: java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because "uninitializedString" is null BSF info: script at line: 0 column: columnNo
        at bsh.util.BeanShellBSFEngine.eval(BeanShellBSFEngine.java:111) ~[classes/:?]
        at org.apache.bsf.BSFManager$5.run(BSFManager.java:445) ~[bsf.jar:?]
        at java.security.AccessController.doPrivileged(AccessController.java:569) ~[?:?]
        at org.apache.bsf.BSFManager.eval(BSFManager.java:442) ~[bsf.jar:?]
        at sailpoint.server.BSFRuleRunner.runScript(BSFRuleRunner.java:347) ~[identityiq.jar:8.4 Build bdd0ed4de58-20230919-192552]
        at sailpoint.server.InternalContext.runScript(InternalContext.java:1350) ~[identityiq.jar:8.4 Build bdd0ed4de58-20230919-192552]
        at sailpoint.server.ScriptletEvaluator.doScript(ScriptletEvaluator.java:263) ~[identityiq.jar:8.4 Build bdd0ed4de58-20230919-192552]
        ... 13 more

In this stack trace, you can see that the information about how the exception was thrown in the Java code is missing. All you see is that BeanShell executed a script and that an exception occurred.

The Root Cause

The issue lies in how the eval() method from the BeanShellBSFEngine class handles exceptions. When decompiled, the relevant portion of the code looks like this:

public Object eval(String source, int lineNo, int columnNo, Object expr) throws BSFException {
        if (!(expr instanceof String)) {
            throw new BSFException("BeanShell expression must be a string");
        } else {
            try {
                return this.interpreter.eval((String)expr);
            } catch (InterpreterError var6) {
                throw new BSFException("BeanShell interpreter internal error: " + var6.getMessage() + this.sourceInfo(source, lineNo, columnNo));
            } catch (TargetError var7) {
                throw new BSFException("The application script threw an exception: " + var7.getTarget() + this.sourceInfo(source, lineNo, columnNo));
            } catch (EvalError var8) {
                throw new BSFException("BeanShell script error: " + var8.getMessage() + this.sourceInfo(source, lineNo, columnNo));
            }
        }
    }

The key point of interest here is the handling of the TargetError exception. As you can see, any detailed information about the exception is not passed down the invocation stack, leading to the incomplete stack trace.

The Solution

There are several ways to address this issue, but the simplest involves modifying the TargetError catch block as follows:

catch (TargetError var7) {
	var7.printStackTrace();
    throw new BSFException("The application script threw an exception: " + var7.getTarget() + this.sourceInfo(source, lineNo, columnNo));
}

Alternatively, you could modify BSFException to accept both a message and a Throwable, but I’ll leave the more sophisticated implementations up to you.

Incorporating the Fix

To implement this change, you need to decompile the BeanShellBSFEngine class found in the bsh-2.1.8.jar file (as of IdentityIQ 8.4). After applying the fix, recompile the class and store it in a jar file.

It’s crucial that this jar file is named such that it appears alphabetically before bsh-2.1.8.jar (for example aCustomized-bsh-2.1.8.jar). This ensures that it will be loaded by the classloader before the original, causing your modified BeanShellBSFEngine class to be used instead of the out-of-the-box one.

Finally, place the jar file in the same lib directory as the other IdentityIQ jar files, and restart the server. If you are using SSB you would put it in web/WEB-INF/lib directory of you IIQ build.

The stack trace should now appear as follows:

Target exception: java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because "uninitializedString" is null

        at bsh.BSHMethodInvocation.eval(BSHMethodInvocation.java:97)
        at bsh.BSHPrimaryExpression.eval(BSHPrimaryExpression.java:102)
        at bsh.BSHPrimaryExpression.eval(BSHPrimaryExpression.java:47)
        at bsh.Interpreter.eval(Interpreter.java:664)
        at bsh.Interpreter.eval(Interpreter.java:758)
        at bsh.Interpreter.eval(Interpreter.java:747)
        at bsh.util.BeanShellBSFEngine.eval(BeanShellBSFEngine.java:105)
        at org.apache.bsf.BSFManager$5.run(BSFManager.java:445)
        at java.base/java.security.AccessController.doPrivileged(AccessController.java:569)
        at org.apache.bsf.BSFManager.eval(BSFManager.java:442)
        at sailpoint.server.BSFRuleRunner.runScript(BSFRuleRunner.java:347)
        at sailpoint.server.InternalContext.runScript(InternalContext.java:1350)
        at sailpoint.server.ScriptletEvaluator.doScript(ScriptletEvaluator.java:263)
        at sailpoint.server.ScriptletEvaluator.evalSource(ScriptletEvaluator.java:71)
        at sailpoint.api.Workflower.evalSource(Workflower.java:5937)
        at sailpoint.api.Workflower.advanceStep(Workflower.java:5176)
        at sailpoint.api.Workflower.advance(Workflower.java:4563)
        at sailpoint.api.Workflower.startCase(Workflower.java:3149)
        at sailpoint.api.Workflower.launchInner(Workflower.java:2818)
        at sailpoint.api.Workflower.launch(Workflower.java:2668)
        at sailpoint.api.Workflower.launch(Workflower.java:2502)
        at com.itf.handler.WflHandler.runWorkflow(WflHandler.java:339)
        at com.itf.handler.WflHandler.execute(WflHandler.java:411)
        at com.itf.engine.TestCaseExecutionEnvironment.runTest(TestCaseExecutionEnvironment.java:281)
        at com.itf.remote.TestCaseRunnable.run(TestCaseRunnable.java:73)
        at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because "uninitializedString" is null
        at sailpoint.workflow.CustomWflLibrary.methodThatActuallyThrows(CustomWflLibrary.java:131)
        at sailpoint.workflow.CustomWflLibrary.methodThatThrowsException(CustomWflLibrary.java:125)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:568)
        at bsh.Reflect.invokeMethod(Reflect.java:166)
        at bsh.Reflect.invokeObjectMethod(Reflect.java:99)
        at bsh.Name.invokeMethod(Name.java:858)
        at bsh.BSHMethodInvocation.eval(BSHMethodInvocation.java:75)
        ... 25 more

As you can see, the stack trace now includes all the methods from the custom Java code, allowing simpler and more informed debugging.

Disclaimer

As mentioned earlier, this solution is a hack. You’ll need to adjust the code to match the version of BeanShell you’re using, and extensive testing is required if you plan to deploy this in a production environment.

Back to work then :wink:

9 Likes

Thank you for this blog, great read

1 Like

It’s good for a developer to explore and experiment with such code snippets.

Thanks for the post!

1 Like