Add Javascript Scripting to your Java Application

Extend your application’s usability with a scripting interface

“Success is not how high you have climbed, but how you make a positive difference to the world.” ― Roy T. Bennett, The Light in the Heart

1. Introduction

A non-trivial program of any type has a reasonably complex interface and probably supports multiple options with several ways of controlling input and output. Invoking such a program can be done via the command line, but here we run into limitations of what you can do with the command line. You can probably add options for all supported settings and multiple modes for the program to run.

There is however an easier way to offer all the power of your program to your users. This is by adding a scripting interface and exposing the main functionalities of the program to the script environment. Let us examine how we can do this in this article.

We use the Javascript scripting engine called Nashorn which is built into the JDK. This means you can add the power of scripting to your application with requiring any external libraries.

2. Create a Script Engine

First step is to create a script engine. You can do it as follows:

ScriptEngineManager mgr = new ScriptEngineManager();
ScriptEngine engine = mgr.getEngineByName("javascript");
Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);

The Bindings object is where you can expose the functionality of your application to the scripting environment. More on this topic below. For now, let us get the script environment running.

3. Execute Script from the Command Line

Let us add the ability to our program to treat the first argument to the program as a script and run it. This is done using the eval() method as follows.

if ( args.length > 0 ) {
    String arg = args[0];
    try { engine.eval(new FileReader(arg)); }
    catch(Exception ex) {
        System.err.println("Error executing " + arg + ": " + ex.getMessage());
    }
}

4. Adding a Read-Eval-Print-Loop (REPL)

To allow the user to enter commands to drive the program, let us add a simple REPL (Read-Eval-Print-Loop).

BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String line = null;
String prompt = "js >> ";
for (System.out.print(prompt) ;
     (line = in.readLine()) != null ; System.out.print(prompt) ) {
    line = line.trim();
    if ( line.isEmpty() ) continue;
    try { engine.eval(line); }
    catch(Exception ex) {
        System.err.println("Error: " + ex.getMessage());
    }
}

And that is all there is to it! Now your program is scriptable with Javascript.

5. The Bindings Glue

Till now, we have covered the basics of adding scripting to our application. However, the scripting environment does not yet have anything exposed from the application. In other words, it is time to add application-specific functionality to the scripting environment.

This is done using the Bindings object mentioned above.

First, let us expose the command line arguments passed to the application.

String[] args = ...;
bindings.put("argv", args);

Now, the arguments to the script can be accessed as argv from the script as shown here.

if ( argv.length == 1 ) {
  print("usage: sample.js file");
  exit(1);
}

Note that print() and exit() functions are installed into the script environment by Nashorn, and hence are always available.

6. Adding Shortcuts to Classes

While in the script environment, you can access java classes using Java.type().

var system = Java.type('java.lang.System');
print(system.getProperties());

If you find it somewhat cumbersome to access java classes this way, you can add a few bindings to your jave code as follows.

bindings.put("system", engine.eval("Java.type('java.lang.System')"));
bindings.put("Paths", engine.eval("Java.type('java.nio.file.Paths')"));
bindings.put("Files", engine.eval("Java.type('java.nio.file.Files')"));
bindings.put("File", engine.eval("Java.type('java.io.File')"));

Now, we can also access Paths, Files and File directly.

var file = 'image.png';
var data = Files.readAllBytes(Paths.get(file));

7. Adding Global Functions

As we saw above, it is a simple enough matter to expose classes to the script environment. How about adding some global functions? It is slightly involved as you can see below.

As an example of exposing application functionality to a script, let us add a function sha256() to Javascript to compute the SHA-256 hash of a binary array.

First wrap the function in a Function as follows:

static private class SHA256 implements Function<byte[],byte[]> {
}

This declaration specifies that the function accepts a byte[] array and returns a byte[] array. The computation is specified in the apply() method.

static private class SHA256 implements Function<byte[],byte[]> {
    public byte[] apply(byte[] barr) {
        String algo = "SHA-256";
        try {
            MessageDigest sha256 = MessageDigest.getInstance(algo);
            return sha256.digest(barr);
        } catch(java.security.NoSuchAlgorithmException ex) {
            System.err.println("Error: " + ex.getMessage());
            return null;
        }
    }
}

The binding to the above class is defined as follows:

bindings.put("sha256", new SHA256());

This function is available from javascript as sha256().

var data = Files.readAllBytes(Paths.get(file));
var sha = sha256(data);

In a similar way, here is the definition of a function which can convert binary to Base64 text.

static private Base64.Encoder encoder = Base64.getEncoder();
...
static private class B64Encode implements Function<byte[],String> {
    public String apply(byte[] src) {
        return encoder.encodeToString(src);
    }
}
...
bindings.put("btoa", new B64Encode());

And this function is available as btoa() in javascript.

var data = Files.readAllBytes(Paths.get(file));
var sha = sha256(data);
print(btoa(sha));

Note: btoa() is available as a global function in the browser environment. It is not available by default in Nashorn.

Conclusion

We have covered the basics of exposing your application functionality to a scripting environment using Nashorn. This is a very useful capability in any reasonably complex application, and is made very easy by the availability of Nashorn within the JDK.

Leave a Reply

Your email address will not be published. Required fields are marked *