Returning from Ruby or JavaScript called from the Java Scripting API

Since the Java Scripting API makes it easy to execute external scripts written in a variety of dynamic languages, I tried to find a consistent way to return early from top-level code written in JavaScript and Ruby. My goal was to be able to structure short Ruby and JavaScript scripts by coding everything at the "top level," that is, outside of any defined function, method, or class. That way, the Ruby or JavaScript scriptlets would be easier to write and I could eval them from Java without having to call a specific function or method by name.

After hunting around, I found no simple or easy way a JavaScript or Ruby script could return early from being evaluated when the scripting code is outside of a function or method. A return statement is not allowed outside a function in JavaScript, nor is it allowed outside a method in Ruby. The only consistent language feature I found that guaranteed early script exit was for the code to throw an exception.

If you're unfamiliar with the Java Scripting API (JSR-223, Scripting for the Java Platform), it was added in Java Standard Edition 6 to provide a consistent way to embed scripting-language interpreters into a Java application. The API's javax.script package contains classes and interfaces that let you call and share data with an external script written in dozens of scripting languages, including powerful dynamic languages like Ruby and Groovy. The Java Scripting API is based primarily on the Apache Jakarta Bean Scripting Framework project, but provides extra features and is now built into the Java language. You can use the Scripting API in Java 1.5 by adding the new packages, available by downloading the JSR-223 reference implementation.

Here is what I set out to accomplish.

I wanted to be able to pass Java objects to scripts written in Ruby and JavaScript and let those scripts process the shared Java objects. The goal was to take advantage of the cleaner, more concise syntax these languages offer and allow end-users the ability to supply the Ruby and JavaScript code. That was why I didn't want to require script providers to code their logic inside a method or function. But by placing all code at the top level, the script writer would have no language feature available to return early from script processing.

For example, the Java code that called the script would look something like:
// Java objects to share with the scripts:
String textToProcess = ... // Text for scripts to process
int myStatus = ...         // Some type of status indicator
// etc.
ScriptEngineManager scriptEngineMgr = new ScriptEngineManager();
ScriptEngine rubyEngine = scriptEngineMgr.getEngineByName("ruby");
rubyEngine.put("textToProcess", textToProcess);
rubyEngine.put("status", Integer.valueOf(myStatus));
// ...
// Put a shared object the script will use to return results.
ResultsObject result = new ResultsObject();
rubyEngine.put("result", result);
// Read Ruby script from external source and execute it
String rubyScript = ...
rubyEngine.eval(rubyScript);
// Read results set by the script.
Long resultCode = result.getResultCode();
// etc...
The Ruby script would look something like:
# Don't process the text if the status is greater than 200
if $status > 200
return   # <-- This is illegal Ruby!
end
# Process the $textToProcess text...
...
although the conditions in which the script writer would want to exit could be a lot more complicated and couldn't be structured around an if-else statement.

The problem here is the Ruby script has no simple, clear way to prevent the entire script from being run, short of raising an exception. It is possible to work around the problem by requiring the script to be coded inside of a method. You also could require script writers to code around the problem by wrapping all code inside a needless outer loop and using a break statement to serve the purpose of a return statement.

The above code could thus be replaced by:
1.times do
# Don't process the text if the status is greater than 200
if $status > 200
break # This does work.
end
# Process the $textToProcess text...
...
end
An extra outer loop should work for JavaScript, too.

The problem with using an outer loop to provide a script return is that it requires the script writer to code the loop. That solution violates my goal of making the scripts as easy as possible to write -- and read.

My eventual solution, which I'm not satisfied with, was to allow the script to perform the equivalent of a top-level return statement by throwing an exception. To make the solution more palatable and cleaner for the script writer, I created a Java class that would throw the actual exception. The Java class also permits the script to return an optional reason message when exiting.

Here is the revised Java code that would call the scripts:
// Java objects to share with the scripts:
String textToProcess = ... // Text for scripts to process
int myStatus = ...         // Some type of status indicator
// etc.
ScriptEngineManager scriptEngineMgr = new ScriptEngineManager();
ScriptEngine rubyEngine = scriptEngineMgr.getEngineByName("ruby");
rubyEngine.put("textToProcess", textToProcess);
rubyEngine.put("status", Integer.valueOf(myStatus));
// ...
// Put a shared object the script will use to return results.
ResultsObject result = new ResultsObject();
rubyEngine.put("result", result);
// Add an object scripts can call to exit early from processing.
rubyEngine.put("scriptExit", new ScriptEarlyExit());
// Read Ruby script from external source and execute it
String rubyScript = ...
rubyEngine.eval(rubyScript);
// Read results of the script.
Long resultCode = result.getResultCode();
// etc...
The Java code now supplies all scripts with a ScriptEarlyExit object they can use to invoke the equivalent of a return statement. Here is the ScriptEarlyExit class:
/** Object passed to all scripts so they can indicate an early exit. */
public class ScriptEarlyExit {
public void withMessage(String msg) throws ScriptEarlyExitException {
throw new ScriptEarlyExitException(msg);
}
public void noMessage() throws ScriptEarlyExitException {
throw new ScriptEarlyExitException(null);
}
}
The ScriptEarlyExitException class is a simple Exception subclass:
/** Internal exception so ScriptEarlyExit methods can exit scripts early */
public class ScriptEarlyExitException extends Exception {
public ScriptEarlyExitException(String msg) {
super(msg);
}
}
With the ScriptEarlyExit object made available to scripts by the call to rubyEngine.put("scriptExit", new ScriptEarlyExit()), any script in any language should now be able to exit early. The Ruby script revised to use the new object would be coded like:
# Don't process the text if the status is greater than 200
if $status > 200
$scriptExit.with_message 'Not processing because of invalid status'
end
# Continue processing
...
The Java method call from the script provides a consistent, fairly clean way to return early from script processing. I tested calling this ScriptEarlyExit object from Ruby using JRuby 1.0, from JavaScript using the Rhino interpreter built into Sun's Java 1.6, and from Groovy 1.0. It worked well with them all.

This solution did require solving another problem. Using a Java exception to end script processing means the script engine is going to bubble up a javax.script.ScriptException back to Java. I needed a way to determine whether that exception was a real ScriptException or my fake ScriptEarlyExitException.

The solution was to check the script exception message to see if my special exception was embedded in the string. The coded ended up looking like:
try {
rubyEngine.eval(rubyScript);
} catch (ScriptException se) {
// Re-throw exception unless it's our early-exit exception.
if (se.getMessage() == null ||
!se.getMessage().contains("ScriptEarlyExitException")
) {
throw se; // a real ScriptException
}
// Set script result message if early-exit exception embedded.
// Will not work with Java 6's included JavaScript engine.
Throwable t = se.getCause();
while (t != null) {
if (t instanceof ScriptEarlyExitException) {
result.setExitMessage(t.getMessage());
break;
}
t = t.getCause();
}
}
The catch block examines the exception's message for the "ScriptEarlyExitException" string, and ignores the ScriptException if found. The code in the catch block then looks to see if one of the causes of the ScriptException was the ScriptEarlyExitException. If so, the ScriptEarlyExitException exception's message string will hold the value set when the script called the withMessage method on the shared ScriptEarlyExit object. That is, when Ruby calls:
$scriptExit.with_message 'Not processing because of invalid status'
the
ScriptEarlyExitException.getMessage()
will contain the string "Not processing because of invalid status". The catch clause sets that string to the ResultsObject object's exitMessage property using the code:
result.setExitMessage(t.getMessage());
As the comment in the above code indicates, retrieving the "exit" message from the Rhino JavaScript engine doesn't work. Or at least finding and parsing the exit string out of the resulting ScriptException is more tedious. That's because the Rhino script engine does not wrap caught Java exceptions into the resulting stack trace. With Rhino, the loop:
Throwable t = se.getCause();
while (t != null) {
if (t instanceof ScriptEarlyExitException) {
result.setExitMessage(t.getMessage());
break;
}
t = t.getCause();
}
never finds a ScriptEarlyExitException.

As I mentioned, this solution of having scripts call a method on a shared Java object in order to exit script processing early by throwing an exception isn't elegant. But it does work to let scripts execute the equivalent of a top-level "return" statement. This solution likely will work with other JSR-223 scripting engines besides the ones I tested. It seems, though, that there must be a better way. Groovy, by the way, permits a return statement in top-level code. That's pretty nice.

Are you a Ruby or JavaScript pro with a better solution? Is there an easier way for Ruby or JavaScript to return from a script even when the script code is outside a method/function? If you would like to share better techniques, please post a comment here or email me at the address shown in the right-hand column under the "Feedback" heading. If you post a comment on this blog, I ask your forgiveness in that comments are moderated before appearing, but there is no indication of that when you click the "Post" button.