Rhino is an open-source implementation of JavaScript written in the Java language. Like many other languages in this series, Rhino is a dynamically typed, object-based scripting language that permits easy access to the vast Java class library. Rhino borrows numerous syntactic aspects from JavaScript to enable you to quickly write powerful programs. Most notably, Rhino omits the statement terminator (;), loosens the rules for declaring variables, and greatly simplifies the syntax for modifying and retrieving object properties (without resorting to calls to accessor methods).
Because it's a Java-based implementation of JavaScript, Rhino should be especially easy for Java developers to pick up. JavaScript's (and thus Rhino's) syntax is quite similar to that of the Java programming language. Both languages employ the same loop and conditional constructs as the Java programming language and follow similar syntactic patterns for expressing these constructs.
While Rhino has much in common with the other alternate languages profiled in this series, it also brings something unique (and, perhaps, somewhat foreign) to your programming efforts on the Java platform. Rhino is a prototype-based, rather than a class-based, language. In Rhino, you build objects, not classes. This lets you avoid the development, deployment, and execution costs of building and manipulating classes in addition to the object instances of those classes. As you'll discover in the course of this article, prototype-based languages such as Rhino can be especially effective when it comes to developing and running GUI-based applications.
You can download the latest release of the Rhino engine (Rhino-1.5r5 as of this writing) from the Mozilla Web site (see Resources). Simply unzip the archive into a directory of your choosing. A top-level directory named rhino1_5r5 is a part of the archive. The directory contains documentation, examples, the source code, and the js.jar file, which should be included on your classpath.
I'll make extensive use of the interactive shell, which is
implemented in the org.mozilla.javascript.tools.shell package. The
shell can be invoked to run in either interactive mode (in which case
you can enter expressions to be evaluated or blocks of code to be
executed) or batch mode. In batch mode, the -e option can be used to run a string containing
JavaScript/Rhino source and the -f option can
be used to execute a file containing script code. For example,
entering the following command will invoke the interpreter in interactive mode:
javaorg.mozilla.javascript.tools.shell.Main |
You should then see the
version number of the interpreter followed by the prompt,
js>. To exit the shell select Ctrl+Z (on Windows systems) or
Ctrl+D (on Unix systems).
Before you jump into learning the fundamentals of Rhino, it may be helpful to know something about the origin and purpose of JavaScript, the language that affords Rhino many of its unique features. The history of JavaScript coincides with the ability of Web browsers to dynamically render and manipulate Web-page content. The first version of JavaScript (originally named LiveScript) was released by Netscape Communications Corp. in 1995, as a part of the Netscape Navigator 2.0 Web browser. JavaScript was meant to provide programmers with an easy and intuitive way to write simple scripts that would perform tasks in the context of a Web page. The following year, Microsoft introduced JScript, its own port of JavaScript for Internet Explorer.
Both versions of JavaScript included an object-based API for accessing and manipulating the content of Web pages called the Document Object Model or DOM. A third implementation of this new scripting language, called EcmaScript, was meant to standardize the language itself and the DOM. Unfortunately, neither Microsoft nor Netscape ever fully implemented the EcmaScript standard, and so compatibility issues continue to be an issue to this day.
With the success of the Java programming language by the late 1990s, Netscape planned a release of Javagator, a 100% pure Java implementation of its Navigator browser. Although Javagator never came to fruition, the Netscape port of JavaScript, called Rhino, has survived the test of time. Rhino is a 100% pure Java implementation of the JavaScript 1.5 scripting language without the DOM API. In fact, Rhino is sometimes still referred to as Netscape's Java-based JavaScript.
With the first release of Rhino, Netscape obviously wanted to capitalize on the new success of the Java programming language. The company clearly borrowed some of the basic syntax of its scripting language from the Java language. This makes it especially easy for Java developers to learn and use. For example, consider the similarities between a Java function and the Rhino function in Listing 1, which tests whether or not a given number is prime:
Listing 1. A Rhino function: Is this number prime?
function isPrime (num)
{
if (num <= 1) {
print("Please enter a positive integer >= 2.")
return false
}
var prime = true
var sqrRoot = Math.round(Math.sqrt(num))
for (var n = 2; prime & n <= sqrRoot; ++n) {
prime = (num % n != 0)
}
return prime
}
|
With a few exceptions, this program looks remarkably like a Java program:
- Curly braces are used to delimit blocks of code.
- The syntax for
forandifconstructs is the same as in the Java language. - Rhino employs the same arithmetic and conditional operators as the
Java language (for instance, the assignment to the sqrRoot variable), and even supports a similar means to access other
arithmetic functions.
- Rhino allows the use of pre-defined boolean literals
trueandfalse.
While not shown here, note that the syntax of Rhino's
while and do...while
loop constructs matches that of the Java language.
There are, of course, some noteworthy differences between Rhino
and the Java language. First, because
Rhino is a dynamically typed language, you won't find types in its
declarations of functions or variables. You use the function keyword to denote the start of a function
and the var keyword to declare local (as
opposed to global) variables, but you do not include the type of the variable
being declared. The Rhino runtime will infer the type during
execution. Unlike the Java language, Rhino has no statement terminator (the semicolon in the Java language), although it supports the use of statement
terminators as an option.
The other major distinction between Rhino and the Java language is
that you can run a program like the one shown in Listing 1 from the interpreter. If you assume that the function definition is in a file called isprime.js for example, then you would simply type the following command at the interpreter prompt, where path specifies the absolute path to the directory in which the file is stored:
load("<path>/isprime.js")
|
Upon the return of the prompt, you would type the following command and then immediately receive the result, true, from the Rhino interpreter:
isPrime(37) |
In this way, the Rhino interpreter can function like a "scratch pad" in which to enter simple blocks of Java code for testing and debugging. Because the syntax of the two languages is so similar, this usually involves cutting the relevant code from the Java program, pasting it into a .js file and loading it into the interpreter, and then invoking it from the shell.
Of course, not all of Rhino's distinctions from the Java language are so subtle. In the sections that follow, you'll see some of the most useful ways that Rhino differentiates itself, starting with its support for literal representations of familiar and frequently used data types, including arrays, hashes (or associative arrays), regular expressions, functions, and, as you shall see, objects themselves.
The array in Rhino can be represented as a comma-separated list of values between square brackets. Thus, the following are arrays in Rhino:
numbers = [0, 1, 2, 3, 5, 24] names = ["Mike", "Joe", "Betty", "Therese", "Lynn Marie"] arrayOfArrays = [1, [2, 3], [3, 4, 5, [6, 7]], 8] |
The third example demonstrates that elements of an array need not be of the same type.
The associative array is another data type that can be represented as a literal. An associative array, sometimes referred to in other languages as a
dictionary or a hash, is a list of key-value pairs, with the key and value being separated by a colon (:). An associative array in Rhino functions much like a java.util.HashMap in the Java language. The following are associative arrays in Rhino:
person =
{name:"Mike Squillace", age:37, position:"software engineer"}
link = {text:"IBM Home", url:"http://www.ibm.com"}
|
You can refer to elements of these lists in one of two ways. To set
the age property of the just-defined person hash, you could write person["age"] =
39 or person.age = 39. To retrieve
the value and store it in a variable named myAge, you could
write either myAge = person["age"]
or myAge = person.age.
Rhino treats typical indexed arrays as special kinds of associative arrays. They just happen to be associative arrays with positive integers as keys. Thus, the following lines define precisely the same array:
a1 = ["fee", "fi", "fo"]
a2 = {0:"fee", 1:"fi", 2:"fo"}
|
Rhino provides a special loop construct, the for...in construct, for iterating through the
properties in an associative array. The following code prints the
properties of the person associative array I've just defined, along with the values for these properties:
for (prop in person) {
print("person[" + prop + "] = " + person[prop])
}
|
Of course, I'm iterating over a hash-like structure, so there is no guarantee of the order in which the properties and their values will be printed.
Like arrays, regular expressions can be represented literally in Rhino, where their syntax should be familiar to users of Perl and other scripting languages. A regular expression in Rhino is simply delimited by a forward-slash (/) when represented as a literal.
In Rhino, regular expressions are passed to methods of string objects to make text-processing tasks more straightforward. For example, the following code first defines a regular expression that matches either positive integers or the standard arithmetic operations in an arithmetic expression. The second line then tokenizes the given expression by invoking the match function, as shown below:
tokenExpr = /\d+|[\+\-\*\/]/g "38-4+98/5".match(tokenExpr) |
The result is a Rhino array of string elements containing the following:
"38", "-", "4", "+","98", "/", and "5" |
Finally, Rhino affords you the function data type. As already noted, Rhino supports functions as first-class data types -- they can be returned from and passed to functions and used in variable declarations. Hence, I could write the following at the interpreter prompt and obtain 9, the expected result to define the square function:
square = function (x) { return x * x }
|
Following this definition of square, I would type
square(3) |
Functions defined in this manner can be used anywhere other literals are used. For example, I could define an associative array of functions like so:
fnList = {
square:function (x) {return x * x},
cube:function (x) {return x * x * x},
sqrt:function (x) {return Math.sqrt(x)}
}
|
I could then iterate over the functions in the list, printing out the values for each as they were applied to a particular number. For instance, if I wrote the following snippet
for (fnName in fnList) {
print("The " + fnName + " of " + 3 +
" is " + fnList[fnName](3))
}
|
I would obtain something like this:
The square of 3 is 9 the sqrt of 3 is 1.7320508075688772 The cube of 3 is 27 |
Given the associative array and the ability to represent functions as literals, I am able to represent any object as an associative array in Rhino. Indeed, the literal representation of an object is simply an associative array that may contain functions for some of its values. An example will illustrate just how simple and powerful it is to work with objects in Rhino. To start, consider, once again, the person hash:
person = {name:"Mike Squillace", age:37, position:"software engineer"}
|
This is a literal representation of an associative array in Rhino but it is, more generally, the literal representation of an object. Such a representation is also called an object initializer in Rhino. The type of the value just defined is confirmed by the interpreter which, after I enter the above definition, responds with the following code:
[object Object] |
The above code indicates that the value held by the variable person is of type Object.
Next, I'll show you what happens when I redefine the person object to include a function for retrieving the first name of the object. I do this by writing the following:
person = {
name:"Mike Squillace",
age:37,
position:"software engineer",
getFirstName:function () {return this.name.split(" ")[0]}
}
|
The function (or, more precisely, method), getFirstName,
uses the this pointer to refer to the current
object and invokes the split method on the
name property. The split method then
returns an array holding the strings that resulted from separating the
given string into substrings,as delimited by the space character. I then retrieve the first value of this array and return it.
I invoke the new getFirstName function
in a familiar manner:
person.getFirstName() |
The parentheses indicate to the Rhino interpreter that I'm invoking a
function rather than simply referring to a property of the object.
Notice, however, that the function itself is just another property of
the person object and that, without the parentheses,
it would refer to an undefined value.
Rhino also allows you to add properties and methods to objects on the fly. For example, if I wanted to add a method to retrieve the last name of the person represented by the person object, I would simply type the following:
person.getLastName = function () {return this.name.split(" ")[1]}
|
Now, when I enter the method below, I'll get the desired and expected result, in this case "Squillace":
person.getLastName() |
(Note that you can use the delete operator to remove any property, as in delete person.getLastName.)
While the above examples are interesting, you probably don't want to keep using object initializers to define individual persons. Fortunately, Rhino provides another way to create objects, using the constructor function. For example, the following function might serve as a constructor function for an object of type Person:
function Person (name, age, job) {
this.name = name || "<unknown>"
this.age = age
this.job = job || "<unemployed>"
this.getFirstName = function () {return this.name.split(" ")[0]}
this.getLastName = function () {return this.name.split(" ")[1]}
}
|
Once I have a constructor function, I can use the new operator to create objects, as shown below:
mike = new Person("Mike Squillace", 37, "software engineer")
|
Any function can serve as a constructor function, although you'll typically
want to use functions that define properties for an object (using
the this pointer to refer to the object being
defined) and to assign values (or functions) to these properties.
As a Java developer, you're likely thinking that your next step will
be to define the Person class in Rhino. In fact, Rhino doesn't have a way to define classes -- because it doesn't use them!
There are no classes or class instances in Rhino, only particular objects.
When invoked with the new operator, the
constructor function creates what is known as a prototype for an
object; that is, it creates a template from which to build objects of the given kind.
In a prototype-based language like Rhino, you can modify the
properties of either a particular object or its prototype. For example, if I
wanted to add a special property to the just-defined mike object, I would do so as follows:
mike.disability = "blind" |
I could also modify the Person prototype by referring to the prototype property of the Person constructor function. If I then wanted to add a birthdate property to all objects formed from this constructor function, I would write:
Person.prototype.birthdate = null |
This would be followed by:
mike.birthDate = new Date(66, 10, 3) // months are zero-based: this is 11/3/66 |
Also note that any new objects created based on the Person prototype would have the birthdate property, making the following code valid:
jami = new Person("Jami Bomer", 25, "unemployed")
jami.birthdate = new Date(79, 5, 28)
|
I'll close this introduction to Rhino with a working example. Because prototype-based languages are especially useful when it comes to building GUI applications, I'll walk through the steps of building a GUI app in Rhino.
This example is designed to tie together many of the facets and concepts of Rhino discussed so far, and introduce you to a few new ones. Then, I'll review a GUI-based application that is used to enter personnel records for the employees of a large software company. The GUI itself will be the front end of a system that manages employee records for a large corporation. The example will, of course, be far from complete, but it will underscore some of the most important concepts of the Rhino scripting language.
The GUI is based on the Java Swing GUI framework and consists of two
basic panels. The top panel holds a javax.swing.JTabbedPane containing any number of
panels for entering employee data. You're interested in only two of the
panels: one that lets you enter general employee information and the
other that lets you record each employee's published works, if any. The general information panel is shown in Figure 1:
Figure 1. The general information panel
Figure 2 shows the employee published works panel:
Figure 2. The published works panel
The bottom panel of the main GUI consists of buttons for adding or modifying employee records, for clearing the form fields of a panel, and for exiting the application. The first two of the buttons will behave somewhat differently depending on the panel of information that is currently being edited. For instance, the Clear button should clear all the fields of the panel of the currently active tabbed pane, while the Add button should modify only the data associated with that panel. (The Exit button will always exit the application without saving data.)
To keep things simple, the back-end consists of only two entities.
First, I'll construct and modify an associated array whose keys (or
indecies) are employee social security numbers. Corresponding to each
social security number is the employee data entered on each panel.
I'll also use an Employee prototype to hold
general information about any employee entered into the first panel of
the tabbed pane. All other records will be created and added to the
employee instance being edited only as needed.
The code for the MainFrame constructor function and for the instantiation and display of the frame is provided in Listing 2:
Listing 2. The constructor function for the Employee GUI mainframe
importPackage(java.awt, java.awt.event)
importPackage(Packages.javax.swing)
importClass(java.lang.System)
:
// the main frame of the GUI:
// consists of a tabbed pane to which other panels for additional employee
// information may be entered
function MainFrame ()
{
var cp = this.contentPane
var tabbedPane = new JTabbedPane
this.gip = new GeneralInfoPanel
tabbedPane.addTab(this.gip.id, this.gip.infoPanel)
this.pp = new PublicationsPanel
tabbedPane.addTab(this.pp.id, this.pp.pubPanel)
var compMap = {}
compMap[this.gip.id] = this.gip
compMap[this.pp.id] = this.pp
var buttonPanel = new JPanel(new GridLayout(1, 3))
var addButton = new JButton("Add")
var clearButton = new JButton("Clear")
var exitButton = new JButton("Exit")
addButton.setMnemonic(KeyEvent.VK_A)
addButton.addActionListener(
new JavaAdapter(
ActionListener,
{actionPerformed:function (event) {
compMap[tabbedPane.selectedComponent.name].doAdd()
}}
)
)
buttonPanel.add(addButton)
clearButton.setMnemonic(KeyEvent.VK_R)
clearButton.addActionListener(
new JavaAdapter(
ActionListener,
{actionPerformed:function (event) {
compMap[tabbedPane.selectedComponent.name].clearFields()
}}
)
)
buttonPanel.add(clearButton)
exitButton.setMnemonic(KeyEvent.VK_X)
exitButton.addActionListener(
new JavaAdapter(
ActionListener,
{actionPerformed:function (event) {
var confirm = JOptionPane.showConfirmDialog(
cp,
"Confirm exit",
"Confirm Exit Dialog",
JOptionPane.YES_NO_OPTION
)
if (confirm == JOptionPane.YES_OPTION) {
System.exit(0)
}
}}
)
)
buttonPanel.add(exitButton)
cp.add(tabbedPane, BorderLayout.CENTER)
cp.add(buttonPanel, BorderLayout.SOUTH)
} // MainFrame
MainFrame.prototype = new JFrame
:
// display GUI
main = new MainFrame("Employee Record Example")
main.setSize(400, 400)
main.visible = true
|
Listing 2 begins with three importXXX
statements. Two versions of the importPackage
statement exist. For importing packages in the java package, I use the importPackage statement with a comma-separated list
of the names of the packages I wish to import. For all other packages
(such as javax.swing), I must use the Packages object to refer to the package I want to
import, as in Packages.javax.swing. Finally,
I use the importClass statement to import
individual classes.
As I previously mentioned, you can create only individual objects, not
classes, in prototype-based languages like Rhino. Here, I'm creating
a Rhino MainFrame object that will serve as
the mainframe of the GUI. For convenience, I begin by creating two
local variables to hold the frame's content pane and the JTabbedPane.
Next, I create the Rhino GeneralInfoPanel
object and make it a member of the MainFrame object by assigning it to this.gip. The GeneralInfoPanel will be used for entering
general information such as the name, social security number,
department, and rank of the employee. Finally, I add the infoPanel (the actual javax.swing.JPanel) as a member of the GeneralInfoPanel object. The infoPanel is added to the tabbed pane with the
name given to it by the ID member of the newly instantiated GeneralInfoPanel.
Notice in all of this that the GeneralInfoPanel is not a subclass and does not inherit in anyway from javax.swing.JPanel. Rather, one of its members is
a JPanel that I can access via the infoPanel member. (You'll better understand the rationale behind this approach momentarily.)
The GUI's PublicationsPanel object is processed
similarly to the GeneralInfoPanel panel. Once it's
completed, I'm ready to create the component map, compMap.
The component map is keyed by the ID of the Rhino Panel objects. Because the buttons of the GUI must
perform somewhat differently depending on the panel that is active in
the tabbed pane, I want to refer not only to these panels but also to
their members. In particular, I want to refer to the methods that
perform the functions required when the Add or Clear
buttons are pressed. (To put it in terms more familiar to Java
developers, you require the Panel objects to
implement an interface that includes methods for performing the desired
clear and add
functions.)
Because the name of the component in the tabbed pane and the key in the component map are both defined by the id attribute of the Panel objects, I can easily access the active panel with the following call:
compMap[tabbedPane.selectedComponent.name].doAdd() |
Finally, I create the buttonPanel and the buttons and add everything to the content pane of the JFrame.
One peculiarity of Rhino is the way it has you attach java.awt.event.ActionListener implementations to each button. For example, consider the ActionListener for the addButton:
addButton.addActionListener(
new JavaAdapter(
ActionListener,
{actionPerformed:function (event) {
compMap[tabbedPane.selectedComponent.name].doAdd()
}}
)
)
|
The JavaAdapter class of the org.mozilla.javascript package makes it especially
easy to add listeners to components in Rhino. A JavaAdapter is created from the interface (or
interfaces) to be implemented along with an object that includes a key
matching the name of the method(s) to be implemented for the
interface(s). The value is the function to be invoked when the event
occurs. Here, the function simply passes on the work of the addButton to the doAdd
method of the panel that is active in the tabbed pane.
Notice, also, that I do not yet have a JFrame for this GUI. In fact, I never really have a JFrame but, rather, a MainFrame Rhino object. The MainFrame
object I have just built behaves like a JFrame because of the statement following its constructor function:
MainFrame.prototype = new JFrame |
This statement causes the properties and methods of the newly
instantiated JFrame object to be shared with
your MainFrame object. It is the JFrame object with which you share properties and
methods -- I do not inherit or subclass the JFrame class. (Of course, any MainFrame objects I create would also share the
properties and methods of the JFrame
instance.)
The fact that I am dealing with a Rhino (and not a Java) object
means that I could not hope to follow a similar technique when
building the Rhino XXXPanel objects. If I
were to create these objects and then follow their constructor functions
with the statement below, I would not have a JPanel and, hence, could not pass it to the add method of the contentPane object in the MainFrame or to the addTab method of the JTabbedPane:
XXXPanel.prototype = new JPanel |
This is another reminder that in Rhino, you work with objects and that the relation between objects is one of sharing properties or methods, not of membership in a hierarchy of classes.
The GeneralInfoPanel and the Employee prototype
The first panel of the tabbed pane in the MainFrame is the GeneralInfoPanel, which contains fields for
entering general employee information. For the most part, the code is
quite similar to Java code, as demonstrated by the excerpt in Listing
3:
Listing 3. Excerpt of the GeneralInfoPanel constructor function
function GeneralInfoPanel ()
{
this.infoPanel = new JPanel()
with (this.infoPanel) {
setLayout(new BoxLayout(this.infoPanel, BoxLayout.Y_AXIS))
name = this.id = "general"
var box1 = new Box(BoxLayout.X_AXIS)
var box2 = new Box(BoxLayout.X_AXIS)
var box3 = new Box(BoxLayout.X_AXIS)
add(Box.createVerticalGlue())
box1.add(box1.createHorizontalGlue())
var nameLabel = new JLabel("Name:")
nameLabel.horizontalAlignment = JLabel.RIGHT
box1.add(nameLabel)
box1.add(box1.createHorizontalStrut(4))
this.nameField = new JTextField(30)
nameLabel.labelFor = this.nameField
box1.add(this.nameField)
box1.add(box1.createHorizontalStrut(8))
:
} // GeneralInfoPanel
|
The chief difference between the above code and Java code is that I
have not used inheritance to form the GeneralInfoPanel. Rather, I've made the JPanel an instance member of the object, as should
be clear for reasons already discussed.
Adding/modifying employee records
If I were writing Java code, my next step would be to extend JPanel and implement the appropriate listener
interfaces all in one class. In Rhino, I mirror this behavior by
holding on to the components to be added to the JPanel and assigning functions to data members of
this object to handle events. For instance, the addButton of the MainFrame always calls the doAdd method of the panel object. Listing 4 shows
the doAdd method for the GeneralInfoPanel:
Listing 4. Code for the doAdd method of the GeneralInfoPanel
this.doAdd = function () {
with (this) {
if (empMap[this.ssnField.getText()] == null) {
empMap[this.ssnField.getText()] = new Employee(
nameField.getText(), ssnField.getText(), deptField.getText(),
cityField.getText(), stateField.selectedItem, rankField.selectedItem,
payField.getText(), formulaField.getText()
)
} else {
var emp = empMap[this.ssnField.getText()]
emp.name = nameField.getText()
emp.ssn = ssnField.getText()
emp.dept = deptField.getText()
emp.city = cityField.getText()
emp.state = stateField.getSelectedItem
emp.rank = rankField.getSelectedItem
emp.basePay = parseInt(payField.getText())
emp.payFunction = new Function (
"with(this){var pay = basePay; " +
formulaField.getText() + "}; return pay")
}
}
} // doAdd
|
The next step is to test the results by attempting to retrieve the
desired employee. If the record is found, I'll simply modify the
record upon an add operation. If the record
is not found, I create a new Employee
prototype by invoking the Employee
constructor function with the appropriate fields from the panel. Here's
the Employee constructor function:
// employee prototype
function Employee (name, ssn, dept, city, state, rank, basePay, payFormula)
{
this.name = name
this.ssn = ssn
this.dept = dept
this.city = city
this.state = state
this.rank = rank
this.basePay = parseInt(basePay)
this.payFunction = new Function(
"with(this){var pay = basePay; " +
payFormula + "}; return pay")
} // Employee
|
You have probably noted that the Employee
prototype does not include a field for employee publications. While I
want to record this data, it's important to account for the fact that
not all employees will have published works. If I had written this
application in the Java language, the Employee would be required to hold a publications
data member upon its compilation; otherwise, no such field could ever be
added. In Rhino, however, you can add and modify the Employee prototype as you see fit, as Listing 5
demonstrates:
Listing 5. The doAdd method for the PublicationsPanel
this.doAdd = function () {
var employee = empMap[main.gip.ssnField.getText()]
if (employee != null) {
if (employee.publications == null) {
employee.publications = []
}
with (this) {
employee.publications.push({
title:titleField.getText(),
date:new Date(Date.parse(dateField.getText())),
source:sourceField.getText(),
coauthor:authField.getText(),
type:typeField.selectedItem
})
}
}
} // doAdd
|
Again, one of the advantages to building objects rather than classes is that you get to avoid the overhead of storing data that isn't there.
The key difference between the PublicationsPanel and the GeneralInfoPanel (besides the obvious differences
in required fields) is the doAdd method,
which is given in Listing 5. I check first to see if this employee has
a publications list. (Rhino always returns the value null as the result of accessing a nonexistent
field.) If not, I create an empty list in which to store publication
records. If so, I simply push the record onto the existing list.
Notice that this field is created only once and only if the Add
button is pressed on the PublicationsPanel.
I could, of course, add other panels to add or modify additional
employee records. In this way, Rhino lets you literally build employee
objects step-by-step, adding to and modifying them as needed without
worrying about their conformance to a previously defined and static
representation.
Rhino is a lightweight yet powerful scripting language. Its familiar syntax and extensive use of data types not available in the Java programming language make it an excellent tool for rapid development on the Java platform. As demonstrated in this article, Rhino's use of prototypes, rather than classes, makes it more suited than many scripting languages to developing GUI-intensive applications, especially with regard to performance and style.
Rhino's major weakness is also its strength: Its basis in the Java language makes it both easy to learn and, at times, unduly verbose for a scripting language. Rhino's prototype-based approach is similarly double-edged; the very qualities that make it uniquely suited to some tasks make it seem tedious and cumbersome in other instances. This will be particularly so for the developer who approaches Rhino from a Java-centric perspective.
That said, Rhino is perhaps the most popular of all the scripting languages discussed in this series, and so merits the attention of any curious and well-informed developer. Rhino is also undoubtedly the language of choice when working in the context of a Web browser to dynamically render and manipulate Web content.
- Don't miss a single installment in the new alt.lang.jre series!
Visit the alt.lang.jre series page for a complete listing of all columns.
- The IBM Reflexive User Interface Builder (RIB) (developerWorks, August 2004) is an application and toolkit for building and rendering Java GUIs in a variety of scripting languages, including Rhino, Jython, and JRuby. RIB was developed by Michael Squillace and Barry Feigenbaum and is available from IBM alphaWorks.
- Sing Li explores the potential of EcmaScript and the Free EcmaScript Interpreter in his article on "Quick-and-dirty Java programming" (developerWorks, July 2001).
- Get a primer on using Standard JavaScript (aka Rhino) for server-side
programming, with Chrisopher Vincent's two-part "IBM Lightweight Services, Part 1: Server-side scripting" (developerWorks, November 2002).
- The Rhino Download page is the site for downloading Rhino. The latest release as of this writing is Rhino 1.5 release 5. Be sure to download the archive that contains the js.jar file; contrary to the instructions on the Web site, most of the other archives contain only the source and the build.xml files.
- You'll find articles about every aspect of Java programming in
the developerWorks Java technology
zone.
- Browse for books on these and other technical topics.

Dr. Michael Squillace is a member of the IBM Worldwide Accessibility Center, where he develops tools for enabling IBM's software products for accessibility. He holds a Ph.D. in philosophy and is currently a student at the University of Texas majoring in computer science. He is a professional pianist. He is also blind. You can contact Dr. Squillace at masquill@us.ibm.com.




