Using Liferay's rules engine

EE Only Feature

Liferay Portal Enterprise Edition provides an implementation of a JSR-94 compliant rules engine. This rules engine is provided as a Web Plugin and is based on the popular open source Drools project.

Why use a rules engine?

If you are not familiar with rules engines, you may be wondering why you would want to use one. In most applications, complex rule processing often takes the form of nested if-else blocks of code which can be very difficult to decipher and to maintain. If rules change, a developer must work with a business user to define the new rules. The developer must then read through the existing logic to understand what is happening and make the necessary modifications. The changes must then be recompiled, tested, and redeployed. A rules engine provides a means to separate the rules or logic of an application from the remaining code. Separating these rules provides several distinct advantages.

  • A rules engine allows for a more declarative style of programming where the rules define what is happening, without describing how it is happening. This makes it much easier to read than nested ‘if-else’ blocks of code. It’s also easier to make changes without introducing bugs in your code.

  • The rules are written in a language that is easier for non-developers to understand. This makes it easier for business users to validate and even modify the rules without having to involve developers.

  • A rules engine allows for changes to be made to the rules without requiring that you recompile your application. If your code must pass through a strict deployment workflow, this can be a huge time saver and can also save a significant amount of money.

After all this, you may be interested in using Liferay’s rules engine, so let’s get started with it.

Installation

The Drools web plugin is available to Liferay Enterprise Edition customers through Liferay Marketplace. It’s called the Drools EE app and you’ll find it categorized as a Utility app.

The Drools web plugin provides a rules engine implementation. By itself, it doesn’t provide any observable changes to the portal user interface or any additional functionality. To see the rules engine in action, you can download and install the Sample Drools portlet that contains two rule definitions that illustrate how to leverage the rules engine in your custom code. The Sample Drools portlet is available through the Customer Portal.

Let’s examine the sample portlet to see how it works.

Configuring the Sample Drools Portlet

Begin by downloading and installing the Sample Drools portlet. The Sample Drools portlet is available to Liferay Enterprise Edition customers through the customer portal. The name is sample-drools-portlet, and you’ll find it in the list of web plugins.

After installation is complete, log in as an administrator and add the portlet to a page. Initially, the portlet indicates the name of the currently logged in user and a message stating that there are no results. To see results in the portlet, we need to create and tag assets in the site to which you added the portlet.

From the Dockbar, click AdminContent and create a new web content instance in your site. Before publishing the web content instance, tag the article with americas. Then, navigate to My Account from the Dockbar and click on the Addresses link on the right side of the screen. Enter a Brazilian, Canadian, Mexican, or US based address and click Save. Navigate back to the page with the Sample Drools portlet. The Sample Drools portlet should now be displaying the web content instance that you created.

The default rule that’s being evaluated displays a list of assets based on the current user’s address. For example, if the current user’s country is set to Brazil, Canada, Mexico, or the United States, the Sample Drools portlet displays a list of assets that have been tagged with the americas tag.

Figure 8.9: By default, the Sample Drools portlet returns personalized content based on the addresses set on the form for editing a user account. This form is accessible from the Dockbar via \[User Name\] → My Account. Users with addresses in Brazil, Canada, Mexico, or the United States will see a list of assets tagged with the americas tag in the Sample Drools portlet.

Figure 8.9: By default, the Sample Drools portlet returns personalized content based on the addresses set on the form for editing a user account. This form is accessible from the Dockbar via *[User Name]* → *My Account*. Users with addresses in Brazil, Canada, Mexico, or the United States will see a list of assets tagged with the "americas" tag in the Sample Drools portlet.

Now that you’ve seen how the Sample Drools portlet works, let’s take a look closer at the rules themselves.

Rules Definitions

Rule definitions can be written using Drools’ declarative language. Rule files are text files that often have a .drl extension. A rule file can contain multiple rules. In addition to the standard Drools’ declarative language, a domain specific language (DSL) can be created for your specific problem domain.

Creating a DSL can make your rules even easier for

business users to create and maintain your applications rules but does require some additional work up front. For additional information on creating a DSL for your problem domain please refer to the Domain Specific Languages section of the official Drools Documentation.

To see examples of a rules definition file, access the following directory in the Sample Drools portlet sample-drools-portlet/WEB-INF/src/com/liferay/sampledrools/dependencies. To see how rules work in action, we’ll look at the rule defined in rules_user_address_content.drl.

At first glance, this .drl file looks a lot like a Java class file. This example starts with a comment describing the rule. Single line comments can begin with either a # or // and multi-line comments begin with /* and end with */.

##
## Rules
##
## This sample program will return personalized content based on the user's
## addresses set in the My Account section of the Control Panel.
##
## For example, suppose the current user has an address in the United States and
## is a member of the Liferay site. All assets within the Liferay site
## that are tagged with "West Coast Symposium" will be returned.
##

Following the comments is a package declaration. The package declaration is optional in a Drools, but if it appears, it must be at the beginning of the file. The package denotes a collection of rules. Unlike Java, the package name does not represent a folder structure; it’s only a unique namespace. The ; at the end of the package declaration and all other statements is optional.

package com.liferay.sampledrools.dependencies;

After the package declaration are a series of import statements. Import statements in the rule file are used just like the import statements in Java classes. Classes that are imported can be used in the rules.

import com.liferay.portal.kernel.search.BooleanClause;
import com.liferay.portal.kernel.search.BooleanClauseOccur;
import com.liferay.portal.kernel.search.BooleanClauseFactoryUtil;
import com.liferay.portal.kernel.search.BooleanQuery;
import com.liferay.portal.kernel.search.BooleanQueryFactoryUtil;
import com.liferay.portal.kernel.search.Document;
import com.liferay.portal.kernel.search.FacetedSearcher;
import com.liferay.portal.kernel.search.Field;
import com.liferay.portal.kernel.search.Hits;
import com.liferay.portal.kernel.search.Indexer;
import com.liferay.portal.kernel.search.SearchContext;
import com.liferay.portal.kernel.search.facet.AssetEntriesFacet;
import com.liferay.portal.kernel.search.facet.Facet;
import com.liferay.portal.kernel.search.facet.ScopeFacet;
import com.liferay.portal.kernel.util.GetterUtil;
import com.liferay.portal.kernel.util.KeyValuePair;
import com.liferay.portal.kernel.util.LocaleUtil;
import com.liferay.portal.kernel.util.StringUtil;
import com.liferay.portal.kernel.util.Validator;
import com.liferay.portal.model.Address;
import com.liferay.portal.model.Group;
import com.liferay.portal.model.Contact;
import com.liferay.portal.model.User;
import com.liferay.portal.service.AddressLocalServiceUtil;
import com.liferay.portal.util.PortalUtil;
import com.liferay.portlet.asset.model.AssetEntry;
import com.liferay.portlet.asset.service.AssetEntryLocalServiceUtil;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

The next line declares the dialect for the package. In this case, we will be using Java for any of the code expressions that we’ll encounter. The other possible value for the dialect is MVEL, which is a powerful expression language for Java-based applications. See http://mvel.codehaus.org/ for details. If necessary, the dialect can also be specified at the rule level to override the package level dialect.

    dialect "java"

In this rule file, we have only a single function, which is listed next. Functions allow you to insert Java code that can be evaluated at run time into your rule file. Functions are commonly used as a as part of the consequence clause of the rule statement. Functions are similar to Java methods but you use the function keyword to define a function. The function’s parameters and the return type are declared as they would be in a Java method. In this example, the getAssetEntries function returns a java.util.List object that contains AssetEntry objects based on the user, classNameIds, and assetTagName provided in the function call.

function List getAssetEntries(
    User user, long[] classNameIds, String assetTagName) {

    List<Group> groups = user.getMySites();

    long[] groupIds = new long[groups.size()];

    for (int i = 0; i < groups.size(); i++) {
        Group group = groups.get(i);

        groupIds[i] = group.getGroupId();
    }

    if ((classNameIds == null) || (classNameIds.length == 0) ||
        (groupIds.length == 0) || Validator.isNull(assetTagName)) {

        return Collections.emptyList();
    }

    SearchContext searchContext = new SearchContext();

    Facet assetEntriesFacet = new AssetEntriesFacet(searchContext);

    assetEntriesFacet.setStatic(true);

    searchContext.addFacet(assetEntriesFacet);

    Facet scopeFacet = new ScopeFacet(searchContext);

    scopeFacet.setStatic(true);

    searchContext.addFacet(scopeFacet);

    searchContext.setAttribute("paginationType", "regular");
    searchContext.setCompanyId(user.getCompanyId());
    searchContext.setGroupIds(groupIds);
    searchContext.setIncludeStagingGroups(false);
    searchContext.setScopeStrict(false);
    searchContext.setUserId(user.getUserId());

    BooleanQuery booleanQuery = BooleanQueryFactoryUtil.create(searchContext);

    booleanQuery.addExactTerm(Field.ASSET_TAG_NAMES, assetTagName);

    BooleanClause booleanClause = BooleanClauseFactoryUtil.create(
        searchContext, booleanQuery, BooleanClauseOccur.MUST.getName());

    searchContext.setBooleanClauses(new BooleanClause[] {booleanClause});

    List<AssetEntry> assetEntries = new ArrayList<AssetEntry>();

    Indexer indexer = FacetedSearcher.getInstance();

    Hits hits = indexer.search(searchContext);

    for (int i = 0; i < hits.getDocs().length; i++) {
        Document document = hits.doc(i);

        String entryClassName = document.get(Field.ENTRY_CLASS_NAME);

        long entryClassPK = GetterUtil.getLong(
            document.get(Field.ENTRY_CLASS_PK));
        long rootEntryClassPK = GetterUtil.getLong(
            document.get(Field.ROOT_ENTRY_CLASS_PK));

        if (rootEntryClassPK > 0) {
            entryClassPK = rootEntryClassPK;
        }

        AssetEntry assetEntry = AssetEntryLocalServiceUtil.fetchEntry(
            entryClassName, entryClassPK);

        if (assetEntry != null) {
            assetEntries.add(assetEntry);
        }
    }

    return assetEntries;
}

Alternatively, this function could’ve been written in a helper class and then imported using a function import. So if we had created a helper class called AddressContentHelper the import would look like this:

    import function
    com.liferay.sampledrools.dependencies.AddressContentHelper.getAsetEntries;

The last section of the rules file contains the actual rules. The syntax of a rule is very straightforward.

    rule "name" attribute when condition then consequence end

The rule name is required and it must be unique within the package as declared above. Names with spaces must be enclosed in double quotes. It is considered a best practice to always enclose your rule names in double quotes.

    rule "Initialize Rules"

The attributes section is optional. In our first rule, we have a single attribute called salience. The salience attribute is an integer value that acts as a priority weighting. The number can be positive or negative and defaults to the value of 0. Rules with a higher salience value are given a higher priority. It is considered a best practice to write attributes one to a line. In our example, the first rule is one that should be evaluated before any other so it is given a high salience value of 1000. None of our other rules have a salience attribute set, so they all default to a value of 0.

    salience 1000

The when keyword marks the conditional section of the rule. It is also referred to as the Left Hand Side (LHS). The when keyword is followed by zero or more condition elements that must be met before the consequence will be called. If there are no condition elements, then the condition is always evaluated as true.

The most common type of conditional element is a pattern. Patterns match against facts. Facts are statements that are known to be true. Facts can be inserted by the Java code that invokes the rules engine or they can be declared in the rule file itself.

In the first rule of our rule file (Initialize Rules), the only condition is that the rule must operate on a User object.

when user : User();

In more complex rules, the pattern may include constraints or may evaluate the properties of Java objects. For example, the second rule of this rule file is called Get European Content. This rule includes the following pattern which ensures that a user’s address contains the country name France, Germany, or Spain.

userAddress : Address(country.getName(Locale.US) in ("France", "Germany", "Spain"));

The consequence section of the rule follows the conditional section. It’s also known as the Right Hand Side (RHS) or action part of the rule. The consequence section begins with the keyword then and it is intended to modify the working memory data. Drools provides some convenience methods that make it easier to modify the working memory. In this rule, we use the insertLogical method which places a new object into the working memory and retracts it when there are no more facts supporting the truth of the current rule. After the consequence section of the rule, the rule is terminated with the keyword end.

then List<Address> userAddresses = AddressLocalServiceUtil.getAddresses(
        user.getCompanyId(), Contact.class.getName(), user.getContactId());

    for (Address userAddress : userAddresses) {
        insertLogical(userAddress); } end

Following the initial rule in our example, there are three additional rules that will be evaluated. Each of these rules evaluates the userAddress that was inserted into the working memory to determine what type of content should be displayed to the end user.

For additional documentation on the Drools rules language, please see the official Drools documentation.

« Using Application Display TemplatesSummary »
Was this article helpful?
0 out of 0 found this helpful