Scripting power saves the day for your Java apps

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

1 2 3 Page 2
Page 2 of 3

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.

1 2 3 Page 2
Page 2 of 3