Taming Mustang, Part 3: A New Script Engine
Welcome to the final installment in a three-part series that explores some of the new features in Java Standard Edition 6. If you recall, the first part focused on enhancements to the Collections API; the second part toured the new Scripting API. Because I'm not finished with the Scripting API, Part 3 introduces you to my miniature expression language and shows you how to implement a script engine for this language.
Implement Your Own Script Engine
Implementing a custom script engine is not as difficult as it might seem. To prove this to you, I’ve developed a script engine that executes scripts written in my miniature expression language (Minexp). This language lets you create expressions involving integer literals, five operators, and parentheses (to change precedence). The following Backus-Naur-type notation describes this language:
expression := term | term ( ’+’ | ’-’ ) expression term := factor | factor ( ’*’ | ’/’ | ’%’ ) term factor := number | ’-’ factor | ’(’ expression ’)’ number := digit | digit number digit := ( ’0’ | ’1’ | ’2’ | ’3’ | ’4’ | ’5’ | ’6’ | ’7’ | ’8’ | ’9’ )
This notation identifies the language’s grammar in terms of rules. Each rule specifies a name on the left side of := (which reads "is a"), and the elements on the right, which are read in left-to-right order. Also, | indicates a choice, () indicates a grouping of similar elements, and ’’ indicates a literal. Example: A number is a digit or a digit followed by a number.
I’ve created a MyScriptEngine class that interprets this grammar via a recursive-descent parser. This parser is implemented by MyScriptEngine’s private Tokenizer inner class, and its private int expr(Tokenizer t), private int term(Tokenizer t), and private int factor(Tokenizer t) methods—each method throws ScriptException.
Listing 1 MyScriptEngine.java
// MyScriptEngine.java import java.io.*; import javax.script.*; public class MyScriptEngine extends AbstractScriptEngine { public Bindings createBindings () { return null; // Uninitialized bindings not needed because Minexp does // not support bindings. } public Object eval (Reader reader, ScriptContext context) throws ScriptException { if (reader == null || context == null) throw new NullPointerException (); StringBuffer sb = new StringBuffer (50); // Assume scripts <= 50 chars try { int ch; while ((ch = reader.read ()) != -1) sb.append ((char) ch); } catch (IOException e) { throw new ScriptException ("Unable to read stream", "<unknown>", -1, -1); } return eval (sb.toString (), context); } public Object eval (String script, ScriptContext context) throws ScriptException { if (script == null || context == null) throw new NullPointerException (); // Create a tokenizer to return tokens from the script. Tokenizer t = new Tokenizer (script); // Use the tokenizer to help execute the script expression. int i = expr (t); // A valid expression contains no extra characters. if (t.getType () != Tokenizer.EOS) throw new ScriptException ("Extra characters: " + t.getToken (), "<unknown>", -1, t.getPos ()-1); return new Integer (i); } public ScriptEngineFactory getFactory () { return new MyScriptEngineFactory (); } private int expr (Tokenizer t) throws ScriptException { int res = term (t); String tok = t.getToken (); while (tok.equals ("+") || tok.equals ("-")) { if (tok.equals ("+")) res += term (t); else if (tok.equals ("-")) res -= term (t); tok = t.getToken (); } return res; } private int term (Tokenizer t) throws ScriptException { int res = factor (t); String tok = t.getToken (); while (tok.equals ("*") || tok.equals ("/") || tok.equals ("%")) { if (tok.equals ("*")) res *= factor (t); else if (tok.equals ("/")) try { res /= factor (t); } catch (ArithmeticException e) { throw new ScriptException ("Divide by zero", "<unknown>", -1, t.getPos ()-1); } else if (tok.equals ("%")) try { res %= factor (t); } catch (ArithmeticException e) { throw new ScriptException ("Divide by zero", "<unknown>", -1, t.getPos ()-1); } tok = t.getToken (); } return res; } private int factor (Tokenizer t) throws ScriptException { t.nextToken (); String tok = t.getToken (); if (t.getType () == Tokenizer.NUMBER) try { int i = Integer.parseInt (tok); t.nextToken (); return i; } catch (NumberFormatException e) { throw new ScriptException ("Invalid number: " + tok, "<unknown>", -1, t.getPos ()-1); } if (tok.equals ("-")) return -factor (t); if (tok.equals ("(")) { int res = expr (t); tok = t.getToken (); if (!tok.equals (")")) throw new ScriptException ("Missing )", "<unknown>", -1, t.getPos ()); t.nextToken (); return res; } if (t.getType () == Tokenizer.EOS) throw new ScriptException ("Missing token", "<unknown>", -1, t.getPos ()); else throw new ScriptException ("Invalid token: " + tok, "<unknown>", -1, t.getPos ()-1); } private class Tokenizer { final static int EOS = 0; // end of string final static int NUMBER = 1; // integer final static int OTHER = 2; // single character private String text, token; private int len, pos, type; Tokenizer (String text) { this.text = text; len = text.length (); pos = 0; } int getPos () { return pos; } String getToken () { return token; } int getType () { return type; } void nextToken () { // Skip leading whitespace. while (pos < len && Character.isWhitespace (text.charAt (pos))) pos++; // Test for NUMBER token. if (pos < len && Character.isDigit (text.charAt (pos))) { StringBuffer sb = new StringBuffer (); do { sb.append (text.charAt (pos++)); } while (pos < len && Character.isDigit (text.charAt (pos))); type = NUMBER; token = sb.toString (); return; } // Must be either a single-character OTHER token or an EOS token. if (pos < len) { token = "" + text.charAt (pos++); type = OTHER; } else { token = ""; type = EOS; } } } }
The getFactory() method returns a new MyScriptEngineFactory instance. This class implements ScriptEngineFactory's methods, which return minimal information about the script engine and the Minexp language, and a new MyScriptEngine instance (via getScriptEngine()). Listing 2 presents MyScriptEngineFactory.
Listing 2 MyScriptEngineFactory.java
// MyScriptEngineFactory.java import java.util.*; import javax.script.*; public class MyScriptEngineFactory implements ScriptEngineFactory { public String getEngineName () { return "My Scripting Engine for Minexp"; } public String getEngineVersion () { return "1.0"; } public List<String> getExtensions () { List<String> list = new ArrayList<String> (); list.add ("me"); return Collections.unmodifiableList (list); } public String getLanguageName () { return "Minexp"; } public String getLanguageVersion () { return "0.1"; } public String getMethodCallSyntax (String obj, String m, String... args) { return null; // Minexp has no methods } public List<String> getMimeTypes () { List<String> list = new ArrayList<String> (); list.add ("text/Minexp"); // Illustration only -- not official return Collections.unmodifiableList (list); } public List<String> getNames () { List<String> list = new ArrayList<String> (); list.add ("MyScriptForMinexp"); return Collections.unmodifiableList (list); } public String getOutputStatement (String toDisplay) { return null; // Minexp has no I/O capability } public Object getParameter (String key) { // I’m not sure what to do with ScriptEngine.ARGV and // ScriptEngine.FILENAME -- not even Rhino JavaScript recognizes these // keys. if (key.equals (ScriptEngine.ENGINE)) return getEngineName (); else if (key.equals (ScriptEngine.ENGINE_VERSION)) return getEngineVersion (); else if (key.equals (ScriptEngine.NAME)) return getNames ().get (0); else if (key.equals (ScriptEngine.LANGUAGE)) return getLanguageName (); else if (key.equals (ScriptEngine.LANGUAGE_VERSION)) return getLanguageVersion (); else if (key.equals ("THREADING")) return null; // Until thoroughly tested. else return null; } public String getProgram (String... statements) { return null; // Minexp does not understand statements } public ScriptEngine getScriptEngine () { return new MyScriptEngine (); } }
Let’s implement this script engine. Begin by compiling Listings 1 and 2. Assuming that the current directory contains a META-INF directory with a services subdirectory, and assuming that this subdirectory contains a javax.script.ScriptEngineFactory text file with MyScriptEngineFactory as its one line of text, issue the following JAR command to create the JAR:
jar cf myscript.jar -C META-INF/ services *.class
This command creates myscript.jar, which packages the script engine. Before you can access the JAR file’s script engine, you need to either copy it to Java’s extensions directory, which happens to be \Program Files\Java\jdk1.6.0\jre\lib\ext on my Windows platform, or include the JAR file in the CLASSPATH—java -cp myscript.jar;. classfilename is an example.
I’ve created a MyScriptDemo application (see Listing 3) that demonstrates the script engine. This application lets you specify a script via a command-line argument or a text file. For example, java MyScriptDemo "3 * (8 + 6)" executes a script via the command line. Also, java MyScriptDemo x.me file reads and executes x.me’s script.
Listing 3 MyScriptDemo.java
// MyScriptDemo.java import java.io.*; import javax.script.*; public class MyScriptDemo { public static void main (String [] args) throws Exception { // Verify correct command-line arguments. if (args.length == 0 || args.length > 2 || (args.length == 2 && !args [1].equalsIgnoreCase ("file"))) { System.err.println ("usage: java MyScriptDemo script [file]"); return; } // Create a ScriptEngineManager that discovers all script engine // factories (and their associated script engines) that are visible to // the current thread’s classloader. ScriptEngineManager manager = new ScriptEngineManager (); // Obtain a ScriptEngine that supports the MyScriptForMinexp short name. ScriptEngine engine = manager.getEngineByName ("MyScriptForMinexp"); // Execute the specified script, output the returned object, and prove // that this object is an Integer. Object o; if (args.length == 1) o = engine.eval (args [0]); else o = engine.eval (new FileReader (args [0])); System.out.println ("Object value: " + o); System.out.println ("Is integer: " + (o instanceof Integer)); } }
Although it serves its illustration purpose, the Minexp scripting language is trivial—it could be improved by introducing floating-point literals, variables, and other features. I’ve also skimped on the script engine’s implementation by avoiding script contexts, bindings, and other features. While enhancing the language, think about addressing the engine’s shortcomings, and further your understanding of the Scripting API.