Hi All,

We’re evaluating a expression evaluator for our use case.

*Example Use Case:*
The expressions needs to contain Java specific code for evaluating once and
running the same for every tuple.
For e.g. a POJO has following definition:

public class POJO {
  String firstname;  // Firstname
  String lastname;  // Lastname
  Date dob;            // Date of birth
}

>From this POJO, we need to generate fullname as concatenation of firstname
& lastname and age which will be derived from dob field.
The expressions for those might look like following:
For full name : ${inp.firstname} + “ “ + ${inp.lastname}
For Age : new Date().getYear() - ${inp.dob}.getYear()

Currently, I have a implementation using Janino library for expression
evaluation. Code (ExpressionEvaluator.java) and Test code (Main.java)
attached.
As performance is an important concern, we chose a custom evaluator using
Janino’s fast script evaluator.

*Design of the custom expression evaluator:*

*ExpressionEvaluator class is used for evaluating expressions which takes
multiple parameter object and the result is returned for that expression.*

*The way to reference a variable in an object is ${placeholder.varname}.*
*The variable will be resolved to its accessible variable or getter method
in order. After this the variable can be used as if its a Java variable.*

*ExpressionEvaluator also allows you to set extra imports that needs to be
added over default is java.lang.**

*ExpressionEvaluator needs to be configured with following configurations
as minimal configuration:*
*1. Mapping of input object place holders to it corresponding types.*
*    This can be done with setInputObjectPlaceholders method.*
*2. Return type of of expression eveluation.*
*3. Expression to be evaluated. This is a standard java expression except
for referencing the variable inside object JEL syntax needs to be used i.e.
${objectPlaceHolder.varName}*

*Example Use of custom expression evaluator:*

    ExpressionEveluator ee = new ExpressionEvaluator();
    // Let expression evaluator know what are the object mappings
present in expressions and their class types.
    ee.setInputObjectPlaceholders(new String[]{"input"}, new
Class[]{Test.class});

    // Generate expression for finding age from Date object.
    String expression = "${input.firstname} + \" \" + ${input.lastname}";
    ExpressionEvaluator.DataGetter<String> getter4 =
ee.createGetter(expression, String.class);
    inp1.firstname = "ABC";
    inp1.lastname = "XYZ";
    String fullname = getter4.get(inp1);
    System.out.println("Fullname is: " + fullname);

*Output:*

Fullname is: ABC XYZ


Can you please suggest for any improvements in this OR is there a better
option to achieve expression evaluation?

Can this code possibly go into Malhar library?

~ Chinmay.
​
package com.datatorrent.test;

import java.util.Date;

public class Main
{
  public static class Test
  {
    private int a;
    public int b;
    private Date d;
    public String firstname;
    public String lastname;

    public Test(int a, int b, Date d)
    {
      this.a = a;
      this.b = b;
      this.d = d;
    }

    public int getA()
    {
      return a;
    }

    public void setA(int a)
    {
      this.a = a;
    }

    public int getB()
    {
      return b;
    }

    public void setB(int b)
    {
      this.b = b;
    }

    public Date getD()
    {
      return d;
    }

    public void setD(Date d)
    {
      this.d = d;
    }
  }

  public static void main(String[] arg) throws Exception
  {
    Test inp1 = new Test(1, 2, new Date(1988-1900, 2, 11));

    Test inp2 = new Test(3, 4, new Date(1993-1900, 2, 11));

    /**
     * Test1
     */
    ExpressionEvaluator ee = new ExpressionEvaluator();
    // Let expression evaluator know if there are other imports that needs to be added other than java.lang.*
    ee.addDefaultImports(new String[]{"java.util.Date"});
    // Let expression evaluator know what are the object mappings present in expressions and their class types.
    ee.setInputObjectPlaceholders(new String[]{"inpA", "inpB"}, new Class[]{Test.class, Test.class});

    // Generate expression for finding age from Date object.
    String expression = "new Date().getYear() - ${inpA.d}.getYear()";
    ExpressionEvaluator.DataGetter<Integer> getter1 = ee.createGetter(expression, Integer.class);
    Integer age = getter1.get(inp1, inp2);
    System.out.println("Age is: " + age);

    /**
     * Test2
     */    ee = new ExpressionEvaluator();
    ee.setInputObjectPlaceholders(new String[]{"inpA", "inpB"}, new Class[]{Test.class, Test.class});
    // Generate expression conditional expression.
    expression = "(${inpA.a} * ${inpA.b}) > 0 ? ${inpB.a} : ${inpB.b}";
    ExpressionEvaluator.DataGetter<Integer> getter2 = ee.createGetter(expression, Integer.class);

    // Positive result
    inp1.setA(1);
    Integer computation = getter2.get(inp1, inp2);
    System.out.println("Computation is: " + computation);

    // Negative result
    inp1.setA(-1);
    computation = getter2.get(inp1, inp2);
    System.out.println("Computation is: " + computation);


    /**
     * Test3
     */
    ee = new ExpressionEvaluator();
    // Let expression evaluator know if there are other imports that needs to be added other than java.lang.*
    ee.addDefaultImports(new String[]{"java.util.Date"});
    // Let expression evaluator know what are the object mappings present in expressions and their class types.
    ee.setInputObjectPlaceholders(new String[]{"inp"}, new Class[]{Test.class});

    // Generate expression for finding age from Date object.
    expression = "${inp.d} == null ? new Date() : ${inp.d}";
    ExpressionEvaluator.DataGetter<Date> getter3 = ee.createGetter(expression, Date.class);

    Date date = getter3.get(inp1);
    System.out.println("Date is: " + date);

    inp1.setD(null);
    date = getter3.get(inp1);
    System.out.println("Date is: " + date);

    /**
     * Test4
     */
    ee = new ExpressionEvaluator();
    // Let expression evaluator know what are the object mappings present in expressions and their class types.
    ee.setInputObjectPlaceholders(new String[]{"input"}, new Class[]{Test.class});

    // Generate expression for finding age from Date object.
    expression = "${input.firstname} + \" \" + ${input.lastname}";
    ExpressionEvaluator.DataGetter<String> getter4 = ee.createGetter(expression, String.class);
    inp1.firstname = "ABC";
    inp1.lastname = "XYZ";
    String fullname = getter4.get(inp1);
    System.out.println("Fullname is: " + fullname);

  }
}
package com.datatorrent.test;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.codehaus.commons.compiler.CompilerFactoryFactory;
import org.codehaus.commons.compiler.IScriptEvaluator;

/**
 * This class is used for evaluating expressions which takes multiple parameter object and the result is returned for that expression.
 *
 * The way to reference a variable in an object is ${placeholder.varname}.
 * The variable will be resolved to its accessible variable or getter method in order. After this the variable can be used as if its a Java variable.
 *
 * ExpressionEvaluator also allows you to set extra imports that needs to be added over default is java.lang.*
 *
 * ExpressionEvaluator needs to be configured with following configurations as minimal configuration:
 * 1. Mapping of input object place holders to it corresponding types.
 *    This can be done with setInputObjectPlaceholders method.
 * 2. Return type of of expression eveluation.
 * 3. Expression to be evaluated. This is a standard java expression except for referencing the variable inside object JEL syntax needs to be used i.e. ${objectPlaceHolder.varName}
 */
public class ExpressionEvaluator
{
  private static final String INP_OBJECT_FUNC_VAR = "obj";

  private static final String GET = "get";
  private static final String IS = "is";

  private IScriptEvaluator se;
  private Map<String, Class> placeholderClassMapping = new HashMap<String, Class>();
  private Map<String, Integer> placeholderIndexMapping = new HashMap<String, Integer>();

  public ExpressionEvaluator() throws Exception
  {
    se = CompilerFactoryFactory.getDefaultCompilerFactory().newScriptEvaluator();
  }

  public interface DataGetter<O>
  {
    O get(Object... obj);
  }

  public <O> DataGetter<O> createGetter(String expression, Class<?> returnType)
  {
    Pattern entry = Pattern.compile("\\$\\{(.*?)\\.(.*?)\\}");
    Matcher matcher = entry.matcher(expression);
    StringBuffer sb = new StringBuffer();
    while (matcher.find()) {
      String obj = matcher.group(1);
      String var = matcher.group(2);
      if (placeholderClassMapping.containsKey(obj)) {
        matcher.appendReplacement(sb, getObjectJavaExpression(obj, var));
      } else {
        throw new RuntimeException("Invalid expression: " + matcher.group());
      }
    }

    matcher.appendTail(sb);

    String code = getFinalMethodCode(sb.toString(), returnType);
    try {
      return (DataGetter)se.createFastEvaluator(code, DataGetter.class, new String[]{INP_OBJECT_FUNC_VAR});
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  private String getFinalMethodCode(String code, Class<?> returnType)
  {
    StringBuilder sb = new StringBuilder();
    sb.append("if (")
      .append(INP_OBJECT_FUNC_VAR)
      .append(".length != ")
      .append(placeholderClassMapping.size())
      .append(") {")
      .append("throw new RuntimeException(\"Incorrect number of parameters passed to DataGetter.\");")
      .append("}\n")
      .append("return (")
      .append(returnType.getName())
      .append(")(")
      .append(code)
      .append(");");

    return sb.toString();
  }

  private String getObjectJavaExpression(String obj, String var)
  {
    Class classType = this.placeholderClassMapping.get(obj);
    String replacement = INP_OBJECT_FUNC_VAR + "[" + this.placeholderIndexMapping.get(obj) + "]";

    StringBuilder sb = new StringBuilder();
    sb.append("(")
      .append("(")
      .append(classType.getName().replace("$", "\\$"))
      .append(")")
      .append("(")
      .append(replacement)
      .append(")")
      .append(")")
      .append(".")
      .append(getGetterForVariable(var, classType));

    return sb.toString();
  }

  private String getGetterForVariable(String var, Class inputClassType)
  {
    try {
      final Field field = inputClassType.getField(var);
      if (Modifier.isPublic(field.getModifiers())) {
        return var;
      }
      System.out.println("Field " + var + " is not publicly accessible. Proceeding to locate a getter method.");
    } catch (NoSuchFieldException ex) {
      System.out.println(inputClassType.getName() + " does not have field " + var + ". Proceeding to locate a getter method.");
    } catch (SecurityException ex) {
      System.out.println(inputClassType.getName() + " does not have field " + var + ". Proceeding to locate a getter method.");
    }

    String methodName = GET + var.substring(0, 1).toUpperCase() + var.substring(1);
    try {
      Method method = inputClassType.getMethod(methodName);
      if (Modifier.isPublic(method.getModifiers())) {
        return methodName + "()";
      }
      System.out.println("Method " + methodName + " of " + inputClassType.getName() + " is not accessible. Proceeding to locate another getter method.");
    } catch (NoSuchMethodException ex) {
      System.out.println(inputClassType.getName() + " does not have method " + methodName + ". Proceeding to locate another getter method");
    } catch (SecurityException ex) {
      System.out.println(inputClassType.getName() + " does not have method " + methodName + ". Proceeding to locate another getter method");
    }

    methodName = IS + var.substring(0, 1).toUpperCase() + var.substring(1);
    try {
      Method method = inputClassType.getMethod(methodName);
      if (Modifier.isPublic(method.getModifiers())) {
        return methodName + "()";
      }
      System.out.println("Method " + methodName + " of " + inputClassType.getName() + " is not accessible. Proceeding to locate another getter method.");
    } catch (NoSuchMethodException ex) {
      System.out.println(inputClassType.getName() + " does not have method " + methodName + ". Proceeding to locate another getter method");
    } catch (SecurityException ex) {
      System.out.println(inputClassType.getName() + " does not have method " + methodName + ". Proceeding to locate another getter method");
    }

    return var;
  }

  public void addDefaultImports(String[] imports)
  {
    se.setDefaultImports(imports);
  }

  public void setInputObjectPlaceholders(String[] inputObjectPlaceholders, Class[] classTypes)
  {
    for (int i = 0; i < inputObjectPlaceholders.length; i++) {
      this.placeholderClassMapping.put(inputObjectPlaceholders[i], classTypes[i]);
      this.placeholderIndexMapping.put(inputObjectPlaceholders[i], i);
    }
  }
}

Reply via email to