11. Embedding Jess in a Java Application

11.1. Introduction

In the first part of this chapter, we'll look at one way to embed Jess into a Java application. It's a pricing engine which determines the prices individual customers will pay for goods ordered through our e-commerce site. The material presented here will be easier to understand if you are familiar with Jess's Java APIs and how to write rules in the Jess programming language.

At the end of this chapter, I'll talk about some general considerations that are useful when planning an application that embeds the Jess rule engine.

11.2. Motivation

Imagine you're working on a pricing engine for online sales. The engine is supposed to look at each order, together with a customer's purchasing history, and apply various discounts and offers to the order. Imagine further that you've coded this up in a traditional Java class.

Your boss comes in, says, "Give a 10% discount to everybody who spends more than $100." Great. You add a single if-then statement, recompile, and you're back in business.

Boss comes in, says, "Give a 25% discount on items the customer buys three or more of." Another if-then, another recompile.

Boss comes in, says "Revoke 10% discount per-order, make it 10% if you spend $250 or more in one month. Also, if somebody buys a CD writer, send them a free sample of CD-RW disks; but only if they're a repeat customer. Oh, and by the way, we need to price shipping based on geographic zone, except for people with multiple locations..."

After a few weeks of this, if you've been using traditional programming techniques, your code is a gnarled mess -- and it's slow too, as it has to do all sorts of database queries for each order that comes in.

If you had written this using a rule engine, though, you'd have nice clean code, with one rule for each pricing policy. If somebody needed to know where the "Wacky Wednesday" pricing policy is implemented, it would be easy to find it.

And if that rule engine is a Jess-like system, it's not slow, either; the rule engine itself indexes all the orders and no lookups are required; the rule engine just "knows" what it needs to know about past orders.

11.3. Doing it with Jess

A note about error handling: I've exposed JessException in the interface to all of these methods. I could have hidden JessException inside them by catching JessException and rethrowing a non-Jess exception. I've chosen not to do that here just to make the code shorter.

Your Java code needs to create an instance of Jess, load in the catalog data, then load in the rules (which you'll be writing in a moment.) This one instance of Jess can then be reused to process each order. (You only have to load the catalog data once; Jess will index it and later accesses will be fast.) You've already got some data access objects you can pull from your database layer: Order , OrderItem , CatalogItem .


public class PricingEngine {
    private Rete engine;
    private WorkingMemoryMarker marker;

    public PricingEngine() throws JessException {

        // Create a Jess rule engine
        engine = new Rete();
        engine.reset();

        // Load your rules
        engine.batch("myrules.clp");

        // Load up the catalog data
        Database d = Database.getInstance();
        engine.addAll(d.getCatalogItems());

        // Mark known good state
        marker = engine.mark();
    }

Note that the call to "batch" will find the file "myrules.clp" not only in the current directory but even if it's packaged in a jar file with the PricingEngine class, or put into the WEB-INF/classes directory of a Web application. Jess tries to find the file using a succession of different class loaders before giving up.

Then whenever you want to process an order, the pricing engine needs to do four things: reset the engine back to its initial state; load the order data; execute the rules; and extract the results. We'll write a short private routine for one of these steps, and then a single public method that performs all four steps on behalf of clients of the pricing engine.

First, a short routine to load the order data into Jess:

private void loadOrderData(int orderNumber) throws JessException {
    Database d = Database.getInstance();
    Order order = d.getOrder(orderNumber);
    engine.add(order);
    engine.addAll(order.getItems());
}
            

Now the pricing engine's business method, which takes an order number and returns an Iterator over the applicable offers. We use one of Jess's predefined jess.Filter implementations to select only the Offer objects from working memory.

public Iterator run(int orderNumber) {
    engine.resetToMark(marker);
    loadOrderData(orderNumber);
    engine.run();
    return engine.getObjects(new Filter.ClassFilter(Offer.class));
}
            

That's it! Now any servlet, EJB, or other Java code can instantiate a PricingEngine and use it to find the offers that apply to a given order... once we write the rules, that is.

11.4. Making your own rules

Now all we have to do is express the business rules as Jess rules. Every rule has a name, an optional documentation string, some patterns , and some actions. A pattern is statement of something that must be true for the rule to apply. An action is something the rule should do if it does apply. Let's see how some of our pricing rules would look in Jess.

"Give a 10% discount to everybody who spends more than $100."
(defrule 10%-volume-discount
    "Give a 10% discount to everybody who spends more than $100."
    (Order {total > 100})
    =>
    (add (new Offer "10% volume discount" (/ ?total 10))))
            
The patterns come before the "=>" symbol, and the actions come after it. This rule applies to orders with a total greater than $100, and if it applies, a new Offer is created with a value 10% of the order total. Inside the curly braces, we can write expressions which look just like Boolean expressions in Java. All the properties of an object are available to these expressions just by using their property names.
"Give a 25% discount on items the customer buys three or more of."
(defrule 25%-multi-item-discount
    "Give a 25% discount on items the customer buys three or more of."
    (OrderItem {quantity >= 3} (price ?price))
    =>
    (add (new Offer "25% multi-item discount" (/ ?price 4))))
            
We use the value of the "price" property to compute the discount. Because we don't otherwise mention the "price" property, we include the simple expression "(price ?price)", which assigns the value of the property to the variable "?price."
"If somebody buys a CD writer, send them a free sample of CD-RW disks; but only if they're a repeat customer."
(defrule free-cd-rw-disks
    "If somebody buys a CD writer, send them a free sample of CD-RW
     disks, catalog number 782321; but only if they're a repeat customer."
    (CatalogItem (partNumber ?partNumber) (description /CD Writer/)
    (OrderItem (partNumber ?partNumber))
    (Customer {orderCount > 1})
    =>
    (add (new OrderItem 782321 0.0)))
            
We've tested the "description" property of the CatalogItem object with a simple regular expression. If the phrase "CD Writer" appears anywhere in the description, that CatalogItem will match the pattern.

11.5. Multiple Rule Engines

Each jess.Rete object represents an independent reasoning engine. A single program can include any number of engines. The individual Rete objects each have their own working memories, agendas, and rulebases, and can all function in separate threads. You can use multiple identical engines in a pool, or each engine can have its own rules, perhaps because you intend for them to interact in some way.

11.6. Jess in a Multithreaded Environment

Jess can be used in a multithreaded environment. The jess.Rete class internally synchronizes itself using several synchronization locks. The most important lock is a lock on working memory: only one thread will be allowed to change the working memory of a given jess.Rete object at a time.

The Rete.run() method, like the (run) function in the Jess programming language, returns as soon as there are no more applicable rules. In a multithreaded environment, it is generally appropriate for the engine to simply wait instead, because rules may be activated due to working memory changes that happen on another thread. That's the purpose of the Rete.runUntilHalt() method and the (run-until-halt) function, which use Java's wait()/notify() system to pause the calling thread until active rules are available to fire. runUntilHalt and (run-until-halt) won't return until Rete.halt() or (halt) are called, as the names suggest.

11.7. Error Reporting and Debugging

I'm constantly trying to improve Jess's error reporting, but it is still not perfect. When you get an error from Jess (during parsing or at runtime) it is generally delivered as a Java exception. The exception will contain an explanation of the problem. If you print a stack trace of the exception, it can also help you understand what went wrong. For this reason, it is very important that, if you're embedding Jess in a Java application, you don't write code like this:

// Don't ignore exceptions like this!
try {
Rete engine = new Rete();
engine.eval("(gibberish!)");
} catch (JessException ex) { /* ignore errors */ }
            
If you ignore the Java exceptions, you will miss Jess's explanations of what's wrong with your code. Don't laugh - more people code this way than you'd think!

Anyway, as an example, if you attempt to load the folowing rule in the standard Jess command-line executable,
; There is an error in this rule
Jess> (defrule foo-1
(foo bar)
->
(printout "Found Foo Bar" crlf))
            
You'll get the following printout:
Jess reported an error in routine Jesp.parseDefrule.
Message: Expected '=>' at token '->'.
Program text: ( defrule foo-1 ( foo bar ) ->  at line 3.
            
This exception, like all exceptions reported by Jess, lists a Java routine name. The name parseDefrule makes it fairly clear that a rule was being parsed, and the detail message explains that -> was found in the input instead of the expected => symbol (we accidentally typed -> instead). This particular error message, then, was fairly easy to understand.

Runtime errors can be more puzzling, but the printout will generally give you a lot of information. Here's a rule where we erroneously try to add the number 3.0 to the word four:
Jess> (defrule foo-2
=>
(printout t (+ 3.0 four) crlf))
            
This rule will compile fine, since the parser doesn't know that the + function won't accept the symbol four as an argument. When we (reset) and (run), however, we'll see:
Jess reported an error in routine +
  while executing (+ 3.0 four)
  while executing (printout t (+ 3.0 four) crlf)
  while executing defrule MAIN::foo-2
  while executing (run).
Message: Not a number: four.
  Program text: ( run )  at line 12.
            
In this case, the error message is also pretty clear. It shows the offending function (+ 3.0 four); then the function that called that (printout); the message then shows the context in which the function was called (defrule MAIN::foo-2), and finally the function which caused the rule to fire (run).

The message 'Not a number: four' tells you that the + function wanted a numeric argument, but found the symbol four instead.

If we make a similar mistake on the LHS of a rule:
Jess> (defrule foo-3
(test (eq 3 (+ 2 one)))                                         -
=>
)
            
We see the following after a reset:
Jess reported an error in routine +
  while executing (+ 2 one)
  while executing (eq 3 (+ 2 one))
  while executing 'test' CE
  while executing rule LHS (TECT)
  while executing (reset).
Message: Not a number: one.
  Program text: ( reset )  at line 22.
            
Again, the error message is very detailed, and makes it clear, I hope, that the error occurred during rule LHS execution, in a test CE, in the function (+ 2 one). Note that Jess cannot tell you which rule this LHS belongs to, since rule LHSs can be shared.

11.8. Creating Rules from Java

It is now possible to create Jess rules and queries using only the Java API -- i.e., without writing either a Jess language or XML version of the rule. This isn't recommended -- part of the power of a rule engine is the ability it gives you to separate your rules from your other code -- but sometimes it may be worth doing.

Defining rules from Java is complex, and is still an undocumented process. If you're interested in doing it, your best resource is the source code for the jess.xml package, which uses Jess's public APIs to build rules. Be aware that these APIs may change without notice.