Overview

Recently we put together a demo site for a prospective customer, and I was asked to create a couple of portlets using the Drools rules engine for personalization.

Implementation

The requirements called for one portlet to display the user’s training level and a second portlet to show the sales level. In both cases, the Drools rules would select content based on asset categories. The rules picked up personalization data placed in the session at login time by a separate login hook. This information could alternately have been directly retrieved from the database.

The portlets were developed for Liferay EE 6.1 GA2 and used Drools 5.4.0. The rules engine was loaded into Liferay by the OOTB Liferay Drools portlet (drools-web) available in the Liferay Marketplace. Although rules could be customized for each instance of a portlet on a page via portlet preferences, portlets preconfigured with the required rules for sales or training allowed:

  1. rules to be saved in the source code configuration system
  2. updating the rules by simply deploying the portlet

Follow The Rules

The Drools rules are fairly simple, with both rule sets following a similar pattern. At the top of the file are the include directives followed by a function definition (getAssetEntries()) to retrieve a list of assets. We’ll discuss the function shortly.

Following the function is the first rule, which as stated performs initialization:


rule "Initialize Rules"
 salience 1000
 when
 user : User();
 session : HttpSession();
 then
 //System.out.println("Initialize rules for user="             + user.getDisplayEmailAddress());
 Double sales = (Double)session.getAttribute("LIFERAY_SHARED_sales");
 if (sales == null) {
 sales = -1.0;
 }
 insertLogical(sales);
 System.out.println("Sales Rules initialized for "
 + user.getDisplayEmailAddress() + " sales=" + sales);
 end

The when clause requires two conditons to be true before this rule can be invoked. 1) A Liferay User must be defined and 2) An HttpSession is defined. When this rule is matched, the first thing is to retrieve the attribute from the session (sales) that was placed there by the login hook. Since this user may not have one of these extended attributes (saved in a separate personalization table), the rule uses a negative value to indicate this condition. Finally, the rule makes the sales value available to other rules, and logs a message.

The following rules take the sales amount and categorize into Silver, Gold, and Platinum levels. The case where no sales data was found also has a rule, but we’ll skip over that one for now.

Take a look at the rule defining Silver sales producers:

rule "Get silverSalesProducer Content"
 when
 classNameIds : KeyValuePair(key == "classNameIds");
 results : List();
 user : User();
 sales: Double(sales >= 0.0 && sales < 500000.0);    // sales < $500k
then
System.out.println("sales = " + sales);
List assetEntries = getAssetEntries(user, StringUtil.split(
classNameIds.getValue(), 0L), "silversalesproducer");
assetEntries.removeAll(results);
modify(results) {
addAll(assetEntries);
};
retract(sales);
end

The key part of the when clause is sales: Double(sales >= 0.0 && sales < 500000.0). This checks to make sure that the sales, initialized in the earlier rule, meet the criteria for Silver producer. Currently anything less that $500K is at the Silver level, and similar rules define Gold and Platinum levels, as well as no sales (the sales equal -1.0 that we set in the initialization rule). So we end up with rules containing the following conditions:

  • Silver: Double(sales >= 0.0 && sales < 500000.0)
  • Gold: Double(sales >= 500000.0 && sales < 2000000.0)
  • Platinum: Double(sales >= 2000000.0)
  • No sales data: Double(sales < 0.0)

When the rule is matched, the code logs the sales and then calls the function getAssetEntries() that was defined at the top of the rules file. The call to this function is where we map the Silver level to the asset category “silversalesproducer”.  The function returns a list of zero or more assets tagged with this category. The remainder of the rule adds the selected assets to the rule results and then removes sales from the possible conditions (retract(sales)). This last step means no other rules can match sales again, although it’s perfectly fine to match multiple rules if that’s what you want.

Displaying Results

Matching the rules is great, but it would be helpful if we actually display the results of matched rules. This requires three steps:

  1. Create categories; we used “silversalesproducer”, “goldsalesproducer”, “platinumsalesproducer”, and “nosalesdata”.
  2. Create and tag appropriate assets with these categories. If you don’t want to display something for a particular category, just don’t tag anything with that category.
  3. Display the assets from your JSP.

Here is the main JSP display code:

 

<%@ include file="/init.jsp" %>
<c:choose>
<c:when test="<%= themeDisplay.isSignedIn() %>">
<%
List<Fact<?>> facts = new ArrayList<Fact<?>>();
facts.add(new Fact<KeyValuePair>("classNameIds", new KeyValuePair(
"classNameIds", StringUtil.merge(classNameIds))));
facts.add(new Fact<KeyValuePair>("parentGroupId", new KeyValuePair(
"parentGroupId", String.valueOf(themeDisplay.getParentGroupId()))));
facts.add(new Fact<User>("user", user));
facts.add(new Fact<HttpSession>("session", request.getSession()));
facts.add(new Fact<KeyValuePair>("userCustomAttributeNames", new
KeyValuePair("userCustomAttributeNames", userCustomAttributeNames)));
facts.add(new Fact<List<AssetEntry>>("results", new ArrayList<AssetEntry>()));
if (!RulesEngineUtil.containsRuleDomain(domainName)) {
RulesResourceRetriever rulesResourceRetriever = new RulesResourceRetriever
(new StringResourceRetriever(rules), String.valueOf(RulesLanguage.DROOLS_RULE_LANGUAGE));
RulesEngineUtil.update(domainName, rulesResourceRetriever, PortalClassLoaderUtil.getClassLoader());
}
//System.out.println("domainName is " +domainName);
Map<String, ?> results = RulesEngineUtil.execute(domainName, facts, Query.createStandardQuery(),
PortalClassLoaderUtil.getClassLoader());
List<AssetEntry> assetEntries = (List<AssetEntry>)results.get("results");
%>
<c:choose>
<c:when test="<%= !assetEntries.isEmpty() %>">
<%
for (AssetEntry assetEntry : assetEntries) {
AssetRendererFactory assetRendererFactory = AssetRendererFactoryRegistryUtil.
getAssetRendererFactoryByClassName(assetEntry.getClassName());
if (assetRendererFactory == null) {
continue;
}
AssetRenderer assetRenderer = assetRendererFactory.getAssetRenderer(assetEntry.getClassPK());
request.setAttribute(WebKeys.ASSET_RENDERER_FACTORY, assetRendererFactory);
request.setAttribute(WebKeys.ASSET_RENDERER, assetRenderer);
// Do not show title
%>
<liferay-util:include
page="<%= assetRenderer.render(renderRequest, renderResponse,
AssetRenderer.TEMPLATE_FULL_CONTENT) %>"
portletId="<%= assetRendererFactory.getPortletId() %>"
/>
<%
}
%>
</c:when>
<c:otherwise>
<liferay-ui:message key="there-are-no-results" />
</c:otherwise>
</c:choose>
</c:when>
<c:otherwise>
<div class="portlet-msg-info">
<aui:a href="<%= themeDisplay.getURLSignIn() %>" label="sign-in-to-your-account" />
</div>
</c:otherwise>
</c:choose>

We won’t go into a deep explanation of the JSP code, but here is an outline of how the code works.

  1. Collect “facts” or resources that will list be used by the Drools rules engine. For instance, the Liferay User and HttpSession variable is added to the facts.
  2. Run the rules engine using the rules and facts.
  3. Get the result list, which contains a (possibly empty) list of assets. This is the list that was selected in the rule by calling getAssetEntries().
  4. For each asset in the list, get the asset renderer for the asset (the code used to display the asset) and display the asset.

That’s all there is to it.

Gotchas

There are a few gotchas that you may run into when dealing with Drools and this portlet, or at least some things that may not be immediately obvious.

  1. Category names: While category names can be almost anything you want, they will be forced to lower case. In addition, the code seems to run into problems if the category name has spaces. So do not use “Gold Sales Producer”; use “goldsalesproducer” instead.
  2. Rules matching numeric values: Drools makes use of Java objects, so for instance you must use Integer and not int when defining rules. And most examples I found use Strings in the rule conditions (the when clause). It was confusing when trying to use numbers in the conditions instead of strings. The solution was to wrap the condition in Double() or Integer().
  3. Document Library assets: Originally the assets to be displayed were defined as web content. Later, it was decided to upload images and display those files instead. The problem with this was the default asset renderer (display code) for files shows the file name as well as the image itself, which didn’t look good. The solution was to create a simple piece of web content that solely consists of an image. The image was in turn retrieved from the document library where it had been uploaded. The result was just the image, with no file name displayed.
  4. Double for money: The code used Double for money, a horrible practice since Double cannot precisely represent certain values. The reason Double was used in the Drools portlet was because the user attribute was defined as Double. The correct approach would be to use BigDecimal.
Share This