15.7.10

Scripting for the Java Platform

By John O'Conner, July 2006

Articles Index

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:

  1. Access and control Java technology-based objects from a scripting environment
  2. Create web content with scripting languages
  3. 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 ScriptEngineobjects that interpret scripts written in a specific scripting language. The simplest way to use this API is to do the following:

  1. Create a ScriptEngineManager object.
  2. Retrieve a ScriptEngine object from the manager.
  3. 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 ScriptEngineobject 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 associatedScriptEngine 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 theClass-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 amanifest.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 invokeMethodmethod. 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 evenNullPointerException exceptions. See the documentation on the Invocable interface for details.

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. ABindings 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 theinvokeFunction 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();
}

Message dialog

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).

ScriptCalc image

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.

Mortgage calculation

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.

For More Information


http://java.sun.com/developer/technicalArticles/J2SE/Desktop/scripting/

No comments: