Use an external properties file to store expressions used in an application. For this recipe,
imagine yourself creating a system to sort Ball
objects based on a set of arbitrary
criteria. Instead of hard-coding criteria in a series of Java if
-else
clauses, create a framework with loads sorting criteria from a
properties file containing boolean
JEXL expressions. For instance, the first line in this properties file
would be:
Hvy-Green-Basket = ball.color == 'Green' && (ball.weight > 1000)
This translates to "If the ball's color is Green and the weight is
over 1000, put this ball into the Heavy Green basket." The name of each
property is the name of the basket into which a Ball
matching the criteria is placed. The
contents of the criteria file are:
Hvy-Green-Basket = ball.color == 'Green' && (ball.weight > 1000) Sm-Yellow-bin = ball.color == 'Yellow' && (ball.weight < 100) Transparent-Bin = ball.isTransparent( ) Lrg-Yellow-Basket = ball.color == 'Yellow' &&(ball.weight >= 100) Misc-Bin = true
Each criterion is applied to each Ball
object in the order it appears in the
criteria file. The heavy green sorting criteria is applied first, and
each criterion is evaluated until the last criterion is reached. The
last criteria always evaluates to true
—similar to a switch
-case
control statement, the "Misc-bin" is the default. The following code
reads this criteria file and evaluates each JEXL expression in order to
sort a collection of Ball
objects:
import org.apache.commons.jexl.Expression; import org.apache.commons.jexl.ExpressionFactory; import org.apache.commons.jexl.JexlContext; import org.apache.commons.jexl.JexlHelper; // Load in our criteria properties Properties criteria = new Properties( ); criteria.load( getClass( ).getResourceAsStream("criteria.txt") ); Set binNames = criteria.getKeys( ); // Load our ball objects into a List List balls = getBalls( ); Iterator ballsIter = balls.iterator( ); while( ballsIter.hasNext( ) ) { Ball ball = (Ball) ballsIter.next( ); // Iterate through every rule, until you find a match... Iterator binIter = binName.iterator( ); while( ruleIter.hasNext( ) ) { // Get the name of the basket String basket = (String) binIter.next( ); // Get the expression corresponding to this bin. String expr = conditions.get( bin ); Expression e = ExpressionFactory.createExpression( expr ); // Populate the context with the current Ball object JexlContext jc = JexlHelper.createContext( ); jc.getVars( ).put("ball", ball); // Evaluate the Expression. Boolean result = (Boolean) e.evaluate(jc); // If the expression evaluated to true, add this Ball to the bin. if( result.booleanValue( ) == true ) { sendBall( ball, basket ); } } }
The result of the Expression
evaluation is a Boolean
value, and, if the Boolean
result is true
, the matching Ball
is sent to the specified basket.
Using this technique, as the number of criteria increases, the
code to implement this sorting algorithm remains unchanged. The behavior
of the system can be altered by changing the criteria file; compiled
code is left untouched. The code in this Solution section is longer than
a series of if
-else
clauses to implement these criteria in
code, but, as the number of sorting criteria increases, you will be glad
you took the extra time to create a general solution without hard coding
system behavior in Java code.
This was the first example that involved something more than printing out a pretty message for human consumption. JEXL has been used to create a "language" for sorting criteria; if a client wants to change the rules, you can now train someone familiar with simple logic statements to change a system to meet changing requirements.
This recipe demonstrated a system that uses a simple set of rules to categorize balls. For more information about a serious open source Rule Engine for Java named JESS, take a look at http://herzberg.ca.sandia.gov/jess/index.shtml. If you are interested in Rule Engines, take a look at JSR 94: Java Rule Engine API (http://jcp.org/en/jsr/detail?id=94) or Ernest Friedman-Hill's "Jess in Action" (Manning).