Section 12.2. Scripting Java


12.2. Scripting Java

JavaScript interpreters often include a feature that allows JavaScript code to query and set the fields and invoke the methods of Java objects. If a script has access to an object through a method argument or a Bindings object, it can operate on that Java object almost as if it were a JavaScript object. And even if a script is not passed any references to Java objects, it can create its own. Netscape was the first to implement Java scripting with JavaScript when they enabled their Spidermonkey interpreter to script applets in a web browser. Netscape called their technology LiveConnect. The Rhino interpreter and Microsoft's JScript interpreter have adopted LiveConnect syntax, and the name LiveConnect is used throughout this chapter to refer to any implementation of JavaScript-to-Java scripting.

Let's begin this section with an overview of LiveConnect features. This overview is followed by subsections that explain LiveConnect in detail.

Note that Rhino and Spidermonkey implement somewhat different versions of LiveConnect. The features described here are Rhino features that can be used in scripts embedded in Java 6. Spidermonkey implements a subset of these features, and this is discussed in Chapter 23.

When a Java object is passed to a JavaScript script through a Bindings object or passed to a JavaScript function, JavaScript can manipulate that object almost as if it were a native JavaScript object. All the public fields and methods of the Java object are exposed as properties of the JavaScript wrapper object. For example, suppose a script is passed a reference to a Java object that draws charts. Now, suppose this object defines a field named lineColor whose type is String and that the JavaScript script stores its reference to the charting object in a variable named chart. JavaScript code can then query and set this field with code like this:

 var chartcolor = chart.lineColor;  // Read a Java field. chart.lineColor = "#ff00ff";       // Set a Java field. 

JavaScript can even query and set the values of fields that are arrays. Suppose that the chart object defines two fields declared as follows (Java code):

 public int numPoints; public double[] points; 

A JavaScript program might use these fields with code like this:

 for(var i = 0; i < chart.numPoints; i++)     chart.points[i] = i*i; 

In addition to querying and setting the fields of a Java object, JavaScript can also invoke the methods of an object. Suppose, for example, that the chart object defines a method named redraw( ). This method takes no arguments and simply serves to tell the object that its points[] array has been modified and it should redraw itself. JavaScript can invoke this method just as if it were a JavaScript method:

 chart.redraw( ); 

JavaScript can also call methods that take arguments and return values. Type conversions are performed as necessary on the method arguments and return values. Suppose the chart object defines Java methods like these:

 public void setDomain(double xmin, double xmax); public void setChartTitle(String title); public String getXAxisLabel( ); 

JavaScript can call these methods with code like this:

 chart.setDomain(0, 20); chart.setChartTitle("y = x*x"); var label = chart.getXAxisLabel( ); 

Finally, note that Java methods can return Java objects as their return values, and JavaScript can read and write the public fields and invoke the public methods of these objects as well. JavaScript can also use Java objects as arguments to Java methods. Suppose the chart object defines a method named getXAxis( ) that returns another Java object that is an instance of a class named Axis. Suppose that the chart object also defines a method named setYAxis( ) that takes an Axis argument. Now, suppose further that Axis has a method named setTitle( ). You might use these methods with JavaScript code like this:

 var xaxis = chart.getXAxis( );  // Get an Axis object. var newyaxis = xaxis.clone( );  // Make a copy of it. newyaxis.setTitle("Y");        // Call a method of it... chart.setYAxis(newyaxis);      // ...and pass it to another method 

LiveConnect allows JavaScript code to create its own Java objects so that JavaScript can script Java even if no Java objects are passed to it.

The global symbol Packages provides access to all the Java packages that the JavaScript interpreter knows about. The expression Packages.java.lang refers to the java.lang package, and the expression Packages.java.lang.System refers to the java.lang.System class. For convenience, the global symbol java is a shortcut for Packages.java . JavaScript code might invoke a static method of this java.lang.System class as follows:

 // Invoke the static Java method System.getProperty( ) var javaVersion = java.lang.System.getProperty("java.version"); 

This use of LiveConnect is not limited to system classes because LiveConnect allows you to use the JavaScript new operator to create new instances of Java classes. As an example, consider the following JavaScript code that creates and displays Java Swing GUI components:

 // Define a shortcut to the javax.* package hierarchy. var javax = Packages.javax; // Create some Java objects. var frame = new javax.swing.JFrame("Hello World"); var button = new javax.swing.JButton("Hello World"); var font = new java.awt.Font("SansSerif", java.awt.Font.BOLD, 24); // Invoke methods on the new objects. frame.add(button); button.setFont(font); frame.setSize(200, 200); frame.setVisible(true); 

To understand how LiveConnect does its job of connecting JavaScript to Java, you have to understand the JavaScript datatypes that LiveConnect uses. The following sections explain these JavaScript datatypes.

12.2.1. The JavaPackage Class

A package in Java is a collection of related Java classes. The JavaPackage class is a JavaScript datatype that represents a Java package. The properties of a JavaPackage are the classes that the package contains (classes are represented by the JavaClass class, which you'll see shortly), as well as any other packages that the package contains. The classes in a JavaPackage are not enumerable, so you cannot use a for/in loop to inspect package contents.

All JavaPackage objects are contained within a parent JavaPackage; the global property named Packages is a top-level JavaPackage that serves as the root of this package hierarchy. It has properties such as java and javax, which are JavaPackage objects that represent the various hierarchies of Java classes that are available to the interpreter. For example, the JavaPackage Packages.java contains the JavaPackage Packages.java.awt. For convenience, the global object also has a java property that is a shortcut to Packages.java. Thus, instead of typing Packages.java.awt, you can simply type java.awt.

To continue with the example, java.awt is a JavaPackage object that contains JavaClass objects such as java.awt.Button, which represents the java.awt.Button class. But it also contains yet another JavaPackage object, java.awt.image, which represents the java.awt.image package in Java.

The JavaPackage class has a few shortcomings. There is no way for LiveConnect to tell in advance whether a property of a JavaPackage refers to a Java class or to another Java package, so JavaScript assumes that it is a class and tries to load a class. Thus, when you use an expression such as java.awt, LiveConnect first looks for a class by that name. If LiveConnect does not find a class, it assumes that the property refers to a package, but it has no way to ascertain that the package actually exists and has real classes in it. This causes the second shortcoming: if you misspell a class name, LiveConnect happily treats it as a package name, rather than telling you that the class you are trying to use does not exist.

12.2.2. The JavaClass Class

The JavaClass class is a JavaScript datatype that represents a Java class. A JavaClass object does not have any properties of its own: all of its properties represent (and have the same name as) the public static fields and methods of the represented Java class. These public static fields and methods are sometimes called class fields and class methods to indicate they are associated with a class rather than an object instance. Unlike the JavaPackage class, JavaClass does allow the use of the for/in loop to enumerate its properties. Note that JavaClass objects do not have properties representing the instance fields and methods of a Java class; individual instances of a Java class are represented by the JavaObject class, which is documented in the next section.

As shown earlier, JavaClass objects are contained in JavaPackage objects. For example, java.lang is a JavaPackage that contains a System property. Thus, java.lang.System is a JavaClass object, representing the Java class java.lang.System. This JavaClass object, in turn, has properties such as out and in that represent static fields of the java.lang.System class. You can use JavaScript to refer to any of the standard Java system classes in this same way. The java.lang.Double class is named java.lang.Double (or Packages.java.lang.Double), for example, and javax.swing.JButton class is Packages.javax.swing.JButton.

Another way to obtain a JavaClass object in JavaScript is to use the getClass( ) function. Given any JavaObject object, you can obtain a JavaClass object that represents the class of that Java object by passing the JavaObject to getClass( ).[*]

[*] Don't confuse the JavaScript getClass( ) function, which returns a JavaClass object, with the Java getClass( ) method, which returns a java.lang.Class object.

Once you have a JavaClass object, you can do several things with it. The JavaClass class implements the LiveConnect functionality that allows JavaScript programs to read and write the public static fields of Java classes and invoke the public static methods of Java classes. For example, java.lang.System is a JavaClass. You can read the value of a static field of java.lang.System like this:

 var java_console = java.lang.System.out; 

Similarly, you can invoke a static method of java.lang.System with a line like this:

 var java_version = java.lang.System.getProperty("java.version"); 

Recall that Java is a typed language: all fields and method arguments have types. If you attempt to set a field or pass an argument of the wrong type, an exception is thrown.

The JavaClass class has one more important feature. You can use JavaClass objects with the JavaScript new operator to create new instances of Java classesi.e., to create JavaObject objects. The syntax for doing so is just as it is in JavaScript (and just as it is in Java):

 var d = new java.lang.Double(1.23); 

Finally, having created a JavaObject in this way, we can return to the getClass( ) function and show an example of its use:

 var d = new java.lang.Double(1.23);   // Create a JavaObject. var d_class = getClass(d);            // Obtain the JavaClass of the JavaObject. if (d_class == java.lang.Double) ...; // This comparison will be true. 

Instead of referring to a JavaClass with a cumbersome expression such as java.lang.Double, you can define a variable that serves as a shortcut:

 var Double = java.lang.Double; 

This mimics the Java import statement and can improve the efficiency of your program because LiveConnect does not have to look up the lang property of java and the Double property of java.lang.

12.2.3. Importing Packages and Classes

The Rhino version of LiveConnect defines global functions for importing Java packages and classes. To import a package, pass a JavaPackage object to importPackage( ). To import a class, pass a JavaClass object to importClass( ):

 importPackage(java.util); importClass(java.awt.List); 

importClass( ) copies a single JavaClass object from its JavaPackage object into the global object. The importClass( ) call above is equivalent to this code:

 var List = java.awt.List; 

importPackage( ) does not actually copy all JavaClass objects out of a JavaPackage and into the global object. Instead (and with much the same effect), it adds the package to an internal list of packages to search when unresolved identifiers are encountered and copies only the JavaClass objects that are actually used. Thus, after making the importPackage( ) call shown above, you might use the JavaScript identifier Map. If this is not the name of a declared variable or function, it is resolved as the JavaClass java.util.Map and then stored in a newly defined Map property of the global object.

Note that it is a bad idea to call importPackage( ) on the java.lang package because java.lang defines a number of classes with the same name as built-in JavaScript constructors and conversion functions. As an alternative to importing packages, you simply copy the JavaPackage object to a more convenient place:

 var swing = Packages.javax.swing; 

The importPackage( ) and importClass( ) functions are not available in Spidermonkey, but single-class importing is easy to simulate and is safer than cluttering up the global namespace with imported packages.

12.2.4. The JavaObject Class

The JavaObject class is a JavaScript datatype that represents a Java object. The JavaObject class is, in many ways, analogous to the JavaClass class. As with JavaClass, a JavaObject has no properties of its own; all of its properties represent (and have the same names as) the public instance fields and public instance methods of the Java object it represents. As with JavaClass, you can use a JavaScript for/in loop to enumerate all the properties of a JavaObject object. The JavaObject class implements the LiveConnect functionality that allows you to read and write the public instance fields and invoke the public methods of a Java object.

For example, if d is a JavaObject that represents an instance of the java.lang.Double class, you can invoke a method of that Java object with JavaScript code like this:

 n = d.doubleValue( ); 

As shown earlier, the java.lang.System class also has a static field out. This field refers to a Java object of class java.io.PrintStream. In JavaScript, the corresponding JavaObject is referred to as:

 java.lang.System.out 

and a method of this object can be invoked like this:

 java.lang.System.out.println("Hello world!"); 

A JavaObject object also allows you to read and write the public instance fields of the Java object it represents. Neither the java.lang.Double class nor the java.io.PrintStream class used in the preceding examples has any public instance fields, however. But suppose you use JavaScript to create an instance of the java.awt.Rectangle class:

 r = new java.awt.Rectangle( ); 

You can then read and write its public instance fields with JavaScript code like the following:

 r.x = r.y = 0; r.width = 4; r.height = 5; var perimeter = 2*r.width + 2*r.height; 

The beauty of LiveConnect is that it allows a Java object, r, to be used just as if it were a JavaScript object. Some caution is required, however: r is a JavaObject and does not behave identically to regular JavaScript objects. The differences will be detailed later. Also, remember that unlike JavaScript, the fields of Java objects and the arguments of their methods are typed. If you do not specify JavaScript values of the correct types, you cause JavaScript exceptions.

12.2.5. Java Methods

Because LiveConnect makes Java methods accessible through JavaScript properties, you can treat those methods as data values, just as you can with JavaScript functions. Note, however, that Java instance methods are in fact methods and not functions, and they must be invoked through a Java object. Static Java methods can be treated like JavaScript functions, however, and may be imported into the global namespace for convenience:

 var isDigit = java.lang.Character.isDigit; 

12.2.5.1. Property accessor methods

In the Rhino version of LiveConnect, if a Java object has instance methods that look like property accessor (getter/setter) methods according to the JavaBeans naming conventions, LiveConnect makes the property exposed by those methods available directly as a JavaScript property. For example, consider the javax.swing.JFrame and javax.swing.JButton objects shown earlier. JButton has setFont( ) and getFont( ) methods, and JFrame has setVisible( ) and isVisible( ) methods. LiveConnect makes these methods available, but it also defines a font property for JButton objects and a visible property for JFrame objects. As a result, you can replace lines of code like these:

 button.setFont(font); frame.setVisible(true); 

with lines like these:

 button.font = font; frame.visible = true; 

12.2.5.2. Overloaded methods

Java classes can define more than one method by the same name. If you enumerate the properties of a JavaObject that has an overloaded instance method, you will see only a single property with the overloaded name. Usually, LiveConnect will invoke the correct method for you, based on the types of arguments you are supplying.

Occasionally, however, you may need to explicitly refer to a single overloading of a method. JavaObject and JavaClass make overloaded methods available through special properties that include the method name and the method argument types. Suppose you have a JavaObject o that has two methods named f, one that accepts an int argument and another that accepts a boolean argument. o.f is a function that invokes whichever Java method better matches its own argument. However, you can explicitly distinguish between the two Java methods with code like this:

 var f = o['f'];                     // either method var boolfunc = o['f(boolean)'];     // boolean method var intfunc = o['f(int)'];          // int method 

When you specify parentheses as part of a property name, you cannot use the regular "." notation to access it and must express it as a string within square brackets.

Note that the JavaClass type can also distinguish overridden static methods.

12.2.6. The JavaArray Class

The final LiveConnect datatype for JavaScript is the JavaArray class. As you might expect by now, instances of this class represent Java arrays and provide the LiveConnect functionality that allows JavaScript to read the elements of Java arrays. Like JavaScript arrays (and like Java arrays), a JavaArray object has a length property that specifies the number of elements it contains. The elements of a JavaArray object are read with the standard JavaScript [] array index operator. They can also be enumerated with a for/in loop. You can use JavaArray objects to access multidimensional arrays (actually arrays of arrays), just as in JavaScript or Java.

For example, let's create an instance of the java.awt.Polygon class:

 p = new java.awt.Polygon( ); 

The JavaObject p has properties xpoints and ypoints that are JavaArray objects representing Java arrays of integers. (To learn the names and types of these properties, look up the documentation for java.awt.Polygon in a Java reference manual.) You can use these JavaArray objects to randomly initialize the Java polygon with code like this:

 for(var i = 0; i < p.xpoints.length; i++)     p.xpoints[i] = Math.round(Math.random( )*100); for(var i = 0; i < p.ypoints.length; i++)     p.ypoints[i] = Math.round(Math.random( )*100); 

12.2.6.1. Creating Java arrays

LiveConnect has no built-in syntax for creating Java arrays or for converting JavaScript arrays to Java arrays. If you need to create a Java array, you must do so explicitly with the java.lang.reflect package:

 var p = new java.awt.Polygon( ); p.xpoints = java.lang.reflect.Array.newInstance(java.lang.Integer.TYPE,5); p.ypoints = java.lang.reflect.Array.newInstance(java.lang.Integer.TYPE,5); for(var i = 0; i < p.xpoints.length; i++) {     p.xpoints[i] = i;     p.ypoints[i] = i * i; } 

12.2.7. Implementing Interfaces with LiveConnect

Rhino LiveConnect allows JavaScript scripts to implement Java interfaces using a simple syntax: simply treat the interface's JavaClass object as a constructor and pass a JavaScript object that has properties for each of the interface's methods. You can use this feature, for example, to add an event handler to the GUI creation code shown earlier:

 // Import the stuff we'll need. importClass(Packages.javax.swing.JFrame); importClass(Packages.javax.swing.JButton); importClass(java.awt.event.ActionListener); // Create some Java objects. var frame = new JFrame("Hello World"); var button = new JButton("Hello World"); // Implement the ActionListener interface. var listener = new ActionListener({         actionPerformed: function(e) { print("Hello!"); }     }); // Tell the button what to do when clicked. button.addActionListener(listener); // Put the button in its frame and display. frame.add(button); frame.setSize(200, 200); frame.setVisible(true); 

12.2.8. LiveConnect Data Conversion

Java is a strongly typed language with a relatively large number of datatypes, while JavaScript is an untyped language with a relatively small number of types. Because of this major structural difference between the two languages, one of the central responsibilities of LiveConnect is data conversion. When JavaScript sets a Java field or passes an argument to a Java method, a JavaScript value must be converted to an equivalent Java value, and when JavaScript reads a Java field or obtains the return value of a Java method, that Java value must be converted to a compatible JavaScript value. Unfortunately, LiveConnect data conversion is done somewhat differently than the data conversion that is performed by the javax.script package.

Figures 12-1 and 12-2 illustrate how data conversion is performed when JavaScript writes Java values and when it reads them, respectively.

Figure 12-1. Data conversions performed when JavaScript writes Java values


Figure 12-2. Data conversions performed when JavaScript reads Java values


Note the following points about the data conversions illustrated in Figure 12-1:

  • Figure 12-1 does not show all possible conversions from JavaScript types to Java types. This is because JavaScript-to-JavaScript type conversions can occur before the JavaScript-to-Java conversion takes place. For example, if you pass a JavaScript number to a Java method that expects a java.lang.String argument, JavaScript first converts that number to a JavaScript string, which can then be converted to a Java string.

  • A JavaScript number can be converted to any of the primitive Java numeric types. The actual conversion performed depends, of course, on the type of the Java field being set or the method argument being passed. Note that you can lose precision doing this, for example, when you pass a large number to a Java field of type short or when you pass a floating-point value to a Java integral type.

  • A JavaScript number can also be converted to an instance of the Java class java.lang.Double but not to an instance of a related class, such as java.lang.Integer or java.lang.Float.

  • JavaScript does not have any representation for character data, so a JavaScript number may also be converted to the Java primitive char type.

  • A JavaObject in JavaScript is "unwrapped" when passed to Javathat is, it is converted to the Java object it represents. Note, however, that JavaClass objects in JavaScript are not converted to instances of java.lang.Class, as might be expected.

  • JavaScript arrays are not converted to Java arrays. JavaScript objects, arrays, and functions are converted to Java objects that do not have a standardized API and are usually considered opaque.

Also note these points about the conversions illustrated in Figure 12-2:

  • Since JavaScript does not have a type for character data, the Java primitive char type is converted to a JavaScript number, not a string, as might be expected.

  • A Java instance of java.lang.Double, java.lang.Integer, or a similar class is not converted to a JavaScript number. Like any Java object, it is converted to a JavaObject object in JavaScript.

  • A Java string is an instance of java.lang.String, so like any other Java object, it is converted to a JavaObject object rather than to an actual JavaScript string.

  • Any type of Java array is converted to a JavaArray object in JavaScript.

12.2.8.1. JavaScript conversion of JavaObjects

Notice in Figure 12-2 that quite a few Java datatypes, including Java strings (instances of java.lang.String), are converted to JavaObject objects in JavaScript rather than being converted to actual JavaScript primitive types, such as strings. This means that when you use LiveConnect, you'll often be working with JavaObject objects. JavaObject objects behave differently than other JavaScript objects, and you need to be aware of some common pitfalls.

First, it is not uncommon to work with a JavaObject that represents an instance of java.lang.Double or some other numeric object. In many ways, such a JavaObject behaves like a primitive number value, but be careful when using the + operator. When you use a JavaObject (or any JavaScript object) with +, you are specifying a string context, so the object is converted to a string for string concatenation instead of being converted to a number for addition. To make the conversion explicit, pass the JavaObject to the JavaScript Number( ) conversion function.

To convert a JavaObject to a JavaScript string, use the JavaScript String( ) conversion function rather than calling toString( ). All Java classes define or inherit a Java toString( ) method, so calling toString( ) on a JavaObject invokes a Java method and returns another JavaObject that wraps a java.lang.String, as demonstrated in the following code:

 var d = new java.lang.Double(1.234); var s = d.toString( );  // Converts to a java.lang.String, not a JavaScript string print(typeof s);       // Prints "object" since s is a JavaObject s = String(d);         // Now convert to a JavaScript string print(typeof s);       // Displays "string". 

Note also that JavaScript strings have a length property that is a number. A JavaObject that wraps a java.lang.String, on the other hand, has a length property that is a function representing the length( ) method of the Java string.

Another strange case is the JavaObject java.lang.Boolean.FALSE. Used in a string context, this value converts to false. Used in a Boolean context, however, it converts to true! This is because the JavaObject is non-null. The value held by the object simply does not matter for this conversion.




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