Name: Pet Store Main class: org.drools.examples.PetStore Type: Java application Rules file: PetStore.drl Objective: Demonstrate use of Agenda Groups, Global Variables and integration with a GUI (including callbacks from within the Rules)
The Pet Store example shows how to integrate Rules with a GUI (in this case a Swing based Desktop application). Within the rules file, it shows how to use agenda groups and auto-focus to control which of a set of rules is allowed to fire at any given time. It also shows mixing of Java and MVEL dialects within the rules, the use of accumulate functions and calling of Java functions from within the ruleset.
Like the rest of the the samples, all the Java Code is contained in one file. The PetStore.java contains the following principal classes (in addition to several minor classes to handle Swing Events)
Petstore - containing the main() method that we will look at shortly.
PetStoreUI - responsible for creating and displaying the Swing based GUI. It contains several smaller classes , mainly for responding to various GUI events such as mouse and button clicks.
TabelModel - for holding the table data. Think of it as a JavaBean that extends the Swing AbstractTableModel class.
CheckoutCallback - Allows the GUI to interact with the Rules.
Ordershow - the items that we wish to buy.
Purchase - Details of the order and the products we are buying.
Product - JavaBean holding details of the product available for purchase, and it's price.
Much of the Java code is either JavaBeans (simple enough to understand) or Swing based. We will touch on some Swing related points in the this tutorial , but a good place to get more Swing component information is http://java.sun.com/docs/books/tutorial/uiswing/
available at the Sun Swing website.[]
There are two important Rules related pieces of Java code in Petstore.java.
Example 8.52. Creating the PetStore RuleBase - extract from PetStore.java main() method
KnowledgeBuilder kbuilder = KnowledgeBuilderFactory.newKnowledgeBuilder(); kbuilder.add( ResourceFactory.newClassPathResource( "PetStore.drl", PetStore.class ), ResourceType.DRL ); KnowledgeBase kbase = KnowledgeBaseFactory.newKnowledgeBase(); kbase.addKnowledgePackages( kbuilder.getKnowledgePackages() ); //RuleB Vector<Product> stock = new Vector<Product>(); stock.add( new Product( "Gold Fish", 5 ) ); stock.add( new Product( "Fish Tank", 25 ) ); stock.add( new Product( "Fish Food", 2 ) ); //The callback is responsible for populating working memory and // fireing all rules PetStoreUI ui = new PetStoreUI( stock, new CheckoutCallback( kbase ) ); ui.createAndShowGUI();
This code above loads the rules (drl) file from the classpath. Unlike other examples where the facts are asserted and fired straight away, this example defers this step to later. The way it does this is via the second last line where the PetStoreUI is created using a constructor the passes in the Vector called stock containing products, and an instance of the CheckoutCallback class containing the RuleBase that we have just loaded.
The actual Javacode that fires the rules is within the CheckoutCallBack.checkout() method. This is triggered (eventually) when the 'Checkout' button is pressed by the user.
Example 8.53. Firing the Rules - extract from the CheckOutCallBack.checkout() method
public String checkout(JFrame frame, List<Product> items) { Order order = new Order(); //Iterate through list and add to cart for ( int i = 0; i < items.size(); i++ ) { order.addItem( new Purchase( order, (Product) items.get( i ) ) ); } //add the JFrame to the ApplicationData to allow for user interaction StatefulKnowledgeSession ksession = kbase.newStatefulKnowledgeSession(); ksession.setGlobal( "frame", frame ); ksession.setGlobal( "textArea", this.output ); ksession.insert( new Product( "Gold Fish", 5 ) ); ksession.insert( new Product( "Fish Tank", 25 ) ); ksession.insert( new Product( "Fish Food", 2 ) ); ksession.insert( new Product( "Fish Food Sample", 0 ) ); ksession.insert( order ); ksession.fireAllRules(); //returns the state of the cart return order.toString(); }
Two items get passed into this method; A handle to the JFrame Swing Component surrounding the output text frame (bottom of the GUI if / when you run the component). The second item is a list of order items; this comes from the TableModel the stores the information from the 'Table' area at the top right section of the GUI.
The for() loop transforms the list of order items coming from the GUI into the Order JavaBean (also contained in the PetStore.java file). Note that it would be possible to refer to the Swing dataset directly within the rules, but it is better coding practice to do it this way (using Simple Java Objects). It means that we are not tied to Swing if we wanted to transform the sample into a Web application.
It is important to note that all state in this example is stored in the Swing components, and that the rules are effectively stateless. Each time the 'Checkout' button is pressed, this code copies the contents of the Swing TableModel into the Session / Working Memory.
Within this code, there are nine calls to the working memory. The first of these creates a new workingMemory (StatefulKnowledgeSession) from the Knowledgebase - remember that we passed in this Knowledgebase when we created the CheckoutCallBack class in the main() method. The next two calls pass in two objects that we will hold as Global variables in the rules - the Swing text area and Swing frame that we will use for writing messages later.
More inserts put information on products into the working memory, as well as the order list. The final call is the standard fireAllRules(). Next, we look at what this method causes to happen within the Rules file.
Example 8.54. Package, Imports , Globals and Dialect - extract (1) from PetStore.drl
package org.drools.examples import org.drools.WorkingMemory import org.drools.examples.PetStore.Order import org.drools.examples.PetStore.Purchase import org.drools.examples.PetStore.Product import java.util.ArrayList import javax.swing.JOptionPane; import javax.swing.JFrame global JFrame frame global javax.swing.JTextArea textArea
The first part of the PetStore.drl file contains the standard package and import statement to make various Java classes available to the rules. What is new are the two globals frame and textArea. These hold references to the Swing JFrame and Textarea components that were previous passed by the Java code calling the setGlobal() method. Unlike normal variables in Rules , which expire as soon as the rule has fired, Global variables retain their value for the lifetime of the (Stateful in this case) Session.
The next extract (below) is from the end of the PetStore.drl file. It contains two functions that are referenced by the rules that we will look at shortly.
Example 8.55. Java Functions in the Rules - extract (2) from PetStore.drl
function void doCheckout(JFrame frame, WorkingMemory workingMemory) { Object[] options = {"Yes", "No"}; int n = JOptionPane.showOptionDialog(frame, "Would you like to checkout?", "", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, options[0]); if (n == 0) { workingMemory.setFocus( "checkout" ); } } function boolean requireTank(JFrame frame, WorkingMemory workingMemory, Order order, Product fishTank, int total) { Object[] options = {"Yes", "No"}; int n = JOptionPane.showOptionDialog(frame, "Would you like to buy a tank for your " + total + " fish?", "Purchase Suggestion", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, options[0]); System.out.print( "SUGGESTION: Would you like to buy a tank for your " + total + " fish? - " ); if (n == 0) { Purchase purchase = new Purchase( order, fishTank ); workingMemory.insert( purchase ); order.addItem( purchase ); System.out.println( "Yes" ); } else { System.out.println( "No" ); } return true; }
Having these functions in the rules file makes the PetStore sample more compact - in real life you probably have the functions in a file of their own (within the same rules package), or as a static method on a standard Java class (and import them using the import function my.package.Foo.hello syntax).
The above functions are
doCheckout() - Displays a dialog asking the user if they wish to checkout. If they do, focus is set to the checkOut agenda-group, allowing rules in that group to (potentially) fire.
requireTank() - Displays a dialog asking the user if they wish to buy a tank. If so, a new FishTank Product added to the orderlist in working memory.
We'll see later the rules that call these functions.The next set of examples are from the PetStore rules themselves. The first extract is the one that happens to fire first (partly because it has the auto-focus attibute set to true).
Example 8.56. Putting each (individual) item into working memory - extract (3) from PetStore.drl
// insert each item in the shopping cart into the Working Memory // insert each item in the shopping cart into the Working Memory rule "Explode Cart" agenda-group "init" auto-focus true salience 10 dialect "java" when $order : Order( grossTotal == -1 ) $item : Purchase() from $order.items then insert( $item ); drools.getKnowledgeRuntime().getAgenda().getAgendaGroup( "show items" ).setFocus(); drools.getKnowledgeRuntime().getAgenda().getAgendaGroup( "evaluate" ).setFocus(); end
This rule matches against all orders that do not yet have an Order.grossTotal calculated . It loops for each purchase item in that order. Some of the Explode Cart Rule should be familiar ; the rule name, the salience (suggesting of the order that the rules should be fired in) and the dialect set to java. There are three new items:
agenda-group "init" - the name of the agenda group. In this case, there is only one rule in the group. However, nothing in Java code / nor a rule sets the focus to this group , so it relies on the next attibute for it's chance to fire.
auto-focus true - This is the only rule in the sample, so when fireAllRules() is called from within the Java code, this rule is the first to get a chance to fire.
drools.setFocus() This sets the focus to the show items and evaluate agenda groups in turn , giving their rules a chance to fire. In practice , we loop through all items on the order, inserting them into memory, then firing the other rules after each insert.
The next two listings shows the rules within the show items and evaluate agenda groups. We look at them in the order that they are called.
Example 8.57. Show Items in the GUI extract (4) from PetStore.drl
rule "Show Items" agenda-group "show items" dialect "mvel" when $order : Order( ) $p : Purchase( order == $order ) then textArea.append( $p.product + "\n"); end
The show items agenda-group has only one rule, also called Show Items (note the difference in case). For each purchase on the order currently in the working memory (session) it logs details to the text area (at the bottom of the GUI). The textArea variable used to do this is one of the Global Variables we looked at earlier.
The evaluate Agenda group also gains focus from the explode cart rule above. This Agenda group has two rules (below) Free Fish Food Sample and Suggest Tank.
Example 8.58. Evaluate Agenda Group extract (5) from PetStore.drl
// Free Fish Food sample when we buy a Gold Fish if we haven't already bought // Fish Food and dont already have a Fish Food Sample rule "Free Fish Food Sample" agenda-group "evaluate" dialect "mvel" when $order : Order() not ( $p : Product( name == "Fish Food") && Purchase( product == $p ) ) not ( $p : Product( name == "Fish Food Sample") && Purchase( product == $p ) ) exists ( $p : Product( name == "Gold Fish") && Purchase( product == $p ) ) $fishFoodSample : Product( name == "Fish Food Sample" ); then System.out.println( "Adding free Fish Food Sample to cart" ); purchase = new Purchase($order, $fishFoodSample); insert( purchase ); $order.addItem( purchase ); end // Suggest a tank if we have bought more than 5 gold fish and dont already have one rule "Suggest Tank" agenda-group "evaluate" dialect "java" when $order : Order() not ( $p : Product( name == "Fish Tank") && Purchase( product == $p ) ) ArrayList( $total : size > 5 ) from collect( Purchase( product.name == "Gold Fish" ) ) $fishTank : Product( name == "Fish Tank" ) then requireTank(frame, drools.getWorkingMemory(), $order, $fishTank, $total); end
The Free Fish Food Sample rule will only fire if
We don't already have any fish food.
We don't already have a free fish food sample.
We do have a Gold Fish in our order.
If the rule does fire, it creates a new product (Fish Food Sample), and adds it to the Order in working memory.
The Suggest Tank rule will only fire if
We don't already have a Fish Tank in our order
If we can find more than 5 Gold Fish Products in our order.
If the rule does fire, it calls the requireTank() function that we looked at earlier (showing a Dialog to the user, and adding a Tank to the order / working memory if confirmed). When calling the requireTank() function the rule passes the global frame variable so that the function has a handle to the Swing GUI.
The next rule we look at is do checkout.
Example 8.59. Doing the Checkout - extract (6) from PetStore.drl
rule "do checkout" dialect "java" when then doCheckout(frame, drools.getWorkingMemory()); end
The do checkout rule has no agenda-group set and no auto-focus attribute. As such, is is deemed part of the default (MAIN) agenda-group - the same as the other non PetStore examples where agenda groups are not used. This group gets focus by default when all the rules/agenda-groups that explicity had focus set to them have run their course.
There is no LHS to the rule, so the RHS will always call the doCheckout() function. When calling the doCheckout() function the rule passes the global frame variable so the function has a handle to the Swing GUI. As we saw earlier, the doCheckout() function shows a confirmation dialog to the user. If confirmed, the function sets the focus to the checkout agenda-group, allowing the next lot of rules to fire.
Example 8.60. Checkout Rules- extract (7) from PetStore.drl
rule "Gross Total" agenda-group "checkout" dialect "mvel" when $order : Order( grossTotal == -1) Number( total : doubleValue ) from accumulate( Purchase( $price : product.price ), sum( $price ) ) then modify( $order ) { grossTotal = total }; textArea.append( "\ngross total=" + total + "\n" ); end rule "Apply 5% Discount" agenda-group "checkout" dialect "mvel" when $order : Order( grossTotal >= 10 && < 20 ) then $order.discountedTotal = $order.grossTotal * 0.95; textArea.append( "discountedTotal total=" + $order.discountedTotal + "\n" ); end rule "Apply 10% Discount" agenda-group "checkout" dialect "mvel" when $order : Order( grossTotal >= 20 ) then $order.discountedTotal = $order.grossTotal * 0.90; textArea.append( "discountedTotal total=" + $order.discountedTotal + "\n" ); end
There are three rules in the checkout agenda-group
Gross Total - if we haven't already calculated the gross total, accumulates the product prices into a total, puts this total into working memory, and displays it via the Swing TextArea (using the textArea global variable yet again).
Apply 5% Discount - if our gross total is between 10 and 20, then calculate the discounted total and add it to working memory / display in the text area.
Apply 10% Discount - if our gross total is equal to or greater than 20, calculate the discounted total and add it to working memory / display in the text area.
Now we've run through what happens in the code, lets have a look at what happens when we run the code for real. The PetStore.java example contains a main() method, so it can be run as a standard Java application (either from the command line or via the IDE). This assumes you have your classpath set correctly (see the start of the examples section for more information).
The first screen that we see is the Pet Store Demo. It has a List of available products (top left) , an empty list of selected products (top right), checkout and reset buttons (middle) and an empty system messages area (bottom).
To get to this point, the following things have happened:
The main() method has run and loaded the RuleBase but not yet fired the rules. This is the only rules related code to run so far.
A new PetStoreUI class is created and given a handle to the RuleBase (for later use).
Various Swing Components do their stuff, and the above screen is shown and waits for user input.
Clicking on various products from the list might give you a screen similar to the one below.
Note that no rules code has been fired here. This is only swing code, listening for the mouse click event, and added the clicked product to the TableModel object for display in the top right hand section (as an aside , this is a classic use of the Model View Controller - MVC - design pattern).
It is only when we press the Checkout that we fire our business rules, in roughly the same order that we walked through the code earlier.
The CheckOutCallBack.checkout() method is called (eventually) by the Swing class waiting for the click on the checkout button. This inserts the data from the TableModel object (top right hand side of the GUI), and handles from the GUI into the session / working memory. It then fires the rules.
The Explode Cart rule is the first to fire, given that has auto-focus set to true. It loops through all the products in the cart, makes sure the products are in the working memory, then gives the Show Items and Evaluation agenda groups a chance to fire. The rules in these groups, add the contents of the cart to the text area (bottom), decide whether or not to give us free fish food and whether to ask if we want to buy a fish tank (Figure 3 below).
The Do Checkout rule is the next to fire as it (a) No other agenda group currently has focus and (b) it is part of the default (MAIN) agenda group. It always calls the doCheckout() function which displays a 'Would you like to Checkout?' Dialog Box.
The doCheckout() function sets the focus to the checkout agenda-group, giving the rules in that group the option to fire.
The rules in the the checkout agenda-group, display the contents of the cart and apply the appropriate discount.
Swing then waits for user input to either checkout more products (and to cause the rules to fire again) or to close the GUI - Figure 4 below.
Should we choose, we could add more System.out calls to demonstrate this flow of events. The current output of the console of the above sample is as per the listing below.
Example 8.61. Console (System.out) from running the PetStore GUI
Adding free Fish Food Sample to cart SUGGESTION: Would you like to buy a tank for your 6 fish? - Yes