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

So without further ado, we'll implement the above idea with Jess. We're going to write an explicit "pricing library" which can be invoked from a Web app or from any other kind of Java code. There will be both a Java component and a Jess language component to what we'll write; we'll look at the Java part first. Note that all the code for this example, including a test driver, comes with Jess; it's in the directory Jess70p2/examples/pricing_engine. There's a README file there with some information about building and running the example.

Your Java code needs to create an instance of the Jess rule engine, load in the catalog data, then load in the rules (which we'll be writing later in this chapter.) 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.) We'll package all this up in a class named PricingEngine:

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

    public PricingEngine(Database aDatabase) throws JessException {
        // Create a Jess rule engine
        engine = new Rete();

        // Load the pricing rules

        // Load the catalog data into working memory
        database = aDatabase;

       // Mark end of catalog data for later
       marker = engine.mark();

(The Database interface is an application-specific data access wrapper; it's an interface and a trivial implementation is included in the example code.) Note that the call to jess.Rete.batch(java.lang.String) 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. In this case the results will be Offer objects in working memory, put there by the rules; each Offer represents some kind of special pricing deal to be applied to the order. 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 {
        // Retrive the order from the database
        Order order = database.getOrder(orderNumber);

        if (order != null) {
           // Add the order and its contents to working memory

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) throws JessException {
        // Remove any previous order data, leaving only catalog data

        // Load data for this order

        // Fire the rules that apply to this order

        // Return the list of offers created by the rules
        return engine.getObjects(new Filter.ByClass(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.

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.

11.4. Making your own rules

All that's left is to write the rules. Our rules will be matching against some simple Java model objects. Imagine you've already got some model objects being used elsewhere in your system: Customer , Offer , Order , OrderItem , and CatalogItem .

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."
Jess> (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))))

In a Jess rule, the patterns come before the "=>" symbol. This rule applies to Order with a total property greater than $100. 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. You can learn more about writing rule patters here.

The actions come after the "=>" symbol. If this rule applies to an Order, a new Java Offer object is created with a value 10% of the order total, and that Offer is added to working memory using the add Our PricingEngine class will find these Offer objects later.

"Give a 25% discount on items the customer buys three or more of."
Jess> (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))))

In this rule, 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."
Jess> (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.
    We use a regular expression to match the CD writer's description."
    (CatalogItem (partNumber ?partNumber) (description /CD Writer/))
    (CatalogItem (partNumber 782321) (price ?price))
    (OrderItem (partNumber ?partNumber))
    (Customer {orderCount > 1})
    (add (new Offer "Free CD-RW disks" ?price)))

This rule is more complicated because it includes correlations between objects. Because we used the same variable ?partNumber in two different patterns, the rule will only match when the corresponding properties of two objects are equal -- i.e., when the partNumber of an OrderItem is the same as the partNumber of a CatalogItem that represents a CD writer.

Also noteworthy: we've tested the "description" property of the CatalogItem object with a simple Java 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.

Our PricingEngine class is stateful and not meant to be used in a multithreaded way. This is by design. If you want to use a PricingEngine in a Web application, then your choices are analagous to your choices in using JDBC Connection objects: either create new ones as needed, which is simple, but inefficient; or use an object pool and a finite collection of objects which are checked out, used, and returned to the pool. The important point is that multiple PricingEngine objects will be absolutely independent, and you can create as few or as many as you need for a given application.

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();
} 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.