Introduction
I'm working in a logistic company from Belgium. The company aims at delivering parcels from distant seller partners (called DSP in the remainder) to their customers. The company is currently using a "track and trace system" which helps in determining where (track) a parcel is located at a given time (trace).
With this system, the company can inform the customer about the parcel location through a web interface. When a parcel is in a specific state the system performs some specific actions. For example, when a parcel is at the end of its delivery path, the customer is notified by SMS that his/her parcel arrived at the pick-up location.
We are using java as main development langage and J2EE as framework. In the early days of our application, all the business was distributed between different session beans. It was very easy to maintain since the business logic we applied was very limited. Then, the company grew and ideas flew out: the need to have more control on the business and to ease the maintenance of the code emerged. This can be accomplished with a rule engine: a rule engine gives you more control over the business through their rules, the business logic is easier to implement and clearer.
Here below, I'm going to introduce not all the business of the company but the architecture of the application, how we integrated the rule engine (drools in this case) and how we dealed with the management of the rules.
The text here below assumes that you are familiar with all the vocabulary related to rule engine. When a specific word is used, we give a reference to its definition.
The architecture - a macro view
The application is running in a J2EE environment - jboss - and is made of several modules. We distinguish between two kinds of modules: inbound modules and outbound modules. A third module is central to the architecture, called the kernel.
As you probably guessed it, the role of the inbound modules is to get information from the outside and to feed the kernel with the collected information. On the other end, once the information has been handled/treated by the kernel, the outbound modules do their respective jobs.
A bit further inside the kernel
A working memory per parcel
The system allows us to track and trace parcels. We have two ways to use the working memory . The first choice could be to use only one working memory for all the parcels. This solution is in my opinion not flexible enough, it can slow down the application if we put too many objects in the working memory and we have to keep an eye on performance. The second solution is to have a working memory for each parcel. This is a better solution since:
- The business rules are applied on a per-parcel basis;
- The amount of data placed into the working memory is very limited.
In the architecture, we decided that only the kernel is able to create the working memory associated to the parcel. Only the kernel know the business. The other modules must be simple and just do the job they have to do. For example, an outbound module which has to export data to another system must only know the business logic to apply to this other system but should not perform any business logic related to the core system. The working memory is not serialized as such into the parcel bean but we keep a list of serializable objects that we call DataObjects. The DataObjects represent a piece of information about a parcel. These objects are fullyserializable. Only serializing DataObjets ease the manageability of the rulebase ; see next point.
The rulebase
The rulebase is the same for all parcels to avoid to compile it every time we create a new one. To achieve this, either we bind the rulebase to the JNDI Context, or we keep a static instance of the rule base in memory. For those who don't know JNDI, JNDI is a naming directory for java objects . We chose the second solution by writing a wrapper (see code 1). An MBean is instanciating this wrapper at start up (see code 2). When we need the rule base, we do /wrapper.getRuleBase()/ and get the compiled ruleBase.
public class RuleBaseWrapper { private static RuleBase ruleBase = null; public static RuleBase getRuleBase() { return ruleBase; } static void init(RuleBase r) { ruleBase = r; } }
public class WorkingMemoryService { public static final String RULEBASE_RESOURCE = "parcel.drl"; public void start() throws Exception { loadDefaultRuleBase(); } public void loadDefaultRuleBase() throws WorkingMemoryException { InputStream is = this.getClass().getResourceAsStream(RULEBASE_RESOURCE); Reader reader = new BufferedReader(new InputStreamReader(is)); refreshRuleBase(reader); } /** * @jmx.managed-operation description="Build a new ruleBase and register in JNDI context" * @jmx.managed-parameter name="ruleBaseString" type="java.lang.String" description="XML that defines the rules for the new RuleBase" * * @param ruleBaseString the XML containing the ruleBase definition. * */ public void loadRuleBase(String ruleBaseString) throws WorkingMemoryException { refreshRuleBase(new StringReader(ruleBaseString)); } private void refreshRuleBase(Reader ruleBaseReader) throws WorkingMemoryException { try { RuleBaseWrapper.init(RuleBaseLoader.loadFromReader(ruleBaseReader)); } catch (Exception e) { throw new WorkingMemoryException("Could not create new ruleBase [" + e.getMessage() + "]", e); } } }
This solution with the MBean is quite nice since you can test different rulebases by refreshing the static instance in the wrapper (see the loadRuleBase(String ruleBase) method). You don't have to redeploy the application to test your modified rules.
The flow
A parcel is always created by an inbound module. The inbound module informs the kernel about the creation of the entity by generating an event (no matter the way how this event is forwarded, this is not the purpose of this article). The kernel receives it, checks whether this freshly created parcel entity has a working memory. If not, it creates one (see code 3).
public void createWorkingMemory(ParcelLocal parcel) throws WorkingMemoryException { if (parcel.hasWorkingMemory()) { throw new WorkingMemoryException("Cannot create working memory for parcel [" + parcel.getParcelId() + "]. This parcel already has a working memory"); } try { List parcelData = ParcelBuilder.buildParcelWorkingMemory(parcel); ArrayList workingMemoryStatus = new ArrayList(); workingMemoryStatus.addAll(parcelData); parcel.setWorkingMemoryObjects(workingMemoryStatus); parcel.setWorkingMemoryVersion(ParcelConstants.WORKING_MEMORY_VERSION); } catch (IOException e) { throw new WorkingMemoryException(e.getMessage(), e); } catch( NamingException e) { throw new WorkingMemoryException(e.getMessage(), e); } }
To track and trace the parcel, we receive simple piece of information describing the position of the parcel during its trip. This is simply called a parcel history. When we receive a parcel history, we just add it to the list of already processed DataObjects (see code 4):
- We first build the working memory;
- We add the new parcel history represented by a data object;
- And finally, we fire the rules.
The first step is achieved by getting the list of DataObjects of the parcel, getting the rulebase, instanciating the working memory and assert all the objects. This is at this step that we assert applicationData since they may vary during the life cycle of the parcel.
public void addToWorkingMemoryAndFireRules(ParcelLocal parcel, List workingMemoryDataList, boolean saveWm) throws WorkingMemoryException { WorkingMemory wm = getWorkingMemory(parcel); String parcelId = parcel.getParcelId() ; Iterator it = workingMemoryDataList.iterator() ; while(it.hasNext()) { Serializable o = (Serializable) it.next() ; if (o == null) { StringBuffer buf = new StringBuffer("Could not add null object to working memory of parcel [") ; buf.append(parcelId) ; buf.append("]"); throw new WorkingMemoryException(buf.toString()) ; } try { wm.assertObject(o); } catch(FactException fe) { StringBuffer buf = new StringBuffer("Could not add object [") ; buf.append(o) ; buf.append("] to working memory of parcel [") ; buf.append(parcelId) ; buf.append("]"); throw new WorkingMemoryException(buf.toString(), fe); } } try { wm.fireAllRules(); if (saveWm) { setWorkingMemory(parcel, wm) ; } } catch(FactException fe) { // Avoid invalid contents of the working memory ctx.setRollbackOnly(); StringBuffer buf = new StringBuffer("Error occurred firing rules. ErrorMsg=[") ; buf.append(fe.getMessage()) ; buf.append("].") ; String str = buf.toString() ; logger.error(str, fe); throw new WorkingMemoryException(str,fe); } } public WorkingMemory getWorkingMemory(ParcelLocal parcel) throws WorkingMemoryException { if (parcel.hasWorkingMemory()) { try { ArrayList workingMemoryObjects = parcel.getWorkingMemoryObjects(); WorkingMemory wm = RuleBaseWrapper.getRuleBase().newWorkingMemory(); wm.setApplicationData("parcelIdData", new ParcelIdData(parcel.getParcelId())); wm.setApplicationData("codAmountData", new CodAmountData(parcel.getCodAmount(), parcel.getCodCurrency())); wm.setApplicationData("returnAmountData", new ReturnAmountData(parcel.getReturnAmount(), parcel.getReturnCurrency())); wm.setApplicationData("lastScandateData", parcel.getLastScandate() == null ? new Date(-1) : new Date(parcel.getLastScandate().getTime())); for (Iterator iterator = workingMemoryObjects.iterator(); iterator.hasNext();) { wm.assertObject(iterator.next()); } wm.addEventListener(new KernelTracker()); wm.clearAgenda(); // Restored state of working memory return wm; } catch(IOException e) { throw new WorkingMemoryException("Unable to deserialize working memory objects for parcel [" + parcel.getParcelId() + "]", e); } catch(ClassNotFoundException e) { throw new WorkingMemoryException("Unable to deserialize working memory objects for parcel [" + parcel.getParcelId() + "]", e); } catch(FactException e) { throw new WorkingMemoryException("Unable to restore working memory for parcel [" + parcel.getParcelId() + "]", e); } } else { return null; } }
Conclusion
Integrating drools in a java application is very simple. The most difficult part is, in my opinion, to define the rules related to your business.
In this article I didn't discuss conflict resolution , nor temporal rules but I hope that this article did help you to understand how drools can be used in a J2EE environment.
Acknowledgments
A lot of thanks to Mark Proctor and all the drool team to have built a great tool and for their support. I would like to thank the company for which I'm working for the support during development and last but not least Steve Uhlig for his corrections, advice and support during the writing of this article.


