Hacking workflow

Published on September 1, 2016 by Rich Gange



If you're an old skool user of Magnolia, then you may remember a time when Magnolia used OpenWFE as its workflow engine. OpenWFE was a Java-based open source workflow engine. During the planning phases of Magnolia 5, it was determined that Magnolia could no longer depend on OpenWFE as its workflow engine since the project was no longer actively developed. Eventually, it was rewritten in Ruby and renamed ruote. Therefore, Magnolia opted for a new workflow solution based on JBoss's jBPM. While a very powerful tool, it's not as easy to modify as the old OpenWFE implementation. Both are based on XML which describe the process, but OpenWFE's XML was much easier to understand and follow. It really wasn't difficult at all to modify directly. Removing the approval step or sending an email could be accomplished without a whole lot of effort. While jBPM requires that you use a modeling tool, it would be far too difficult to change the XML directly in this case. Even when the process file is ready, it must then be deployed. Gone are the days of changing the workflow at Magnolia runtime. Any changes to jBPM process files require a server restart so that they can be loaded into the WFE. Similarily, changes to work item handlers, the subroutines of the workflow, also require restart to be loaded.

So now what? Do we have to become amateur jBPM developers? I did write a couple of tutorials to try and help people through the process of custom workflow, which can be found in the Community wiki. When I wrote those, I was trying to apply a 4.4 solution to Magnolia 5. In 4.4, we changed the workflow. That's how we removed the approval step or added email notification. While that worked well in 4.4, taking that approach in Magnolia 5.4 can be time consuming. When 5.4 was released two new workfow features were introduced. Being able to abort and an interface for monitoring scheduled workflows. Absolutely essential features. However, this adds more complexity to the OOTB 4-eye workflow and thus more complexity to any custom workflows that may have been created for Magnolia 5.3. So maybe changing the workflow isn't the best approach anymore. Let's take another look at a couple of old use cases: scheduled publishing and email notification.

 

Scheduled publishing

Not everyone wants the second set of eyes to approve a publishing request. However, if you remove the default four-eye workflow then you also remove the ability to schedule publication. Ok then, so you think, I'll just create a custom workflow. I'll start with Magnolia's OOTB four-eye workflow and tweak it a bit. Remove the human task, connect the remaining dots, and voila! Except... wait a second, schedule task work item handler expects for a task to have already been created by the time it needs to do its job (See MGNLWORKFLOW-330). So then what? You could rewrite the work-item handler for scheduled task, put it into your custom workflow, get all the configuration straight, and then finally the deployment process to production. That's a lot of work (See MGNLWORKFLOW-320). The customer may not be willing to invest so much into what feels like such a small change. After all in Magnolia 4.4 all I needed to do was delete a few lines of XML and save.

It's really not fair to compare jBPM to OpenWFE. I mean jBPM is like a chainsaw where OpenWFE is like a pair of hand shears. Sure, OpenWFE provided everything you really needed with very low complexity but we broke up, ok. It's over. We have to pick up the pieces and move on now. So let's think... let's not fall into the trap of only seeing the one path, changing the workflow. That worked ok in Magnolia 4.4 but now things are a little more sophisticated.

How about instead of trying to attack this problem by changing the workflow, which we know is complicated, we instead look for another approach? The fact that the approval step is there isn't such a big deal, if we could automate it. Once the editor publishes a page, then a task is created. A task is nothing more than a record - a node - in the tasks workspace. Each task has a status property associated with it. Once the publisher approves the task, it is resolved by the task manager. What we would like to do is go ahead and approve any new publishing task created in the workspace. Easy enough, we could do that with observation. So whenever a new node of type mgnl:task is added to the tasks workspace, call on the task manager to resolve it immediately. If it has a publication date then it will become a scheduled task, otherwise it will be activated immediately. For those comfortable with observation this might be the best approach for you.

How about a slightly easier approach? Well of course that's my opinion, but humor me for a second. We're going to need to get the task id and task manager anyway in order to resolve the task. The current OOTB workflow is wired to use the human task found in the workflow-jbpm module, why don't we just extend the current handler and auto approve the task there. Meaning, I'm going to extend HumanTaskWorkItemHandlerDefinition and HumanTaskWorkItemHandler. In this case the new definition will do nothing more than set the implementation class, which is the new handler. So my new definition class looks like this.

info.magnolia.groovy.workflow.AutoApproveHumanTaskWorkItemHandlerDefinition

package info.magnolia.groovy.workflow;
 
import info.magnolia.groovy.workflow.AutoApproveHumanTaskWorkItemHandler;
import info.magnolia.module.workflow.jbpm.humantask.handler.definition.HumanTaskWorkItemHandlerDefinition;
 
public class AutoApproveHumanTaskWorkItemHandlerDefinition extends HumanTaskWorkItemHandlerDefinition {
     
    public AutoApproveHumanTaskWorkItemHandlerDefinition() {
        setImplementationClass(AutoApproveHumanTaskWorkItemHandler.class);
    }
}

The handler will be extended from HumanTaskWorkItemHandler. Since I'm going to need the TasksManager and KieSession to execute the work-item, I have created private members for each. I set those in my constructor. Finally, I have overridden executeWorkItem(). I copied the code from HumanTaskWorkItemHandler and replaced the auto-claim option with auto-approve code. So my handler ends up looking like this

info.magnolia.groovy.workflow.AutoApproveHumanTaskWorkItemHandler

package info.magnolia.groovy.workflow;
 
import java.util.HashMap;
import java.util.Map;
 
import org.kie.api.runtime.KieSession;
 
import info.magnolia.module.workflow.jbpm.humantask.HumanTask;
import info.magnolia.module.workflow.jbpm.humantask.handler.HumanTaskWorkItemHandler;
import info.magnolia.module.workflow.jbpm.humantask.parameter.HumanTaskParameterResolver;
import info.magnolia.objectfactory.ComponentProvider;
import info.magnolia.registry.RegistrationException;
import info.magnolia.task.TasksManager;
import info.magnolia.task.definition.registry.TaskDefinitionRegistry;
 
import org.kie.api.runtime.process.WorkItem;
import org.kie.api.runtime.process.WorkItemManager;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
public class AutoApproveHumanTaskWorkItemHandler extends HumanTaskWorkItemHandler {
     
    private static final Logger log = 
        LoggerFactory.getLogger(AutoApproveHumanTaskWorkItemHandler.class);
     
    private TasksManager tasksManager;
    private KieSession kieSession;
 
    public AutoApproveHumanTaskWorkItemHandler(TaskDefinitionRegistry taskDefinitionRegistry,
            ComponentProvider componentProvider, TasksManager tasksManager, KieSession kieSession) {
        super(taskDefinitionRegistry, componentProvider, tasksManager, kieSession);
         
        this.tasksManager = tasksManager;
        this.kieSession = kieSession;
    }
     
    public void executeWorkItem(WorkItem workItem, WorkItemManager manager) {
 
        try {
            HumanTaskParameterResolver parameterResolver = 
                getParameterResolver((String) workItem.getParameter(TASK_NAME));
            HumanTask task = parameterResolver.createTask(workItem, kieSession);
 
            tasksManager.addTask(task);
             
            // Auto approve the task
            Map<String, Object> result = new HashMap<String, Object>();
            result.put("decision", "approve");
            this.tasksManager.resolve(task.getId(), result);
             
        } catch (RegistrationException e) {
            log.error("Could not retrieve task definition.", e);
        }
    }
}

I've got a running 5.4.8 bundle so I'll implement this as Groovy classes for simplicity's sake. Plus, I like to show what you can do using Groovy. It can be nice for these "one-offs" or patches of the system. From the app launcher,you will find the Groovy app under the DEV menu. When you open the app, you will be looking at the scripts workspace. Here is a screenshot of what I have after adding my two classes.

A little gotcha about the process, if you try and use the above code and you implement the definition class first, then you will need to comment out the lines that refer to the non-existing handler class. When you save your class, it will go through a compilation as part of the field's validation process. The easiest way around is to add your handler class first.

Next you will need to configure the existing human task work item to use the new auto approver.

In the log we can see the change was picked up by the system. However, as the message points out, we will need to restart the system to reload the human task work item handler.

2016-08-17 10:43:20,543 INFO  handler.registry.WorkItemHandlerDefinitionRegistry: Definitions for workItemHandlers will be reloaded on system restart. Currently registered [[rejectNotification, Human Task, asyncCommand, scheduleTask, errorHandler, command]]

Once the instance is back up, test it out. Publish a page, schedule it, and verify it skips the approval step and moves right into the scheduled state. Implementing this doesn't take a whole lot effort and we shouldn't have to worry about it breaking from an upgrade. Just always be sure to read the release notes in the planning stages of your upgrade. That includes all versions you may have skipped over. If there are changes to workflow, then be sure to test that this mechanism still works as expected.

 

Email notifications

Magnolia isn't always the center of everyone's life. I know, I know, hard to believe. But it's true! While the Pulse provides a centralized messaging service inside Magnolia, that doesn't really help much if you don't login into Magnolia regularly. If you are part of the publishers group or legal team, it's certainly helpful to get an email in the event something needs your attention. A while back I wrote a tutorial in the community wiki about adding email notifications to the workflow. You can find that here: Email Workflow.

There are a few issues with taking that approach. For one, creating a new workflow using jBPM can come with a bit of a learning curve. What's more, simply adding email notifications to a workflow is a lot of work with a lot of moving pieces. If you just want to send email for a new publication then you could use the same approach as with the scheduled notification workaround. Just override the human task and make it part of that work item. Another approach would be to use command chaining and send the email before the workflow is started. The problem here could be if the workflow failed to start. Someone might get an email about a publication request that never made it into the Pulse. It could be confusing for someone.

What if the requirements go further than just sending an email on a new publication request? Maybe you want to send an email for a publication that was scheduled alerting the publisher that the page is now live. Whatever the requirement might be, a lot of the time people will turn to the custom workflow as a solution. However, what if we took a less invasive approach?

Let's tackle the problem of sending an email when a new publication request is started. As we saw with the scheduled publication workaround, when a workflow is started a task is created. This is a node added event in the tasks workspace. By using observation, we can listen for this event and trigger a command, like send mail. Using this approach we don't need to alter anything with the OOTB workflow. Nor do we have the problem of sending an email before a failed workflow. We know that the work-item has to run in order to create the new task, thus firing the node added event.

So what I have now is my bundle with the observation module installed. This module comes with a few example listener configurations. One of these is the sendMailOnPageChanges, which I will use as my copy base since it has a lot of the configuration I want to use. Here is my new sendMailOnPublicationTaskCreated listener configuration.

I've added an additional property eventTypes with a value of NODE_ADDED. However, that is not enough since I only want to act on task nodes. Meaning, those nodes which have a type of mgnl:task. For this I will use the RestrictToNodeTypeEventListener.

The RestrictToNodeTypeEventListener is a CommandEventListener. This allows me to configure a command to be executed when my node type condition is met. Since we want to send a mail I will use our OOTB MailCommand. During execution of the mail command, it will expect to find a configured mailTemplate. The mail module provides a workflowNotifcation template which I have configured as my mailTemplate. I've also set the mailTo address which in this case is a distribution address for all publishers. I'm not going to use a model for my template but if you would like to, you can configure one on the model subnode of the params node. If you wanted to take this one step further, you might even consider extending RestrictToNodeTypeEventListener to make sure the new task is a publication request. In most cases it will be, but tasks is an independent mechanism in the system. Workflow just happens to be using it. So you may already have other types of tasks being created.

The last thing that I'm going to do is make a few changes to the mail template. On the template I will set the from address, the subject, and the templateFile properties. I'm going to create a custom script for this and to make it easy I will simply add it to the mtk module through Resources app. Ideally you would want to put it in your custom module but for the sake of demonstration I'll take this shortcut. In the end my template looks like this.

I'm not going to get real fancy with the script. I just want to show a couple of things and then you can take it from there. Inside your script you will have a context, or ctx, object available. It will be an instance of SimpleContext

workflowNotification.ftl

<div>You have a publication request that needs your review.</div>
 
<!-- The path attribute will be the task path -->
[#assign path = ctx.getAttribute("path")]
 
<!-- Get the session attached to the tasks workspace -->
[#assign session = ctx.getJCRSession("tasks")]
 
<!-- Information about the activated content will be found on a content subnode -->
[#assign content = session.getNode(path+"/content")]
 
<div>Requestor: ${content.getProperty("requestor").getString()}</div>
<div>Path: ${content.getProperty("path").getString()}</div>

Final thoughts

The need for scheduled publication without approval steps has been around for a while. It's never been a feature of Magnolia OOTB, but always very easy to implement. If you use Magnolia 5.3.x, then you have the option to switch to the publish workflow, rather than the use reviewForPublication, but you need to be aware of this MGNLWORKFLOW-331. There is a workaround for it mentioned on that ticket. If you use Magnolia 5.4.x, then you can use my hack. Is it the greatest solution in the world, not really, but it will get the job done and only take about 5 or 10 minutes if you choose to use Groovy. Otherwise, a little bit longer if you would prefer to package it into a module.

Observation can be a great way to fill-in-the-gaps or be the glue of some automated mechanism needed in the system. In the example we tackled the need for a publication request email but there may be other events that you want to act on. For example, you may want to listen for PROPERTY_CHANGED event on a task node where the status property changed to "Resolved". Here you check what the decision was and send an email to the original requester and let him know about the decision. Another scenario that you might want to send an email on would be after a scheduled publication finally gets activated.



Comments



{{item.userId}}   {{item.timestamp | timestampToDate}}

About the author Rich Gange

Richard Gange is the support engineer and trainer for Magnolia and has been working with the Magnolia product for over 6 years. He was a client with Manatee County before coming to work for Magnolia in late 2012. During his time at Manatee, Rich was responsible for developing custom applications on the Magnolia platform. Rich holds bachelor’s degrees in Mathematics and Computer Engineering from the Florida State University.


See all posts on Rich Gange

Demo site Contact us Free trial