Integrating Audience Targeting with your Apps – Part II: Rules

Liferay Developer Conference and Unconference 2015 were simply great. Lots of sessions, games and chances to meet some of the most active Liferay developers. It was also very exciting and encouraging to register the attention that Audience Targeting has drawn among developers in just one year. Both the session and the workshop led by Julio, Pavel and me were packed and we hope that you enjoyed them as much as we did.
 
 

Previously on Integrating Audience Targeting with your Apps…

A couple of weeks ago we started a series of blog entries about the integrability features included in Audience Targeting 2.0. As you may recall, in the first entry we compared Audience Targeting with a machine that helped you to move users from one state to other. This machine consists of three processes: classification, targeting and monitoring. 

User classification based on 3rd party information: Rules as integration points

In this entry we will focus on the first process of our Audience Targeting machine: user classification. User classification is based on rules that evaluate if a certain user attribute matches a given configuration. Audience Targeting provides out of the box a number of rules that evaluate in-Liferay information such as user profile attributes or session attributes, but also information from external sources such as Facebook.
 
Therefore, rules are a very powerful integration point since they allow to classify users based on information provided by any external app that exposes an API. As promised, this time we’ll show some code that you can take as example for your own experiments. 
 

Segmenting users by their influence in Twitter

Imagine that your company wants to increase the impact of a certain topic on the social networks. A good start would be to find the most influent users (e.g. in terms of connections) and have them talking about your topic. In other words, you need a rule that evaluates if the user’s influence is above a certain threshold, so that you can create user segments with the most influent users.
 
Most popular social networks offer APIs and documentation for developers to obtain information from a user’s profile. In this example we’ll use the Twitter API to obtain the number of followers of the current user (if he/she has filled the Twitter profile field in the Liferay user profile). Admins will be able to set a threshold that determines if the user is influent or not based on the number of followers.
 

Setting up the environment

In order to follow this example you need to setup a suitable development environment. Read the instructions in our Github repository and in the Liferay Development Network to install the latest version of the Audience Targeting SDK (use the develp branch).  This is how your environment should look like:

Creating the rule project

Audience Targeting SDK is basically an extension of the Liferay Plugins SDK. If you have ever worked with it, the steps will sound familiar. Let’s start by creating a new rule project with these very simple commands:
at-sdk-dir>./create_rule.sh twitter-sample “Twitter Sample” 
 
You should now find a rule-twitter-sample folder in your SDK. It contains the skeleton of our rule project. Move to that directory and execute this command:
at-sdk-dir/rule-twitter-sample>ant deploy
 
Now your rule has been deployed in your Liferay bundle and you will find it in the list of rule (under the Sample category) when editing a user segment in Audience Targeting:
 

Customizing the rule

It’s been quite easy to add a new rule to Audience Targeting, but so far it’s nothing but a dummy box in our user segment builder. It’s time to implement our user classification project based on Twitter followers. Let’s start by changing some visual aspects such as the icon or the category:
 
TwitterSampleRuleCategory.java:
package com.liferay.content.targeting.rule.twitter.sample;

import com.liferay.content.targeting.api.model.BaseRuleCategory;
import com.liferay.content.targeting.api.model.RuleCategory;

import org.osgi.service.component.annotations.Component;

/**
 * @author Eduardo Garcia
 */
@Component(immediate = true, service = RuleCategory.class)
public class TwitterSampleRuleCategory extends BaseRuleCategory {

	public static final String KEY = "twitter";

	@Override
	public String getCategoryKey() {
		return KEY;
	}

	@Override
	public String getIcon() {
		return "icon-twitter";
	}

}
 
TwitterSampleRule.java
...
	@Override
	public String getIcon() {
		return "icon-twitter";
	}

	@Override
	public String getRuleCategoryKey() {
		return TwitterSampleRuleCategory.KEY;
	}
...
 
With this our rule looks more “Twitter”.  Now since we want our rule to accept a threshold value for user classification, we’ll customize the UI of our rule this way:
 
ct_fields.ftl
...
<@liferay_ui["message"] arguments=selectorField key="users-that-have-more-than" />

<@aui["input"] inlineField=true label="" name="followersThreshold" style="margin-bottom: 0; width: auto;" suffix="followers" title="number-of-followers" type="text" value=followersThreshold>
	<@aui["validator"] name="number" />
</@>
 
TwitterSampleRule.java
...
	@Override
	public String processRule(
		PortletRequest request, PortletResponse response, String id,
		Map<String, String> values) {

		int followersThreshold = GetterUtil.getInteger(
			values.get("followersThreshold"));

		JSONObject jsonObj = JSONFactoryUtil.createJSONObject();

		jsonObj.put("followersThreshold", followersThreshold);

		return jsonObj.toString();
	}

	@Override
	protected void populateContext(
		RuleInstance ruleInstance, Map<String, Object> context,
		Map<String, String> values) {

		int followersThreshold = 0;

		if (!values.isEmpty()) {
			followersThreshold = GetterUtil.getInteger(
				values.get("followersThreshold"));
		}
		else if (ruleInstance != null) {
			String typeSettings = ruleInstance.getTypeSettings();

			try {
				JSONObject jsonObj = JSONFactoryUtil.createJSONObject(
					typeSettings);

				followersThreshold = GetterUtil.getInteger(
					jsonObj.getInt("followersThreshold"));
			}
			catch (JSONException jse) {
			}
		}

		context.put("followersThreshold", followersThreshold);
	}
...
 
Notice that the processRule and populateContext methods simply store/read the value of the threshold field. Don’t forget to add your keys and translations to the Language.properties files.
 
So far so good but, where’s the Twitter-integration logic? The classification algorithm of Audience Targeting rules is defined in the evaluate method of the Rule.class. Here is where we take the context (request) and the rule configuration (stored in the RuleInstance object) and determine if the current user (represented by the AnonymousUser object) matches or not the rule. 
 

Consuming the Twitter API

To consume Twitter’s API you need an access token. Depending on your use case, you can obtain it in different ways. In our simple example we’ll create and use our own account to access the API
 
Twitter exposes several flavours of APIs for different purposes (Web, mobile…), and there are also many libraries available to consume them . In our example we’ll use Twitter4j  by @yusuke under Apache License 2.0, but of course you can use your favorite one.
 
This is how the evaluate method of our rule looks with the Twitter-integration logic: 
 
TwitterSampleRule.java
...
	@Override
	public boolean evaluate(
			HttpServletRequest request, RuleInstance ruleInstance,
			AnonymousUser anonymousUser)
		throws Exception {

		User user = anonymousUser.getUser();

		if (user == null) {
			return false;
		}

		Contact contact = user.getContact();

		String twitterScreenName = contact.getTwitterSn();

		if (Validator.isNull(twitterScreenName)) {
			return false;
		}

		JSONObject jsonObj = JSONFactoryUtil.createJSONObject(
			ruleInstance.getTypeSettings());

		int followersThreshold = jsonObj.getInt("followersThreshold");

		ConfigurationBuilder cb = new ConfigurationBuilder();

		cb.setDebugEnabled(true);
		cb.setOAuthConsumerKey(_CONSUMER_KEY);
		cb.setOAuthConsumerSecret(_CONSUMER_SECRET);
		cb.setOAuthAccessToken(_ACCESS_KEY);
		cb.setOAuthAccessTokenSecret(_ACCESS_SECRET);

		try {
			TwitterFactory twitterFactory = new TwitterFactory(cb.build());

			Twitter twitter = twitterFactory.getInstance();

			IDs followerIDs = twitter.getFollowersIDs(
				twitterScreenName, -1, followersThreshold);

			long[] ids = followerIDs.getIDs();

			if (followersThreshold == ids.length) {
				return true;
			}
		}
		catch (TwitterException te) {
			_log.error("Cannot retrieve data from Twitter", te);
		}

		return false;
	}
...
 
First we discard non registered users (they don’t have a Liferay profile we can get the Twitter profile from). Then, we discard registered users without a Twitter profile. After that we access the Twitter API with our credentials and obtain the number of followers for the user’s profile. Finally, we compare this number with the threshold stored in the rule configuration and return true if it’s equal or greater*, or false otherwise.
 
*Notice that the third parameter getFollowersIDs is the limit to the results and has been set to the threshold value. Thus, it’s enough if the result is equal to the threshold (greater than would return the same). 
 

Setting rule dependencies

You can declare twitter4j as a build-time dependency the ivy.xml file of your rule project, so that the library is resolved and downloaded when you deploy the rule:
<ivy-module>
	...
	<dependencies defaultconf="default">
		...
		<dependency name="twitter4j-core" org="org.twitter4j" rev="4.0.4" />
	</dependencies>
</ivy-module>
 
To make sure the library is packaged with your rule (to resolve runtime-dependencies) add this line to the bnd.bnd file of your rule project:
...
Include-Resource:\
 	...,\
	@lib/twitter4j-core.jar
...
 

Testing our rule

Did it work? Well, let’s test it by following these steps:
  1. Create a couple of users (A and B), each with a different Twitter profile. 
  2. Add a user segment “Twitter Influencers” with your brand new rule and set a number of followers that is greater than the number of followers of B but lower than the number of followers of A. 
  3. Go to the homepage and add a User Segment Content Display portlet. 
  4. Edit the portlet configuration so that a certain image is displayed for users that belong to the “Twitter Influencers” user segment. 
  5. Finally, log in with users A and B. User A will see the image, while user B won’t.
Now your company is ready to target Twitter influencers to increase its presence in social networks! How? We’ll you’ll have to wait for the next entrywink.
 

Best practices and improvements

That our example is functional doesn’t mean that it’s ready for use. Make sure you follow the API best practices and note the possible limitations. For instance, Twitter’s REST API has a limit of 15 to 180 calls every 15 mins. You should consider caching or using other API such as the Streaming API.
 
Regarding Twitter credentials, it’s not a good idea to have them declared as constants in your code even if tokens can be reverted and regenerated. In this series of blog entries we’ll show you a better approach based on Consumer Manager, a new Liferay tool to manage application settings.
 

Try this at home!

Though this example was quite simple, you can take it as a basis for more complex rules. Just image the possibilities, given the amount of potential external sources for user classification. Try to build your own rules and if you feel proud of them, contribute the code to our repository and write an entry about them!