loading...

JavaScript – Scripting Java with Rhino

Rhino is a JavaScript interpreter written in Java and designed
to make it easy to write JavaScript programs that leverage the power
of the Java platform APIs. Rhino automatically handles the conversion
of JavaScript primitives to Java primitives, and vice versa, so
JavaScript scripts can set and query Java properties and invoke Java
methods.

Obtaining Rhino

Rhino is free software from Mozilla. You can download a copy
from http://www.mozilla.org/rhino/. Rhino
version 1.7r2 implements ECMAScript 3 plus a number of the language
extensions described in Chapter 11. Rhino is
mature software and new versions are not released often. At the time
of this writing, a prerelease version of 1.7r3 is available from the
source repository and includes a partial implementation of ECMAScript 5.

Rhino is distributed as a JAR archive. Start it with a command
line like this:

java -jar rhino1_7R2/js.jar program.js

If you omit program.js, Rhino
starts an interactive shell, which is useful for trying out simple
programs and one-liners.

Rhino defines a handful of important global functions that are
not part of core JavaScript:

// Embedding-specific globals: Type help() at the rhino prompt for more
print(x);           // Global print function prints to the console
version(170);       // Tell Rhino we want JS 1.7 language features
load(filename,...); // Load and execute one or more files of JavaScript code
readFile(file);     // Read a text file and return its contents as a string
readUrl(url);       // Read the textual contents of a URL and return as a string
spawn(f);           // Run f() or load and execute file f in a new thread
runCommand(cmd,     // Run a system command with zero or more command-line args
           [args...]);
quit()              // Make Rhino exit

Notice the print() function:
we’ll use it in this section instead of console.log(). Rhino represents Java
packages and classes as JavaScript objects:

// The global Packages is the root of the Java package hierarchy
Packages.any.package.name // Any package from the Java CLASSPATH
java.lang                 // The global java is a shortcut for Packages.java
javax.swing               // And javax is a shortcut for Packages.javax

// Classes: accessed as properties of packages
var System = java.lang.System;
var JFrame = javax.swing.JFrame;

Because packages and classes are represented as JavaScript
objects, you can assign them to variables to give them shorter names.
But you can also more formally import them, if you want to:

var ArrayList = java.util.ArrayList; // Create a shorter name for a class
importClass(java.util.HashMap);      // Same as: var HashMap = java.util.HashMap

// Import a package (lazily) with importPackage(). 
// Don't import java.lang: too many name conflicts with JavaScript globals.
importPackage(java.util);
importPackage(java.net);

// Another technique: pass any number of classes and packages to JavaImporter()
// and use the object it returns in a with statement
var guipkgs = JavaImporter(java.awt, java.awt.event, Packages.javax.swing);
with (guipkgs) {
    /* Classes like Font, ActionListener and JFrame defined here */
}

Java classes can be instantiated using new, just like JavaScript classes
can:

// Objects: instantiate Java classes with new
var f = new java.io.File("/tmp/test"); // We'll use these objects below
var out = new java.io.FileWriter(f);

Rhino allows the JavaScript instanceof operator to work with Java
objects and classes:

f instanceof java.io.File        // => true
out instanceof java.io.Reader    // => false: it is a Writer, not a Reader
out instanceof java.io.Closeable // => true: Writer implements Closeable

As you can see, in the object instantiation examples above,
Rhino allows values to be passed to Java constructors and the return
value of those constructors to be assigned to JavaScript variables.
(Note the implicit type conversion that Rhino performs in this
example: the JavaScript string “/type/test” is automatically converted
into a Java java.lang.String value.) Java methods
are much like Java constructors, and Rhino allows JavaScript programs
to invoke Java methods:

// Static Java methods work like JavaScript functions
java.lang.System.getProperty("java.version") // Return Java version
var isDigit = java.lang.Character.isDigit;   // Assign static method to variable
isDigit("٢")                                 // => true: Arabic digit 2

// Invoke instance methods of the Java objects f and out created above
out.write("Hello World\n");     
out.close();                    
var len = f.length();

Rhino also allows JavaScript code to query and set the static
fields of Java classes and the instance fields of Java objects. Java
classes often avoid defining public fields in favor of getter and
setter methods. When getter and setter methods exist, Rhino exposes
them as JavaScript properties:

// Read a static field of a Java class
var stdout = java.lang.System.out;

// Rhino maps getter and setter methods to single JavaScript properties
f.name          // => "/tmp/test": calls f.getName()
f.directory     // => false: calls f.isDirectory()

Java allows overloaded methods that have the same name but
different signatures. Rhino can usually figure out which version of a
method you mean to invoke based on the type of the arguments you pass.
Occasionally, you need to specifically identify a method by name and
signature:

// Suppose the Java object o has a method named f that expects an int or
// a float. In JavaScript, you must specify the signature explicitly:
o['f(int)'](3);            // Call the int method
o['f(float)'](Math.PI);    // Call the float method

You can use a for/in loop to
iterate through the methods, fields, and properties of Java classes
and objects:

importClass(java.lang.System);
for(var m in System) print(m); // Print static members of the java.lang.System
for(m in f) print(m);          // Print instance members of java.io.File

// Note that you cannot enumerate the classes in a package this way
for (c in java.lang) print(c); // This does not work

Rhino allows JavaScript programs to get and set the elements of
Java arrays as if they were JavaScript arrays. Java arrays are not the
same as JavaScript arrays, of course: they are fixed length, their
elements are typed, and they don’t have JavaScript methods like slice(). There is no natural
JavaScript syntax that Rhino can extend to allow JavaScript programs to create new Java
arrays, so you have to do that using the java.lang.reflect.Array
class:

// Create an array of 10 strings and an array of 128 bytes
var words = java.lang.reflect.Array.newInstance(java.lang.String, 10);
var bytes = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 128);

// Once arrays are created, you can use them much like JavaScript arrays:
for(var i = 0; i < bytes.length; i++)  bytes[i] = i;

Java programming often involves implementing interfaces. This is
particularly common in GUI programing, where each event handler must
implement an event listener interface, and the following examples
demonstrate how to implement Java event listeners:

// Interfaces: Implement interfaces like this:
var handler = new java.awt.event.FocusListener({
    focusGained: function(e) { print("got focus"); },
    focusLost: function(e) { print("lost focus"); }
});

// Extend abstract classes in the same way
var handler = new java.awt.event.WindowAdapter({
    windowClosing: function(e) { java.lang.System.exit(0); }
});

// When an interface has just one method, you can just use a function instead
button.addActionListener(function(e) { print("button clicked"); });

// If all methods of an interface or abstract class have the same signature,
// then you can use a single function as the implementation, and Rhino will
// pass the method name as the last argument
frame.addWindowListener(function(e, name) {
    if (name === "windowClosing") java.lang.System.exit(0);
});

// If you need one object that implements multiple interfaces, use JavaAdapter:
var o = new JavaAdapter(java.awt.event.ActionListener, java.lang.Runnable, {
    run: function() {},              // Implements Runnable
    actionPerformed: function(e) {}  // Implements ActionListener
});

When a Java method throws an exception, Rhino propagates it as a
JavaScript exception. You can obtain the original Java
java.lang.Exception object through the java Exception property of
the JavaScript Error object:

try {
    java.lang.System.getProperty(null);  // null is not a legal argument
}
catch(e) {                       // e is the JavaScript exception
    print(e.javaException);      // it wraps a java.lang.NullPointerException
}

One final note about Rhino type conversion is necessary here.
Rhino automatically converts primitive numbers and booleans and the
null value as needed. Java’s
char type is treated as a
JavaScript number, since JavaScript doesn’t have a character type.
JavaScript strings are automatically converted to Java strings but
(and this can be a stumbling block) Java strings left as
java.lang.String objects are not converted back
to JavaScript strings. Consider
this line of code from earlier:

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

After calling this code, the variable version holds a
java.lang.String object. This usually behaves
like a JavaScript string, but there are important differences. First,
a Java string has a length() method
rather than a length property.
Second, the typeof operator returns
“object” for a Java string. You can’t convert a Java string to a
JavaScript string by calling its toString() method, because all Java objects
have their own Java toString()
method that returns a java.lang.String. To
convert a Java value to a string, pass it to the JavaScript String() function:

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

Rhino Example

Example 12-1 is a simple Rhino
application that demonstrates many of the features and techniques
described above. The example uses the
javax.swing GUI package, the
java.net networking package, the
java.io streaming I/O package, and Java’s
multithreading capabilities to implement a simple download manager
application that downloads URLs to local files and displays download
progress while it does so. Figure 12-1
shows what the application looks like when two downloads are
pending.

Figure 12-1. A GUI created with Rhino

Example 12-1. A download manager application with Rhino

/*
 * A download manager application with a simple Java GUI
 */

// Import the Swing GUI components and a few other classes
importPackage(javax.swing); 
importClass(javax.swing.border.EmptyBorder);
importClass(java.awt.event.ActionListener);
importClass(java.net.URL);
importClass(java.io.FileOutputStream);
importClass(java.lang.Thread);

// Create some GUI widgets
var frame = new JFrame("Rhino URL Fetcher");     // The application window
var urlfield = new JTextField(30);               // URL entry field
var button = new JButton("Download");            // Button to start download
var filechooser = new JFileChooser();            // A file selection dialog
var row = Box.createHorizontalBox();             // A box for field and button
var col = Box.createVerticalBox();               // For the row & progress bars
var padding = new EmptyBorder(3,3,3,3);          // Padding for rows

// Put them all together and display the GUI
row.add(urlfield);                               // Input field goes in the row
row.add(button);                                 // Button goes in the row
col.add(row);                                    // Row goes in the column
frame.add(col);                                  // Column goes in the frame
row.setBorder(padding);                          // Add some padding to the row
frame.pack();                                    // Set to minimum size
frame.visible = true;                            // Make the window visible

// When anything happens to the window, call this function.
frame.addWindowListener(function(e, name) {
    // If the user closes the window, exit the application.
    if (name === "windowClosing")                // Rhino adds the name argument
        java.lang.System.exit(0);
});

// When the user clicks the button, call this function
button.addActionListener(function() {
    try {
        // Create a java.net.URL to represent the source URL.
        // (This will check that the user's input is well-formed)
        var url = new URL(urlfield.text);
        // Ask the user to select a file to save the URL contents to.
        var response = filechooser.showSaveDialog(frame);
        // Quit now if they clicked Cancel
        if (response != JFileChooser.APPROVE_OPTION) return;
        // Otherwise, get the java.io.File that represents the destination file
        var file = filechooser.getSelectedFile();
        // Now start a new thread to download the url
        new java.lang.Thread(function() { download(url,file); }).start();
    }    catch(e) {
        // Display a dialog box if anything goes wrong
        JOptionPane.showMessageDialog(frame, e.message, "Exception",
                                     JOptionPane.ERROR_MESSAGE);
    }
});

// Use java.net.URL, etc. to download the content of the URL and use
// java.io.File, etc. to save that content to a file.  Display download
// progress in a JProgressBar component.  This will be invoked in a new thread.
function download(url, file) {
    try {
        // Each time we download a URL we add a new row to the window
        // to display the url, the filename, and the download progress
        var row = Box.createHorizontalBox();     // Create the row
        row.setBorder(padding);                  // Give it some padding
        var label = url.toString() + ": ";       // Display the URL 
        row.add(new JLabel(label));              //   in a JLabel
        var bar = new JProgressBar(0, 100);      // Add a progress bar
        bar.stringPainted = true;                // Display filename in
        bar.string = file.toString();            //   the progress bar
        row.add(bar);                            // Add bar to this new row
        col.add(row);                            // Add row to the column
        frame.pack();                            // Resize window

        // We don't yet know the URL size, so bar starts just animating
        bar.indeterminate = true; 

        // Now connect to the server and get the URL length if we can
        var conn = url.openConnection();         // Get java.net.URLConnection
        conn.connect();                          // Connect and wait for headers
        var len = conn.contentLength;            // See if we have URL length
        if (len) {                               // If length known, then 
            bar.maximum = len;                   //   set the bar to display 
            bar.indeterminate = false;           //   the percent downloaded
        }

        // Get input and output streams
        var input = conn.inputStream;            // To read bytes from server
        var output = new FileOutputStream(file); // To write bytes to file
        
        // Create an array of 4k bytes as an input buffer
        var buffer = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE,
                                                         4096);
        var num;
        while((num=input.read(buffer)) != -1) {  // Read and loop until EOF
            output.write(buffer, 0, num);        // Write bytes to file
            bar.value += num;                    // Update progress bar
        }
        output.close();                          // Close streams when done
        input.close();
    }
    catch(e) { // If anything goes wrong, display error in progress bar
        if (bar) {
            bar.indeterminate = false;           // Stop animating
            bar.string = e.toString();           // Replace filename with error
        }
    }
}

Comments are closed.

loading...