Skip to main content

Making Scripting Languages JSR-223-Aware

{cs.r.title}






The new Java Scripting API integrates scripting languages into the Java environment. JSR-223-aware applications can execute scripts and, if the scripting language supports this feature, exchange data objects with them. My article "Scripting for the Java Platform" shows you how to query available languages and how to communicate with them. But what does it take to make existing scripting languages JSR-223-aware? This article is based on my work on Java-AppleScript-Connector, a bridge between AppleScript and Java. I will explain important classes and interfaces you need to provide and offer sample implementations as well as best practices.

javax.script.ScriptEngineManager

For a client application, the entry class to scripting is javax.script.ScriptEngineManager. Its methods getEngineByExtension(), getEngineByMimeType(), and getEngineByName() return instances of javax.script.ScriptEngine. A JSR-223-compliant scripting language must implement this interface, and its methods must be fully functional. Alternatively, programs can invoke getEngineFactories(), which returns instances of ScriptEngineFactory. A factory exposes metadata that describes the corresponding engine. You must implement ScriptEngineFactory, too. ScriptEngineManager uses the service provider mechanism to obtain instances of all available ScriptEngineFactorys.

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 scripting, the service consists of javax.script.ScriptEngineFactory. All classes that implement this interface 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 service provider. For example, the factory class of Java-AppleScript-Connector is called AppleScriptScriptEngineFactory. Its fully qualified class name is de.thomaskuenneth.jasconn.AppleScriptScriptEngineFactory. So the file META-INF/services/javax.script.ScriptEngineFactory contains one line with exactly this class name.

To sum it up, a JSR-223-compliant scripting language must at least:

  • Implement javax.script.ScriptEngine.
  • Implement javax.script.ScriptEngineFactory.
  • Register itself as a service provider for javax.script.ScriptEngineFactory.

Though it is not a requirement, I suggest you give the classes implementing these interfaces names that start with the name of the language and end in ScriptEngine or ScriptEngineFactory as appropriate, which makes it easy to identify what these classes do. This article is accompanied by a sample implementation called DummyLanguage. (Please refer to the Resources section for details.) The sample code consists of two base classes called DummyLanguageScriptEngine and DummyLanguageScriptEngineFactory. We will now take a closer look at some of their methods.

Implementing ScriptEngine

Currently, classes implementing ScriptEngine must provide 14 methods. They can be divided into several groups:

  • A factory method
  • Methods dealing with contexts
  • Methods dealing with bindings
  • Methods dealing with key-value pairs
  • Methods that run scripts

Each script engine has a corresponding factory, which is returned by getFactory(). DummyLanguage always returns the same instance, which has been created upon initialization and is kept in a static variable.

Execution of scripts takes place in a so-called script context, which exposes Readers and Writers that can be used by the script engine. For example, getErrorWriter() tells the engine where error messages should go and setWriter() sets the Writer for scripts to use when displaying output. It is important to distinguish these from the Reader arguments of the eval() method, which specify the source of a script.

The script context also maintains so-called bindings, which are bound name-value pairs. Globally scoped attributes are visible by all engines that have been created by the same ScriptEngineManager. Engine-scoped key-value pars, on the other hand, belong to individual instances of script engines and are visible only during their lifetime. Script contexts are maintained by classes implementing the ScriptContext interface. Sun provides a basic implementation called SimpleScriptContext.

Two methods of ScriptEngine deal with script contexts. getContext() returns the default context that is used if no script context is specified; for example, as an argument to eval(). Appropriately, setContext() sets it. The initial default context of DummyLanguage is an instance of SimpleScriptContext.

Bindings are name-value pairs whose keys are always Strings. Their behavior is defined through the javax.script.Bindings interface. As for ScriptContext, Sun provides a basic implementation called SimpleBindings. Although bindings belong to script contexts, ScriptEngine provides createBindings(), which returns an uninitialized binding. DummyLanguage simply creates an instance of SimpleBindings. Another method, getBindings(), exists to return the bindings of a certain scope. As you have already seen, there are at least two scopes, ScriptContext.GLOBAL_SCOPE and ScriptContext.ENGINE_SCOPE. They represent key-value pairs that are either visible to all instances of a script engine that have been created by the same ScriptengineManager, or visible only during the lifetime of a certain script engine instance. Though this may seem quite difficult to implement, the documentation gives us a clue how to do it. It says:

The Bindings instances that are returned must be identical to those returned by the getBindings() method of ScriptContext called with corresponding arguments on the default ScriptContext of the ScriptEngine.

So we just have to call getContext() to retrieve the current default context and then invoke its getBindings() method, passing the requested scope as its only parameter. The same applies to setBindings(), which sets the bindings of some scope. DummyLanguage just does this:

getContext().setBindings(bindings, scope); ScriptEngine has two more methods that access key-value pairs. get() retrieves a value that has been set in the engine scope. So DummyLanguage just has to return the result of getBindings(ScriptContext.ENGINE_SCOPE).get(). Its counterpart, put(), sets a key-value pair in the engine scope, so you first need to call getBindings(ScriptContext.ENGINE_SCOPE) and then invoke its put() method.

There are currently seven predefined names that have a special meaning. ENGINE maps to the name of the script engine implementation. ENGINE_VERSION identifies its version. LANGUAGE holds the full name of the scripting language supported by this implementation, whereas NAME contains its short name. The version of the scripting language being implemented is specified through the LANGUAGE_VERSION key. The name of the current script is stored in FILENAME. Finally, ARGV contains an array of positional arguments. A good place to set these key-value pairs is the constructor, where you can invoke put().

There are a few more methods of ScriptEngine we need to look at. All six deal with script execution. Generally, there are three versions:

  • One takes just the script.
  • One takes the script and a script context.
  • One takes the script and a binding.

The script can be passed as a string or through a Reader. In order to minimize the implementation effort, you can convert Reader-based scripts into strings by utilizing StringWriter and then invoking the corresponding method that expects a String. The DummyLanguageScriptEngine shows you how to do that. If no script context is specified, the default one is used. Consequently, your implementation might look like this: return eval(script, getContext()). If a Bindings argument has been passed, it is used as the ENGINE_SCOPE bindings of the script engine. Once again, the documentation gives us a clue what to do here.

The Reader, Writer, and non-ENGINE_SCOPE Bindings of the default ScriptContext are used [for script execution]. The ENGINE_SCOPE Bindings of the ScriptEngine is not changed, and its mappings are unaltered by the script execution.

The underlying idea is to temporarily change the engine scope bindings and re-set the original version after the script execution. Here is what DummyLanguage does.

Bindings current = getContext().getBindings(ScriptContext.ENGINE_SCOPE);
getContext().setBindings(bindings, ScriptContext.ENGINE_SCOPE);
Object result = eval(reader);
getContext().setBindings(current, ScriptContext.ENGINE_SCOPE);

How to implement the actual script execution (i.e., what is done within eval()) of course depends on the scripting language in question. If it has been implemented using Java, you may be able to pass bindings; on the other hand, this will be quite difficult if you need to start external processes that know nothing about Java objects. Still, depending on the target language, basic script invocation may be quite easy. Here is how Java-AppleScript-Connector interacts with native resources.

public Object eval(String string, 
                   ScriptContext scriptContext)
                   throws ScriptException {
  NSAppleScript script = new NSAppleScript(string);
  NSMutableDictionary errors = new NSMutableDictionary();
  NSAppleEventDescriptor results = script.execute(errors);
  if (errors.count() == 0) {
    return results;
  }
  System.err.println(errors.toString());
  return null;
}

As you can see, invoking AppleScript scripts involves just three steps: first, we transform the String-based representation into an object. Then we instantiate a container object that will collect errors. Finally, we execute the script.

The following section covers the second mandatory interface for JSR-223-compliant scripting languages, ScriptEngineFactory.

ScriptEngineFactory

A ScriptEngineFactory provides information about script engines and allows you to instantiate them. Once you have a ScriptEngineManager object, you can invoke its getEngineFactories() to get a java.util.List of ScriptEngineFactory instances.

getScriptEngine() returns an instance of the script engine that is represented by this factory. Implementations may choose to pool, share, or reuse instances, but should generally create new ones. For the sake of brevity, DummyLanguage always returns the same object.

getEngineName() and getEngineVersion() return the full name of the script engine and its version. getLanguageName() and getLanguageVersion() provide similar information regarding the language the script engine is implementing. ScriptEngineFactory implementations could store such values as local copies. However, some of them are available through bindings, too. So it is easy to obtain them like this:

return getScriptEngine().get(ScriptEngine.ENGINE).toString();

The same applies to getParameter(), which can be implemented easily as follows:

 
return getScriptEngine().get(key).toString();


The documentation of this method describes a reserved key called THREADING
, which seems to be described nowhere else. I suggest you set this value just like the other keys that already are defined as constants in ScriptEngine.

Other methods return immutable lists of names, MIME types, or extensions. DummyLanguage creates them using Arrays.asList(). The remaining methods deal with language-specific features. For example, getProgram() returns a valid scripting language program consisting of the passed statements. Please refer to the preliminary API documentation of ScriptEngineFactory for more information on these methods. When concatenating Strings, please consider the use of StringBuffer for efficiency reasons. This applies to getOutputStatement() and getMethodCallSyntax(), as well.

Conclusion

Establishing a basic link between Java and existing scripting languages using JSR 223 is quite easy. The number of interfaces you need to implement is fairly small. Additionally, Sun provides useful helper classes that you can take advantage of in your implementation. However, invoking existing scripts is just the beginning. True interaction takes place only if both Java and the scripting language can access each other's variables. My experience from my work on Java-AppleScript-Connector is that this is much harder to achieve.

Also, please take into account that there are more interfaces that an advanced implementation of JSR 223 should offer. These are javax.script.Compilable and javax.script.Invocable.

A final point I would like to make is that although there is now a proposed final draft of the JSR 223 specification, the new package javax.script still might be subject to change. So if you decide to jump on the new Java Scripting API please take a close look at the documentation once the specification is final.

Resources

Related Topics >> Programming   |