Liferay 7.3 Workflow inspection, Part 1

and how to localize it

Diving into the "Simple Approver" workflow and trying to localize it, I made some observations I'd like to share with you.

Some of them may or may not be considered as bugs, but I'll get to that later.

When talking about localization I wanted to have the text in my portlets resource bundle. So the first challenge I faced: How to access my language.properties from the Freemarker templates?

Luckily there's 'serviceLocator'. This useful helper can 'find' OSGI services that are registered with the container.

So I created a small interface with only two methods in the context of my portlet:

package path.to.my.portlet.service;

import java.util.Locale;

public interface MyPortletLanguageService {
    public String getLanguage(Locale locale, String key);
    public String formatLanguage(Locale locale, String pattern, String[] arguments);
}

The implementation is done like this:

@Component(
    immediate = true,
    service = MyPortletLanguageService.class
)
public class MyPortletLanguageServiceImpl implements MyPortletLanguageService{

 @Override
  public String getLanguage(Locale locale, String key) {
        ResourceBundle resourceBundle = ResourceBundle.getBundle("content.Language", locale, UTF8Control.INSTANCE);
        
        return LanguageUtil.get(resourceBundle, key);
    }

....
    
 And now you'll need an Activator that registers the service:

public class MyPortletLanguageServiceActivator implements BundleActivator{
   
    @Override
    public void start(BundleContext context) throws Exception {
               Registry registry = RegistryUtil.getRegistry();
        _serviceRegistration = registry.registerService(MyPortletLanguageService.class, new MyPortletLanguageServiceImpl());
    }
 
    @Override
    public void stop(BundleContext context) throws Exception {
        if (_serviceRegistration != null) {
            _serviceRegistration.unregister();
            _serviceRegistration = null;
        }
    }
    
    private ServiceRegistration<MyPortletLanguageService> _serviceRegistration = null;
}

Once registered, you are able to access it  from a Freemarker template notification with:

myPortletLanguageService serviceLocator.findService("path.to.my.portlet.service.MyPortletLanguageService")

So far so good. But before we can populate fancy messages in E-Mails, we need to understand the flow of the work a little better. Especially who does what in the two existing tasks 'review' and 'update'.

Let's say we have to persons involved.

  1. Administrator (Role)
  2. Site Member (User)

 Usually Site Member starts the workflow by submitting something new or changed.

If you've implemented workflow for a custom asset before, you know that the generated request passes your WorkflowHandlers 'updateStatus' method and is then passed on to the equally named method of  your MyEntityLocalServiceImpl class.

So these are good places to start manipulating the information that is flowing between the Freemarker template and my services I thought.

Have you noticed that the Worflow E-Mails don't have a subject. At least in 7.3 they don't have one (And that is probably Bug 1, ...)

But you can easily set this in your updateStatus method like this:

workflowContext.put(WorkflowConstants.CONTEXT_NOTIFICATION_SUBJECT, "My Subject")

But hey! Didn't we talk about localization? So hardcoding that text is not the best option. For this we need a 'Locale' that we can pass to our language service. I choosed to use the users locale.

But who's the user in this situation, when the workflow is just created?

If you do

long userId = GetterUtil.getLong((String)workflowContext.get(WorkflowConstants.CONTEXT_USER_ID));

You'll find out, what a surprise, It's Site Member.  But the notification is send to the Administrator role. So we can not really localize for the target user it here :-(  because there's no real assignee.

Let's go for the root locale:

workflowContext.put(WorkflowConstants.CONTEXT_NOTIFICATION_SUBJECT, myPortletLanguageService.getLanguage(Locale.ROOT, "my-workflow-mail-subject"))

OK, now the Mail has a subject. Let's see what happens next.

One of the Administrators assigns the role to someone. Let's say himself. If you placed debug logs in your workflow handler (which is a really good idea..) you'll find out that the 'updateStatus' is not called.

Administrator is not sufficient with the work of Site Member and rejects the task. Now the MyWorkflowHandler is back into the game!

It's 'updateStatus' is immediately called two times! The first call set's the status of the asset to 'denied' and the second set's it to 'pending'.

There's a 'HTTPServletRequest' which you can obtain with

        ServiceContext serviceContext = (ServiceContext)workflowContext.get(WorkflowConstants.CONTEXT_SERVICE_CONTEXT);
        
        //try to get the workflowTaskId from the request to create a link
        HttpServletRequest httpServletRequest = serviceContext.getRequest();

Yes! The request carries the 'workflowTaskId' that we later need to create a direct link to the workflow. Having such a link in the incoming mail is comfortable. And to be honest: I wonder why  Liferay is making the thing that hard for us developers! But let's see...

Back to localization: The rejection mail is now send to Site Member. So we need the Locale of that user for getting the subject right, don't we.

In the request object you'll also find a 'assigneeUserId'. But wait! The 'updateStatus' was called two times. The first call triggers the 'onExit' notification in the workflow XML, because the Administrator finished his review. You can expect his userId as the assigneeUserId here and you are correct.

The second call starts the 'update' task for Site Member and the second mail is send from the configured 'onAssignment' notification.

Surprisingly enough the asigneeUserId is that of the Administrator in both calls! (And that's probably Bug 2)

And the userId we can obtain from the workflow context (see above) is also that of the Administrator in both calls.

Bug or not? I'm looking forward to feedback in the comments...

But now I'm left alone in the dark without the Id of Site Member, who is the receiver of the mail. Getting the subject right in this situation is hard.  The only workaround I found seems hackish, if someone has a better solution... your welcome!

Remember that the 'updateStatus' of your MyEntityLocalServiceImpl is also called? In that class your already using 'workflowInstanceLinkLocalService' to delete a worflow in case an asset is deleted.

with

WorkflowInstanceLink workflowInstanceLink = workflowInstanceLinkLocalService.fetchWorkflowInstanceLink(myEntity.getCompanyId(), myEntity.getGroupId(),MyEntity.class.getName(), myEntityId);

long workflowInstanceCreatorUserId = workflowInstanceLink.getUserId();

you can get your hands on the user that created that workflow instance. And in our case that was Site Member!

As we need that id in the WorkflowHandler I simply passed the workflow context to MyEntityLocalServiceImpl .updateStatus(...) and stuffed this Id in one of it's attributes only to retrieve it back out in the workflow handler. (This should also be possible with the ServiceContext object btw...)

Anyway, problem solved for now. Get the users locale and localize the subject of the mail.

User workflowInstanceCreator = _userLocalService.getUserById(workflowInstanceCreatorUserId);
workflowContext.replace(WorkflowConstants.CONTEXT_NOTIFICATION_SUBJECT, myPortletLanguageService.getLanguage(workflowInstanceCreator.getLocale(), "my-workflow-mail-subject"));

That's it for now.

In part 2 I'll report the adventures I luckily survived when I wanted to localize the Freemarker templates as well.

 

 

 

 

Blogs

Hello Andre,

 

I am stuck at fetching workflowTaskId in liferay 7.3 ,

I am using below methodology,

 

HttpServletRequest httpServletRequest = serviceContext.getRequest();

WorkflowTask workflowTask = (WorkflowTask)httpServletRequest.getAttribute(WebKeys.WORKFLOW_TASK);

long workflowTaskId = workflowTask.getWorkflowTaskId();

 

But, workflowTask object is null.

Do you have any inputs on this, where I might be going wrong?

 

Thanks and Regards,

Anishq