Scripting for the Java Platform
By John O'Conner, July 2006
The Java platform provides rich resources for both desktop and web application development. However, using those resources from outside the platform has been impractical unless you resort to proprietary software solutions. No industry standard has defined or clarified how developers can use Java class files from other programming languages. Scripting languages haven't had a standard, industry-supported way to integrate with Java technologies. However, as Bob Dylan once said, " the times, they are a changin'." One change is Java Specification Request (JSR) 223, which helps developers integrate Java technology and scripting languages by defining a standard framework and application programming interface (API) to do the following:
- Access and control Java technology-based objects from a scripting environment
- Create web content with scripting languages
- Embed scripting environments within Java technology-based applications
This article focuses on the specification's third goal and will show you how to use an embedded scripting environment from a Java platform application. A demo application called ScriptCalc will provide a working example of how to extend your applications with user-defined scripts in the JavaScript programming language.
Note: Any API additions or other enhancements to the Java SE platform specification are subject to review and approval by the JSR 270 Expert Group.
Reasons to Use a Scripting Language
Most scripting languages are dynamically typed. You can usually create new variables without predetermining the variable type, and you can reuse variables to store values of different types. Also, scripting languages tend to perform many type conversions automatically, for example, converting the number 10 to the text "10" as necessary. Although some scripting languages are compiled, most languages are interpreted. Script environments generally perform the script compilation and execution within the same process. Usually, these environments also parse and compile scripts into intermediate code when they are first executed.
These qualities of scripting languages help you write applications faster, execute commands repeatedly, and tie together components from different technologies. Special-purpose scripting languages can perform specific tasks more easily or more quickly than can more general-purpose languages. For example, many developers think that the Perl scripting language is a great way to process text and to generate reports. Other developers use the scripting languages available in bash
or ksh
command shells for both command and job control. Other scripting languages help to define user interfaces or web content conveniently. Developers might use the Java programming language and platform for any of these tasks, but scripting languages sometimes perform the job as well or better. This fact doesn't detract from the power and richness of the Java platform but simply acknowledges that scripting languages have an important place in the developer's toolbox.
Combining scripting languages with the Java platform provides developers an opportunity to leverage the abilities of both environments. You can continue to use scripting languages for all the reasons you already have, and you can use the powerful Java class library to extend the abilities of those languages. If you are a Java language programmer, you now have the ability to ship applications that your customers can significantly and dynamically customize. The synergy between the Java platform and scripting languages produces an environment in which developers and end users can collaborate to create more useful, dynamic applications.
For example, imagine a calculator with a set of core operations. Although the base calculator may have only four or five fundamental operations, you can provide programmable function keys that the user can customize. Customers can use whatever scripting language they prefer to add mortgage calculations, temperature conversions, or even more complex functionality to the calculator. Another example of this collaboration could be a word processor that allows customers to provide customized filters for generating various file formats. Examples throughout the remainder of this article will show how to use scripting to provide customizable Java applications for your customers.
JSR 223 Implementation
Version 6 of the Java Platform, Standard Edition (Java SE), does not mandate any particular script engine. The Mozilla Rhino engine for the JavaScript programming language, however, is currently included as a feature in the JDK 6 and JRE 6 libraries. The Java SE 6 platform implements the java.script APIs, which allow you to use script engines that comply with JSR 223. The web site scripting.dev.java.net hosts an open project to maintain several script engines that conform to JSR 223. The site also links to engines maintained elsewhere. You can learn more about the the embedded JavaScript technology engine by visiting the Mozilla Rhino web site.
Ways to Use the Scripting API
The scripting API is in the javax.script
package available in the Java SE 6 platform. The API is still relatively small, composed of six interfaces and six classes, as Table 1 indicates.
Table 1: Interfaces and Classes in the Java SE 6 Platform
Interface | Class |
---|---|
Bindings |
AbstractScriptEngine |
Compilable |
CompiledScript |
Invocable |
ScriptEngineManager |
ScriptContext |
SimpleBindings |
ScriptEngine |
SimpleScriptContext |
ScriptEngineFactory |
ScriptException |
Your starting point should be the ScriptEngineManager
class. A ScriptEngineManager
object can tell you what script engines are available to the Java Runtime Environment (JRE). It can also provide ScriptEngine
objects that interpret scripts written in a specific scripting language. The simplest way to use this API is to do the following:
- Create a
ScriptEngineManager
object. - Retrieve a
ScriptEngine
object from the manager. - Evaluate a script using the
ScriptEngine
object.
That sounds easy enough, but what does the code look like? Code Example 1 performs all three steps, printing Hello, world!
to the console.
Code Example 1: Create a ScriptEngine
object using the engine name.
ScriptEngineManager mgr = new ScriptEngineManager();
ScriptEngine jsEngine = mgr.getEngineByName("JavaScript");
try {
jsEngine.eval("print('Hello, world!')");
} catch (ScriptException ex) {
ex.printStackTrace();
}
The API is only slightly more complex if you want to query the list of supported scripting engines, to pass values back and forth to the scripting environment, or to compile a script for repeated execution. Additional APIs allow you to query the ScriptEngineManager
for engines that associate a particular file extension, to execute the script from a file, and to call a specific function in a script. This article describes many of those features.
Available Script Engines
A ScriptEngineManager
object provides the discovery mechanism for the the scripting framework. A manager finds ScriptEngineFactory
classes, which create ScriptEngine
objects. Developers can add script engines to a JRE with the JAR Service Provider specification. Although this specification is beyond the scope of this article, you can find more information in the JAR File Specification.
Code Example 1 retrieved a scripting engine directly from a script manager. However, that way of accessing a ScriptEngine
object works only when you know the engine's name. If you need to retrieve a ScriptEngine
object using more complicated criteria, you may need to get the entire list of supported ScriptEngineFactory
objects first. A ScriptEngineFactory
can create ScriptEngine
objects for a specific scripting language.
Code Example 2 provides a list of discovered factories.
Code Example 2: You can retrieve a list of all engines installed to your Java platform.
ScriptEngineManager mgr = new ScriptEngineManager();
List<ScriptEngineFactory> factories = mgr.getEngineFactories();
Once you have a script-engine factory, you can retrieve various details about the scripting language that the factory supports:
- The script-engine name and version
- The language name and version
- Aliases used for the script engine
- A
ScriptEngine
object for the scripting language
Code Example 3 shows how to retrieve this information.
Code Example 3: A ScriptEngineFactory
object provides detailed information about the engine it provides.
ScriptEngineManager mgr = new ScriptEngineManager();
List<ScriptEngineFactory> factories =
mgr.getEngineFactories();
for (ScriptEngineFactory factory: factories) {
System.out.println("ScriptEngineFactory Info");
String engName = factory.getEngineName();
String engVersion = factory.getEngineVersion();
String langName = factory.getLanguageName();
String langVersion = factory.getLanguageVersion();
System.out.printf("\tScript Engine: %s (%s)\n",
engName, engVersion);
List<String> engNames = factory.getNames();
for(String name: engNames) {
System.out.printf("\tEngine Alias: %s\n", name);
}
System.out.printf("\tLanguage: %s (%s)\n",
langName, langVersion);
}
Code Example 3 produces the following output:
ScriptEngineFactory Info
Script Engine: Mozilla Rhino (1.6 release 2)
Engine Alias: js
Engine Alias: rhino
Engine Alias: JavaScript
Engine Alias: javascript
Engine Alias: ECMAScript
Engine Alias: ecmascript
Language: ECMAScript (1.6)
Notice that the list of script-engine factories contains only one entry for the Mozilla Rhino engine. Currently, Rhino is the only engine included in the core JDK 6 libraries, although it is not dictated by the platform. You can add additional engines by installing a JAR file-based service provider into your JRE, as mentioned earlier. This article's code examples use the Mozilla Rhino engine. Notice that the script-engine factory provides many engine name aliases to help you retrieve an engine for the JavaScript programming language.
Ways to Create a ScriptEngine
Once you have all this information about a factory and the engine it supplies, you can decide at runtime which engine factory to use. If you find the appropriate ScriptEngineFactory
, creating the associated ScriptEngine
is easy. Ask the factory for the actual engine as in Code Example 4, with the factory's getScriptEngine
method. This code iterates through all known factories, searching for one that meets specific criteria for language name and version. In this example, the criteria are hardcoded. The code is searching for a factory that supports ECMAScript version 1.6.
Code Example 4: You can search for script engines that meet your application's requirements.
List<ScriptEngineFactory> scriptFactories =
mgr.getEngineFactories();
for (ScriptEngineFactory factory: scriptFactories) {
String langName = factory.getLanguageName();
String langVersion = factory.getLanguageVersion();
if (langName.equals("ECMAScript") &&
langVersion.equals("1.6")) {
engine = factory.getScriptEngine();
break;
}
}
Of course, if you already know that an engine is available, you can ask a ScriptEngineManager
object for it directly by name, file extension, or even MIME type. The following line of code will retrieve a JavaScript programming language engine because js
is the common file extension for JavaScript programming language files.
engine = mgr.getEngineByExtension("js");
How to Run a Script
A ScriptEngine
object runs script code. An engine's eval
method evaluates the script, which is a character sequence obtained from either a String
or java.io.Reader
object. A Reader
object can get its characters from a file too. You can use this ability to read the scripts that customers provide even after you have deployed your application.
Code Example 1 used the eval
method to evaluate a String
character sequence:
try {
jsEngine.eval("print('Hello, world!')");
} catch (ScriptException ex) {
ex.printStackTrace();
}
Rhino's implementation of the print
method sends its argument data to the console. The Hello, world!
message appears in your command-shell console. If you run this in an integrated development environment (IDE) such as NetBeans or Eclipse, the output appears in the IDE's debug or output window.
One of the best reasons to use scripting in your application is to allow users to customize its functionality. The easiest way to allow this customization is read script files that customers provide. An overloaded eval
method can use a Reader
parameter, which you can use to process scripts from an external file.
Finding resources outside of your application's JAR file can be problematic. However, if you place scripts in a relative directory on the classpath or in a well-defined absolute location that the user defines, your application can reliably find the scripts. If you decide that all user-defined scripts will exist in a scripts
subdirectory under your application's JAR file directory, you should ensure that the JAR file's subdirectory is on the classpath. As long as your application's directory is on the classpath, your application should consistently find customer-defined scripts in the scripts
subdirectory. You can put the JAR file's relative directory location in the classpath using the Class-path
statement in a manifest file that will be stored in the application's JAR file. The relative path for the JAR file's location is denoted by the .
character. This article's ScriptCalc demo application uses a manifest.xml
file similar to the one in Code Example 5.
Code Example 5: Adding .
to the classpath helps your application find scripts that have paths relative to the JAR file.
Manifest-Version: 1.0
Ant-Version: Apache Ant 1.6.5
Created-By: 1.6.0-rc-b89 (Sun Microsystems Inc.)
Main-Class: com.sun.demo.calculator.Calculator
Class-Path: .
Code Example 6 shows how to evaluate a file that the customer has supplied. The file name is /scripts/F1.js
, and it is located under the application directory.
Code Example 6: The eval
method can read script files.
ScriptEngineManager engineMgr = new ScriptEngineManager();
ScriptEngine engine = engineMgr.getEngineByName("ECMAScript");
InputStream is =
this.getClass().getResourceAsStream("/scripts/F1.js");
try {
Reader reader = new InputStreamReader(is);
engine.eval(reader);
} catch (ScriptException ex) {
ex.printStackTrace();
}
How to Invoke a Script Procedure
Running entire scripts is useful, but you may want to invoke only specific script procedures. Some script engines implement the Invocable
interface. If an engine implements this interface, you can call or invoke specific methods or functions that the engine has already evaluated.
Script engines are not required to support the Invocable
interface. However, the Rhino JavaScript technology implementation included in JDK 6 does. If your script contains a function called sayHello
, you could invoke it repeatedly by casting your ScriptEngine
object to an Invocable
object and by calling its invokeFunction
method. Alternatively, if your script defines objects, you can call object methods using the invokeMethod
method. Code Example 7 demonstrates how to use this interface.
Code Example 7: You can use the Invocable
interface to call specific methods in a script.
jsEngine.eval("function sayHello() {" +
" println('Hello, world!');" +
"}");
Invocable invocableEngine = (Invocable) jsEngine;
invocableEngine.invokeFunction("sayHello");
Code Example 7 prints Hello, world!
to the console.
Be aware that invokeMethod
and invokeFunction
methods can throw several exceptions, so you must be prepared to catch ScriptException
, NoSuchMethodException
, and perhaps even NullPointerException
exceptions.
How to Access Java Objects From Script
JSR 223 implementations provide programming language bindings that allow access to Java platform classes, methods, and properties. The access mechanism will usually follow the scripting language's conventions for native objects in that particular scripting environment.
How do you get Java objects into the script environment? You can pass objects into script procedures as arguments using the Invocable
interface. Alternatively, you can "put" them there: Your Java programming language code can place Java objects into the scripting environment by invoking a script engine's put
method. This method places key-value pairs into a javax.script.Bindings
object, which is maintained by a script engine. A Bindings
object is a map of key-value pairs that can be accessed from within an engine.
Imagine you have a list of names for a script to process. Code Example 8 in the Java programming language might produce the list.
Code Example 8: Java programming language code adds names to a list.
List<String> namesList = new ArrayList<String>();
namesList.add("Jill");
namesList.add("Bob");
namesList.add("Laureen");
namesList.add("Ed");
After creating a ScriptEngine
object called jsEngine
, you can put the namesList
Java object into the scripting environment. The put
method requires String
and Object
arguments that represent a key-value pair. In Code Example 9, the script code can use the namesListKey
reference to access the namesList
Java object.
Code Example 9: Script code can both access and modify native Java objects.
jsEngine.put("namesListKey", namesList);
System.out.println("Executing in script environment...");
try {
jsEngine.eval("var x;" +
"var names = namesListKey.toArray();" +
"for(x in names) {" +
" println(names[x]);" +
"}" +
"namesListKey.add(\"Dana\");");
} catch (ScriptException ex) {
ex.printStackTrace();
}
System.out.println("Executing in Java environment...");
for (String name: namesList) {
System.out.println(name);
}
Having placed the namesListKey
key-value binding into the script-engine scope, you can use the Java object as a script object. Using the namesListKey
variable, the script can access the namesList
object. In Code Example 9, the script prints out the list's names and adds the name Dana. By printing the namesList
contents after returning from the eval
method, the example shows that the script has successfully accessed and modified the list.
The output from Code Example 9 shows the list twice. The script produces the first listing, then adds a name. After evaluating the script, the code prints the list again, showing that the script successfully modified the list as well:
Executing in script environment...
Jill
Bob
Laureen
Ed
Executing in Java environment...
Jill
Bob
Laureen
Ed
Dana
You can pass the same namesList
object to the scripting code using the Invocable
interface too. Instead of using the key-value pair binding mechanism, scripting code can access and modify procedure arguments that are provided through the Invocable
interface. Code Example 10 shows how to use Java objects through the Invocable
interface. The code passes the namesList
value to the script environment as a parameter of the invokeFunction
method.
Code Example 10: Applications can pass values to script using the Invocable
interface.
Invocable invocableEngine = (Invocable)jsEngine;
try {
jsEngine.eval("function printNames1(namesList) {" +
" var x;" +
" var names = namesList.toArray();" +
" for(x in names) {" +
" println(names[x]);" +
" }" +
"}" +
"function addName(namesList, name) {" +
" namesList.add(name);" +
"}");
invocableEngine.invokeFunction("printNames1", namesList);
invocableEngine.invokeFunction("addName", namesList, "Dana");
} catch (ScriptException ex) {
ex.printStackTrace();
} catch (NoSuchMethodException ex) {
ex.printStackTrace();
}
You can also create new Java objects in the scripting environment. After importing the necessary Java platform packages, your script can use any native Java class. Instead of printing messages to the console, you could create a Swing message dialog box from your script, as in Code Example 11 and its output, shown in Figure 1.
Code Example 11: Scripts can import Java platform packages.
try {
jsEngine.eval("importPackage(javax.swing);" +
"var optionPane = " +
" JOptionPane.showMessageDialog(null, 'Hello, world!');");
} catch (ScriptException ex) {
ex.printStackTrace();
}
Figure 1. Script code can create native Java platform objects.
How to Access Script Objects
The eval
, invokeMethod
, and invokeFunction
methods always return an Object
instance. For most script engines, this object is the last value that your script calculated. So the easiest way to access objects in the scripting environment is to return them from your script procedures or make sure that your script evaluates to the desired object.
The script-engine implementation will map some script types to their equivalents in the Java programming language. For example, the Mozilla Rhino script-engine maps number
and string
types to the Java programming language Double
and String
types. You can cast the return value of the eval
, invokeMethod
, or invokeFunction
methods if you know that a mapping exists. You should always consult your script-engine documentation for details of the type mappings. Of course, your script can create and return native Java objects too.
The ScriptCalc Demo
The ScriptCalc demo application implements parts of a postfix calculator. Figure 2 shows the calculator's graphical user interface (GUI).
Figure 2. The ScriptCalc Demo uses an embedded script engine.
The demo calculator provides basic calculator operations: add, subtract, multiply, and divide. Because this calculator's primary purpose is to show how to allow users to provide user-defined scripts that extend the core application, the calculator has four programmable function keys: F1, F2, F3, and F4. Users can extend the basic calculator by adding scripts to implement additional calculator functionality for these keys.
In this example, the customer can add scripts into a scripts
subdirectory under the application's JAR file location. The scripts should be named F1.js
, F2.js
, F3.js
, and F4.js
. When the user presses any of the programmable function keys, the application invokes the corresponding function script. Each script file should implement a calculate
method that uses a java.util.Stack
argument. When you press the F1 key on the application GUI, the calculator invokes the calculate
method of the /scripts/F1.js
script.
The script has access to the calculator stack because the calculator model passes a java.util.Stack
object to the F1.js
script as an argument. The calculator model invokes the script's calculate
method as shown in Code Example 12.
Code Example 12: Invoke the script's calculate
method.
private Stack<Number> numStack;
...
Invocable invocable = invocableEngines[funcNumber];
result = (Double) invocable.invokeFunction("calculate", numStack);
Imagine that you run a mortgage company. Estimating monthly mortgage payments on loans is a common operation when you talk with customers, so you need a calculator that can perform that operation. If you want to associate that operation with the F1 key, you would create a /scripts/F1.js
file with a calculate(stack)
procedure. Code Example 13 shows what the script code for the F1 key's mortgage calculation might look like.
Code Example 13: Calculate monthly mortgage payments.
function calculate(stack) {
var monthlyPayment = Number.NaN;
var size = stack.size();
if (size >= 3) {
var years = Number(stack.pop());
var annualInterest = Number(stack.pop());
var principal = Number(stack.pop());
var monthlyInterest = annualInterest / 100 / 12;
var numberOfPayments = years * 12;
var x = Math.pow(1+monthlyInterest, numberOfPayments);
monthlyPayment = (principal*x*monthlyInterest)/(x-1);
if (!isNaN(monthlyPayment) &&
(monthlyPayment != Number.POSITIVE_INFINITY) &&
(monthlyPayment != Number.NEGATIVE_INFINITY)) {
monthlyPayment = Math.round(monthlyPayment*100)/100;
stack.push(monthlyPayment);
} else {
stack.push(principal);
stack.push(annualInterest);
stack.push(years);
monthlyPayment = Number.NaN;
}
}
return monthlyPayment;
}
The script's responsibility is to pop as many operands from the stack as necessary, perform the calculation, push the result back on the stack, and return the result at the end of the procedure. If a calculation problem occurs, the script tries to restore the stack and returns a NaN
value to indicate an error.
For the mortgage calculation, the script checks the stack to make sure that at least three values are available: the principal amount of the loan, the annual interest rate, and the number of years allowed for payment. If those values are on the stack, the script performs the calculation, pushes the result back on the stack, and returns the result from the procedure.
This script is in the application's scripts
subdirectory. To use the script, execute the application and input the required three arguments: principal amount, annual interest rate, and loan payment period in years. For example, input the principal amount of 300000
, and press the Enter key. Using the Enter key after each value, input the annual interest amount of 7.5
and the loan period of 30
years. All the operands are now on the stack. Press the F1 key to execute the script and to invoke the calculate
method. The calculator should then display the result 2097.64
as shown in Figure 3.
Figure 3. Press the F1 key to invoke the mortgage calculation.
Of course, you don't have to keep or use this particular script. You can change it as you like, or you can add other scripts for the F2, F3, and F4 keys on the calculator. After all, being able to extend core functionality is one of the primary benefits of scripting in the Java platform.
Summary
The JSR 223 specification defines scripting in the Java platform. The Java SE 6 platform implements this specification and currently JDK 6 and JRE 6 provide the Mozilla Rhino script engine for JavaScript technology support. Other script engines are available, and you can add them to your runtime environment as common JAR extensions.
You may include scripting support in your application for a variety of reasons:
- Sophisticated configuration options.
- User-defined functionality.
- Ease of maintenance after application release.
- Skill sets of users -- end users may be familiar with scripting languages but not the Java programming language.
- Reuse of code modules in other programming languages.
Using scripting from the Java platform is easy because the API is relatively small. You can quickly add scripting support to your application using only a handful of interfaces and classes in the javax.script
package.
Use the included demo application to get started with the scripting API. As a scripting demo, the ScriptCalc application does not implement all common calculator features. However, it does demonstrate how to mix scripting with the Java platform. You can create user-defined scripts that add functionality for programmable function keys in the GUI. The demo provides a script that adds a mortgage calculation to the F1 key.