Skip to main content

Scripting for the Java Platform

April 11, 2006

{cs.r.title}







One of the exciting new features of Java Standard Edition 6 will allow the tight integration of Java and scripting languages through a standard Java Scripting API. In this article I will take a closer look at how the current Mustang beta implements the underlying JSR-223 (Scripting for the Java Platform) and introduce you to its classes and interfaces. We will, however, concentrate on client-side scripting. Web scripting, which is also described in JSR-223, might be subject of a later article.

Benefits of Integrating Java and Scripting Languages

The JSR-223 specification (see the Resources section for details) identifies several areas where Java and scripting technologies interact. Among others are:

  • Java implementations of script interpreters
  • Embedded Java interpreters
  • Scripting of Java objects

Java has often been used to implement scripting languages, so it seems natural to embed them into applications. In doing so, you can greatly enhance the user experience of your program; for example, by providing an infrastructure to automate repetitive tasks. Prominent examples of such languages are Jython, Groovy, and the BeanShell.

But what does "scripting of Java objects" mean? It means Java classes and objects are exposed to a scripting language so that you can instantiate classes and access public members from within a script, using the syntax of the scripting language. This way, scripting languages can control Java applications but also benefit from the wealth of Java class libraries.

What can we expect from a Java scripting API? An application that wishes to execute commands in a certain scripting language must be able to determine which engines are present and what features these interpreters offer. This implies that a scripting language must be able to register itself, telling some instance what it can do. Also, the API needs to provide the means to actually execute statements or run scripts. Finally, it must model the exposure of Java objects to an interpreter.

Introducing javax.script

Currently, javax.script contains six interfaces, five classes, and one exception. From a client applications point of view, the entry class to scripting is ScriptEngineManager, which implements a discovery and instantiation mechanism for script engines. The Script Engines section contains details on how to implement this feature.

Applications utilize this mechanism by instantiating ScriptEngineManager. They can then obtain instances of specific script engines; for example, by invoking getEngineByExtension(), getEngineByMimeType(), or getEngineByName(). Alternatively, you can call getEngineFactories() and examine its results, which are instances of ScriptEngineFactory. We will take a closer look at this shortly.

Which mechanism you choose to obtain a script engine depends on how you embed scripting into your application. If you offer some editor into which the user can enter commands, he must be able to select the desired interpreter. I will show you how you can collect such information. On the other hand, if you offer a menu item like "Execute script" and open a file dialog, you can easily detect the required engine with getEngineByExtension().

ScriptDemo1.java shows how to obtain the available script engines using getEngineFactories().

package script;

import java.util.List;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class ScriptDemo1 {
 
  public static void main(String [] args) {
    ScriptEngineManager sem = new ScriptEngineManager();
    List list = sem.getEngineFactories();
    ScriptEngineFactory f;
    for (int i = 0; i < list.size(); i++) {
      f = (ScriptEngineFactory) list.get(i);
      String engineName = f.getEngineName();
      String engineVersion = f.getEngineVersion();
      String langName = f.getLanguageName();
      String langVersion = f.getLanguageVersion();
      System.out.println(engineName + " " +
        engineVersion + " " +
        langName + " " +
        langVersion);
      String statement = f.getOutputStatement(""hello, world"");
      System.out.println(statement);
      ScriptEngine e = f.getScriptEngine();
      try {
        e.eval(statement);
      } catch (ScriptException ex) {
        ex.printStackTrace();
      }
    }
  }
}

The program invokes several methods of a ScriptEngineFactory object, which provides metadata of a script engine. The current Mustang beta ships with one pre-installed script engine, the Mozilla Rhino implementation of JavaScript. Figure 1 shows the output of ScriptDemo1.

 ScriptDemo1 output
Figure 1. ScriptDemo1 output

Besides metadata, ScriptEngineManager provides some information about how to use a script engine and its underlying language. We will now examine this feature.

Constructing Scripts

As you have seen in ScriptDemo1.java, our use of getOutputStatement() returns a statement to display a message in the syntax of the language. You can even build complete scripts, passing their commands to getProgram(). ScriptDemo2.java shows you how to do this.

package script;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class ScriptDemo2 {
   
  public static void main(String [] args) {
    ScriptEngineManager sem = new ScriptEngineManager();
    ScriptEngine e = sem.getEngineByName("ECMAScript");
    ScriptEngineFactory f = e.getFactory();
    String [] statements = {
        "var a = 2;",
        "var b = 3;",
        "var c = a + b;",
        "print("a + b = " + c);"
    };
    String program = f.getProgram(statements);
    try {
      e.eval(program);
    } catch (ScriptException ex) {
      ex.printStackTrace();
    }
  }
}

The commands that make up a script must of course comply with the syntax of a certain language. Therefore, ScriptDemo2 requests the Rhino implementation by calling getEngineByName() with its short name ECMAScript. After a corresponding instance of ScriptEngineFactory has constructed the program, eval() actually runs it. As in the first example, you need to catch ScriptExceptions.

We now know how to determine which script engines are present and how we can run predefined scripts. However, they still can't communicate with your application. We will now learn how this is done from a scripts point of view. The Bindings and ScriptContext section discusses the underlying technologies.

The ScriptEngineFactory interface defines getMethodCallSyntax(). This method returns a statement that invokes a method of a Java object, in the syntax of the language associated with this factory. ScriptDemo3.java shows how to use this feature. Figure 2 displays its output.

package script;

import java.text.SimpleDateFormat;
import java.util.Date;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class ScriptDemo3 {
   
  public static void main(String [] args) {
    ScriptEngineManager sem = new ScriptEngineManager();
    ScriptEngine e = sem.getEngineByName("ECMAScript");
    ScriptEngineFactory f = e.getFactory();
    Date date = new Date();
    SimpleDateFormat sdf = new SimpleDateFormat();
    String statement = f.getMethodCallSyntax("sdf", "format", "date");
    System.out.println(statement);
    statement = f.getOutputStatement(statement);
    System.out.println(statement);
    e.put("sdf", sdf);
    e.put("date", date);
    try {
      e.eval(statement);
    } catch (ScriptException ex) {
      ex.printStackTrace();
    }
  }
}

ScriptDemo3 output
Figure 2. ScriptDemo3 output

Of course, the syntax of a method invocation depends on the scripting language, so the result of getMethodCallSyntax() will vary among engines. To actually make a Java object available to a script engine, your application uses put(), which is an instance method of ScriptEngine. But what exactly is a script engine?

Script Engines

Generally, script engines evaluate well-formed expressions and statements that comply with a certain language specification. Usually, scripting languages are interpreted, but this is no requirement in terms of javax.script. JSR-223 defines a script engine as a software component that consists of a so-called front end that parses the source code and produces an internal representation of this program. Such intermediate code is then executed by the back end. The front end and back end are reflected in two interfaces: Compilable and Invocable. I will turn to them later.

All script engines must implement javax.script.ScriptEngine. This interface supports basic scripting activities, such as executing scripts, setting and obtaining variables, etc. These variables are stored as key-value pairs. There are several reserved keys, such as ARGV, ENGINE, and ENGINE_VERSION, which all have a well-specified meaning. Implementors may add additional predefined keys.

Other key-value pairs are used by applications in order to expose Java objects. The Bindings and ScriptContext section will cover this topic.

Each script engine also has a corresponding ScriptEngineFactory. Engines may be instantiated using their constructors or by using the methods of ScriptEngineManager, as you have seen in ScriptDemo1 and ScriptDemo2. However, engines that are instantiated without the use of ScriptEngineManager do not have access to its global collection of key-value pairs.

How does ScriptEngineManager know which engines are present? It uses the Service Provider mechanism of the JAR file specification to discover and instantiate script engines. From a client application's point of view, it is sufficient to put the JAR file containing the engine on its class path; for example, by using the -cp option of the Java launcher. You can also put them in the global extension directory. Please refer to my article "The Java Extension Mechanism" for details.

The JAR file specification defines a service as a well-known set of interfaces and (usually) abstract classes. A service provider is a specific implementation of such a service. For javax.script, a service consists of one interface: ScriptEngineFactory. So classes implementing ScriptEngineFactory are service providers.

Service providers identify themselves by placing a so-called provider-configuration file in META-INF/services. Its filename corresponds to the fully qualified name of the service class, which is javax.script.ScriptEngineFactory. Each line of this file contains the fully qualified name of a class that implements ScriptEngineFactory.

You have seen that scripts can access members of Java objects and classes that have been exposed to a script engine by invoking their put() methods. The underlying mechanisms are referred to as Java language bindings. JSR-223 defines the technical infrastructure for them. Please keep in mind that, for example, the notation for a method invocation depends on the syntax of a particular scripting language.

Bindings and ScriptContext

A scope represents a set of key-value pairs. It is stored in objects implementing javax.script.Bindings. It maps the names of script variables to their values. As you have seen in ScriptDemo3, adding an object (which is done by calling put() on an instance of ScriptEngine) makes it available to scripts, using the specified key as its variable name. Reading the value of a key in the engine scope returns the value of the corresponding script variable.

One such scope is called engine scope. Its attributes are visible during the lifetime of a script engine. Each engine maintains its individual set of attributes. The so-called global scope is shared by all script engines that have been instantiated by the ScriptEngineFactory.

Sets of scopes are passed to script engines using instances of ScriptContext. Whenever a script engine executes a script, it uses a ScriptContext, which may have been passed to eval(). If this is not the case, a default script context, which is maintained by the script engine, is used. The default script context can be changed at any time.

Besides scopes, ScriptContext offers methods returning Readers and Writers. Script engines use them for input and output purposes.

Compilable and Invocable

In the Script Engines section, I briefly mentioned that, conceptually, a script engine consists of two parts, known as front end and back end. Using Compilable you can compile scripts to intermediate code and save them for later use. This code can then be executed without recompilation.

The advantage of this feature is an increase of efficiency if you need to run scripts often, because these scripts will be processed by the front end only once. The intermediate code produced by the script engine is stored by instances of CompiledScript. However, not all interpreters will expose their front ends and back ends programmatically, so Compilable is optional.

ScriptDemo4.java shows you how to compile scripts and how to invoke the generated code later on.

package script;

import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class ScriptDemo4 {
   
  public static void main(String [] args) {
    ScriptEngineManager sem = new ScriptEngineManager();
    ScriptEngine e = sem.getEngineByName("ECMAScript");
    ScriptEngineFactory f = e.getFactory();
    String statement = f.getOutputStatement(""hello"");
    if (e instanceof Compilable) {
      try {
        CompiledScript cs = ((Compilable) e).compile(statement);
        cs.eval();
        cs.eval();
      } catch (ScriptException ex) {
        ex.printStackTrace();
      }
    } else {
      System.out.println("sorry, Compilable is not implemented");
    }
  }
}

Whereas Compilable is based on intermediate code of complete scripts, Invocable allows the repeated invocation of individual procedures or functions of a script. Script engines are not required to implement this interface, so you need to check the availability of this feature; for example, by using instanceof.

ScriptDemo5.java shows you how to invoke a JavaScript function several times.

package script;

import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class ScriptDemo5 {
   
  public static void main(String [] args) {
    ScriptEngineManager sem = new ScriptEngineManager();
    ScriptEngine e = sem.getEngineByName("ECMAScript");
    ScriptEngineFactory f = e.getFactory();
    String statement = "function square(number) {return number * number;};\n" +
      "print(square(3));";
    try {
      e.eval(statement);
      if (e instanceof Invocable) {
        Invocable iv = (Invocable) e;
        try {
          System.out.println(iv.invoke("square", new Integer(4)));
          System.out.println(iv.invoke("square", new Integer(5)));
          System.out.println(iv.invoke("square", new Integer(6)));
        } catch (NoSuchMethodException ex) {
          ex.printStackTrace();
        } catch (ScriptException ex) {
          ex.printStackTrace();
        }
      }
    } catch (ScriptException ex) {
      ex.printStackTrace();
    }
  }
}

The script defines a function named square and calls it, passing 3. The Rhino implementation keeps the intermediate representation of this function, so it can be called from ScriptDemo5. Please note that the invocation of print is not part of the function, so we need to call System.out.println() from ScriptDemo5 to show the result of the computation.

Conclusion

The Java Scripting API is an exciting addition to the core class library. Embedding scripting languages into your application can significantly enhance the overall user experience of your program. I have already mentioned that the user can automate repetitive tasks. What is even more important is that she will be able combine scripting-enabled Java applications to solve complex chains of tasks using her favorite language.

Please keep in mind, however, that at the time of writing this article JSR-223 is available as a public review draft specification only. Until Java SE 6 is released later this year, there might be changes to the specification as well as the implementation. So what I have discussed in this article shows work in progress that might not represent the final state of scripting for the Java platform.

Resources

width="1" height="1" border="0" alt=" " />
Thomas Kunneth works as a software architect at the German authorities, specializing in Java-based rich clients.
Related Topics >> Programming   |