Section 12.1. Embedding JavaScript


12.1. Embedding JavaScript

We 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.[*]

[*] At the time of this writing, Java 6 is still under development. The javax.script package appears to be stable enough to document, but there is a chance that the APIs documented here may change before the final release.

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

 import javax.script.*; import java.io.*; // Evaluate a file of JavaScript and print its result public class RunScript {     public static void main(String[] args) throws IOException {         // Obtain an interpreter or "ScriptEngine" to run the script.         ScriptEngineManager scriptManager = new ScriptEngineManager( );         ScriptEngine js = scriptManager.getEngineByExtension("js");         // The script file we are going to run         String filename = null;         // A Bindings object is a symbol table for or namespace for the         // script engine. It associates names and values and makes         // them available to the script.         Bindings bindings = js.createBindings( );         // Process the arguments. They may include any number of         // -Dname=value arguments, which define variables for the script.         // Any argument that does not begin with -D is taken as a filename         for(int i = 0; i < args.length; i++) {             String arg = args[i];             if (arg.startsWith("-D")) {                 int pos = arg.indexOf('=');                 if (pos == -1) usage( );                 String name = arg.substring(2, pos);                 String value = arg.substring(pos+1);                 // Note that all the variables we define are strings.                 // Scripts can convert them to other types if necessary.                 // We could also pass a java.lang.Number, a java.lang.Boolean                 // or any Java object or null.                 bindings.put(name, value);             }             else {                 if (filename != null) usage( ); // only one file please                 filename = arg;             }         }         // Make sure we got a file out of the arguments.         if (filename == null) usage( );         // Add one more binding using a special reserved variable name         // to tell the script engine the name of the file it will be executing.         // This allows it to provide better error messages.         bindings.put(ScriptEngine.FILENAME, filename);         // Get a stream to read the script.         Reader in = new FileReader(filename);         try {             // Evaluate the script using the bindings and get its result.             Object result = js.eval(in, bindings);             // Display the result.             System.out.println(result);         }         catch(ScriptException ex) {             // Or display an error message.             System.out.println(ex);         }     }     static void usage( ) {         System.err.println(                  "Usage: java RunScript [-Dname=value...] script.js");         System.exit(1);     } } 

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.[*]

[*] As shown later in this chapter, JavaScript code can use any public members of any public class. For this reason, you will typically run any Java code that executes a user-defined script with a restricted set of security permissions. A discussion of the Java security framework is beyond the scope of this chapter, however.

Example 12-2. A Java configuration file utility that interprets JavaScript expressions

 import javax.script.*; import java.util.*; import java.io.*; /**  * This class is like java.util.Properties but allows property values to  * be determined by evaluating JavaScript expressions.  */ public class Configuration {     // Here is where we store name/value pairs of defaults.     Map<String,Object> defaults = new HashMap<String,Object>( );     // Accessors for getting and setting values in the map     public Object get(String key) { return defaults.get(key); }     public void put(String key, Object value) { defaults.put(key, value); }     // Initialize the contents of the Map from a file of name/value pairs.     // If a value is enclosed in curly braces, evaluate it as JavaScript.     public void load(String filename) throws IOException, ScriptException {         // Get a JavaScript interpreter.         ScriptEngineManager manager = new ScriptEngineManager( );         ScriptEngine engine = manager.getEngineByExtension("js");         // Use our own name/value pairs as JavaScript variables.         Bindings bindings = new SimpleBindings(defaults);         // Create a context for evaluating scripts.         ScriptContext context = new SimpleScriptContext( );         // Set those Bindings in the Context so that they are readable         // by the scripts but so that variables defined by the scripts do         // not get placed into our Map object.         context.setBindings(bindings, ScriptContext.GLOBAL_SCOPE);         BufferedReader in = new BufferedReader(new FileReader(filename));         String line;         while((line = in.readLine( )) != null) {             line = line.trim( );  // strip leading and trailing space             if (line.length( ) == 0) continue;    // skip blank lines             if (line.charAt(0) == '#') continue; // skip comments             int pos = line.indexOf(":");             if (pos == -1)                 throw new IllegalArgumentException("syntax: " + line);             String name = line.substring(0, pos).trim( );             String value = line.substring(pos+1).trim( );             char firstchar = value.charAt(0);             int len = value.length( );             char lastchar = value.charAt(len-1);             if (firstchar == '"' && lastchar == '"') {                 // Double-quoted quoted values are strings                 defaults.put(name, value.substring(1, len-1));             }             else if (Character.isDigit(firstchar)) {                 // If it begins with a number, try to parse a number                 try {                     double d = Double.parseDouble(value);                     defaults.put(name, d);                 }                 catch(NumberFormatException e) {                     // Oops.  Not a number.  Store as a string                     defaults.put(name, value);                 }             }             else if (value.equals("true"))         // handle boolean values                 defaults.put(name, Boolean.TRUE);             else if (value.equals("false"))                 defaults.put(name, Boolean.FALSE);             else if (value.equals("null"))                 defaults.put(name, null);             else if (firstchar == '{' && lastchar == '}') {                 // If the value is in curly braces, evaluate as JavaScript code                 String script = value.substring(1, len-1);                 Object result = engine.eval(script, context);                 defaults.put(name, result);             }             else {                 // In the default case, just store the value as a string.                 defaults.put(name, value);             }         }     }     // A simple test program for the class     public static void main(String[] args) throws IOException, ScriptException     {         Configuration defaults = new Configuration( );         defaults.load(args[0]);         Set<Map.Entry<String,Object>> entryset = defaults.defaults.entrySet( );         for(Map.Entry<String,Object> entry : entryset) {             System.out.printf("%s: %s%n", entry.getKey( ), entry.getValue( ));         }     } } 

12.1.1. Type Conversion with javax.script

Whenever 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:

  • Boolean objects convert to JavaScript booleans.

  • All java.lang.Number objects convert to Java numbers.

  • Java Character and String objects convert to JavaScript strings.

  • The Java null value converts to the JavaScript null value.

  • Any other Java object is simply wrapped in a JavaScript JavaObject object. You'll learn more about the JavaObject type when I discuss how JavaScript can script Java later in the chapter.

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:

  • JavaScript boolean values are converted to Java Boolean objects.

  • JavaScript string values are converted to Java String objects.

  • JavaScript numbers are converted to Java Double objects. Infinity and NaN values are properly converted.

  • The JavaScript null and undefined values convert to the Java null value.

  • JavaScript objects and arrays convert to Java objects of opaque type. These values can be passed back to JavaScript but have an unpublished API that is not intended for use by Java programs. Note that JavaScript wrapper objects of type String, Boolean, and Number are converted to opaque Java objects rather than to their corresponding Java types.

12.1.2. Compiling Scripts

If 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 Functions

The 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 JavaScript

The 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

 import javax.script.*; import java.io.*; import java.awt.event.*; import javax.swing.*; public class Keys {     public static void main(String[] args) throws ScriptException, IOException     {         // 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 definitions in it.         js.eval(new FileReader("listener.js"));         // Cast to Invocable and get an object that implements KeyListener.         Invocable invocable = (Invocable) js;         KeyListener listener = invocable.getInterface(KeyListener.class);         // Now use that KeyListener in a very simple GUI.         JFrame frame = new JFrame("Keys Demo");         frame.addKeyListener(listener);         frame.setSize(200, 200);         frame.setVisible(true);     } } 

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.




JavaScript. The Definitive Guide
JavaScript: The Definitive Guide
ISBN: 0596101996
EAN: 2147483647
Year: 2004
Pages: 767

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net