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.