Channels ▼
RSS

JVM Languages

Scripting Java Applications with Sleep

Source Code Accompanies This Article. Download It Now.


July, 2005: Scripting Java Applications with Sleep

Raphael lives with his Apple Powerbook in Kaiserslautern, Germany. He spends his time exploring Europe, cooking, and hacking code. Raphael can contacted at http://www.hick.org/~raffi/.


Sleep is a freely available library for adding scripting services to Java applications (http://sleep.hick.org/). The Sleep library provides a parser, interpreter, and default library. Embedding Sleep consists of extending the language to include functions, variables, and other features to access an application's functionality. Scripters can then use these additions to manipulate the application from a script.

Originally, Sleep was an experiment to play with some concepts I was learning at the time. Since then, Sleep has evolved into a library for adding scripting services to Java applications. This evolution came out of my need for a heavily extendable language solution for jIRCii, an Internet relay chat client I've been developing for a few years now. jIRCii uses Sleep to let users respond to events, define shortcut aliases, bind keyboard shortcuts, and script their own menus. In short, jIRCii is a very complete example of what Sleep can do.

Sleep's syntax was heavily influenced by Perl. The default library includes most of the functionality you would expect from a more general-purpose scripting language. This includes string manipulation functions, regular expressions, and an I/O framework that handles processes, sockets, and files with the same interface.

Competition for Diebold: The Vote Application

To illustrate how you can use Sleep, I present here a simple application called "VoteApp" that represents the next generation of voting applications. It is a simple Swing application that displays three candidates and lets voters select a candidate and click Vote. VoteApp keeps track of the total number of votes each candidate has received. Each time a Vote is cast, a little message is displayed at the bottom of the window; see Figure 1.

Right now, VoteApp is not competitive in the gazillion dollar voting software industry because VoteApp has all of the candidate values hardcoded. To make VoteApp competitive, it needs to be customizable, and Sleep lets you do this.

A Sample Vote Application Script

The first step to embedding Sleep is to decide what you want the scripters to be able to do.

One technique is to write a mock-up script before developing the scripting interface. When creating mock-up scripts, I use functions, keywords, and variables that do not exist yet. It helps to write mock-up scripts that add features users might add. This gives you an idea of what users may want from the scripting interface.

In the case of VoteApp, I want scripters to be able to define candidates and the applications response when a candidate receives a vote. Listings One, Two, and Three are a mock-up of vote.sl, the Sleep script that will drive the VoteApp application. This Sleep script adds three candidates George, Howard, and Raphael, respectively. Each time a candidate gets a vote, the status line is updated with the candidate's current status in the race. The keyword candidate is actually a bridge implemented as part of the Sleep/VoteApp integration. Sleep lets you extend the language in this way with "environment bridges." A bridge is a Java class that implements an interface specific to the type of language addition.

Environments consist of a keyword and a string identifier tied to a block of code. In lines 14-17 (see Listing One), the keyword is candidate, the string identifier is Raphael, and the block of code says to update the results for Raphael. The candidate bridge executes the block of code whenever the candidate specified as the string identifier gets a vote. Listing Two defines a subroutine to update results. Parameters passed to Sleep subroutines are available as $1 for the first parameter, $2 for the second parameter, and so on.

The if statement at line 25 (see Listing Two) queries whether the candidate $1 is winning. Predicates for use in control statements can be defined with a predicate bridge.

The functions getVotesFor("candidate"), Display("string"), and -iswinning("candidate") are tied to VoteApp. For example, Display("string") changes the text of the label at the bottom of the VoteApp window. These functions were created as part of a "function bridge."

The subroutine getWinner() defined in lines 36-47 (see Listing Three) is a scripted utility function to return the name of the currently winning candidate. The data structure with all of the vote information is the Sleep hash table %results. In Sleep, all strings, numbers, and reference variables are $scalar variables whose name begins with "$" (dollar sign). Dictionary-style data structures are Sleep hash tables. Sleep %hashes always begin with "%" (percent symbol). Sleep @array variables always begin with "@" (the at symbol). %results is a global variable tied to a java.util.HashMap from VoteApp.

The Scriptable Vote Application

The architecture for the scriptable VoteApp consists of two classes and one script file. The already existing VoteApp class contains all of the core application logic. A new class VoteScript contains all of the necessary Sleep integration code to drive the script file vote.sl; see Figure 2.

The class VoteScript sits between the Sleep interpreter and the application logic in VoteApp. When a script calls a function, Sleep finds the bridge that owns the function. The bridge then does any necessary processing, including calling APIs within the application itself. The bridge then passes a value back to the calling script. This is the architecture of VoteApp and Sleep scriptable applications in general.

The purpose of VoteScript is twofold. It acts as an interface between the VoteApp and Sleep. It also adds VoteApp-specific functionality to the Sleep language. This bridged functionality is what lets scripters manipulate VoteApp from a script file.

It is a good idea to minimize the impact of the scripting interface on the application logic. This makes it easier to change or remove the scripting interface in the future. VoteApp ties into the VoteScript class in two places. VoteApp instantiates VoteScript and calls init() on the VoteScript object in its constructor. The other point is the event listener for the Vote button.

When the Vote button is clicked, the method candidatedVotedFor(String name) in VoteScript is called. Instantiating the VoteScript class and notifying it of vote events are the only changes as far as the core VoteApp logic is concerned.

Loading and Managing Scripts

When the VoteApp is launched, it loads and runs a script called "vote.sl." Listing Four sets up a script loader, loads the vote.sl script, and executes it when the VoteScript method init() is called.

ScriptLoader is a Sleep class that manages loaded/unloaded scripts. ScriptLoader provides methods for loading a script from a stream, file, or even a string. The general pattern to load and run a script can be seen in lines 27 and 28. If the script-loading process fails, the script loader will throw an exception appropriate to the type of failure.

Installing Bridges into the Sleep Language

The Sleep script loader manages the details of loading Sleep bridges into the environment of each script. Loadable bridges are special bridges that get installed into a Sleep ScriptLoader. Whenever a script is loaded, the script loader notifies each installed Loadable bridge. The notified loadable bridge then has the opportunity to install Function bridges into the environment of the newly loaded script.

Implementing the sleep.interfaces.Loadable interface creates loadable bridges. The VoteScript class is an example of a Loadable bridge.

Loadable bridges should be installed into the script loader before any scripts are loaded. The VoteScript instance is installed into the script loader at line 24 (see Listing Five). Loadable bridges define two methods—scriptLoaded and scriptUnloaded. The scriptLoaded method is called each time a script is loaded. Usually, this method is used to install other bridges into the environment of the newly loaded script.

A script environment is nothing more than a Java Hashtable instance that references all available bridges. Line 47 (see Listing Six) installs the Display function into the script environment. Sleep expects all installed function names to begin with "&," an ampersand character. The implementation of the Display function is covered in the next section.

Adding Built-In Functions

Built-in functions are defined as classes that implement the sleep.interfaces.Function interface. Listing Seven is the implementation of the Display("string") function. Arguments are passed to function bridges as a Java Stack object containing Sleep Scalar objects. Scalars are the universal container for all Sleep data. A Scalar can be a string, a number, an object, or nothing. The BridgeUtilities class is used to safely extract Java types from the argument stack as shown in line 67.

The real meat of the Display implementation is line 68. This is where the VoteApp API is accessed to change the displayed message. The variable dialog is a reference to the VoteApp object.

The VoteScript class has an instance variable named "dialog" that is a reference to the VoteApp object. The Display bridge is implemented as a nonstatic inner class to provide easy access to VoteScript variables such as dialog.

All function bridges in Sleep have to return a Scalar. Here, I just return the empty Scalar, the Sleep equivalent of null. The SleepUtils class contains several static methods for easily wrapping Java types into Scalar objects. The implementation of the other built-in functions is very similar to the Display function.

The Candidate Keyword

A Sleep environment is nothing more than a keyword and string identifier associated with a block of Sleep code. Environments are used to add new constructs to the Sleep language.

Environment bridges implement the sleep.interfaces.Environment interface. Listing Eight is the implementation of the candidate environment. The nonstatic inner class SetupCandidate defines a function Environment bridge. Line 104 calls the VoteApp API to add the identifier funcName as a candidate in the VoteApp. The ready-to-execute function body is stored in a hash map called candidates.

The VoteApp calls the candidateVotedFor(String name) method in VoteScript when a candidate receives a vote. This method can be used to execute the appropriate function body for a candidate when they receive a vote.

Listing Nine retrieves the block of code associated with the candidate that was voted for. In line 61, this code is executed. That is all there is to making a script capable of reacting to a vote. Use Listing Ten to install the candidate environment within a loadable bridge.

Adding a Predicate

The VoteApp scripting interface calls for a -iswinning predicate. Predicates in Sleep are used within if statements, while/for loops, and so on. The unary predicate -iswinning determines if the specified candidate is winning or not. All unary predicates in Sleep begin with the "-" (dash) character. Predicate bridges are created by implementing the sleep.interfaces.Predicate interface; see Listing Eleven.

Use Listing Twelve to install a predicate bridge into a script environment.

Wrapping Data Structures

The SleepUtils class has several static methods for wrapping Java objects into Sleep Scalar objects. SleepUtils also contains methods for wrapping Java data structures that implement the Map or Collection interface.

VoteApp keeps track of all votes with a Java HashMap data structure. Listing Thirteen makes this data structure available to scripters as a Sleep hash %results.

One caveat about wrapping Map or Collection data structures with SleepUtils is that they are read only. Data accessed from wrapped data structures is turned into a Scalar string by default. Wrapping %hash and @array data structures is a quick-and-dirty way to make application data available to scripters.

If you'd like flexibility beyond what wrapping provides, you can write your own implementation of Sleep $scalar, @array, or %hash containers.

Where To Go From Here

If you look at the VoteScript class, you may notice a pattern. The VoteScript class is a Loadable bridge that contains a reference to an object with methods/data I want scripters to be able to use. Bridges installed by the VoteScript class are defined as nonstatic inner classes. These inner classes all have access to the object with the API they are bridging. This pattern is what I have used in my application jIRCii and it has served me well so far.

The simple 100-line vote application (available electronically; see "Resource Center," page 3, for both Windows and UNIX) is a good example of how easy it is to start creating a flexible scriptable interface for Java applications. Writing scripts for an application is a lot of fun, especially when the interface is simple and empowering.

DDJ



Listing One

(vote.sl - VoteApp Sleep Script)

3   candidate "George"
4   {
5      addVoteFor("Raphael");    # guess who will win?
6      updateResults("George");
7   }
8   
9   candidate "Howard"
10  {
11     updateResults("Howard");
12  }
13  
14  candidate "Raphael"
15  {
16     updateResults("Raphael");
17  }
Back to article


Listing Two
(vote.sl - continued)

21  sub updateResults
22  {
23     $votes = getVotesFor($1);
24  
25     if (-iswinning $1)
26     {
27        Display("Your candidate is winning with $votes vote(s)");
28     }
29     else
30     {
31        $winner = getWinner();
32        Display("$1 is losing to $winner with $votes vote(s)");
33     }
34  }
Back to article


Listing Three
(vote.sl - continued)

36  sub getWinner
37  {
38     foreach $candidate (keys(%results))
39     {
40        if (-iswinning $candidate)
41        {
42           return $candidate;
43        }      
44     }
45  
46     return $null;
47  }
Back to article


Listing Four
(VoteScript.java)

8   public class VoteScript implements Loadable
9   { 
10     private VoteApp        dialog;
11     private ScriptLoader   loader;
12     private ScriptInstance script;
13     private HashMap        candidates = new HashMap();
  ...
20     public void init()
21     {
22        loader = new ScriptLoader();
23        loader.addGlobalBridge(this);
24  
25        try
26        {
27           script = loader.loadScript("vote.sl");
28           script.runScript();
29        }
30        catch (Exception ex)
31        {
Back to article


Listing Five
(VoteScript.java)

8   public class VoteScript implements Loadable
9   {
  ...
20     public void init()
21     {
22        loader = new ScriptLoader();
23        loader.addGlobalBridge(this);
Back to article


Listing Six
(VoteScript.java)

37  public boolean scriptLoaded(ScriptInstance script)
38  {
39     Hashtable environment = script.getScriptEnvironment().getEnvironment();
 ...
47     environment.put("&Display", new Display());
Back to article


Listing Seven
(VoteScript.java)

63     private class Display implements Function
64     {
65        public Scalar evaluate(String funcName, ScriptInstance script, 
                                 Stack arguments)
66        {
67           String message = BridgeUtilities.getString(arguments, "eh?!?");
68           dialog.setDisplay(message);
69           return SleepUtils.getEmptyScalar();
70        }
71     }
Back to article


Listing Eight
(VoteScript.java)

8   public class VoteScript implements Loadable
9   {
  ...
13     private HashMap        candidates = new HashMap();
  ...
98     private class SetupCandidate implements Environment
99     {
100       public void bindFunction(ScriptInstance script, String keyword, 
                                   String funcName, Block body)
101       {
102          candidates.put(funcName, body);
103          dialog.addCandidate(funcName);
104       }
105    }
Back to article


Listing Nine
(VoteScript.java)

57     public void candidateVotedFor(String name)
58     {
59        Block code = (Block)candidates.get(name);      
60        SleepUtils.runCode(code, script.getScriptEnvironment());
61     }
Back to article


Listing Ten
(VoteScript.java)

37   public boolean scriptLoaded(ScriptInstance script)
38   {
39     Hashtable environment = script.getScriptEnvironment().getEnvironment();
40  
41     environment.put("candidate",  new SetupCandidate());
Back to article


Listing Eleven
(VoteScript.java)

89     private class IsWinning implements Predicate
90     {
91        public boolean decide(String condition, ScriptInstance script, 
                                Stack arguments)
92        {
93           String candidate = BridgeUtilities.getString(arguments, "none");
94           return dialog.isWinning(candidate);
95        }
96     }
Back to article


Listing Twelve
(VoteScript.java)

37   public boolean scriptLoaded(ScriptInstance script)
38   {
39     Hashtable environment = script.getScriptEnvironment().getEnvironment();
  ...
42     environment.put("-iswinning", new IsWinning());
Back to article


Listing Thirteen
(VoteScript.java)

37  public boolean scriptLoaded(ScriptInstance script)
38  {
39    Hashtable environment = script.getScriptEnvironment().getEnvironment();
  ...
49    HashMap results = dialog.getAllVotes();
50    script.getScriptVariables().putScalar("%results", 
                    SleepUtils.getHashWrapper(results));
Back to article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video