DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations
11 Monitoring and Observability Tools for 2023
Learn more
  1. DZone
  2. Coding
  3. Languages
  4. Embedding Rules into Java Programs with the Mandarax Compiler

Embedding Rules into Java Programs with the Mandarax Compiler

Jens Dietrich user avatar by
Jens Dietrich
·
Dec. 06, 10 · Interview
Like (0)
Save
Tweet
Share
8.59K Views

Join the DZone community and get the full member experience.

Join For Free

A common task for programmers is to implement the business logic described in requirement documents. Often, this logic is expressed as business rules: simple statements that describe constraints on and relationships between entities ("business objects"). Using object-oriented or functional programming languages to express those rules is not very productive: the rules have to be mapped to objects, classes, functions, methods and procedural constructs like conditionals and loops. This is error-prone, and important information about the rules is lost in the process. This problem is addressed by rule engines: libraries that can interpret and execute rules formally defined using a rule language. The Mandarax compiler has similar goals, but uses a different approach: instead of interpreting rules, they are compiled into standard Java classes. These classes can then be integrated into applications. The main advantage of this approach is to remove the runtime overhead of the rule engine from the application. A unique feature of the Mandarax compiler is that rule meta data are made accessible in the generated classes. With this feature, applications cannot only compute results based on rules, but also get access to an explanation how these results have been computed.

Defining Rules

In Mandarax, rules are defined using a domain specific language (Mandarax script). The language extends the Java expression syntax. Each script defines a relationship between objects of certain types. For instance, consider the following script: 
package test.org.mandarax.compiler.reldef6;
import test.org.mandarax.compiler.*;
Discount goldDiscount = new Discount(20,true);
Discount silverDiscount = new Discount(10,true);
Discount specialDiscount = new Discount(5,false);
rel Discount(Customer customer,Discount discount) queries
getDiscount(customer),qualifiesForDiscount(customer,discount) {
rule1: c.turnover>1000 -> Discount(c,goldDiscount);
rule2: FrequentCustomer(c) -> Discount(c,silverDiscount);
rule3: c.paymentMethod == "CompanyVisa" -> Discount(c,specialDiscount);
}
rel FrequentCustomer(Customer customer) queries isFrequentCustomer(customer) {
rule1: c.transactionCount>5 -> FrequentCustomer(c);
rule2: c.transactionCount>3 & c.turnover>500 -> FrequentCustomer(c);
}

This script defines two relationships, Discount and FrequentCustomer. The Discount relationship associates customers and discounts, the FrequentCustomer relationship applies to customers (it is technically not a relationship but a so-called unary predicate). The rules define when objects instantiate the relationships. This can be done either by referencing other relationships (for instance, in the second rule for Discount), by using Java expressions (for instance, in the first rule for Discount), or by using a combination of both. Rule are general in the sense that variables are used: the c in rule1 for Discount can represent any instance of Customer.

The expressions used in rules have a syntax that is very similar to Java. However, there are some differences. For instance, c.transactionCount is not a reference to the field transactionCount, but to the bean property getter getTransactionCount(). In this respect, Mandarax script is similar to expression languages such as JUEL, MVEL and OGNL.

Compiling Rules

The following script will compile the rule definition: 
import org.mandarax.compiler.*;
import org.mandarax.compiler.impl.*;
...
private static void compile(File file) throws Exception {
Compiler compiler = new DefaultCompiler();
Location location = new FileSystemLocation(new File("output_folder"));
compiler.compile(location,CompilationMode.RELATIONSHIP_TYPES,file);
compiler.compile(location,CompilationMode.QUERIES,file);
}

The compiler generates Java source code. The Location instance is used to specify where the generated code will be stored. The compiler itself has a compile method that is called twice: the first call (with the CompilationMode.RELATIONSHIP_TYPES parameter) generates classes representing the relationships, while the second call (with the CompilationMode.QUERIES parameter) generated classes with methods to query for instances of the relationship.

  The classes created to represent the relationships are very simple structures representing the relationships. They have public fields for each relationship slot, and equals() and hashCode() methods. These methods are implemented using the Apache commons EqualsBuilder and HashCodeBuilder utilities, respectively.

public class DiscountRel {
public Customer customer = null;
public Discount discount = null;
public DiscountRel() {
super();
}
public DiscountRel(Customer customer, Discount discount) {
super();
this.customer = customer;
this.discount = discount;
}
@Override public boolean equals(Object obj) {...}
@Override public int hashCode() {...}
}

 

The second set of classes generated by the compiler provides methods representing queries. For each query defined in a relationship, a matching static method is generated. The public interface for this class generated for the Discount relationship looks as follows:

 

public class DiscountRelInstances {
public staticDiscount goldDiscount = newDiscount(20, true);
public static Discount silverDiscount = newDiscount(10, true);
public staticDiscount specialDiscount = newDiscount(5, false);

// interface generated for queries
public static ResultSet<discountrel> getDiscount(Customer customer) {...}
public static ResultSet<discountrel> qualifiesForDiscount(Customer customer, Discount discount) {...}

// private methods
...
}
There are several internal methods containing the actual programming logic representing the rules. This code is rather complex. The main challenges are the binding of variables (unification), and backtracking if the evaluation of a precondition of a rule fails. The implementation is based on the combination of iterators such as filtering, chaining and nesting, using ideas from functional programming and projects such as Apache Commons Collection and Google Guava. 

Using The Generated Code

The query methods return a ResultSet of DiscountRel. ResultSet extends Iterator. Therefore, applications can iterate over the computed results as follows:
Customer customer = new Customer("John");
… // set customer properties here
ResultSet<discountrel> rs = DiscountRelInstances.getDiscount(customer);
while (rs.HasNext()) {
DiscountRel record = rs.next();
System.out.println("Customer " + customer + " qualifies for the following discount: " + record.discount);
}
rs.close();

This script will print out all discounts the respective customer qualifies for. The order of results returned depends on the order in which rules are defined. Therefore, the order of rules is often seen as meaningful, and many applications will only use the first result returned by a query. If there is no result, the query will return an empty iterator. This is an iterator for which hasNext() always returns false, and next() always throws an exception.

Note that ResultSet also has a close() method. The intention of this method is to release resources (such as data base connections) allocated by the computation. The computation of results is lazy. That is, the computation is only performed when the next() methods is invoked by the client application. Therefore, the query methods return the result sets almost instantly to the application.

Semantic Reflection

When implementing a set of rules using a mainstream programming language, methods are written to execute the rules and to compute a result. It is generally not possible to find out how the result is computed. This is often of interest, as users want to understand the logic behind computer systems before making important decisions. The ability of a system to explain how a result was computed makes it more trustworthy. In some cases, adding log statements to program code can be used to achieve this. However, if the algorithms to evaluate the rules are complex, log files become very complex as well. In particular, if the evaluation of the rules has to explore and later discard certain computations (because conditions are not satisfied, this is called backtracking), already written log statements have to be marked as invalid.

The code generated by the Mandarax compiler supports an interface to extract information about the rules used at runtime. We call this semantic reflection - it is similar to standard reflection used in object-oriented programming languages. But instead of revealing information about types used in method and class signatures, it extracts information about the logic used within (the query) methods. Consider the following updated rule set for discount policies:
Discount goldDiscount = new Discount(20,true);
Discount silverDiscount = new Discount(10,true);
Discount specialDiscount = newDiscount(5,false);
@author="jens"
@lastupdated="26/10/10"
rel Discount(Customer customer,Discount discount) queries
getDiscount(customer),qualifiesForDiscount(customer,discount) {
@lastupdated="27/10/10"
@description="golden rule"
rule1: c.turnover>1000 -> Discount(c,goldDiscount);
@description="silver rule"
rule2: FrequentCustomer(c) -> Discount(c,silverDiscount);
@description="special rule"
rule3: c.paymentMethod == "CompanyVisa" -> Discount(c,specialDiscount);
}
@author="jens"
rel FrequentCustomer(Customer customer) queries isFrequentCustomer(customer) {
rule1: c.transactionCount>5 -> FrequentCustomer(c);
rule2: c.transactionCount>3 & c.turnover>500 -> FrequentCustomer(c);
}

The rule definitions are unchanged, but there are some additional annotations (the lines starting with @). Annotations are simple key-value pairs. The compiler essentially eliminates the rules and turns them into methods with an internal logic representing the rules. While doing this, the compiler will add the annotations to the generated classes, and create an API to query the result sets for these annotations. The annotations represent meta data about the rule, and can be extremely helpful to trace code back to requirements.

 Both relationships and rules can be annotated. Annotations on relationships apply to all rules for this relationship. If a rule annotation has the same key as a relationship annotation, it wins (the annotation is overridden).

The following script shows how to find the first applicable discount for a customer, and prints out the meta data for the rules that have been used to compute this result.
Customer customer = new Customer("John");
// set customer properties here
ResultSet<discountrel> rs = DiscountRelInstances.getDiscount(customer);
DiscountRel discount = rs.next();
List<derivationlogentry> computation = rs.getDerivationLog();
DiscountRel record = rs.next();
System.out.println("Customer" + customer + " qualifies for the following discount: " + record.discount);
System.out.println("The following rules have been applied to compute this result:");
for (DerivationLogEntry e:computation) {
System.out.println("rule id: " + e.getName());
System.out.println("description: " + e.getAnnotation("description"));
System.out.println("author: " + e.getAnnotation("author"));
System.out.println("last updated: " + e.getAnnotation("lastupdated"));
}</derivationlogentry></discountrel>

A Complex Example: the UServ Application

 

The UServ insurance application is an example based on a Userv Product Derby scenario published by the Business Rule Forum in order to compare business rule engines. The complete application is available in the Mandarax project repository, and can be executed using web start by pointing your browser to the following URL: http://www-ist.massey.ac.nz/JBDietrich/userv-mdrx/userv.jnlp

 

The application consists of 69 rules, organised in 16 rule sets, each set defining one relationship. The application uses a simple user interface that is organised as follows:

 

Userv application screenshot 1

 

In the top half of the screen, three objects representing a car, a driver and a policy can be manipulated using the controls in this panel. Whenever a change occurs, all rules are re-evaluated (i.e., all queries are executed), and the respective results are displayed in the lower half of the screen. By clicking on the "?" buttons next to the results or double clicking on the values in the lists, a window pops up showing the derivation for this result:

 

Finally, clicking on the "show rules" buttons in the main toolbar displays the rules sets from which the application has been generated.

 

Userv application screenshot 2

 

This application uses some features not discussed in this document, including negation as failure (for instance, used in the DriverCategory rules) and aggregation functions (used in the InsuranceEligibility rules). These features are described in the Mandarax manual.

 

More information about the Mandarax project can be found on the Mandarax project web site.
Java (programming language) application Database Annotation

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • gRPC on the Client Side
  • Debezium vs DBConvert Streams: Which Offers Superior Performance in Data Streaming?
  • 10 Most Popular Frameworks for Building RESTful APIs
  • Apache Kafka Is NOT Real Real-Time Data Streaming!

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: