Auditing Liferay’s service builder custom entitities

Goal

Auditing custom service builder entities in Liferay portal

Description

One of the requirements in the latest Liferay portal application I have been working on was auditing the entities of that application, which are being supported with Liferay’s service builder framework.

This recipe explains how we can leverage the Audit EE plugin to support auditing in our custom entities.

How to

First, install the appropriate version of the Audit EE plugin into your Liferay portal installation by copying the plugin’s Liferay package into the deploy folder of Liferay.

Second, as explained in Audit trail section of Liferay’s documentation, define the audit enabling properties in the portal-ext.properties:

# By default, this is set to false, because the audit plugins aren\u2019t installed by 
# default. When you set it to true, the audit hook is able to capture more information 
# about events, such as the client host and the client's IP address.
com.liferay.portal.servlet.filters.audit.AuditFilter=true

# In the code, pages are layouts. Setting this to true, therefore, records audit events 
# for page views. It's turned off by default because this may be too fine-grained for 
#most installations.
audit.message.com.liferay.portal.model.Layout.VIEW=false

Third, we will have to implement a model listener for our audited entities. In our case, we had a wrapper organization class pointing to a Liferay’s organization (it doesn’t really matter the class we chose to add auditing features nor the reason why we had to create one wrapper class for organization, in what concerns this recipe, so just assume it is necessary and it is a very good example đŸ™‚ ). Now, this was the tricky part because we ended up facing a few known Liferay bugs (10988 and 15144) associated to the registration of model listeners for custom entities, but we’ll get there later in this recipe.

So, the first thing we had to do was to create the listener class for our custom entity SigafOrganization (this implementation was adapted from the implementation of the Liferay’s audit hook, namely the class RoleListener – BTW, the classes com.linkare.sigaf.audit.util.Attribute, com.linkare.sigaf.audit.util.AttributeBuilder, com.linkare.sigaf.audit.util.AuditMessageBuilder and com.linkare.sigaf.audit.util.EventTypes were extracted from audit hook’s source code and included directly in the project to avoid static dependencies on that module – however, the code of those classes is not shown due to license reasons!):

package com.linkare.sigaf.hook.listener;

import java.util.List;

import com.liferay.portal.ModelListenerException;
import com.liferay.portal.kernel.audit.AuditMessage;
import com.liferay.portal.kernel.audit.AuditRouterUtil;
import com.liferay.portal.model.BaseModelListener;
import com.linkare.sigaf.audit.util.Attribute;
import com.linkare.sigaf.audit.util.AttributesBuilder;
import com.linkare.sigaf.audit.util.AuditMessageBuilder;
import com.linkare.sigaf.audit.util.EventTypes;
import com.linkare.sigaf.model.SigafOrganization;
import com.linkare.sigaf.service.SigafOrganizationLocalServiceUtil;

/**
 * 
 * @author Paulo Zenida - Linkare TI
 * 
 */
public class SigafOrganizationListener extends 
  BaseModelListener {

  @Override
  public void onBeforeCreate(final SigafOrganization organization) 
                             throws ModelListenerException {
    auditOnCreateOrRemove(EventTypes.ADD, organization);
  }

  @Override
  public void onBeforeUpdate(final SigafOrganization newOrganization) 
                             throws ModelListenerException {
    try {
      final SigafOrganization oldOrganization = 
        SigafOrganizationLocalServiceUtil.fetchSigafOrganization(
                                          newOrganization.getOrganizationId());

      final List attributes = getModifiedAttributes(
                                         newOrganization, oldOrganization);

      if (!attributes.isEmpty()) {
	final AuditMessage auditMessage = 
          AuditMessageBuilder.buildAuditMessage(EventTypes.UPDATE,
                                                newOrganization.getClass().getName(),
                                                newOrganization.getOrganizationId(), 
                                                attributes);
        AuditRouterUtil.route(auditMessage);
      }
    } catch (Exception e) {
      throw new ModelListenerException(e);
    }
  }

  @Override
  public void onBeforeRemove(final SigafOrganization organization)  
                             throws ModelListenerException {
    auditOnCreateOrRemove(EventTypes.DELETE, organization);
  }

  protected void auditOnCreateOrRemove(final String eventType, 
                                       final SigafOrganization organization) 
                                       throws ModelListenerException {
    try {
      final AuditMessage auditMessage = AuditMessageBuilder.
                                       buildAuditMessage(eventType,
                                       organization.getClass().getName(),
                                       organization.getOrganizationId(), null);
      AuditRouterUtil.route(auditMessage);
    } catch (Exception e) {
      throw new ModelListenerException(e);
    }
  }

  protected List getModifiedAttributes(final SigafOrganization newOrganization, 
                                                  final SigafOrganization oldOrganization) {

    final AttributesBuilder attributesBuilder = new AttributesBuilder(newOrganization, 
                                                                      oldOrganization);

    attributesBuilder.add("email");
    attributesBuilder.add("address");
    attributesBuilder.add("location");
    attributesBuilder.add("parish");
    attributesBuilder.add("zipCode1");
    attributesBuilder.add("zipCode2");
    attributesBuilder.add("telephone");
    attributesBuilder.add("website");

    return attributesBuilder.getAttributes();
  }
}

And finally, register the model listener. Now, this is what made me write this recipe, because it was necessary to create a workaround to Liferay. According to Liferay’s documentation, all we have to do is to define a property in our hook portal.properties, having to:

  1. Define the portal-properties property in liferay-hook.xml file
  2. Define the model listener in the specified portal properties file

However, that resulted in an exception being thrown during the deployment of the portlet due to classpath problems: the portlet classes, namely the SigafOrganizationPersistenceImpl is not found, causing an exception during the deployment of the application:

Caused by: com.liferay.portal.kernel.bean.BeanLocatorException: 
   org.springframework.beans.factory.NoSuchBeanDefinitionException: 
   No bean named 'com.linkare.sigaf.service.persistence.SigafOrganizationPersistence' is defined

After some code analysis, we realized that the <CustomEntity>PersistenceImpl class contained a method named afterPropertiesSet() that looks for registered listeners in the service.properties file. Therefore, instead of defining the model listener class in the portal properties file, we defined inside the service.properties file (actually, in service-ext.properties because the service.properties file is always generated and what we would write there would be overridden on the next service builder generation) and everything started working:

...

value.object.listener.com.linkare.sigaf.model.SigafOrganization=\
  com.linkare.sigaf.hook.listener.SigafOrganizationListener

And the result of this implementation was that, after updating a sigaf organization’s data in its own interface, we were able to find an auditing entry in the “Audit Reports” option of Liferay’s control panel, such as the following:

auditing_result

Explanations

The implementation of auditing in Liferay portal with the Liferay Audit EE plugin is actually a very interesting solution because it is based on the model listeners concept and on the Message Bus architecture, making it possible to implement auditing in our projects without the need to pollute our model with that crosscutting concern. The tricky part in this recipe was the registration of the model listener to our custom entity but, after seeing the implementation of the persistence impl class, it was really simple to make it work.

One different approach we were considering (if the service.properties solution did not work) was to use a new custom persistence_impl.ftl file for the service builder generation code, that would provide a different implementation for the afterPropertiesSet() method, that would find the entity’s model listeners differently.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s