Scripting power saves the day for your Java apps

Improve your user's satisfaction with support for user-defined scripting in your Java applications

The developers of today's software products must face some hard realities. There is increasing demand for new features from users, and increasing pressure on vendors to develop products in ever-shorter timeframes. Demands for features also tend to be widely varied and unique, making it nearly impossible to satisfy all users just by adding a fixed list of new capabilities to a piece of software. A typical user will have repetitive tasks that an easy-to-create macro can automate. Moreover, some tasks are inherently complex in nature, and in such a situation, wizard-like guidance in simplifying the task can be helpful.

Most applications today, including Java applications, attempt to satisfy user needs by adding newer functionality to successive versions. With this approach, users are forced to wait for the next product release for their needs to be met -- while their customization needs remain unfulfilled even with newer versions.

If a software tool could allow users to interact with it through user-written scripts, the value of the tool would improve tremendously. Using scripts, users could ease their routine tasks by writing macros, by creating wizard-like functionality to ease complex operations, and by modifying the behavior of the tool to suit their needs. How many Java applications provide such support?

Emacs is a good example of an application that offers rich support for user scripting. Emacs' approach is to provide services that offer users the ability to custom-create the functionality they need, rather than cramming new features into the tool itself. For example, the Emacs core does not offer Java application compiling/debugging functionality, but through the use of scripts, one can transform Emacs into a powerful IDE. Similarly, scripts can configure Emacs to function as a news reader, email client, and so on. Other good examples of script-supporting applications include such Microsoft products as Microsoft Office and Microsoft Visual Studio.

These applications provide rich services and support for user scripting. By providing hooks into an application, we can transform it into an environment. User scripts use service provided by such an environment to write their applications.

In this article, we'll look at a way to infuse applications with scripting support. For that purpose, we'll first write a simple application without scripting support, then incorporate such support into it. This two-step approach demonstrates how to add scripting support to (relatively!) long-standing Java applications. We will also develop a framework for scripting support, which makes support for a new scripting language, or change to a language's interpreter, a simple task.

An application without scripting support

First, let's develop a simple application designed without a scripting extension in mind. For this purpose, we'll develop a very simple drawing application that can draw shapes on a drawing surface -- a trimmed-down version of a typical CAD tool. And since there's no point to using object-oriented design and Java if we can't reuse existing components, we're going to use the slate component developed in "Java Tip 75: Use nested classes for better organization" as the basic building block for our application.

In this section, you will find no reference to scripting support. Remember, this is done deliberately in order to demonstrate that you can add scripting support to an application designed without any prior consideration for scripting.

The application has a frame containing a drawing surface and a menu bar. The menu bar has options for clearing the drawing surface and switching between various shape modes. The drawing surface provides a simple mouse interface; a new shape object is inserted into the drawing area that is bounded points at which mouse is pressed and released. It is not the most intuitive user interface, but a more advanced UI would have shifted the focus away from the main goal of this article -- to demonstrate the incorporation of the scripting facility.

SlateApp, the application's main class, sets up a JFrame with a menu bar and slate components in it. SlateAppMenuBar, the menu bar for the application, extends JMenuBar, adding a few simple menus for clearing the drawing surface and switching between modes to draw various shapes.

(Source code for these first versions of the SlateApp.java and SlateAppMenuBar.java classes is available to download.)

This completes the first step. Now we have an application with no scripting support. This application lets users create simple pictures by drawing rectangles, ovals, and lines via a mouse interface. It also allows the whole drawing surface to be erased. While this application is useful for simple drawings, it is not adequate for creating complex drawings.

Overview of embedding scripting support

Increasing interest in Java's "write once, run anywhere" promise has made it the language of choice for many new scripting-language interpreters. Most of these interpreters allow interaction with Java objects by the scripts. If we embed such interpreters in our Java application, scripts can interact with the application's objects. Thus, to enable scripting support for Java applications, we need to embed the interpreter so that it either runs in same virtual machine as the application, or can interact with the application's objects in some other fashion. We also need to make the application's objects available to the interpreter.

Scripting languages vary widely in their capabilities and suitability for particular tasks. In addition, users frequently prefer one scripting language over another -- many times due to nontechnical reasons. As such, applications should not force users to learn a new scripting language; rather, they should support multiple languages. This enables users to write their scripts in any of the supported languages, which results in higher user satisfaction. Keep in mind, though, that while such support for multiple language is a desirable goal, it is also desirable that such support should require a minimal per-language development effort. A well-designed scripting framework can help achieve both of these goals.

For the purpose of this article, I have chosen to provide support for four popular languages: JavaScript (ECMAScript), Python, Tcl, and Scheme. JavaScript, with its Java-like flavor, is popular for dynamic Web page creation. It also supports some object-oriented features, which makes interacting with Java more natural. Python is another popular object-oriented scripting language that is easy to learn. Tcl is a scripting language designed specifically to provide scripting extensions to applications. Scheme, a compact and elegant dialect of LISP, supports functional programming concepts.

I have chosen FESI (Free EcmaScript Interpreter) as the JavaScript interpreter, JPython as the Python interpreter, Jacl as the Tcl interpreter, and Skij as the Scheme interpreter. Each of these interpreters is written in 100% Pure Java. Each also supports natural Java object interaction for the scripting language it supports. As an added bonus, all four are available for free (though you should, of course, check license agreements).

None of the core definitions of these languages support interaction with Java objects; most of them existed long before Java was born. These interpreters, therefore, amend language specifications to let scripts create new Java objects and call methods on Java classes and objects. Users already familiar with these scripting languages should find these simple extensions easy to learn.

Scripting framework

Let's establish some design goals for our project. Our scripting framework should:

  • Allow for easy addition of new scripting languages and do so without any impact on the rest of the system

  • Allow for the scripting language interpreters to be changed as needed

We design the scripting framework based on the JDBC driver-manager framework. In the same way that the JDBC framework allows support for multiple databases and drivers, our scripting framework allows support for multiple scripting languages with a minimal per-language effort. To add support for a new scripting language, the developer just needs to write a driver for that language and load it in the application. As an added benefit, our framework also isolates changes made to the script interpreter from the rest of the system.

The framework consists of an InterpreterDriver interface implemented by each driver class for our supported scripting languages, and an InterpreterDriverManager class that manages all such drivers. We'll look at the InterpreterDriverManager first.

InterpreterDriverManager

The InterpreterDriverManager isolates the core application from the scripting interpreters and allows applications to support multiple scripting languages with no per-language development effort. In addition, InterpreterDriverManager is responsible for managing drivers and delegating script execution to the appropriate driver.

Under our framework, each driver, upon loading, must register an instance of itself with InterpreterDriverManager. Upon registration, the InterpreterDriverManager queries the driver for its supported languages and script extensions. When the InterpreterDriverManager receives a request to execute a script file, it first finds an appropriate driver by looking for the required extension, then delegates the execution. For executing script strings, the caller must specify the language necessary to interpret it.

(To view the source code for InterpreterDriverManager.java, click here.)

InterpreterDriver

InterpreterDriver provides a common interface between the underlying language interpreter and the rest of the system. Each implementing class implements the exceuteScript() and executeScriptFile() methods by delegating them to the underlying language interpreter. Methods getSupportedExtension() and getSupportedLangauges() declares the file extensions and languages supported by the driver. This information is used by InterpreterDriverManager to find the appropriate driver for delegating the execution of the script.

// InterpreterDriver.java
package scripting;
public interface InterpreterDriver  {
    public void executeScript(String script)
        throws InterpreterDriver.InterpreterException;
    public void executeScriptFile(String scriptFile)
        throws InterpreterDriver.InterpreterException;
    public String[] getSupportedExtensions();
    public String[] getSupportedLanguages();
    public static class InterpreterException extends Exception {
        private Exception _underlyingException;
        public InterpreterException(Exception ex) {
            _underlyingException = ex;
        }
        public String toString() {
            return "InterpreterException: underlying exception: " 
                + _underlyingException;
        }
    }
}

An example InterpreterDriver: JPythonInterpreterDriver

Here, we examine in detail an implementation of JPythonInterpreterDriver, an interpreter driver for Python, which uses JPython as its interpreter engine:

// JPythonInterpreterDriver.java
package scripting;
import java.io.*;
import org.python.util.PythonInterpreter; 
import org.python.core.*; 
public class JPythonInterpreterDriver implements InterpreterDriver {
    private static JPythonInterpreterDriver _instance;
    private PythonInterpreter _interpreter = new PythonInterpreter();
    static {
        _instance = new JPythonInterpreterDriver();
        InterpreterDriverManager.registerDriver(_instance);
    }
    public void executeScript(String script)
        throws InterpreterDriver.InterpreterException {
        try {
            _interpreter.exec(script);
        } catch (PyException ex) {
            throw new InterpreterDriver.InterpreterException(ex);
        }
     }
    public void executeScriptFile(String scriptFile) 
        throws InterpreterDriver.InterpreterException {
        try {
            _interpreter.execfile(scriptFile);
        } catch (PyException ex) {
            throw new InterpreterDriver.InterpreterException(ex);
        }
    }
    public String[] getSupportedExtensions() {
        return new String[]{"py"};
    }
    
    public String[] getSupportedLanguages() {
        return new String[]{"Python", "JPython"};
    }
    public static void main(String[] args) {
        try {
            _instance.executeScript("print \"Hello\"");
            _instance.executeScriptFile("test.py");
        } catch (Exception ex) {
            System.out.println(ex);
        }
    }
}

The JPythonInterpreterDriver's static initialization block creates an instance of itself and registers that instance with InterpreterDriverManager. The methods executeScript() and executeScriptFile() simply delegate to the underlying JPython interpreter the tasks of executing the script string and the script file, respectively. Methods getSupportedExtension() and getSupportedLangauges() return an array of strings containing the file extensions (in this case, .py) and languages (in this case, Python and JPython) supported by this driver. The main() method helps with unit testing of this class by exercising basic functionality.

You can download FESIInterpreterDriver.java (JavaScript/ECMAScript), JaclInterpreterDriver.java (Tcl), and SkijInterpreterDriver.java (Scheme), which provide complete implementation of drivers for the other supported scripting languages.

If you need to support a scripting language other than those listed, it is easy to write your own driver, assuming that a Java interpreter for that language is available and that it supports Java class and object interaction.

Embedding scripting support in an application

Now that we have a scripting framework and a application that does not support scripts, we can incorporate the scripting support into the application. We set the goal that with scripts the user should be able to:

  • Add new menus to the application and set scripts to be invoked upon activating such menus

  • Add any shape to the slate

  • Set scripts for applications to call when important events, such as application start and application termination, occur

To be able to add menus, scripts will have to access the menu bar. Similarly, to be able to add new shapes to the slate, scripts must be able to access the model for that object. To reach either of these objects, we provide access to the top-level application instance by adding the static method getInstance() in SlateApp. We also add methods getMenuBar() and getSlate() to access the menu bar and the slate component, respectively. User scripts can access the object-containment hierarchy by traversing through the application object. The user scripts can, for example, get the model for the slate by first obtaining the slate object and then calling the getModel() method on it. This satisfies our first two goals: we now have exposure to the menu and the model subsystems.

We achieve our third goal through the use of a property file. This file maps events that an application calls to the script file. For this application, we define only two events: application start and application termination. The property file also defines a property script.drivers, which is a colon-separated driver list. By doing so, the property file defines the scripting languages that the application supports. Users can modify the property file to add or remove support for scripting languages.

# script.prop
# Drivers for scripting support
script.drivers=scripting.FESIInterpreterDriver:
  scripting.JPythonInterpreterDriver:scripting.JaclInterpreterDriver:
  scripting.SkijInterpreterDriver
# Mapping of application events to script files
event.start=scripts/js/setupMenus.js
event.end=scripts/js/sayGoodBye.js

As a convenience class, we provide ExecuteScriptAction (which indirectly extends javax.swing.Action) to simplify the task of setting buttons and menus to invoke specified scripts on activation. We also modify SlateAppMenuBar by including convenience methods for adding menus that execute specified script files.

(You can download source code for ExecuteScriptAction.java and our second version of SlateAppMenuBar.java.)

Example scripts

Now, let's examine simple scripts we can write for this application.

setupMenus.js

setupMenus.js inserts menus and sets the scripts to be invoked when a user activates a menu.

// setupMenus.js
var theApp = Packages.version2.SlateApp.getInstance();
var menuBar = theApp.getMenuBar();
menuBar.addInvokeScriptMenu("Clear All [JavaScript]", "scripts/js/clearAll.js");
menuBar.addInvokeScriptMenu("Add Circles [JavaScript]", "scripts/js/addCircles.js");
menuBar.addInvokeScriptMenu("Add Rects [JavaScript]", "scripts/js/addRects.js");
menuBar.addInvokeScriptMenu("Add Smily [JavaScript]", "scripts/js/addSmily.js");

setupMenus.js first obtains reference to the application object. From that reference, it obtains the application's menu bar, then invokes the addInvokeScriptMenu() method on the menu bar to add menus and scripts. For example, it adds a menu with the name "Clear All [JavaScript]" and sets it to invoke the scripts/js/clearAll.js script.

addRectangles.js

Now we'll examine a JavaScript that adds several different-sized rectangles to the slate.

To add a shape object to the drawing surface, addRectangles.js employs the getModel() method to obtain the slate's model, then adds several Rectangle objects to the model:

// addRectangles.js
var theApp = Packages.version2.SlateApp.getInstance();
var slateModel = theApp.getSlate().getModel();
for (var i = 1; i < 20; i++) {
        var tmp = i * 20;
        var rect = new java.awt.Rectangle(tmp, tmp, tmp, tmp);
        slateModel.addShape(rect);
}

addRectangles.py

Here's a Python script that performs the same job:

# addRectangles.py
from version2 import SlateApp
from java.awt import Rectangle
theApp = SlateApp.getInstance()
slateModel = theApp.getSlate().getModel()
for i in range(1, 20):
        tmp = i * 20
        rect = Rectangle(tmp, tmp, tmp, tmp)
        slateModel.addShape(rect)

More example scripts

Table 1 below provides additional example scripts.

Table 1. Example scripts in various scripting languages
Script functionJavaScriptPythonTclScheme

Set up an menu named

"Invoke" that adds a

complex set of shapes

setupMenus.jssetupMenus.pysetupMenus.tclsetupMenus.scm
Say goodbye!sayGoodBye.jssayGoodBye.pysayGoodBye.tclsayGoodBye.scm
Add several rectanglesaddRectangles.jsaddRectangles.pyaddRectangles.tcladdRectangles.scm
Add several circlesaddCircles.jsaddCircles.pyaddCircles.tcladdCircles.scm
Add a smiling faceaddSmily.jsaddSmily.pyaddSmily.tcladdSmily.scm

The pros and cons thus far

The suggested method for adding scripting support outlined above has several advantages:

  • No special work is needed beyond adding access to the top-level application object and its subsystems (getMenuBar(), getSlate(), and so on).

  • The application is fully customizable to the extent that the user script can reach the subsystem.

  • User scripts can immediately leverage, without changes in the application class, the addition of new facilities to subsystems.

However, it suffers from drawbacks as well:

  • The user has to learn a complex API for each subsystem and learn how to correctly access it. Note that the product architect designed the APIs for application developers -- not for script developers.

  • The user must satisfy pre- and postconditions before and after invoking methods from scripts. For example, the user script must add a menu bar before a menu item is added.

  • The user scripts have unlimited access to the entire application. Starting at the top-level application object, the user can reach any object in the application by traversing through the object tree. Poorly written scripts may compromise the integrity of the entire system.

  • Any change in interface or subsystem semantics can potentially break the user scripts. Therefore, changes in the API for each exposed subsystem will have to be restricted from revision to revision.

The Facade pattern to the rescue

We can overcome these drawbacks through use of the Facade design pattern. According to Erich Gamma and his coauthors in Design Patterns: Elements of Reusable Object-Oriented Software, Facade is designed to "provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use." (See Resources for more information.) We use the Facade pattern to restrict access and provide a simpler API to the application. The implementation for the Facade can also provide backward compatibility by absorbing the changes in subsystems and providing the necessary bridge.

Be aware, however, that some applications strive to fulfill the needs of power users. In those cases, the use of the Facade pattern may not be necessary or desirable, as its use will restrict the ability of user scripts to have complex interactions with the system.

A useful Facade interface for scripting purposes should never expose any subsystem. To accomplish this, the Facade methods should be of the form <do><Something>, and not get<Something>.

Therefore, we introduce a nested interface Facade in SlateApp for this purpose:

public static interface Facade  {
    public void addShape(Shape s);
    public void removeShape(Shape s);
    public void removeAllShapes();
    public void addInvokeScriptMenu(String name, String scriptFile);
    public void addInvokeScriptSeparatorMenu();
}

Next, we implement this interface as an inner class of SlateApp -- SlateAppFacadeImpl:

private class SlateAppFacadeImpl implements SlateApp.Facade {
    public void addShape(Shape s) {
        _slate.getModel().addShape(s);
    }
        
    public void removeShape(Shape s) {
        _slate.getModel().removeShape(s);
    }
        
    public void removeAllShapes() {
        _slate.getModel().removeAllShapes();
    }
        
    public void addInvokeScriptMenu(String name, String scriptFile) {
        _menuBar.addInvokeScriptMenu(name, scriptFile);
    }
    public void addInvokeScriptSeparatorMenu() {
        _menuBar.addInvokeScriptSeparatorMenu();
    }
}

Finally, we modify the method getInstance() to return a SlateAppFacadeImpl instance instead of SlateApp itself. This removes the direct access to the subsystem and provides access to the Facade instead.

(The third version of SlateApp.java, which includes the Facade interface, is available to download along with its implementation.)

Example scripts for the Facade-enabled application

Now, let's examine example scripts that use the Facade interface provided by the application.

setupMenus.js

The example setupMenus.js below performs same function as its earlier version; the modifications are due to the way the application allows scripts to interact with it. Notice that the scripts are simpler, perform all the operations on the application object itself, and do not need to traverse the object containment hierarchy:

// setupMenus.js
var app = Packages.version3.SlateApp.getInstance();
app.addInvokeScriptMenu("clearAll [JavaScript]", "scripts/js/clearAll.js");
app.addInvokeScriptMenu("addCircles [JavaScript]", "scripts/js/addCircles.js");
app.addInvokeScriptMenu("addRects [JavaScript]", "scripts/js/addRects.js");
app.addInvokeScriptMenu("addSmily [JavaScript]", "scripts/js/addSmily.js");

addRectangles.js

Below, addRectangles.js also performs the same function as its predecessor. It adds several rectangles to the slate:

// addRectangles.js
var theApp = Packages.version3.SlateApp.getInstance();
for (var i = 1; i < 20; i++) {
        var tmp = i*20;
        var rect = new java.awt.Rectangle(tmp, tmp, tmp, tmp);
        theApp.addShape(rect);
}

More example scripts for the Facade-enabled application

Table 2 includes functionally similar scripts to the corresponding earlier versions, but with the modifications according to the Facade interface provided by the application:

Table 2. Example scripts for the Facade-enabled application in various scripting languages
Script functionJavaScriptPythonTclScheme

Set up a menu called

"Invoke" that adds

complex set of shapes

setupMenus.jssetupMenus.pysetupMenus.tclsetupMenus.scm
Say goodbye!sayGoodBye.jssayGoodBye.pysayGoodBye.tclsayGoodBye.scm
Add several rectanglesaddRectangles.jsaddRectangles.pyaddRectangles.tcladdRectangles.scm
Add several circlesaddCircles.jsaddCircles.pyaddCircles.tcladdCircles.scm
Add a smiling faceaddSmily.jsaddSmily.pyaddSmily.tcladdSmily.scm

Middle ground: the multi-Facade approach

Adding a single Facade interface to the system takes away the advantages offered by directly exposing the object containment hierarchy. The Facade interface essentially flattens the system, which, for a complex application, results in a Facade interface with a large number of methods. Scripts written for such applications will be difficult to understand.

Rather than expose the whole system through a single Facade interface, we can provide a better scripting interface by exposing each important subsystem via its own Facade interface and relating these interfaces to each other through object composition. This multi-Facade approach creates an alternate and simpler object-containment hierarchy well suited to script developers. With this approach, as long as the Facades provide the correct implementation of the exposed methods, the implementation in the main object-containment hierarchy can change between tools versions without affecting the scripts.

Steps to add effective scripting support to your Java application

Finally, let's look at how to systematically add scripting support to your Java applications:

  • Write down scenarios detailing where a user might want to write scripts to interact with the tool.

  • Working from the scenarios, create a single Facade that will allow the user to implement the required functionality.

  • If the number of methods in the Facade interface is too large, consider the multi-Facade approach. To do so, first break the single interface into multiple interfaces, each representing a subsystem. Then create a top-level Facade that exposes these subsystem Facades. You can recursively apply this step to the Facade for each subsystem.

  • Implement each of the Facade interfaces.
  • From the scenarios, decide the events to which the user will need to attach scripting hooks. Decide on a property key for each such event.
  • For each event with a scripting hook, implement invocation of user-specified scripts from the event handler.

By following these steps, you will enable the minimum desired scripting support to the application.

Conclusion

In this article, we examined a method to enable support for user scripting in Java applications. Moreover, we looked at a simple scripting framework to support multiple scripting languages with a minimal per-language development effort. Through an example, we discovered that adding scripting support to legacy Java applications is easy if we use the presented framework.

In addition, we learned how to employ the Facade design pattern to limit the access to the application and promote easier script writing. We also discovered how the multi-Facade approach helps find a middle ground between using a single Facade for the entire application and directly exposing the whole object-containment hierarchy. Finally, we walked through the steps necessary to incorporate effective scripting support in your Java applications.

Java applications have long suffered because they lack the ease-of-use functionality found in many native applications. With the information presented in this article, we have the power to improve the quality of Java tools.

Ramnivas Laddad is a Sun Certified Architect of Java Technology (Java 2). He has a master's degree in electrical engineering, with a specialization in communication engineering. He has six years of experience designing and developing several software projects involving GUIs, networking, and distributed systems. He has developed object-oriented software systems in Java for the last two years and in C++ for the last five years. Ramnivas currently works at Real-Time Innovations Inc. as a software engineer. At RTI, he is presently working to design and develop ControlShell, a component-based programming framework for building complex real-time systems.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more