Home > Articles > Programming > Java

Taming Mustang, Part 3: A New Script Engine

📄 Contents

  1. Implement Your Own Script Engine
  2. What's Next?
  • Print
  • + Share This
In the last installment of his three-part series on Mustang, Java expert Jeff Friesen introduces you to his miniature expression language and shows you how to implement a script engine for this language.
Like this article? We recommend

Like this article? We recommend

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 CLASSPATHjava -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.

  • + Share This
  • 🔖 Save To Your Account