Recently, I was looking for a tool to evaluate some mathematical expressions in Java, which were stored in some configuration file. I like dynamic features of languages like Ruby, Python, Javascript or even Erlang to evaluate snippet of code dynamically. Since, Java does not support this inherently, I tried looking for some scripting language that I can embed inside Java. Also, I needed a scripting language that provides two-way integration, i.e., ability to call scripting language from Java and an ability to call Java from the scripting language. So I chose Rhino implementation of Javascript which met those goals. Here is a small example that shows how you can do that using Java and Javascript:
1 2 import java.util.Collection; 3 import java.util.ArrayList; 4 import java.util.HashSet; 5 import org.mozilla.javascript.*; 6 import java.math.BigDecimal; 7 import junit.framework.TestCase; 8 import junit.framework.Test; 9 import junit.framework.TestSuite; 10 11 12 13 public class JsEvalTest extends TestCase { 14 private static final String FUNCS_PREFIX = "funcs"; 15 private Context cx; 16 private Scriptable scope; 17 18 public void setUp() throws Exception { 19 cx = Context.enter(); 20 cx.setLanguageVersion(Context.VERSION_1_2); 21 scope = cx.initStandardObjects(); 22 ScriptableObject.defineClass(scope, Functions.class); 23 Scriptable funcs = cx.newObject(scope, "Functions", new Object[] {new ArrayList()}); 24 scope.put(FUNCS_PREFIX, scope, funcs); 25 } 26 27 public void tearDown() { 28 Context.exit(); 29 } 30 31 public void testFormula() throws Exception { 32 String formula = "cpp = cp / spr"; 33 addDependentVariables(formula); 34 cx.evaluateString(scope, formula, "<cmd>", 1, null); 35 Object varValue = scope.get("cpp", scope); 36 assertEquals(new Double(0.5), varValue); 37 } 38 39 public void testFunction() throws Exception { 40 String formula = "a = " + FUNCS_PREFIX + ".actuals(cp, spr)"; 41 addDependentVariables(formula); 42 cx.evaluateString(scope, formula, "<cmd>", 1, null); 43 Object varValue = scope.get("a", scope); 44 assertEquals(new Double(20000), varValue); 45 } 46 47 private BigDecimal getValueFor(String var) { 48 if ("cp".equals(var)) return new BigDecimal(100); 49 else if ("spr".equals(var)) return new BigDecimal(200); 50 else return null; 51 } 52 53 public static Test suite() { 54 TestSuite suite = new TestSuite(); 55 suite.addTestSuite(JsEvalTest.class); 56 return suite; 57 } 58 59 ///////////////////////////////////////////////////////////////// 60 // 61 private void addDependentVariables(String formula) { 62 Collection<String> dependentVars = getDependentVars(formula); 63 for (String dependentVar : dependentVars) { 64 Object dependentValue = getValueFor(dependentVar); 65 if (dependentValue != null) scope.put(dependentVar, scope, dependentValue); 66 } 67 } 68 private Collection<String> getDependentVars(String formula) { 69 CompilerEnvirons compilerEnv = new CompilerEnvirons(); 70 compilerEnv.initFromContext(cx); 71 compilerEnv.setGeneratingSource(false); 72 Parser p = new Parser(compilerEnv, compilerEnv.getErrorReporter()); 73 ScriptOrFnNode root = p.parse(formula, null, 1); 74 return getDependentVars(root); 75 } 76 77 private Collection<String> getDependentVars(final ScriptOrFnNode tree) { 78 Collection<String> vars = new HashSet<String>(); 79 getDependentVars(tree, tree, vars); 80 return vars; 81 } 82 83 private void getDependentVars(final ScriptOrFnNode tree, final Node parent, Collection<String> vars) { 84 Node node = null; 85 siblingLoop: 86 for (;;) { 87 Node previous = null; 88 if (node == null) { 89 node = parent.getFirstChild(); 90 } else { 91 previous = node; 92 node = node.getNext(); 93 } 94 if (node == null) { 95 break; 96 } 97 98 int type = node.getType(); 99 if (type == Token.NAME) { 100 String className = node.getClass().getName(); 101 if ("org.mozilla.javascript.Node$StringNode".equals(className)) { 102 vars.add(node.getString()); 103 } 104 } 105 getDependentVars(tree, node, vars); 106 } 107 } 108 109 110 public static void main(String[] args) { 111 junit.textui.TestRunner.run(suite()); 112 } 113 } 114 ~ 115
One gotcha, I came across with Rhino was that I also needed to know the input variables to expressions so that I can bind them properly before executing them. Luckily, Rhino is open source and I looked at the source code and hacked a way to parse the expression and find all named variables. For production, I am caching it, but it’s pretty simple to do. For example:
77 private Collection<String> getDependentVars(final ScriptOrFnNode tree) { 78 Collection<String> vars = new HashSet<String>(); 79 getDependentVars(tree, tree, vars); 80 return vars; 81 } 82 83 private void getDependentVars(final ScriptOrFnNode tree, final Node parent, Collection<String> vars) { 84 Node node = null; 85 siblingLoop: 86 for (;;) { 87 Node previous = null; 88 if (node == null) { 89 node = parent.getFirstChild(); 90 } else { 91 previous = node; 92 node = node.getNext(); 93 } 94 if (node == null) { 95 break; 96 } 97 98 int type = node.getType(); 99 if (type == Token.NAME) { 100 String className = node.getClass().getName(); 101 if ("org.mozilla.javascript.Node$StringNode".equals(className)) { 102 vars.add(node.getString()); 103 } 104 } 105 getDependentVars(tree, node, vars); 106 } 107 } 108
Another nice thing about Javascript is that you can not only call regular Java classes, but can also define Javascriptish methods, e.g.
import org.mozilla.javascript.*; public class Functions extends ScriptableObject { // The zero-argument constructor used by Rhino runtime to create instances public Functions() { } // Method jsConstructor defines the JavaScript constructor public void jsConstructor(Object g) { } // The class name is defined by the getClassName method public String getClassName() { return "Functions"; } // The method jsGet_count defines the count property. public int jsGet_ratio_value() { return 500;} public int jsGet_actuals_value() { return 5000;} // Methods can be defined using the jsFunction_ prefix. Here we define // resetCount for JavaScript. public double jsFunction_actuals(double a, double b) { return a * b; } public double jsFunction_ratio(double a, double b) { return a / b; } }