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))))
"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))))
"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)))
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 */ }
; There is an error in this rule Jess> (defrule foo-1 (foo bar) -> (printout "Found Foo Bar" crlf))
Jess reported an error in routine Jesp.parseDefrule. Message: Expected '=>' at token '->'. Program text: ( defrule foo-1 ( foo bar ) -> at line 3.
Jess> (defrule foo-2 => (printout t (+ 3.0 four) crlf))
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.
Jess> (defrule foo-3 (test (eq 3 (+ 2 one))) - => )
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.
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.