Home > Java > Shoppingcart supercharged by JBoss Drools Rule Engine

Shoppingcart supercharged by JBoss Drools Rule Engine

The previous post on the JBoss Drools Rule Engine gave a high level overview of the benefits of using it in your web application. This post explores a concrete example. JPetstore is a simple shoppingcart application whose products are pets. This online petstore is included as a sample application with the Spring Framework bundle to highlight some of Spring’s capabilities. What follows is how I modified JPetstore to leverage the JBoss Drools Rule Engine 5.2 (the commercial version offered by Red Hat is called JBoss Rules) so JPetstore could have some of the features we commonly see offered by online retailers.

It is common to see online retailers give customers who enter a coupon code during checkout a special offer. Some examples are:

  • Buy 3 goldfish, get the 4th free.
  • Buy “camera model A” and get “tripod model B” for free
  • 10% off your order cost when you enter offer code X
  • All batteries 20% off when using promo code X
  • $10 off any order totaling over $100
  • Buy any 3 car care products, get a free microfiber towel.
  • Free shipping using code X

Many offers of this type are valid for only a short period of time – a week, a weekend, or even a single day (Black Friday offers).

This functionality calls for using a Rule Engine.

A rule engine could also serve as a “recommendation engine” – it can be used to recommend or suggest additional products to the customer based on their past orders, their user profile, etc. For instance, if you purchased car wax, the system might recommend their bestselling microfiber towel or something specific to your car’s year/make/model.

Modifications to JPetstore

Domain Model
Below is the modified domain model from JPetstore.
Jpetstore model class diagram
I added fields to Cart and CartItem to receive the changes from the business rules. For example, CartItem’s calculatedPrice is the modified list price of Item based on the rules. Cart’s createDate is when the shoppingcart was created. It is used in the rules to verify the given coupon has not expired.

It is important to have a rich domain model containing any field you think you’ll use. If it is not in the model, it can’t be used in the rules. Here, Product does not have a weight field. Having a weight field would have been useful for calculating a shipping cost. Product length/width/height could be used to determine if an additional shipping fee is required.

Screen Flow
Previously, the application flow went from viewing the shoppingcart directly to collecting the payment information. I added an intermediary step. Now clicking “Proceed to checkout” on the “view cart” page runs the rule service then displays the modified cart. At that point, the old functionality to collect the payment information is a click away.
jpetstore shoppingcart before

View Cart

jpetstore shoppingcart after rules

View Cart after running rules

Moving to the Spring MVC controllers, I created CheckoutController to perform the checkout step instead of having ViewCartController perform both actions. CheckoutController was configured to require authentication in petstore-servlet.xml to ensure an Account object (containing city, state, zip, etc) would be available to the rules.

The noteworthy thing about CheckoutController is that it calls the injected RuleService with the customers account and shoppingcart.

JPetstore Rules
A few examples of the rules I wrote for JPetstore are shown below.

rule "10 dollars off any order over 100 with 10off100 code"
	when
		$cart : Cart(coupon == "10off100",
			subTotal > 100,
			now >= "25-Nov-2011",
			now 	then
		$cart.setDiscount(10.00);
end

rule "buy 2 iguana, get 5 dollars off"
	when
		$cart : Cart(now <= "27-Nov-2011")
		$ci : CartItem(item.product.productId == "RP-LI-02", 
			quantity >= 2)        
	then
		$cart.setDiscount(5.00);
end

rule "20% off price of any bird"
dialect "mvel"
    when
        #conditions
        $ci : CartItem($item : item,
        	item.product.categoryId == "BIRDS")
    then
        #actions
        $ci.setCalculatedPrice($ci.item.listPrice * 0.8);
end

As you can see with the three rules shown, it is basically pattern matching on object types and field values. You can store matched field values in variables that can later be used in either the LHS or RHS.

Rule Service
RuleServiceImpl is where the rule engine is invoked with the user’s account and shoppingcart objects. The rules operate on objects inserted into the KnowledgeSession. Here you see the Cart, CartItems, and Account inserted into the session so the rules can operate on them. Changes to the objects made made by the triggered rules are visible when fireAllRules returns.

public class RuleServiceImpl implements RuleService {
    final Logger logger = LoggerFactory.getLogger(RuleServiceImpl.class);
    private ItemDao itemDao;
    private KnowledgeAgent kagent;
	
    public RuleServiceImpl() {
        kagent = KnowledgeAgentFactory.newKnowledgeAgent( "MyAgent" );
        kagent.applyChangeSet(ResourceFactory.newClassPathResource(
                "droolsChangeSet.xml"));

        ResourceFactory.getResourceChangeNotifierService().start();
        ResourceFactory.getResourceChangeScannerService().start();
		
        KnowledgeBase kbase = kagent.getKnowledgeBase();
    }
	
    public void execute(Cart cart, UserSession user) {
        KnowledgeBase kbase = kagent.getKnowledgeBase();
        StatefulKnowledgeSession knowledgeSession = 
                                    kbase.newStatefulKnowledgeSession();
        JPetRuleLogger myLogger = new JPetRuleLoggerImpl(logger);
		
        knowledgeSession.setGlobal("myLogger", myLogger);
        knowledgeSession.setGlobal("itemDao", itemDao);
		
        knowledgeSession.insert(cart);
        for (Iterator i = cart.getAllCartItems(); i.hasNext(); ) {
            CartItem ci = (CartItem) i.next();
            knowledgeSession.insert(ci);
        }
		
        Account acct = user.getAccount();
        knowledgeSession.insert(acct);
		
        //run the rules
        knowledgeSession.fireAllRules();
		
        knowledgeSession.dispose();
    }
...

Separating the rule lifecyle from the application lifecyle
One of the advantages of using a rules engine is you can separate the business rule lifecyle from the application lifecyle. An easy way to achieve this separation with Drools is to use the combination of a KnowledgeAgent and a changeset.xml file as shown above in RuleServiceImpl.

<?xml version="1.0" encoding="UTF-8"?>
<change-set xmlns='http://drools.org/drools-5.0/change-set'
	xmlns:xs='http://www.w3.org/2001/XMLSchema-instance'
	xs:schemaLocation='http://drools.org/drools-5.0/change-set 
	http://anonsvn.jboss.org/repos/labs/labs/jbossrules/trunk/drools-api/src/main/resources/change-set-1.0.0.xsd'>
	<add>
	<resource source='http://127.0.0.1:9090/jpetstoreApplication.drl'
		type='DRL' />
	<resource source='http://127.0.0.1:9090/jpetstoreDSL.dsl'
		type='DSL' />
	<resource source='http://127.0.0.1:9090/jpetstoreDSLR.dslr'
		type='DSLR' />  
	</add>
</change-set>

This file allows you to externalize the rule resource names from your code. Here I am using an external server URL to get the rule resource completely off the appserver running the JPetstore application. A more heavy duty approach is to use Guvnor BRMS (Business Rule Management System) and specify your Guvnor package url in changeset.xml instead of the individual resource URLs.

Another benefit of KnowledgeAgent is that it caches the knowledgebase for you. With Drools, it is an expensive operation to create a KB so you do not want to create them with every request. KnowledgeSession, which are obtained from KB are not so costly, so each user request gets their own.

Being able to change the rule resources outside a full application deployment is not helpful unless the application can detect the rule resource was changed and thereafter use the updated resource.
That is the purpose of these 2 lines in RuleServiceImpl:

		ResourceFactory.getResourceChangeNotifierService().start();
		ResourceFactory.getResourceChangeScannerService().start();

The default is to check for changes every 60 seconds. Once a change is detected, the next call to kagent.getKnowledgeBase(); returns the new KnowledgeBase.

Conclusion
As you can see, the Drools rule engine makes offering this type of functionality very easy. A rule engine is a great tool to have in your toolset.

If you would like to get this running on your machine, you can use Eclipse with the SVN plugin and the m2eclipse Maven plugin to create the jpetstore project from Spring’s SVN repository. JPetstore is a Maven project so you’ll just need to add the Drools dependencies to the pom.

Advertisements
  1. No comments yet.
  1. No trackbacks yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: