Improve script support with Bean Scripting Framework

Want to support all the scripting languages for Java? The Bean Scripting Framework, BSF, enables you to do just that with a simple API. We'll show you how in this article.

Want to support all the scripting languages for Java? The Bean Scripting Framework, BSF, enables you to do just that with a simple API. We'll show you how in this article.

Putting scripting support into your Java applications is a powerful tool for prototyping and testing and a way to enable your application to be extensible later in its life cycle. The big question is, which scripting language do you incorporate?

There are the scripting languages that have developed separately from Java and now have Java implementations like Jython (Python), Rhino (ECMAScript), JRuby (Ruby) and Jacl (TCL); these are good if you want to allow people to use their existing skill sets. Then there are the Java implementation-only languages like Groovy, developed with the JVM in mind. And then there are the Java-like scripting languages such as BeanShell, which can actually take Java source code and run it. So many languages, lots of different capabilities, and a big choice to make.

Support all
The solution? Don't make the choice, support them all. The Bean Scripting Framework, BSF, enables you to do just that with a simple API which lets you plug in as many languages as you can handle. The BSF has been around for quite a while; originating within IBM's Alphaworks, it was been donated by IBM to the Apache Jakarta project in 2002, where it is currently maintained at version 2.3.

At the time of writing, JSR223, "Scripting for the Java platform", based on BSF work, is going into its final public review and vote for incorporation into the Java platform. That's in the future though. If you want to get going with scripting now, you'll want to use BSF 2.3; you'll find it at the Jakarta web site (http://jakarta.apache.org/bsf). Unfortunately it has patchy documentation and a resources section with dead links, so we suggest that you download the source code and build the javadoc files using Ant as well, if you want the BSF API documents.

The BSF comes as a single jar file that you'll need to add to your classpath; it doesn't include any languages, but it does support Rhino, Jython, Jacl, NetRexx by default, and BeanShell, JRuby, JudoScript, Groovy and ObjectScript all come with BSF support. You'll need to download whichever languages you're interested in, and add their jar files to your classpath as well. For the example here, you'll need Jython and Groovy.

Once you've assembled your classpath, you can get down to coding. The first thing you need is a BSFManager:

BSFManager bsfmanager=new BSFManager();

The BSFManager is your gateway to the script engines, BSFEngines, which do the work of interfacing the script language to the framework. Each language will have its own BSFEngine which is registered with the BSFManager and BSF comes with engines for the supported set previously mentioned, and registers them automatically.

There are two ways to run a script, you can eval() or exec() it. Eval() returns a result from a script.

try
{
Object r=bsfmanager.eval("jython","Test",0,0,"6*9");
System.out.println(r.getClass().getName()+" "+r);
Integer i=new Integer(r.toString());
}
catch(BSFException bsfe) {}


The first parameter is the name of the language. The second is a 'source' label; for scripts in files this would typically be the filename, in this case though we are evaluating a literal string so we use a dummy value. The next two integers are row and column values to help when the script may generate an error. Finally, we get to the script which in this case is a trivial calculation. Eval() and Exec() both throw BSFException when something goes awry in the script so the code is wrapped with a try…catch block. Running just that code would output

org.python.core.PyInteger 54

You have now run a script. Notice that the returned value in this case is a PyInteger instance; to get it back to an Integer, treat it as a String as the example does.

Exec() is like Eval() except that it doesn't return a value. You may wonder then, how your Java code interacts with the script. What you have to do is make your Java objects visible within the script. BSFManager takes care of this with two methods, registerBean and declareBean. Despite the use of the word Bean, you can pass any Java object to the script.

Targets targets=new Targets();
bsfmanager.registerBean("targets",targets);


This allows a script to lookup an object by asking for it by name; for example in Jython,

bsf.lookupBean("targets")

If you don't want to look up instances, you can use declareBean(). declareBean adds the object to the lookup table, but also tries to create a variable automatically in any script that is invoked.

bsfmanager.declareBean("targets",targets,Target.class);

This means that you have to be more careful naming it to avoid namespace clashes, but it does make for less apparently crufty lookups in the scripts.

The next step
Having got scripts executing in a basic way, the next issue is making the code not rely on having literal strings for scripts and hardwired language names. Let's assume we have scripts in files. This lets the BSFManager tell us, given a filename, what language should be used.

String scriptlanguage=bsfmanager.getLangFromFilename(scriptfilename);

You need to load the actual contents of the file into a string to execute it. This may seem like a strange hoop to jump through, but consider if the scripts you want to load are stored in a database, referenced by filename or if you have implemented a script cache. It's more generically useful for the BSF to work with Strings for scripts and let the programmer manage actually loading the scripts. In the example code, there's a getStringFromFile method which takes care of that so:

String scriptcode=getStringFromFile(scriptfilename);

takes care of that. Finally, we want to execute the code:

bsfmanager.exec(scriptlanguage,scriptfilename,0,0,scriptcode);

And now you can hand code in any supported script language and execute it. You can also mix scripting languages within the same application; there's no reason why not. You may want to ensure though that a particular script is run in a particular language and check it is available at runtime; the BSFManager method isLanguageRegistered can tell you if a named language is available.

For languages not directly supported by BSF, you will need to register them yourself; for Groovy support, you'd add

bsfmanager.registerScriptingEngine("groovy",
"org.codehaus.groovy.bsf.GroovyEngine",
new String[] { "groovy", "gy" });


which tells the BSFManager what to call the language, where the class for the BSFEngine for that language is, and what file name extensions denote a script in that language. These are used by the BSFManager's getLangFromFilename method which we use determine which language, and in turn what BSFEngine a script needs to be executed with.

In the supporting code for this article, you'll find MixedScripts. This is a prototype seed, simplified; the original problem was that a client needed to have a process developed in Java, but was unable to specify the systems they were going to integrate into; they had test data, but nothing more, and deadlines required the Java code be delivered before the systems were ready. The client had administrators capable of writing or modifying scripts that could do the integration later on. So the solution was to wrap the processing element with scripts, which would manage the acquisition of data for the process and output of results. By using scripting, the scripts can be changed without recompiling the core application, which makes the application amenable to being updated or modified in the field, or by another developer or company without exposing your source and without imposing a requirement! to code in Java on them.

For this simplified example, the idea is that there is a script to acquire a list of URLs, that list of URLs is processed by hardwired code, and the results are handed over to a second script for output. For this example, the URL processing is just retrieving the content at the URL has been given.

Applying scripting
Why the mix of native Java code and scripting? Why not implement the entire thing in a scripting language? The thing to bear in mind is that scripting languages have variable performance but will as a rule of thumb be slower than native Java code. This difference is down to the overheads like script compilation/interpretation and the underlying implementation of the script language.

So you do have to be selective over where you apply scripting. This example assumes that you want to wrap an existing Java code base with scripting to just manage the input and output phases. You could develop entirely in a scripting language and during development promote performance hungry code to native Java code.

The code takes the names of two scripts as its command line arguments. The first script is handed a targets object for it to populate. The second script is given a results object to work on and has access to the original targets object. You can run it from the command line with

java -jar MixedScripts.jar acquire.py output.gy

Acquire.py is just a simple Jython script which adds one target, output.gy is a Groovy script which just dumps the results, purely for testing purposes. This may sound like a lot of hoops to jump through to set a variable and print a hashmap, but to change the acquisition element, you only need to change the script. Acquire1.py is an acquisition script which reads from a file.

Although these example scripts are simple, the real implementation that the MixedScripts example is based on ended up getting its input data as the result of a XMLRPC call and emailed the output as XML formatted attachments. Scripting is an enabling technology not a panacea for all ills, but used judiciously in your development cycle, it can give that extra element of flexibility and the Bean Scripting Framework frees you from worrying about writing code to interface with each and every script language out there.

Download the source code for this tutorial at http://www.builderau.com.au/resources/MixedScripts.zip.

DJ Walker-Morgan is a consulting developer, specialising in Java and user-to-user messaging and conferencing. This article was first published in Builder AU