Search code examples
javaspringspring-bootmvel

How to create generic MVEL expression


I am writing generic MVEL expression for Java objects. So for understanding purpose I am taking one example.

class Student {

   String name;

   String rollNo;

   List<Course> courses;
}

class Course {
   
   String courseName;

   String facultyName;

   String fee;

   String classRoom;
}

I can write MVEL expression If my query is: Check If a student has given name and rollNo

MVEL.eval(" name == 'XYZ' && rollNo == '3456' ", Student)

But If the query is: Check If a student attends all the courses in the same classroom?

MVEL.eval(" courses[0].classRoom == 'A' ", Student);

But this checks classRoom in one course only. But I want to check If all courses of a student happens in classRoom 'A'. I couldn't find any resource to solve this problem. I am completely new in MVEL. If you have any doubt please ask me.

Thank you


Solution

  • To do this requires a few steps - you will need to make use of MVEL variables and its foreach operator.

    Here is the approach:

    import org.mvel2.MVEL;
    import java.util.List;
    import java.util.ArrayList;
    import java.util.Map;
    import java.util.HashMap;
    
    public class App {
        public static void main(String[] args) {
            
            List<Course> courses = new ArrayList<>();
            courses.add(new Course("Math", "123"));
            courses.add(new Course("Physics", "123"));
            courses.add(new Course("Chemistry", "123"));
            
            Student student = new Student("XYZ", "3456", courses);
                    
            // Check If a student has given name and rollNo:
            boolean b1 = MVEL.eval(" name == 'XYZ' && rollNo == '3456' ", student, Boolean.class);
            boolean b2 = MVEL.eval(" name == 'XYP' && rollNo == '3456' ", student, Boolean.class);
            //System.out.println(b1);
            //System.out.println(b2);
            
            // Check If a student attends all the courses in the same classroom:
            boolean allClassesInSameRoom = true;
            String prevRoom = null;
            
            Map vars = new HashMap();
            vars.put( "sameRoom", allClassesInSameRoom );
            vars.put( "prevRoom", prevRoom );
            
            String expression = String.join("\n", 
                    "sameRoom = true;",
                    "prevRoom = null;",
                    "foreach (course : courses) {",
                    "  if (sameRoom == true && prevRoom != null && course.classRoom != prevRoom) {",
                    "    sameRoom = false;",
                    "  }",
                    "  prevRoom = course.classRoom;",
                    "}",
                    "sameRoom");
            
            allClassesInSameRoom = MVEL.eval(expression, student, vars, Boolean.class);
            
            System.out.println(allClassesInSameRoom);
            
        }
        
    }
    

    First we define two Java variables, which are needed in the MVEL script. They are passed to the MVEL eval operator via a Java map:

    boolean allClassesInSameRoom = true;
    String prevRoom = null;
    
    Map<String, Object> vars = new HashMap<>();
    vars.put( "sameRoom", allClassesInSameRoom );
    vars.put( "prevRoom", prevRoom );
    

    Then we build a string containing the MVEL script we need to use:

    String expression = String.join("\n", 
            "sameRoom = true;",
            "prevRoom = null;",
            "foreach (course : courses) {",
            "  if (sameRoom == true && prevRoom != null && course.classRoom != prevRoom) {",
            "    sameRoom = false;",
            "  }",
            "  prevRoom = course.classRoom;",
            "}",
            "sameRoom");
    

    The final line returns the sameRoom variable.

    The eval statement assembles the items we need:

    allClassesInSameRoom = MVEL.eval(expression, student, vars, Boolean.class);
    

    Note that the Course class needs to be defined as public:

    public class Course {
    
        private final String courseName;
        private final String classRoom;
    
        public Course (String courseName, String classRoom) {
            this.courseName = courseName;
            this.classRoom = classRoom;
        }
        
        public String getCourseName() {
            return courseName;
        }
    
        public String getClassRoom() {
            return classRoom;
        }
    
    }
    

    It is possible to create MVEL scripts as templates, which I think can help avoid the somewhat messy process of building a Java String line-by-line, as I do here.

    This may all add up to more work than simply checking the student object without using MVEL. But MVEL supports functions and lambda expressions, also - so there are probably ways to streamline my approach and simplify the MVEL script.


    Update

    Here is an example which is a bit more compact:

    Map<String, String> rooms = new HashMap<>();
    Map<String, Object> vars2 = new HashMap<>();
    vars2.put( "rooms", rooms );
            
    String expression2 = String.join("\n", 
            "foreach (course : courses) {",
            "  rooms.put(course.classRoom, course.classRoom);",
            "}",
            "(rooms.size() == 1) ? true : false;");
            
    allClassesInSameRoom = MVEL.eval(expression2, student, vars2, Boolean.class);
    System.out.println(allClassesInSameRoom);
    

    In this case, the script adds each room name to a map. Duplicate room names will have the same map key, and therefore will not increase the size of the map. If we end up with a map containing only one entry, that means there is only one room being used for all courses.


    As an alternative, you can count the number of different room names in the student's list of courses using Java (Java 8 or higher):

    import static java.util.stream.Collectors.toList;
    
    ...
    
    long roomsCount = courses.stream()
            .map(Course::getClassRoom)    // get the room name from each course
            .collect(toList())            // build a list of these room names
            .stream().distinct().count(); // count the number of unique room names
    

    That's one line of code. But since your goal is to explore and understand MVEL, this is just a side note.