Pet Store Example

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)

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

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:

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") &amp;&amp; Purchase( product == $p ) )
    not ( $p : Product( name == "Fish Food Sample") &amp;&amp; Purchase( product == $p ) )
    exists ( $p : Product( name == "Gold Fish") &amp;&amp; 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") &amp;&amp; Purchase( product == $p ) )
    ArrayList( $total : size &gt; 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

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

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 &gt;= 10 &amp;&amp; &lt; 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 &gt;= 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

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).

Figure 8.13. Figure 1 - PetStore Demo just after Launch

Figure 1 - PetStore Demo just after Launch

To get to this point, the following things have happened:

  1. 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.

  2. A new PetStoreUI class is created and given a handle to the RuleBase (for later use).

  3. 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.

Figure 8.14. Figure 2 - PetStore Demo with Products Selected

Figure 2 - PetStore Demo with Products Selected

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.

  1. 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.

  2. 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).

Figure 8.15. Figure 3 - Do we want to buy a fish tank?

Figure 3 - Do we want to buy a fish tank?

  1. 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.

  2. The doCheckout() function sets the focus to the checkout agenda-group, giving the rules in that group the option to fire.

  3. The rules in the the checkout agenda-group, display the contents of the cart and apply the appropriate discount.

  4. 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.

Figure 8.16. Figure 4 - Petstore Demo after all rules have fired.

Figure 4 - Petstore Demo after all rules have fired.

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