Liferay commit transaction listener

Goal

To register and perform an action on a Liferay transaction commit successful

Description

The situation: in a given project, we had created a custom service builder entity named WorkRequest which has a state that controls if the work request should be shared with a 3rd party application or not (if the state is draft, it is simply maintained locally, on Liferay; on the other hand, if it is on state pending, it should be sent to a given external application – lets call it EXTERNAL APP 1 – and, finally, if it is on state pending_documents, it should be sent to another external application – lets call that one EXTERNAL APP 2).

To avoid issues regarding the communication with the external applications (e.g., the external application’s response time is too high or the external application is not online at a given time), we created an asynchronous integration mechanism, through Liferay message bus, trying to send almost all requests ASAP but not in a synchronous manner.

To do so, we have implemented the synchronisation with the external applications in two different situations:

  • ASAP (As soon as possible) – any time the user clicks on the submit button, the work request goes from the state draft to the state pending and at that moment, send a message to Liferay’s message bus so that the work request may be shared with the external application (this resulted in a problem, which may be solved with the current recipe)
  • SCHEDULER – a scheduler in the corresponding Liferay portlet that, from time to time, checks the work requests that are on state pending (to share with EXTERNAL APP 1) or on state pending_documents (to share with EXTERNAL APP 2) and processes them.

During the first situation, we ended up with funny things, such as:

  • The associated files were not shared with the external application
  • We would send more than once the work request to the external application

And we also realized that if we were in debug (actually, if we would include a small delay on the service method), the previous issues were not detected. So, it seemed to be a problem related to the boundaries of the transaction and its visibility. It seemed that, when the asynchronous listener was invoked, the transaction had not been finished yet and so, some of the data associated to the work request was still not available on the transaction that would be spawn on the message bus listener class. After some searches, we found out this Liferay thread which, although is not the exact same situation, is similar and explains a few things (including the trick that will make this recipe interesting).

How to

During this how to, we will include more than the solution in itself so that is may be easier for you to try out the same scenario. So, first, lets start with the class that registers the message destinations and the corresponding message listeners:

/**
 *
 * This class will be used to register all message bus listeners
 * 
 * @author Paulo Zenida - Linkare TI
 *
 */
@ApplicationScoped
// it is a JSF backing bean with eager to true to force initialization on deployment
@ManagedBean(eager = true, name = "messageInitializerBean")
public class MessageInitializerBean {

  public static final String MESSAGE_OBJECT_PROPERTY = "payload";

  public static final String WORK_REQUEST_DESTINATION = "messagebus/addWorkRequest";

  @PostConstruct
  public void init() {
    addDestination(WORK_REQUEST_DESTINATION, new WorkRequestListener());
  }

  private void addDestination(final String destinationName, final MessageListener messageListener) {
    @SuppressWarnings("deprecation")
    final Destination destination = new SerialDestination(destinationName);
    MessageBusUtil.addDestination(destination);
    destination.register(messageListener);
  }
}

Next, we define the message listener class (WorkRequestListener) that will deal with the requests to share the work requests:

/**
 * 
 * @author Paulo Zenida - Linkare TI
 *
 */
public class WorkRequestListener implements MessageListener {

  private static final Log LOG = LogFactoryUtil.getLog(WorkRequestListener.class);

  @Override
  public void receive(final Message message) throws MessageListenerException {
    LOG.debug("Message received " + message);
    final WorkRequest workRequestPayload = (WorkRequest) message.get(MessageInitializerBean.MESSAGE_OBJECT_PROPERTY);

    try {
      final WorkRequest currentWorkRequest = WorkRequestLocalServiceUtil.fetchWorkRequest(workRequestPayload.getId());
      if (currentWorkRequest.getStateEnum() == WorkRequestState.PENDING) {
        WorkRequestLocalServiceUtil.submitWorkRequest2ExternalApp1(currentWorkRequest);
      } else if (currentWorkRequest.getStateEnum() == WorkRequestState.PENDING_DOCUMENTS) {
        // a future recipe will explain about this here.. for now, just assume it necessary to
        // share the documents without problems accessing those documents 
	setPermissionChecked(message);
	WorkRequestLocalServiceUtil.submitWorkRequest2ExternalApp2(currentWorkRequest);
      }
    } catch (SystemException e) {
      throw new MessageListenerException(e);
    }
  }
}

Now, we will show the service class where we have implemented the invocation to the External App 1 (ExternalApp1Client is a utility class that transforms our classes in the corresponding data and classes defined in the WSDL file of the external application client 1). Notice, however, that we do not show the method for the 2nd external application, which is similar to this one, with the exception that it does not need to send an asynchronous request because after the successful integration with the external application 2, there is no need to share anything else externally:

public class WorkRequestLocalServiceImpl extends WorkRequestLocalServiceBaseImpl {
  @Override
  public void submitWorkRequest2ExternalApp1(final WorkRequest workRequest) throws SystemException {

    // first invoke the WS client to add the work request and change it according to the response
    try {
      ExternalApp1Client.addWorkRequest(workRequest);
    } catch (final Exception e) {
      throw new SystemException("Integration with external app 1 has resulted in an exception being thrown", e);
    }

    // only change the state if the external app 1 has returned a processId
    if (StringUtils.isNotBlank(workRequest.getProcessId())) {
      workRequest.setStateEnum(WorkRequestState.PENDING_DOCUMENTS);
    }

    super.updateWorkRequest(workRequest);

    TransactionCommitCallbackRegistryUtil.registerCallback(new Callable() {
      @Override
      public Void call() throws Exception {
        if (workRequest.getStateEnum() == WorkRequestState.PENDING_DOCUMENTS) {
	  MessageSender.addWorkRequestMessage(workRequest);
	}
        return null;
      }
    });
  }
  ...

The trick in the previous code is within the TransactionCommitCallbackRegistry which makes sure that the work request is not sent to the message bus until the transaction has finished.

The previous code version had the invocation to addWorkRequestMessage() from class MessageSender next to the workRequest.setStateEnum(WorkRequestState.PENDING_DOCUMENTS);

And finally, just to simplify the example in case you want to try it out, here goes the MessageSender class:

import com.liferay.portal.kernel.messaging.Message;
import com.liferay.portal.kernel.messaging.MessageBusUtil;
import com.linkare.myproject.model.WorkRequest;

/**
 * 
 * @author Paulo Zenida - Linkare TI
 *
 */
public final class MessageSender {

  private MessageSender() {
  }

  public static void addWorkRequestMessage(final WorkRequest workRequest) {
    send(workRequest, MessageInitializerBean.WORK_REQUEST_DESTINATION);
  }

  private static  void send(final T entity, final String destination) {
    final Message myMessage = new Message();
    myMessage.put(MessageInitializerBean.MESSAGE_OBJECT_PROPERTY, entity);
    MessageBusUtil.sendMessage(destination, myMessage);
  }
}

Explanations

And that’s it. Most of the explanations were made in place so, there’s nothing much to add here.

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