Using Salesforce to power Audience Targeting

Context is king

In 1996, when the web was in its infancy, Bill Gates famously predicted that content would be king on the internet. Twenty years later it has become increasingly important to not just publish your content on the web, but also to understand and respond to the context and purpose with which your visitors are visiting your website. For example, an engineer visiting your site is probably looking for technical documentation, while a C-level executive may be more interested in your case studies. Understanding the context with which your visitors are coming to your website, and responding to it, provides them with a better experience.
 
To let your Liferay site respond to your customers' contexts, you can use Audience Targeting. Audience Targeting allows you to segment your visitors based on what you know about them, and based on that segmentation, show them content that is most relevant for them.
 
In order to understand the visitors to your website, what better system to use than your CRM (Customer Relation Management) system? This is the system where organizations store all information about their business contacts: what industry do they work in? Are we trying to sell them anything? Who’s their local contact person within our organization? 
 
In this blog post I want to show how easy it is to integrate Audience Targeting with your CRM system, in order to present your visitors with the content that’s most relevant for them. As an example I will show how you can use a visitor's email address to find out the industry they are working in, by looking up their Account in Salesforce (one of the most popular CRM systems).
 
Our fictional contact Jane Gray works at the University of Arizona, which is active in the Education Industry
 

The Salesforce API

Salesforce provides a cloud-based system with a number of different APIs, which makes integrating it with any other system pretty straightforward. Which of the available APIs to use depends on your use case. In our case, we want to quickly and synchronously query Salesforce to get information about each individual visitor to our site. Two Salesforce APIs are most suitable for that: REST and SOAP. SOAP would be a good choice if we needed strongly-typed access to Salesforce objects, but SOAP also comes with the downside that we would need to import the SOAP service’s contract and generate classes out of it. In our case, the more light-weight REST API suffices and we will work with the JSON objects that Salesforce’s REST services return.
 
Salesforce provides a query language called Salesforce Object Query Language (SOQL) that we will use to query the objects in Salesforce. We can use this to retrieve those fields that we want to use in Audience Targeting. We will use the REST API to send SOQL statements to Salesforce and use the returned records for evaluating the Audience Targeting rules. SOQL is very similar to good old-fashioned SQL. For example, to retrieve the industry for the Account with which a specific user has been registered, we could use the following SOQL statement:
SELECT Industry FROM Account WHERE Id IN (SELECT AccountId from Contact WHERE Email = 'art@vandelay-industries.com')

In our rule, we will replace the field (Industry) with the specific field that the Administrator has configured, and the email address with the email address of the current Liferay user.

 
If you want to try out Salesforce, you can sign up for a free fully functional developer environment on the Salesforce website. You will need to set up your Salesforce environment for REST API access. Salesforce supports several authentication mechanisms; we want to use the “ Username and Password Flow” in which we use an “API user” to connect to Salesforce and query all the available data. To enable this flow, we first need to add our Liferay environment to Salesforce as a “connected app”, as described in Salesforce’s developer documentation. Don’t forget to enable OAuth. We will need the Customer Key and Customer Secret that are generated once you have created your app. We will also need a personal security token for the user that we will access the Salesforce API with. For security reasons, I would recommend assigning a “Read Only” profile to that user. Next, we need to retrieve a user-specific security token. Log in to Salesforce with the user, click on the user name in the top-right and select “My Settings”. Next, select “Personal” in the menu on the left and choose “Reset My Security Token”. Click on the button “Reset Security Token” and copy and paste the token to a safe location. Finally, we need to know which version of the Salesforce API is running on your environment. The easiest way to find out is by clicking “Setup” in the top right, and selecting "Develop” from the “Build” menu. Next, choose “API” and click “Generate Enterprise WSDL”. The first comment line of the generated WSDL will tell you the version of the API (e.g. 34.0).
 

Let’s start coding already

Now that we have set up our Salesforce environment, we can start implementing our Audience Targeting rule. We are first going to create a project using the Liferay Audience Targeting SDK. Download the SDK and create a new project using the ‘create_rule’ script as described in the documentation on dev.liferay.com. This will generate a boilerplate rule project with a number of files, which we can open in an IDE like Liferay Developer Studio. We will require three dependencies, so add the following lines to the generated ivy.xml file and add the downloaded JARs to your project’s build path:
 
<dependency name="httpclient" org="org.apache.httpcomponents" rev="4.5" />
<dependency name="httpcomponents-core" org="org.apache.httpcomponents" rev="4.4.1" />
<dependency name="json" org="org.json" rev="20141113" />
 
For now, only two of the files that were generated by the ‘create_rule’ script are important: a Java class in which we will implement our rule logic, and a Freemarker template that is used to render the rule’s administrative user interface (used by administrators to configure the rule on our site). Let’s start with implementing our rule class. First, we’ll implement a connect() method that we can use to connect to Salesforce. This method gets called when our rule is activated, and it will maintain our authentication parameters for re-use:
private void connect()
{
	try
	{
		String consumerKey = PropsUtil.get("salesforce.consumerKey");
		String consumerSecret = PropsUtil.get("salesforce.consumerSecret");
		String username = PropsUtil.get("salesforce.username");
		String password = PropsUtil.get("salesforce.password");
		String securityToken = PropsUtil.get("salesforce.securityToken");
		String apiVersion = PropsUtil.get("salesforce.apiVersion");

		HttpClient httpClient = HttpClientBuilder.create().build();

		String loginURL = "https://login.salesforce.com/services/oauth2/token";

		ArrayList postParameters = new ArrayList();
		postParameters.add(new BasicNameValuePair("grant_type", "password"));
		postParameters.add(new BasicNameValuePair("client_id", consumerKey));
		postParameters.add(new BasicNameValuePair("client_secret", consumerSecret));
		postParameters.add(new BasicNameValuePair("username", username));
		postParameters.add(new BasicNameValuePair("password", password + securityToken));

		HttpPost httpPost = new HttpPost(loginURL);
		httpPost.setEntity(new UrlEncodedFormEntity(postParameters, "utf-8"));

		HttpResponse response = httpClient.execute(httpPost);

		if (response.getStatusLine().getStatusCode() == 200)
		{
			String getResult = EntityUtils.toString(response.getEntity());

			JSONObject jsonObject = (JSONObject) new JSONTokener(getResult).nextValue();
			String accessToken = jsonObject.getString("access_token");
			String instanceUrl = jsonObject.getString("instance_url");

			httpPost.releaseConnection();

			_baseUri = instanceUrl + "/services/data/v" + apiVersion;
			_oauthHeader = new BasicHeader("Authorization", "OAuth " + accessToken);
		}
		else
		{
			System.err.println("Salesforce error code "
					+ response.getStatusLine().getStatusCode());
			System.err.println(EntityUtils.toString(response.getEntity()));
		}
	}
	catch (Exception e)
	{
		e.printStackTrace();
	}
}
The connect() method first reads some property values from the portal’s portal-ext.properties file. These are the values we collected when we set up the Salesforce environment for API access. Next, it uses those configured values to make an HTTP request to Salesforce’s authentication service. This service returns an access token and an instance URL. We can use these to create a base URI and a OAuth access header, which we need in order to be able do the actual queries.
 
We will also create a sendQuery() method, which will allow us to do actual Salesforce queries. This method will take a SOQL statement as an argument. It will try to make a REST call with that SOQL statement and return the resulting records:
public JSONArray sendQuery(String query)
{
	JSONArray records = new JSONArray();

	try
	{
		HttpResponse response = makeRestCall(query);

		int statusCode = response.getStatusLine().getStatusCode();

		if (statusCode != 200)
		{
			// Try re-authenticating
			connect();
			response = makeRestCall(query);
		}

		if (statusCode == 200)
		{
			// Successful query
			String response_string = EntityUtils.toString(response.getEntity());

			JSONObject json = new JSONObject(response_string);
			records = json.getJSONArray("records");
		}
		else
		{
			System.err.println("Salesforce error code "
					+ response.getStatusLine().getStatusCode());
			System.err.println(EntityUtils.toString(response.getEntity()));
		}
	}
	catch (Exception e)
	{
		// In case of *any* exception, we'll work with an empty JSON array
		e.printStackTrace();
	}

	return records;
}
This method checks whether the query was successful (response code 200). In case of a response code 401, our authentication token may have expired, and we will try to re-authenticate against Salesforce. 
 
The actual REST call looks like this:
private HttpResponse makeRestCall(String query) throws ClientProtocolException, IOException
{
	RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(500)
			.setSocketTimeout(500).build();
	HttpClient httpClient = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig)
			.build();

	String encodedQuery = URLEncoder.encode(query, "UTF-8");

	String uri = _baseUri + "/query?q=" + encodedQuery;

	HttpGet httpGet = new HttpGet(uri);
	httpGet.addHeader(_oauthHeader);

	return httpClient.execute(httpGet);
}
Note that the connection timeout is configured to be just half a second. We don’t want our page rendering to hang if there is a connectivity issue between Liferay and Salesforce.
 
And finally, our rule class also contains a method that will evaluate whether any of the records returned by Salesforce has a field (for example “Industry”) that has the requested value (for example “Education”):
public boolean checkAny(JSONArray records, String field, String value)
{
	for (int i = 0; i < records.length(); i++)
	{
		try
		{
			if (records.getJSONObject(i).getString(field).equals(value))
			{
				return true;
			}
		}
		catch (Exception e)
		{
			e.printStackTrace();
		}
	}

	return false;
}
 
The rule class has an evaluate() method that was generated by the ‘create_rule’ script. This method gets called to evaluate whether the current user matches the configured rule. In our implementation, we will retrieve the email address of the current user, as well as the configured “field” and “value” of our rule. We use these to call the methods that we have implemented earlier.
@Override
public boolean evaluate(HttpServletRequest request, RuleInstance ruleInstance,
		AnonymousUser anonymousUser) throws Exception
{

	User user = anonymousUser.getUser();

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

	String email = user.getEmailAddress();

	if (email.equals(""))
	{
		return false;
	}

	JSONObject jsonObj = new JSONObject(ruleInstance.getTypeSettings());

	String field = jsonObj.getString("field");
	String value = jsonObj.getString("value");

	String queryString = String
			.format("SELECT %s FROM Account WHERE Id IN (SELECT AccountId from Contact WHERE Email = '%s')",
					field, email);

	JSONArray records = sendQuery(queryString);

	return checkAny(records, field, value);
}
 
Finally, we need to define the user interface that administrators can use to configure the rule. In the “templates” folder that the ‘create_rule’ script created, you will find a generated Freemarker template file. Our user interface will have two textboxes: one to provide the Account field that we want to query (for example “Industry”) and one for the value that the field needs to have in order for the rule to apply (for example “Education”). Replace the contents of the template with the following:
<#assign aui = PortletJspTagLibs["/META-INF/aui.tld"] />
<@aui["input"] label="field" name="field" type="text" value=field>
	<@aui["validator"] name="required" />
</@>
<@aui["input"] label="value" name="value" type="text" value=value>
	<@aui["validator"] name="required" />
</@>
 
 
Our rule class will have to do some bookkeeping in order to retrieve these fields from/to the UI and render them with the configured values. This is not Salesforce-specific, so you can follow the documentation on dev.liferay.com for that, or take a look at my code on Github to see the end result. Similarly, you can use the generated Language.properties file to provide the text strings that you would like displayed in the user interface.
 

Result

Our rule is ready to be deployed! Build the project and deploy the resulting JAR file to your Liferay instance. Once deployed, you will be able to configure the rule through Site Administration and try out the result.
 
Jane Gray (left) is shown more relevant content than unknown visitors (right), because we know through Salesforce that she is active in the Education industry
 

Next steps

The rule as it is now is very limited: it can only query Salesforce Account objects. If you would want to know, for example, whether there are any interesting Opportunities associated with a user, you have to create a separate rule for that. 
 
Another limitation is that a user needs to be logged in to your site, so we know their email address and can do a lookup in Salesforce with it. There are ways around this - some client-side tracking libraries can give you information about users without them having to log in. That functionality could be incorporated in this rule.
 
And finally, the rule is not very performant: it makes a REST call for each configured rule, each time the rule is evaluated (at least once for every page view). This not only takes time; you may also run out of the maximum number of Salesforce API calls that you are allowed to make. A better solution would be to cache values, and query Salesforce only once every day for each user.
 
Blogs
Hi Abdon,

Great stuff here including the limitations/constraints! Great practical showcase too.

Cheers!
Inspiring example and thoroughly described. Well done Abdon!