Hi all again! Good news!
This story has a happy ending thanks to your help!
First of all, let me point out some things I have learned during this journey.
- The best approach for me was to extend the jBPM Mail class. AFAICS, as it has access to the current executionContext there won't be any problem with LIE or transaction locks, because everything is being handled by jBPM. No more @Transactional. Anyway, if one needs to access task and process variables when rendering the email (as it is my case), one should preload them calling task.getVariables() and task.getProcessInstance().getContextInstance().getVariables() (or just task.getContextInstance().getVariables() for preloading both), so there won't be any LIE which would lead to sending the email incompletely rendered. This is necessary because when rendering the email there is no active session in order to get these variables, which are stored in variable containers in current token's context.
- As of jBPM 3.2.6.SP1 which I am using (and still in newest version 3.2.7), there is a bug in the XML schema that prevents proper use of the mail action inside a timer tag (in the designer and when deploying). One could remove all references to the schema in the JPDL, but that is kind of dirty. As the original mail class is basically an ActionHandler, a workaround is to call the extended mail class as an action from inside the timer. As I'm calling it that way for the reminder timer, then I could stop using the mail tag at all inside the event tags and stick to action tags. This way it would not be necessary to add any <string name="jbpm.mail.class.name" value="my.extended.Mail"/> to jbpm.cfg.xml. That's the approach I took.
- Also, it's not really needed to implement an AddressResolver, as one can write the code inside the extended mail class. So there is no need to define it in jbpm.cfg.xml. This way there are no changes at all to jBPM configuration.
- I don't need to define notifiers or reminders on each task. I can define only event nodes at process level for task-create, task-assign and task-end events. This works well because the current task is resolved depending on the current ExecutionContext.
- I was making a mistake when trying to get the pooledActors on task creation. jBPM takes two steps to create tasks, first they are created, then they are assigned to the swimlanes. As I have defined my task assignments with swimlanes based on pooledActors, when the task-create event was triggered by jBPM, there was still no assignment defined for the task, so I could get the pooledActors only when the task-asign event was triggered. Something to be aware of.
- It's also important to make sure that all the needed data from the model is made available and sent to the event observer, because, AFAIK, there won't be any active EntityManager when rendering the email. That's why I used FetchType.EAGER on the needed relationships of my model, which is easier than calling getters.
That's how my process definition section for mail sending now looks like this (no mail tags, as everything is resolved in the extended class and in the mail XHTML template):
<event type="task-create">
<action name="mailTaskCreate" class="org.ostion.siplacad.bpm.SiplacadMail">
<template>
task-create
</template>
</action>
<create-timer duedate="2 minutes" name="mailTaskReminder" repeat="2 minutes">
<action name="mailTaskReminder" class="org.ostion.siplacad.bpm.SiplacadMail">
<template>
task-reminder
</template>
</action>
</create-timer>
</event>
<event type="task-assign">
<action name="mailTaskAssign" class="org.ostion.siplacad.bpm.SiplacadMail">
<template>
task-assign
</template>
</action>
</event>
<event type="task-end">
<cancel-timer name="mailTaskReminder"/>
</event>
Here is my extended mail ActionHandler, based on Craig's idea, where recipients are user entity instances from my model rather than plain email addressess, because I need the name, email and sex - in spanish it's important for the salutation :-)
package org.ostion.siplacad.bpm;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import org.jboss.seam.Component;
import org.jboss.seam.core.Events;
import org.jboss.seam.log.Log;
import org.jboss.seam.log.Logging;
import org.jbpm.graph.exe.ExecutionContext;
import org.jbpm.mail.Mail;
import org.jbpm.taskmgmt.exe.PooledActor;
import org.jbpm.taskmgmt.exe.TaskInstance;
import org.ostion.siplacad.model.Estado;
import org.ostion.siplacad.model.entity.Rol;
import org.ostion.siplacad.model.entity.RolUsuario;
import org.ostion.siplacad.model.entity.Usuario;
import org.ostion.siplacad.session.SiplacadDataHelper;
import org.ostion.util.ui.MailProcessor;
/**
* Extends jBPM Mail ActionHandler to use send asynchronous mails
* through Seam Mail and Events API. Recipients are based on Usuario objects.
*
* Thanks to Craig Bensemann and Leo van den Berg
* See: http://seamframework.org/Community/AsynchronousSeamMailWithJbpmContextAndEntityManager
*
*/
@SuppressWarnings("serial")
public class SiplacadMail extends Mail {
private static final String TEMPLATE_PATH = "/WEB-INF/pages/";
private static final String EMAIL_NEW_TASK = "mailNewTask.xhtml";
private static final String EMAIL_TASK_REMINDER = "mailTaskReminder.xhtml";
private static final String TEMPLATE_TASK_CREATE = "task-create";
private static final String TEMPLATE_TASK_ASSIGN = "task-assign";
private static final String TEMPLATE_TASK_REMINDER = "task-reminder";
private String template = null;
private ExecutionContext executionContext;
/**
* @see org.jbpm.mail.Mail#execute(org.jbpm.graph.exe.ExecutionContext)
*/
@Override
public void execute(final ExecutionContext executionContext) {
this.executionContext = executionContext;
super.execute(executionContext);
}
/**
* @see org.jbpm.mail.Mail#send()
*/
@Override
public void send() {
Log log = Logging.getLog(getClass());
final Map<String, Object> parameters = new HashMap<String, Object>();
// task data
final TaskInstance task = executionContext.getTaskInstance();
// prevent LIE when accessing task variables or process variables from forked tokens
//task.getVariables(); // task variables
//task.getProcessInstance().getContextInstance().getVariables(); // process variables
task.getContextInstance().getVariables(); // both
parameters.put("task", task);
// recipients
AtomicBoolean isPooled = new AtomicBoolean();
AtomicBoolean wasAssigned = new AtomicBoolean();
final List<Usuario> usuarios = this.getUsuariosForTask(task, isPooled, wasAssigned);
if (usuarios.isEmpty()) {
// we have no one to email so dont try to send one!
log.info("No email addresses configured to send {0} emails to.", template);
return;
}
parameters.put("usuarios", usuarios);
// raise asynchronous event for sending mail
if (SiplacadMail.TEMPLATE_TASK_CREATE.equals(template) ||
(SiplacadMail.TEMPLATE_TASK_ASSIGN.equals(template) &&
(isPooled.get() || !wasAssigned.get()))) {
Events.instance().raiseAsynchronousEvent(
MailProcessor.EVENT_SEND_MAIL,
SiplacadMail.TEMPLATE_PATH + SiplacadMail.EMAIL_NEW_TASK,
parameters);
} else if (SiplacadMail.TEMPLATE_TASK_REMINDER.equals(template)) {
// check conditions for sending reminder
Integer reminderStart = (Integer) task.getVariable("reminderStart");
Integer reminderRepeat = (Integer) task.getVariable("reminderRepeat");
boolean isTooEarly = false;
long today = new Date().getTime();
long halfDay = 12*3600*1000;
long oneDay = 24*3600*1000;
halfDay = 1*60*1000; // 1 minutes (for testing)
oneDay = 2*60*1000; // 2 minutes (for testing)
// within first 12 hours of task creation
isTooEarly = isTooEarly || (today < task.getCreate().getTime() + halfDay);
// no due date
isTooEarly = isTooEarly || task.getDueDate() == null;
// before expected reminder start date
isTooEarly = isTooEarly || (today < task.getDueDate().getTime() - reminderStart * oneDay);
// out of reminder repeat interval (window 12 hours)
isTooEarly = isTooEarly || ((today - (task.getDueDate().getTime() - reminderStart * oneDay)) % reminderRepeat * oneDay > halfDay);
if (isTooEarly) {
log.info("It is too early to send {0} emails.", template);
return;
}
Events.instance().raiseAsynchronousEvent(
MailProcessor.EVENT_SEND_MAIL,
SiplacadMail.TEMPLATE_PATH + SiplacadMail.EMAIL_TASK_REMINDER,
parameters);
}
}
private List<Usuario> getUsuariosForTask(TaskInstance task, AtomicBoolean isPooled, AtomicBoolean wasAssigned) {
SiplacadDataHelper siplacadDataHelper = (SiplacadDataHelper) Component.getInstance("siplacadDataHelper");
String actorId = task.getActorId();
Set<PooledActor> pooledActors = task.getPooledActors();
List<Usuario> usuarios = new ArrayList<Usuario>();
isPooled.set(false);
wasAssigned.set(false);
if (actorId != null) {
wasAssigned.set(actorId.equals(task.getPreviousActorId()));
RolUsuario rolUsuario = siplacadDataHelper.getRolUsuarioByActorId(actorId);
Usuario usuario = rolUsuario.getUsuario();
if (rolUsuario.getEstado() == Estado.ACTIVO && usuario.getEstado() == Estado.ACTIVO) {
usuarios.add(usuario);
}
} else if (pooledActors != null) {
isPooled.set(true);
Rol rol = siplacadDataHelper.getRolByPooledActors(pooledActors);
usuarios.addAll(rol.getUsuariosActivos());
}
return usuarios;
}
}
And the mail processor, which is the only Seam component, which observes the events triggered by the extended mail class. It's based on Creig's post. BTW, I just remembered where I saw your solution to extend jBPM mail. I didn't get the idea until you posted it here with more detail... :-)
package org.ostion.util.ui;
import java.io.Serializable;
import java.util.Map;
import java.util.Map.Entry;
import org.jboss.seam.annotations.AutoCreate;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Observer;
import org.jboss.seam.contexts.Contexts;
import org.jboss.seam.faces.Renderer;
import org.jboss.seam.log.Log;
import org.jboss.seam.log.Logging;
/**
* Thanks to Craig Bensemann and Leo van den Berg
* See: http://seamframework.org/Community/AsynchronousSeamMailWithJbpmContextAndEntityManager
*/
@Name("mailProcessor")
@AutoCreate
public class MailProcessor implements Serializable {
private static final long serialVersionUID = 5001647628144517023L;
public static final String EVENT_SEND_MAIL = "org.ostion.sendMail";
private static final Log log = Logging.getLog(MailProcessor.class);
/**
* Process send mail event
* @param template
* @param parameters
*/
@Observer(EVENT_SEND_MAIL)
public void sendMailObserver(final String template,
final Map<String, Object> parameters) {
for (final Entry<String, Object> entry : parameters.entrySet()) {
Contexts.getEventContext().set(entry.getKey(), entry.getValue());
}
try {
Renderer.instance().render(template);
} catch (final Exception e) {
log.error("Unable to send email. Template=[{0}]. Parameters=[{1}]",
e, template, parameters.toString());
}
}
}
My mail XHTML templates must support multiple users as recipients and must be able to access more data about the user. Task data must be available because the localized messages are based on task name and description, as well as on task and process variables (see below). My reminder template looks like this:
<ui:repeat xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:m="http://jboss.com/products/seam/mail"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:s="http://jboss.com/products/seam/taglib"
value="#{usuarios}" var="usuario">
<m:message>
<m:from name="SIPLACAD" address="siplacad@localhost.localdomain" />
<m:to name="#{usuario.nombre}" address="#{usuario.email}" />
<m:cc name="SIPLACAD" address="siplacad@localhost.localdomain" />
<m:subject>Recordatorio de Tarea #<h:outputText value="#{task.id}"><f:convertNumber minIntegerDigits="3" /></h:outputText> en SIPLACAD</m:subject>
<m:body>
<a href="http://localhost:8080/siplacad/" target="_blank">
<img src="cid:#{logo.contentId}" width="325" height="65" border="0" alt="SIPLACAD" title="SIPLACAD" />
</a>
#{usuario.sexo.label == messages.enumSexoMasculino ? messages.labelEstimado : messages.labelEstimada}#{_}#{usuario.nombre}:<br /><br />
Recuerde su tarea en SIPLACAD.<br /><br />
<strong>#{messages.labelId}:</strong>#{_}<h:outputText value="#{task.id}"><f:convertNumber minIntegerDigits="3" /></h:outputText><br /><br />
<strong>#{messages.labelTarea}:</strong>#{_}#{messages[task.name]}<br /><br />
<strong>#{messages.labelDescripcion}:</strong>#{_}#{messages[task.description]}<br /><br />
<strong>#{messages.labelFechaLimite}:</strong>#{_}
<h:outputText value="#{task.dueDate}">
<s:convertDateTime pattern="dd/MMMM/yyyy" />
</h:outputText><br /><br />
<br />Por favor ingrese a: <a
href="http://localhost:8080/siplacad/" target="_blank">http://localhost:8080/siplacad/</a>.<br />
<br />Cordialmente,<br />SIPLACAD<br /><br />
<div style="text-align:right">Para mas informacion visite: <a href="http://localhost:8080/siplacad/" target="_blank">http://localhost:8080/siplacad/</a></div>
</m:body>
</m:message>
</ui:repeat>
Here are some of my messages (see how there are two ways of accessing task and process variables, which should be preloaded):
#taskUnidad=\#{comment \!\= null ? comment.taskInstance.processInstance.contextInstance.variables['codigoUnidad'] \: (task \!\= null ? task.processInstance.contextInstance.variables['codigoUnidad'] \: taskInstance.processInstance.contextInstance.variables['codigoUnidad'])}
#taskTermino=\#{comment \!\= null ? comment.taskInstance.processInstance.contextInstance.variables['termino'] \: (task \!\= null ? task.processInstance.contextInstance.variables['termino'] \: taskInstance.processInstance.contextInstance.variables['termino'])}
#taskArea=\#{comment \!\= null ? comment.taskInstance.variables['nombreArea'] \: (task \!\= null ? task.variables['nombreArea'] \: taskInstance.variables['nombreArea'])}
taskUnidad=\#{comment \!\= null ? comment.taskInstance.getVariable("codigoUnidad") \: (task \!\= null ? task.getVariable("codigoUnidad") \: taskInstance.getVariable("codigoUnidad"))}
taskTermino=\#{comment \!\= null ? comment.taskInstance.getVariable("termino") \: (task \!\= null ? task.getVariable("termino") \: taskInstance.getVariable("termino"))}
taskArea=\#{comment \!\= null ? comment.taskInstance.getVariable("nombreArea") \: (task \!\= null ? task.getVariable("nombreArea") \: taskInstance.getVariable("nombreArea"))}
taskUnidadTermino=\#{messages.taskUnidad} - \#{messages.taskTermino}
taskPlanificarArea=\#{messages.taskUnidadTermino}\: Planificar Area \#{messages.taskArea}
taskDescriptionPlanificarArea=Elaborar la Planificaci\u00F3n Acad\u00E9mica de Docentes y Paralelos para las Materias del Area \#{messages.taskArea} dentro de la Planificaci\u00F3n Acad\u00E9mica de la Unidad \#{messages.taskUnidad} en el T\u00E9rmino \#{messages.taskTermino}, y solicitar al Subdecano su Revisi\u00F3n, aplicando las posibles correcciones, para su Aprobaci\u00F3n.
taskRevisarPlanificacionArea=\#{messages.taskUnidadTermino}\: Revisar Planificaci\u00F3n del Area \#{messages.taskArea}
taskDescriptionRevisarPlanificacionArea=Revisar la planificaci\u00F3n Acad\u00E9mica de Docentes y Paralelos para las Materias del Area \#{messages.taskArea} dentro de la Planificaci\u00F3n Acad\u00E9mica de la Unidad \#{messages.taskUnidad} en el T\u00E9rmino \#{messages.taskTermino}, y aprobarla o solicitar correcciones al Coordinador de Area correspondiente.
Below is one of my task nodes. Task name and description are generic strings which will serve as keys to messages (as shown above), so they will be rendered differently depending on variables in the current context. Remember, I don't need to define notifiers nor reminders here.
<task-node name="Revisar Planificacion del Area">
<description>
El Subdecano debe aprobar la Planificacion de cada Area
</description>
<task name="taskRevisarPlanificacionArea" duedate="#{taskDueDateRevisarPlanificacionArea}" swimlane="subdecano">
<description>
taskDescriptionRevisarPlanificacionArea
</description>
</task>
<transition to="nodeJoinPlanificarAreas" name="Aprobar"></transition>
<transition to="Elaborar Planificacion Academica del Area" name="Devolver"></transition>
</task-node>
So, what else can I say? Thanks a lot and God bless you all!
Best regards,
Luis Tama