I guess we want to use Groovy (or others dynamic language supporting lib) for:

1. Extract expression parsing (checking syntax error, list variables and functions in the expression) and evaluation (executing the expression, get the result) into “outside”.

2. Make the application more flexible on rules evaluation. The application developing would no longer concern on rules defined by customer or non-core source code, for this part, the application only focus on get the result from the rules or expressions.

3. Separate maintenance and R&D. In this case, R&D is responsible for supporting rules parsing and evaluation, for example, adding new variables and functions witch can be used in the rules or expressions.

Currently, not only DN mapping needs expression evaluation, but also Alarm Mapping, Counter Mapping do. I think we can apply the same architecture/library to those SC/PB. Am I right?

For those proposal, I think Groovy, and others libraries, such as BeanShell, JRuby, JavaScript (Java interpreting version ) is not good enough to fulfill our requirements. I am going to explain my point with a simple requirement:

We have a table in Oracle Database named “TestDataSource”, it has three column as:

· ID varchar(64), primary key

· Code varchar(64), formatted code of this record, the first 3 characters indicate the City ID of this record, and the rest is a number indicate some thing else which we don’t care in this case.

· Price double

· Amount double

· Discount double (percentage, 0<=Discount<=1)

The requirement is calculating a new value from a expression for each row in the table:

substring(Code,0,3)+tostring( Discount>=0.5 ? Price*Amount*(Discount+0.1) : Price*Amount*(Discount+0.05) ,”#####0.00”)

As you can see, I marked variables with red color and function/methods with blue color.

There are 4 variables in the expression, “Code”, “Discount”, “Price”, “Amount”, they are exactly using the value of the table columns with the same name. (Who do me a favor to re-organize this sentence?)

And there are two function in the expression: “substring” and “tostring”. The first due to split a String, and the second to convert a double value to a String with the specified format-string.

I am sorry that I can not write a “correct” source code in Groovy, because I rarely use it. To be honest, I only know “Hello world” in Groovy. :D. I use embedded-Groovy (GroovyShell) rather than loading class from outside to implement a sample. Maybe this is a good way to control our business logic.

Let’s see how to implement it using Groovy, the source code fragment is:

String Expression="substring(Code,0,3)+tostring( Discount>=0.5 ? Price*Amount*(Discount+0.1) : Price*Amount*(Discount+0.05) ,'#####0.00') ";
public Object eval(String strExpression)
{
  ...
  Sqlca sqlca=null; //Who have used PowerBuilder? Do you remember the famous SQLCA?
  ...//Connect to database
  sqlca.execute("select * from TestDataSource");//Open a new cursor
  GroovyShell shell = new GroovyShell();
while(sqlca.next())//Move/Forword/Fetch the cursor
  {
    shell.setProperty("Code",sqlca.getString("Code"));
    shell.setProperty("Price",sqlca.getDouble("Price"));
    shell.setProperty("Amount",sqlca.getInt("Amount"));
    shell.setProperty("Discount",sqlca.getDouble("Discount"));
    Object objValue=shell.evaluate(Expression);
      ...//Handle the result.
  }
  ... //Return something.
}

As the method “eval” is designed to take care a series of expressions, I call it is a “core-function”.

Unfortunately, the preceding source code will not work, because the functions “substring” and “tostring”. In this case, we simple change the expression to:

Code.substring(0,3)+( Discount>=0.5 ? Price*Amount*(Discount+0.1) : Price*Amount*(Discount+0.05)).toString()

It works, but the result is not corrent due to the default “toString” on a double value is not what we want. Certainly, this solution can not fulfill the requirement. Let’s see how to resolve it:

  • Add a new helper class “Helper” into Groovy Shell which implemented the function “substring” and “tostring”, and make sure those two method are “static”. (I don’t know how to inject a existing class into GroovyShell, I guess it can be done as the BeanShell does)
  • Change the expression to:

Helper.substring(Code,0,3)+ Helper.tostring( Discount>=0.5 ? Price*Amount*(Discount+0.1) : Price*Amount*(Discount+0.05) ,'#####0.00')

OK, we got the finally solution. But we can not avoid the following impediments:

  • All variables and function/methods must be predefined in the core-function, if a new variable introduced in the expression, we must change the source code of “core-function” to match the new variable, it is definitely a nightmare for coder.
  • All function/method must implemented by Java class, and expression which calls those must add the class name as prefix. For example, if you want invoke “cos(…)” in your expression, you have to write down “Math.cos(…)”.
  • All variables and function/methods are case-sensitive.
  • The type of the return value is unknown, it is a instance of class “Object”.

To sum up, I think we should provide a easy way to do configuration, the people who maintenancing our system may be not familiar with Java or other programming language. I don’t think it is a good change for using Groovy.

I have to admit, the dynamic class loading in Groovy is really groovy, but I don’t know when will use it.

I think we can do the expression handling more agile. I’d like to share my solution about expressions evaluation.

Classes definitions:

  • Customer: Client program for the expression evaluation.
  • Parser: Parsing a expression into syntax-nodes and combining nodes into one binary tree.

interface ISyntaxNode //Binary Tree
{
public Operator getOperator(); //Including +-*/, bracket, and so on.
public ISyntaxNode LeftNode();
public ISyntaxNode RightNode();
}
interface IParser
{
public ISyntaxNode parse(String strExpression);
}

  • Evaluator: “Executing” a binary tree parsed by Parser and return the value to customers.

interface ValuePair
{
public DataType getDataType();
public Object getValue();
}
interface IEvaluator
{
public ValuePair eval(ISyntaxNode node,IPriovider provider);//recursive invoking in the tree
}

  • Provider (“Binding” in Groovy): Variables and function/methods repository.

interface IFunction
{
public String getName();
public ValuePair execute(Object[] parameters);
}
interface IVariable
{
public String getName();
public ValuePair getValue();
}
interface IProvider
{
public IFunction getFunctionByName(String strName);
public IVariable getVariableByName(String strName);
}

The class “Parser” and “Evaluator” is the core part, and class “Provider” should be provided by customers (We should defined some basic functionalities as a default provider.).

Let’s implement the previous expression using the new solution:

public class MyFunctionStub
{
public int tostring(){...}
public int tostring(String strFormat){...}
public int substring(){...}
}
public class MyProvider extends AbstractProvider
{
  Sqlca sqlca=null;
public Hashtable<String,Object> hashValue=new Hashtable<String,Object>();
public IVariable getVariableByName(String strName)
  {
return hashValue.get(strName.toLowerCase());//Exceptions??
  }
protected void prepareResource()
  {
if(null==sqlca)//Synchronized!!!
    {
      ...//Connect to database
    }
    sqlca.execute("select * from TestDataSource");//Open a new cursor
if(sqlca.next())
    {
      hashValue.put("Code".toLowerCase(),sqlca.getString("Code"));
      hashValue.put("Price".toLowerCase(),sqlca.getDouble("Price"));
      hashValue.put("Amount".toLowerCase(),sqlca.getInt("Amount"));
      hashValue.put("Discount".toLowerCase(),sqlca.getDouble("Discount"));
    }    
//Register functionalities handler
this.registerClassMethods(MyFunctionStub.class,""/*No prefix*/,false /*Case-sensitive?*/);  
  }
protected void releaseResource()
  {
if(null!=sqlca)sqlca.closeAll();//Exceptions??
  }
}
public static void main(String[] argvs)
{
String expression="...";
  ValuePair v=Evalutor.eval(DefaultParser.parse(expression),new MyProvider());//Exceptions
if(v.getType().equals(String.class))
  {
//Handle
  }
else
  {
//exception
  }
}

As you can see, variables and function/methods are provided at the customer source code, the core-function do not know any thing about the expression. When encountering a variable or a function-invoking, Evaluator invokes the Provider’s “getFunctionByName” or “getVariableByName” to get the result.

The benefits are:

  • Core-function is responsible for parsing and evaluation, without predefined variables and functionalities.
  • All variables and functionalities are provided by customer source code, generally, the customers should know the name list of method and variables.
  • Case insensitive control depends on customer setting.
  • Whether using class name prefix for methods depends on customer setting.

The impediments are:

  • Only support poor expression in one line, no “if”,”switch”, etc.

My conclusion is:

Groovy is short on expression evaluation due to needing predefine all variables and function/method in the core part, it is not flexible enough. But we can combine the two solution with using Groovy to generate Providers.


Jeason Zhao (沈胜衣,斛律光) ------雪饮再现,一个人的江湖
我知道我是谁,我是沈胜衣,默默的活着,就像空气。