AnsweredAssumed Answered

Automatic email on task assignment: an abstract solution

Question asked by giafar on Dec 11, 2008
Hi all
I think my problem is a common problem.
I'd like alfresco send an email to actor or pooleadactor whenever a task is assigned. The solution I have founded is based on the event process-start and JBPM API but I have a problem with bpm_package.children in freemarker transformation

Here is my code:
on processdefinition.xml

<process-definition  xmlns="urn:jbpm.org:jpdl-3.1"  name="dbwf:amministrazione_insoluti">
      <event type="process-start">
         <action name="prepareWorkflow" class="it.giafar.domina.alfresco.bpm.PrepareWorkflow"></action>
      </event>
                <swimlane name="initiator">
         <assignment class="org.alfresco.repo.workflow.jbpm.AlfrescoAssignment">
            <actor>#{initiator}</actor>
                       </assignment>   
      </swimlane>
      
    <swimlane name="amministrazione">
        <assignment class="org.alfresco.repo.workflow.jbpm.AlfrescoAssignment">
            <pooledactors>#{people.getGroup('GROUP_My Group')}</pooledactors>
        </assignment>
    </swimlane>
   <start-state name="start">
      <task name="dbwf:submitAmministrazioneInsoluti"></task>
      <transition to="info"></transition>
   </start-state>


   <task-node name="info">
      <task name="dbwf:aiTaskInfo" swimlane="initiator"></task>


the PrepareWorkflow class extends JBPMSpringActionHandler and for each TaskNode of the process instance add a new action based on NotifyTaskAssign class for the event Event.EVENTTYPE_TASK_ASSIGN:

package it.giafar.domina.alfresco.bpm;

import java.util.Iterator;

import org.alfresco.repo.workflow.jbpm.JBPMSpringActionHandler;
import org.jbpm.graph.def.Action;
import org.jbpm.graph.def.Event;
import org.jbpm.graph.def.Node;
import org.jbpm.graph.exe.ExecutionContext;
import org.jbpm.graph.exe.ProcessInstance;
import org.jbpm.graph.node.TaskNode;
import org.jbpm.instantiation.Delegation;
import org.springframework.beans.factory.BeanFactory;

public class PrepareWorkflow extends JBPMSpringActionHandler {

   /**
    *
    */
   private static final long serialVersionUID = 1L;

   /*
    * @Override protected void initialiseHandler(BeanFactory factory) { }
    */
   public PrepareWorkflow() {
   }

   @SuppressWarnings("unchecked")
   public void execute(ExecutionContext ec) throws Exception {
      Node cn = ec.getNode();
      if (cn != null) {
         ProcessInstance pi = ec.getProcessInstance();
         Iterator<Node> nodes = pi.getProcessDefinition().getNodes().iterator();
         while (nodes.hasNext()) {
            Node n = nodes.next();
            if (n instanceof TaskNode) {
               Event event = new Event(Event.EVENTTYPE_TASK_ASSIGN);
               Action action = new Action(new Delegation(NotifyTaskAssign.class.getName()));
               event.addAction(action);
               n.addEvent(event);
            }
         }
      }
   }

   @Override
   protected void initialiseHandler(BeanFactory factory) {
   }
}

The magic job is done by the class NotifyTaskAssign:

package it.giafar.domina.alfresco.bpm;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.alfresco.model.ContentModel;
import org.alfresco.module.dominaext.mail2.Mail2ActionExecuter;
import org.alfresco.repo.i18n.MessageService;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.alfresco.repo.workflow.jbpm.JBPMSpringActionHandler;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.action.Action;
import org.alfresco.service.cmr.action.ActionService;
import org.alfresco.service.cmr.model.FileFolderService;
import org.alfresco.service.cmr.model.FileNotFoundException;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.repository.TemplateService;
import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.apache.log4j.Logger;
import org.jbpm.graph.exe.ExecutionContext;
import org.jbpm.graph.exe.ProcessInstance;
import org.jbpm.taskmgmt.exe.PooledActor;
import org.jbpm.taskmgmt.exe.SwimlaneInstance;
import org.jbpm.taskmgmt.exe.TaskInstance;
import org.springframework.beans.factory.BeanFactory;

public class NotifyTaskAssign extends JBPMSpringActionHandler {

   private static final String WORKFLOW_TEMPLATE_EMAIL = "Data Dictionary/Workflow Email Templates";
   
   private static final String DEFAULT_EMAIL_TEMPLATE = "/it/giafar/domina/alfresco/bpm/jbpm-task-assign-template.ftl";
      
   private static final String DEFAULT_CMS_EMAIL_TEMPLATE = "default.ftl";
   
   private static final long serialVersionUID = 1L;

   private ServiceRegistry _services;
   
   private MessageService _messageService;
   
   private static Logger _logger = Logger.getLogger(NotifyTaskAssign.class);

   /*
    * @Override protected void initialiseHandler(BeanFactory factory) { }
    */
   public NotifyTaskAssign() {
   }

   @SuppressWarnings("unchecked")
   public Object doJob(final ExecutionContext ec) throws Exception {

      TaskInstance ti = ec.getTaskInstance();

      if(ti == null) return null;
      
      Map<String, NodeRef> persons = new HashMap<String, NodeRef>();
      
      PersonService personService = this._services.getPersonService();
      AuthorityService authorityService = this._services.getAuthorityService();
      
      SwimlaneInstance swimlaneInstance = ti.getSwimlaneInstance();
      if (swimlaneInstance != null) {
         if (swimlaneInstance.getActorId() != null) {
            String actor = swimlaneInstance.getActorId();
            persons.put(actor, personService.getPerson(actor));
         }
         if (swimlaneInstance.getPooledActors() != null) {
            Iterator<PooledActor> s = swimlaneInstance.getPooledActors().iterator();
            while (s.hasNext()) {
               String actor = s.next().getActorId();
               if(authorityService.authorityExists(actor)) {
                  Iterator<String> containedAuthorities = authorityService.getContainedAuthorities(null, actor, true).iterator();
                  while(containedAuthorities.hasNext()) {
                     String pooledPerson = containedAuthorities.next();
                     if(personService.personExists(pooledPerson))
                        persons.put(pooledPerson, personService.getPerson(pooledPerson));
                  }
               }
            }
         }
      }
         
      if(persons.size()==0) return null;

      ProcessInstance pi = ec.getProcessInstance();
      
      Map<String, Object> templateModel = pi.getContextInstance().getVariables();

      String processNameKey = pi.getProcessDefinition().getName().replace(":","_")+ ".workflow.title";
      String tokenKey = "dbwf_workflow.type." + ti.getName().replace(":", "_") + ".title";
      
      String processName = _messageService.getMessage(processNameKey);
      String taskName = _messageService.getMessage(tokenKey);
      
      templateModel.put("processName", processName);
      templateModel.put("taskName",  taskName);
      templateModel.put("date", new Date(System.currentTimeMillis()));
      
      Iterator<String> personKeyIterator = persons.keySet().iterator();
      
      TemplateService templateService = this._services.getTemplateService();
      String template = loadTemplate(pi.getProcessDefinition().getName(), ti.getName());
      
      while(personKeyIterator.hasNext()) {
         
         NodeRef person = persons.get(personKeyIterator.next());
         
         templateModel.put("swimlane", person);

      String result = templateService.processTemplateString(null, template, templateModel);

         ActionService actionService = this._services.getActionService();
         Action action = actionService.createAction(Mail2ActionExecuter.NAME);
         
         /*
         action.setParameterValue(Mail2ActionExecuter.PARAM_TO,
               (String)this._services.getNodeService().getProperty(person, ContentModel.PROP_EMAIL));
               */
         action.setParameterValue(Mail2ActionExecuter.PARAM_TO, "gianluca.fares@giafar.it");
         action.setParameterValue(Mail2ActionExecuter.PARAM_SUBJECT, "Notifica assegnazione attività");
         action.setParameterValue(Mail2ActionExecuter.PARAM_HTML, result);
         action.setParameterValue(Mail2ActionExecuter.PARAM_FROM, "noreply@dominabusiness.it");
         try {
            actionService.executeAction(action, null);
         } catch (Exception e) {
            _logger.error("Unable to send email to user");
            e.printStackTrace();
         }
      }
      return null;
   }

   private String loadTemplate(String processKey, String tokenKey) throws IOException {
      FileFolderService fileFolderService = this._services.getFileFolderService();
      NodeRef companyHomeNodeRef = getCompanyHomeNodeRef();
      NodeRef templateNodeRef = null;
      String templateName = WORKFLOW_TEMPLATE_EMAIL  + "/" + processKey.replace(":", "_") + "-" + tokenKey.replace(":", "_") + ".ftl";
      try  {
         // Load process-task template if exists
         templateNodeRef = fileFolderService.resolveNamePath(companyHomeNodeRef, Arrays.asList(templateName.split("/"))).getNodeRef();
         return fileFolderService.getReader(templateNodeRef).getContentString();
      } catch (FileNotFoundException fnfe) {
         // Load process template if exists
         templateName = WORKFLOW_TEMPLATE_EMAIL  + "/" + processKey.replace(":", "_") + ".ftl";
         try {
            templateNodeRef = fileFolderService.resolveNamePath(companyHomeNodeRef, Arrays.asList(templateName.split("/"))).getNodeRef();
            return fileFolderService.getReader(templateNodeRef).getContentString();
         } catch (FileNotFoundException fnfe2) {
            templateName = WORKFLOW_TEMPLATE_EMAIL  + "/" + DEFAULT_CMS_EMAIL_TEMPLATE;
            try {
               templateNodeRef = fileFolderService.resolveNamePath(companyHomeNodeRef, Arrays.asList(templateName.split("/"))).getNodeRef();
               return fileFolderService.getReader(templateNodeRef).getContentString();
            } catch (FileNotFoundException fnfe3) {
              // Load default template
              String ret = "";
              InputStream is = this.getClass().getResourceAsStream(DEFAULT_EMAIL_TEMPLATE);
              BufferedReader br = new BufferedReader(new InputStreamReader(is));
              String line = null;
              while( (line = br.readLine())!=null)
                 ret += line;
              return ret;
            }
         }
      }
   }
   
   private final NodeRef getCompanyHomeNodeRef() {
      StoreRef workSpaceStore = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore");
      NodeService nodeService = this._services.getNodeService();
      NodeRef rootWsNode = nodeService.getRootNode(workSpaceStore);
      List<ChildAssociationRef> assocRefs = nodeService.getChildAssocs(
            rootWsNode, ContentModel.ASSOC_CHILDREN, QName.createQName(
                  NamespaceService.APP_MODEL_1_0_URI, "company_home"));
      return assocRefs.get(0).getChildRef();
   }   
   
   @Override
   protected void initialiseHandler(BeanFactory factory) {
      _messageService = (MessageService)factory.getBean("messageService");      
      _services = (ServiceRegistry)factory.getBean(ServiceRegistry.SERVICE_REGISTRY);
   }

   public void execute(ExecutionContext arg0) throws Exception {
      final ExecutionContext finalEx = arg0;
      AuthenticationUtil.runAs(new RunAsWork<Object>(){
         public Object doWork() throws Exception {
            return doJob(finalEx);
         }
      }, AuthenticationUtil.SYSTEM_USER_NAME);   }
}

NotifyTaskAssign uses a custom mail action, Mail2ActionExecuter, which send email as html and attachment. Please change the code to use MailActionExecuter
Worwflow email templates must be uploaded into Company Home/Data Dictionary/Workflow Email Templates, the class search for <process>-<task>.ftl, <process>.ftl, default.ftl and a file inside my jar for fun ;-).
<process> and <task> must replace ':' whit '_'.. Looking at my processdefinition.xml, the class looks for dbwf_amministrazione_insoluti-dbwf_aiTaskInfo.ftl, dbwf_amministrazione_insoluti.ftl, default.ftl and so on.

A very simple default.ftl template look like this, remember that I send email as html, change as you need:

<#assign datetimeformat="dd MMMM yyyy">
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<style>
body,p,td {
   color: black; 
   background-color: #FFFFFF; 
   font-family: Arial; 
   font-size: 10pt;
}
</style>
</head>
<body style="color: black;  background-color: #FFFFFF;  font-family: Arial;  font-size: 10pt;">
Dr. ${swimlane.properties.firstName} <#if swimlane.properties.lastName?exists> ${swimlane.properties.lastName}</#if><br/>
Today, ${date?string(datetimeformat)} a new task has been assigned to you: ${taskName} ${processName}
<p>
<strong>Some info</strong>
<table cellspacing="0" cellpadding="3">
<tr><td>percentComplete</td><td>${bpm_percentComplete} %</td></tr>
<tr><td>workflowDescription</td><td><#if bpm_workflowDescription?exists>${bpm_workflowDescription}</#if></td></tr>
<tr><td>workflowDueDate</td><td><#if bpm_workflowDueDate?exists>${bpm_workflowDueDate?string(datetimeformat)}</#if></td></tr>
<tr><td>Status</td><td>${bpm_status}</td></tr>
<tr><td>Priority</td><td>${bpm_workflowPriority}</td></tr>
<tr><td>Outcome</td><td><#if bpm_outcome?exists>${bpm_outcome}</#if></td></tr>
</table>
</p>
<p><strong>
You receive this email becouse of you are the owner or you are a pooled actor.
</strong></p>
<p>
Thanks in advance and enjoy Alfresco. Thanks Alfresco for the great JOB.
</p>
</body>
</html>

Well I have now a problem and you could help me ….
I receive NullPointerException if I try to enumerate the bpm_package.children inside the template. I'm new to Freemarker and Rhino, can you help me ?

Outcomes