HOWTO: JBPM process engine + appfuse jsf modular
required ressources:
- appfuse 2.0.2 jsf modular
- jbpm 3.2.2
- spring-modules jbpm
integration:
database
use JBPM ddl according to your database. I recommend to put jbpm-db-objects to another schema than your domain-db-objects. Easier for updates.
hibernate
<hibernate-configuration>
<session-factory name="java:/jbpmSessionFactory">
<mapping class="org.appfuse.model.User"/>
<mapping class="org.appfuse.model.Role"/>
...
ALL JBPM *.hbm.xml
give your sessionFactory a name - so you can reference it in JBPM-configuration
Copy the *.hbm.xml files to your classpath. I recommend a new maven module for it.
jbpm.cfg.xml
to your classpath: - jbpm-config
<jbpm-configuration>
<jbpm-context>
<service name="persistence">
<factory>
<bean class="org.jbpm.persistence.db.DbPersistenceServiceFactory">
<!-- let us handle transactions by aop or @Transactional; so we have to turn jbpm tx off -->
<field name="isTransactionEnabled"><false/></field>
<field name="sessionFactoryJndiName">
<!-- same as name of your hibernate session factory -->
<string value="java:/jbpmSessionFactory" />
</field>
</bean>
</factory>
</service>
<service name="tx" factory="org.jbpm.tx.TxServiceFactory" />
<service name="message" factory="org.jbpm.msg.db.DbMessageServiceFactory" />
<service name="scheduler" factory="org.jbpm.scheduler.db.DbSchedulerServiceFactory" />
<service name="logging" factory="org.jbpm.logging.db.DbLoggingServiceFactory" />
<service name="authentication" factory="org.jbpm.security.authentication.DefaultAuthenticationServiceFactory" />
</jbpm-context>
<!-- lets resolve JSF variables in process diagrams -->
<bean name='jbpm.variable.resolver' class='at.iqsoft.jbpm.el.JsfJbpmVariableResolver' singleton='true' />
</jbpm-configuration>
JSF-EL + JBPM
make sure EL expressions from JSF can be used in JBPM process diagrams:
package at.iqsoft.jbpm.el;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.appfuse.webapp.util.FacesUtils;
import org.jbpm.jpdl.el.ELException;
import org.jbpm.jpdl.el.impl.JbpmVariableResolver;
/** jbpm can resolve JSF variables with this
use: have in view something like #{myForm.name}
than you can use this in your process diagram */
public class JsfJbpmVariableResolver extends JbpmVariableResolver {
private static final Log log = LogFactory.getLog(JbpmVariableResolver.class);
public Object resolveVariable(String name) throws ELException {
//let jbpm first try to resolve the variable
Object value = super.resolveVariable(name);
if (value == null) {
//jbpm dont know the var, lets see whether jsf (implicitly) spring can resolve
if (log.isDebugEnabled()) {
log.debug(String.format("resolving %s to: %s", name, value));
}
value = FacesUtils.getManagedBean(name);
}
return value;
}
}
*Note: this class is referenced in the jbpm.cfg.xml
jbpm + spring (spring-modules)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd"
default-lazy-init="true">
<!-- helper for reading jBPM process definitions -->
<bean id="process-todo"
class="org.springmodules.workflow.jbpm31.definition.ProcessDefinitionFactoryBean">
<property name="definitionLocation"
value="classpath:process-todo/processdefinition.xml"/>
</bean>
<!-- jBPM configuration -->
<bean id="jbpmConfiguration"
class="org.springmodules.workflow.jbpm31.LocalJbpmConfigurationFactoryBean">
<property name="sessionFactory" ref="sessionFactory"/>
<property name="configuration" value="classpath:jbpm.cfg.xml"/>
<property name="processDefinitions">
<list>
<ref local="process-todo"/>
</list>
</property>
<property name="createSchema" value="false"/>
</bean>
<!-- jBPM template -->
<bean id="jbpmTemplate" class="org.springmodules.workflow.jbpm31.JbpmTemplate">
<constructor-arg index="0" ref="jbpmConfiguration"/>
<constructor-arg index="1" ref="process-todo"/>
<property name="hibernateTemplate" ref="hibernateTemplate"/>
</bean>
</beans>
Note: I referenced a specific process definition
classpath:process-todo/processdefinition.xml
you can use mine (see below) or define your own with jbpm-eclipse-plugin
hibernate session in view
Make sure you have some open session in view filter active. I usually dont use since i fetch what i need. (web.xml)
simple sample
process definition
a simple workflow
<?xml version="1.0" encoding="UTF-8"?>
<process-definition
name="todo"
xmlns="urn:jbpm.org:jpdl-3.2"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="urn:jbpm.org:jpdl-3.2 http://jbpm.org/xsd/jpdl-3.2.xsd"
>
<start-state name="start">
<transition to="todo"></transition>
</start-state>
<task-node name="todo">
<task name="Todo" blocking="true">
<description>
#{todoList.description}
</description>
<assignment pooled-actors="admin"></assignment>
<controller></controller>
</task>
<transition to="end"></transition>
</task-node>
<end-state name="end"></end-state>
</process-definition>
In simple words: On starting a new process instance, a new TASK is generated. This task will be shown to all appfuse-admins (role admin). Every admin can assign the TASK to himself. Then the task-assignee can finish the task. After that the process will end.
The expression
#{todoList.description}
will be taken from JSF.
view (facelets)
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:c="http://java.sun.com/jstl/core" xmlns:f="http://java.sun.com/jsf/core" xmlns:h="http://java.sun.com/jsf/html" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:a4j="http://richfaces.org/a4j" xmlns:rich="http://richfaces.org/rich" xmlns:jstl="http://java.sun.com/jsp/jstl/functions" > <f:view> <f:loadBundle var="text" basename="#{basePage.bundleName}"/> <head> </head> <body id="bdy"> <c:set var="taskInstancePriorityList" value="#{todoList.taskInstancePriorityList}"/> <a4j:form id="task_list_personal"> <div> <h:outputText value="There are no todo items." rendered="#{empty taskInstancePriorityList}"/> <h2><h:outputText value="todo items." rendered="#{not empty taskInstancePriorityList}"/></h2> <h:dataTable value="#{taskInstancePriorityList}" var="task" rendered="#{not empty taskInstancePriorityList}"> <h:column> <f:facet name="header"> <h:outputText value="Description"/> </f:facet> <h:inputText value="#{task.description}" style="width: 400"/> </h:column> <h:column> <f:facet name="header"> <h:outputText value="Created"/> </f:facet> <h:outputText value="#{task.taskMgmtInstance.processInstance.start}"> </h:outputText> </h:column> <h:column> <f:facet name="header"> <h:outputText value="Priority"/> </f:facet> <h:inputText value="#{task.priority}" style="width: 30"/> </h:column> <h:column> <f:facet name="header"> <h:outputText value="Due Date"/> </f:facet> <h:inputText value="#{task.dueDate}" style="width: 100"> </h:inputText> </h:column> <h:column> <a4j:commandLink action="#{todoList.done}" value="Done" reRender="task_list_personal"> <f:param name="taskId" value="#{task.id}"/> </a4j:commandLink> </h:column> </h:dataTable> </div> </a4j:form> <br/> <c:set var="pooledTaskInstanceList" value="#{todoList.pooledTaskInstanceList}"/> <a4j:form id="task_list_pooled"> <div> <h:outputText value="There are no assignable items." rendered="#{empty pooledTaskInstanceList}"/> <h2><h:outputText value="assignable items." rendered="#{not empty pooledTaskInstanceList}"/></h2> <h:dataTable value="#{pooledTaskInstanceList}" var="task" rendered="#{not empty pooledTaskInstanceList}"> <h:column> <f:facet name="header"> <h:outputText value="Description"/> </f:facet> <h:inputText value="#{task.description}" style="width: 400"/> </h:column> <h:column> <f:facet name="header"> <h:outputText value="Created"/> </f:facet> <h:outputText value="#{task.taskMgmtInstance.processInstance.start}"> </h:outputText> </h:column> <h:column> <f:facet name="header"> <h:outputText value="Priority"/> </f:facet> <h:inputText value="#{task.priority}" style="width: 30"/> </h:column> <h:column> <f:facet name="header"> <h:outputText value="Due Date"/> </f:facet> <h:inputText value="#{task.dueDate}" style="width: 100"> </h:inputText> </h:column> <h:column> <h:column> <a4j:commandLink action="#{todoList.assign}" value="Assign" reRender="task_list_personal,task_list_pooled"> <f:param name="taskId" value="#{task.id}"/> </a4j:commandLink> </h:column> </h:column> </h:dataTable> </div> </a4j:form> <a4j:form id="task_new"> <div> <h:inputText value="#{todoList.description}" style="width: 400"/> <a4j:commandButton value="Create New Item" action="#{todoList.createTodo}" reRender="task_list_pooled"/> </div> </a4j:form> </body> </f:view> </html>
jsf controller (spring way)
package at.iqsoft.templtest.webapp.list; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.lang.StringUtils; import org.jbpm.graph.def.ProcessDefinition; import org.jbpm.graph.exe.ProcessInstance; import org.jbpm.taskmgmt.exe.TaskInstance; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; import org.springmodules.workflow.jbpm31.JbpmTemplate; import at.iqsoft.jbpm.util.JbpmAppfuseUtil; import at.iqsoft.templtest.webapp.action.IqsBasePage; @SuppressWarnings("unchecked") @Controller //spring bean public class TodoList extends IqsBasePage { //my custom basepage private static final long serialVersionUID = 1L; @Autowired private JbpmTemplate jbpm; //like pityful jsf injection mech. but better private List<TaskInstance> taskInstancePriorityList; //tasks for current user private List<TaskInstance> pooledTaskInstanceList; //pooled tasks not yet assigned, but assignable to current user private String description; //field in UI for new tasks public TodoList() {} @Transactional //ok this is very unlike the appfuse way - i just like seam too ;-) public String done() { Long id = getParameterId("taskId"); TaskInstance task = (TaskInstance) jbpm.getHibernateTemplate().get(TaskInstance.class, id); task.start(); task.end(); jbpm.getHibernateTemplate().save(task); //trigger view refresh taskInstancePriorityList = null; pooledTaskInstanceList = null; return null; } /** assign given task to current user */ @Transactional public String assign() { Long id = getParameterId("taskId"); TaskInstance task = (TaskInstance) jbpm.getHibernateTemplate().get(TaskInstance.class, id); task.setActorId(getCurrentUsername()); jbpm.getHibernateTemplate().save(task); log.info(String.format("assigned task: %s to user: %s", task.getId(), getCurrentUsername())); //trigger view refresh taskInstancePriorityList = null; pooledTaskInstanceList = null; return null; } @Transactional public String createTodo() { ProcessDefinition pdef = jbpm.getProcessDefinition(); Map<String, Object> params = new HashMap<String, Object>(); Assert.isTrue(StringUtils.isNotEmpty(description), "no desc from ui"); params.put("#{todoList.description}", description); ProcessInstance p = pdef.createProcessInstance(params); p.signal(); jbpm.getHibernateTemplate().save(p); // log.info(String.format("new process %s started by user %s", pdef.getName(), getCurrentUsername())); //trigger view refresh taskInstancePriorityList = null; pooledTaskInstanceList = null; return null; } @Transactional public List<TaskInstance> getTaskInstancePriorityList() { if (taskInstancePriorityList == null) { //init taskInstancePriorityList = jbpm.findTaskInstances(getCurrentUsername()); } return taskInstancePriorityList; } public void setTaskInstancePriorityList( List<TaskInstance> taskInstancePriorityList) { this.taskInstancePriorityList = taskInstancePriorityList; } @Transactional public List<TaskInstance> getPooledTaskInstanceList() { if (pooledTaskInstanceList == null) { pooledTaskInstanceList = jbpm.findPooledTaskInstances(JbpmAppfuseUtil.getActorAndGroupIds()); } return pooledTaskInstanceList; } public void setPooledTaskInstanceList(List<TaskInstance> pooledTaskInstanceList) { this.pooledTaskInstanceList = pooledTaskInstanceList; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } }
some pity helper class to get all known usernames + roles for pooled actors
package at.iqsoft.jbpm.util; import java.util.ArrayList; import java.util.List; import org.springframework.security.Authentication; import org.springframework.security.GrantedAuthority; import org.springframework.security.context.SecurityContextHolder; public class JbpmAppfuseUtil { public static List<String> getActorAndGroupIds() { List<String> result = new ArrayList<String>(); Authentication currentUser = SecurityContextHolder.getContext().getAuthentication(); result.add(currentUser.getName()); GrantedAuthority[] auths = currentUser.getAuthorities(); for (int i = 0; i < auths.length; i++) { GrantedAuthority auth = auths[i]; result.add(auth.getAuthority()); } return result; } }
thoughts about transaction + db object separation
Usually you have some domain tables + some jbpm tables.
- I dont want to intermix them
- I want transactional behaviour between jbpm + application-domain
possible solutions:
put jbpm tables to different schema in same database. example with postgres:
I put my domain-tables into public-schema and jbpm-tables to jbpm schema.
Then I alter all jbpm *.hbm.xml files with SEARCH&REPLACE over files (jedit) and prefix all table names with "jbpm." Same goes for Foreign Key references.
jbpm tables to different database than domain-app-tables
use 2PC and XA with some nice jta-application server like jboss. Or tomcat/jetty + jotm/... (jta-impl for "lean" app-servers)
I posted some working configuration for seam:
http://www.seamframework.org/Community/TransactionQuestionInJbpmContextNoTransation