12.1. Embedding JavaScriptWe are nearing the end of the Core JavaScript section of this book. Part II of this book is entirely devoted to JavaScript as it is used in web browsers. Before beginning that discussion, however, let's briefly look at how JavaScript can be embedded into other applications. The motivation for embedding JavaScript within applications is typically to allow users to customize the application with scripts. The Firefox web browser, for example, uses JavaScript scripts to control the behavior of its user interface. Many other highly configurable applications use scripting languages of some sort or another. Two open source JavaScript interpreters are available from the Mozilla project. SpiderMonkey is the original JavaScript interpreter and is written in C. Rhino is an implementation in Java. Both have embedding APIs. If you want to add scripting to a C application, you can use Spidermonkey; if you want to add scripting to a Java application, you can use Rhino. You can learn more about using these interpreters in your own applications at http://www.mozilla.org/js/spidermonkey and http://www.mozilla.org/rhino. With the advent of Java 6.0, it becomes particularly easy to add JavaScript scripting functionality to your Java applications, and this is what is discussed here. Java 6 introduces a new javax.script package that provides a generic interface to scripting languages and is bundled with a version of Rhino that uses the new package.[*]
Example 12-1 demonstrates the basic use of the javax.script package: it obtains a ScriptEngine object that represents an instance of a JavaScript interpreter and a Bindings object that holds JavaScript variables. It then runs a script stored in an external file by passing a java.io.Reader stream and the bindings to the eval( ) method of the ScriptEngine. The eval( ) method returns the result of the script or throws a ScriptException if something goes wrong. Example 12-1. A Java program for running JavaScript scripts
The Bindings object created in this code snippet is not static: any variables created by the JavaScript script are stored there as well. Example 12-2 is a more realistic example of scripting Java. It stores its Bindings object in a ScriptContext object in a higher scope so that the variables can be read, but new variables are not stored into the Bindings object. The example implements a simple configuration file utility: a text file is used to define name/value pairs, which can be queried through the Configuration class defined here. Values may be strings, numbers, or booleans, and if a value is enclosed in curly braces, it is passed to a JavaScript interpreter for evaluation. Interestingly, the java.util.Map object that holds these name/value pairs is wrapped in a SimpleBindings object so that the JavaScript interpreter can also access the value of other variables defined in the same file.[*]
Example 12-2. A Java configuration file utility that interprets JavaScript expressions
12.1.1. Type Conversion with javax.scriptWhenever one language is invoked from another, you must consider the question of how the types of one language are mapped to the types of the other language. Suppose you bind a java.lang.String and a java.lang.Integer to variables in a Bindings object. When a JavaScript script uses those variables, what are the types of the values it sees? And if the result of evaluating the script is a JavaScript boolean value, what is the type of object returned by the eval( ) method? In the case of Java and JavaScript, the answer is fairly straightforward. When you store a Java object (there is no way to store primitive values) in a Bindings object, it converts to JavaScript as follows:
There are a few additional details to understand about number conversion. All Java numbers convert to JavaScript numbers. This includes Byte, Short, Integer, Long, Float, Double, and also java.math.BigInteger and java.math.BigDouble. Special floating-point values such as Infinity and NaN are supported in both languages and convert from one to the other. Note that the JavaScript number type is based on a 64-bit floating-point value akin to Java's double type. Not all long values can be precisely represented within a double, so if you pass a Java long to JavaScript, you may lose data. The same caution applies when using BigInteger and BigDecimal: TRailing digits may be lost if the Java value has more precision than JavaScript can represent. Or if the Java value is larger than Double.MAX_VALUE, it will be converted to a JavaScript Infinity value. Conversions in the other direction are similarly straightforward. When a JavaScript script stores a value in a variable (and therefore in a Bindings object) or when a JavaScript expression is evaluated, JavaScript values are converted to Java values as follows:
12.1.2. Compiling ScriptsIf you want to execute the same script repeatedly (presumably using different bindings each time), it is more efficient to compile the script first and then invoke the compiled version. You can do this with code like this: // This is the text of the script we want to compile. String scripttext = "x * x"; // Get the script engine. ScriptEngineManager scriptManager = new ScriptEngineManager( ); ScriptEngine js = scriptManager.getEngineByExtension("js"); // Cast it to the Compilable interface to get compilation functionality. Compilable compiler = (Compilable)js; // Compile the script to a form that we can execute repeatedly. CompiledScript script = compiler.compile(scripttext); // Now execute the script five times, using a different value for the // variable x each time. Bindings bindings = js.createBindings( ); for(int i = 0; i < 5; i++) { bindings.put("x", i); Object result = script.eval(bindings); System.out.printf("f(%d) = %s%n", i, result); } 12.1.3. Invoking JavaScript FunctionsThe javax.script package also allows you to evaluate a script once, and then repeatedly invoke the functions defined by that script. You can do so like this: // Obtain an interpreter or "ScriptEngine" to run the script ScriptEngineManager scriptManager = new ScriptEngineManager( ); ScriptEngine js = scriptManager.getEngineByExtension("js"); // Evaluate the script. We discard the result since we only // care about the function definition. js.eval("function f(x) { return x*x; }"); // Now, invoke a function defined by the script. try { // Cast the ScriptEngine to the Invokable interface to // access its invocation functionality. Invocable invocable = (Invocable) js; for(int i = 0; i < 5; i++) { Object result = invocable.invoke("f", i); // Compute f(i) System.out.printf("f(%d) = %s%n", i, result); // Print result } } catch(NoSuchMethodException e) { // This happens if the script did not define a function named "f". System.out.println(e); } 12.1.4. Implementing Interfaces in JavaScriptThe Invocable interface demonstrated in the previous section also provides the ability to implement interfaces in JavaScript. Example 12-3 uses the JavaScript code in the file listener.js to implement the java.awt.event.KeyListener interface: Example 12-3. Implementing a Java interface with JavaScript code
Implementing an interface in JavaScript simply means defining a function with the same name as each method defined by the interface. Here, for example, is a simple script that implements KeyListener: function keyPressed(e) { print("key pressed: " + String.fromCharCode(e.getKeyChar( ))); } function keyReleased(e) { /* do nothing */ } function keyTyped(e) { /* do nothing */ } Note that the JavaScript keyPressed( ) method defined here accepts a java.awt.event.KeyEvent object as its argument and actually invokes a method on that Java object. The next section explains how this is done. |