package org.jbpm.command; import java.util.HashMap; import java.util.Iterator; import org.jbpm.JbpmException; import org.jbpm.db.AbstractDbTestCase; import org.jbpm.graph.def.ProcessDefinition; import org.jbpm.graph.exe.ProcessInstance; import org.jbpm.graph.exe.Token; import org.jbpm.job.Timer; import org.jbpm.taskmgmt.exe.TaskInstance; /** * Tests for {@link ChangeProcessInstanceVersionCommand} * * @author bernd.ruecker@camunda.com */ public class ChangeProcessInstanceVersionCommandTest extends AbstractDbTestCase { private ProcessDefinition pd1; private ProcessDefinition pd2; protected void tearDown() throws Exception { newTransaction(); // IMPORTANT: The ProcessDefinitions have to be deleted in one transaction, // in the right order (new definition with ProcessInstance first) or the // ProcessInstance has to be deleted first independently. // This is because Logs of the ProcessInstance point to both ProcessDefinitions // (the old and the new one) but only with the new ProcessDefinition the // ProcessInstance is deleted // exceptions look like this: could not delete: [org.jbpm.graph.def.Transition#9] // Integrity constraint violation FK_LOG_TRANSITION table: JBPM_LOG in statement // [delete from JBPM_TRANSITION where ID_=?] // IMPORTANT: Keep this order of deletions! Otherwise if there is // more than one ProcessInstance for the ProcessDefinition a HibernateSeassion.flush // is called when querying the second ProcessInstance after deleting the first // one which may fire an integrity constraint violation (same problem as described // above), in this case I got // could not delete: [org.jbpm.taskmgmt.def.TaskMgmtDefinition#2] // Integrity constraint violation FK_TASKMGTINST_TMD table: JBPM_MODULEINSTANCE in statement // [delete from JBPM_MODULEDEFINITION where ID_=?] graphSession.deleteProcessDefinition(pd2.getId()); graphSession.deleteProcessDefinition(pd1.getId()); super.tearDown(); } /** * test easy version migration (no fork or other stuff) but with name mapping * (different state name in new process definition) */ public void testNameMapping() throws Exception { String xmlVersion1 = "<process-definition name='TestChangeVersion'>" + " <start-state name='start'>" + " <transition name='state1' to='state1' />" + " <transition name='state2' to='state2' />" + " </start-state>" + " <state name='state1'>" + " <transition name='end1' to='end' />" + " </state>" + " <state name='state2'>" + " <transition name='end2' to='end' />" + " </state>" + " <end-state name='end'/>" + "</process-definition>"; pd1 = ProcessDefinition.parseXmlString(xmlVersion1); jbpmContext.deployProcessDefinition(pd1); // start 2 instances ProcessInstance pi1 = jbpmContext.newProcessInstance("TestChangeVersion"); pi1.signal("state1"); ProcessInstance pi2 = jbpmContext.newProcessInstance("TestChangeVersion"); pi2.signal("state2"); String xmlVersion2 = "<process-definition name='TestChangeVersion'>" + " <start-state name='start'>" + " <transition name='state1' to='state1' />" + " <transition name='state2' to='state2b'/>" + " </start-state>" + " <state name='state1'>" + " <transition name='end1' to='end' />" + " </state>" + " <state name='state2b'>" + " <transition name='end2b' to='end' />" + " </state>" + " <end-state name='end' />" + "</process-definition>"; pd2 = ProcessDefinition.parseXmlString(xmlVersion2); jbpmContext.deployProcessDefinition(pd2); // now change all process instances to most current version try { new ChangeProcessInstanceVersionCommand().processName("TestChangeVersion") .execute(jbpmContext); fail("Exception expected, saying that state2 is missing in new version"); } catch (JbpmException ex) { assert ex.getMessage().indexOf("state2") != -1 : ex.getMessage(); } // now supply a mapping for the missing node new ChangeProcessInstanceVersionCommand().nodeNameMappingAdd("state2", "state2b") .processName("TestChangeVersion") .execute(jbpmContext); newTransaction(); pi1 = graphSession.loadProcessInstance(pi1.getId()); pi2 = graphSession.loadProcessInstance(pi2.getId()); assertEquals("state1", pi1.getRootToken().getNode().getName()); assertEquals(pd2.getNode("state1").getId(), pi1.getRootToken().getNode().getId()); assertEquals("state2b", pi2.getRootToken().getNode().getName()); assertEquals(pd2.getNode("state2b").getId(), pi2.getRootToken().getNode().getId()); pi1.getRootToken().signal("end1"); pi2.getRootToken().signal("end2b"); newTransaction(); pi1 = graphSession.loadProcessInstance(pi1.getId()); pi2 = graphSession.loadProcessInstance(pi2.getId()); assertEquals(pd2.getNode("end").getId(), pi1.getRootToken().getNode().getId()); assertTrue(pi1.hasEnded()); assertEquals(pd2.getNode("end").getId(), pi2.getRootToken().getNode().getId()); assertTrue(pi2.hasEnded()); } /** * check that update of nodes work correctly if a fork was involved and * multiple child tokens exist */ public void testSubTokensInFork() throws Exception { String xmlVersion1 = "<process-definition name='TestChangeVersion'>" + " <start-state name='start'>" + " <transition to='fork' />" + " </start-state>" + " <fork name='fork'>" + " <transition name='path1' to='path1' />" + " <transition name='path2' to='path2' />" + " </fork>" + " <state name='path1'>" + " <transition to='join' />" + " </state>" + " <state name='path2'>" + " <transition to='join' />" + " </state>" + " <join name='join'>" + " <transition to='end' />" + " </join>" + " <end-state name='end' />" + "</process-definition>"; pd1 = ProcessDefinition.parseXmlString(xmlVersion1); jbpmContext.deployProcessDefinition(pd1); // start instance ProcessInstance pi1 = jbpmContext.newProcessInstance("TestChangeVersion"); pi1.signal(); Token t1 = pi1.getRootToken().getChild("path1"); Token t2 = pi1.getRootToken().getChild("path2"); String xmlVersion2 = "<process-definition name='TestChangeVersion'>" + " <start-state name='start'>" + " <transition to='fork' />" + " </start-state>" + " <fork name='fork'>" + " <transition name='path1' to='path1' />" + " <transition name='path2' to='path2b' />" + " </fork>" + " <state name='path1'>" + " <transition to='join' />" + " </state>" + " <state name='path2b'>" + " <transition name='2b' to='join' />" + " </state>" + " <join name='join'>" + " <transition to='end' />" + " </join>" + " <end-state name='end' />" + "</process-definition>"; pd2 = ProcessDefinition.parseXmlString(xmlVersion2); jbpmContext.deployProcessDefinition(pd2); // now change all process instances to most current version try { new ChangeProcessInstanceVersionCommand().processInstanceId(pi1.getId()) .execute(jbpmContext); fail("Exception expected, saying that path2 is missing in new version"); } catch (JbpmException ex) { assert ex.getMessage().indexOf("path2") != -1 : ex.getMessage(); } // now supply a mapping for the missing node new ChangeProcessInstanceVersionCommand().nodeNameMappingAdd("path2", "path2b") .processInstanceId(pi1.getId()) .execute(jbpmContext); newTransaction(); t1 = graphSession.getToken(t1.getId()); t2 = graphSession.getToken(t2.getId()); assertEquals(pd2.getNode("path1").getId(), t1.getNode().getId()); assertEquals(pd2.getNode("path2b").getId(), t2.getNode().getId()); t1.signal(); t2.signal("2b"); pi1 = graphSession.getProcessInstance(pi1.getId()); assertEquals(pd2.getNode("end").getId(), pi1.getRootToken().getNode().getId()); assertTrue(pi1.hasEnded()); } public void testTaskInFork() throws Exception { String xmlVersion1 = "<process-definition name='TestChangeVersion'>" + " <start-state name='start'>" + " <transition to='fork' />" + " </start-state>" + " <fork name='fork'>" + " <transition name='path1' to='task1' />" + " <transition name='path2' to='task2' />" + " </fork>" + " <task-node name='task1'>" + " <task name='theTask1' />" + " <transition to='join' />" + " </task-node>" + " <task-node name='task2'>" + " <task name='theTask2' />" + " <transition to='join' />" + " </task-node>" + " <join name='join'>" + " <transition to='end' />" + " </join>" + " <end-state name='end' />" + "</process-definition>"; pd1 = ProcessDefinition.parseXmlString(xmlVersion1); jbpmContext.deployProcessDefinition(pd1); // start instance ProcessInstance pi1 = jbpmContext.newProcessInstance("TestChangeVersion"); pi1.signal(); Token t1 = pi1.getRootToken().getChild("path1"); Token t2 = pi1.getRootToken().getChild("path2"); String xmlVersion2 = "<process-definition name='TestChangeVersion'>" + " <start-state name='start'>" + " <transition to='fork' />" + " </start-state>" + " <fork name='fork'>" + " <transition name='path1' to='task1' />" + " <transition name='path2' to='task2' />" + " </fork>" + " <task-node name='task1b'>" + " <task name='theTask1b' />" + " <transition to='join' />" + " </task-node>" + " <task-node name='task2b'>" + " <task name='theTask2b' />" + " <transition to='join' />" + " </task-node>" + " <join name='join'>" + " <transition to='end' />" + " </join>" + " <end-state name='end' />" + "</process-definition>"; pd2 = ProcessDefinition.parseXmlString(xmlVersion2); jbpmContext.deployProcessDefinition(pd2); HashMap nodeNameMap = new HashMap(); nodeNameMap.put("task1", "task1b"); nodeNameMap.put("task2", "task2b"); HashMap taskNameMap = new HashMap(); taskNameMap.put("theTask1", "theTask1b"); taskNameMap.put("theTask2", "theTask2b"); // now supply a mapping for the missing node new ChangeProcessInstanceVersionCommand().nodeNameMapping(nodeNameMap) .taskNameMapping(taskNameMap) .processInstanceId(pi1.getId()) .execute(jbpmContext); newTransaction(); t1 = jbpmContext.loadTokenForUpdate(t1.getId()); assertEquals(pd2.getNode("task1b").getId(), t1.getNode().getId()); Iterator taskInstanceIter = t1.getProcessInstance() .getTaskMgmtInstance() .getTaskInstances() .iterator(); TaskInstance ti1 = (TaskInstance) taskInstanceIter.next(); if ("theTask2b".equals(ti1.getTask().getName())) { // this was the wrong one ti1 = (TaskInstance) taskInstanceIter.next(); } assertEquals("theTask1b", ti1.getTask().getName()); assertEquals(pd2.getTaskMgmtDefinition().getTask("theTask1b").getId(), ti1.getTask() .getId()); ti1.end(); // ///// newTransaction(); t2 = graphSession.getToken(t2.getId()); assertEquals(pd2.getNode("task2b").getId(), t2.getNode().getId()); taskInstanceIter = t2.getProcessInstance() .getTaskMgmtInstance() .getTaskInstances() .iterator(); TaskInstance ti2 = (TaskInstance) taskInstanceIter.next(); if ("theTask1b".equals(ti2.getTask().getName())) { // this was the wrong one ti2 = (TaskInstance) taskInstanceIter.next(); } assertEquals("theTask2b", ti2.getTask().getName()); assertEquals(pd2.getTaskMgmtDefinition().getTask("theTask2b").getId(), ti2.getTask() .getId()); ti2.end(); newTransaction(); pi1 = graphSession.loadProcessInstance(pi1.getId()); assertEquals(pd2.getNode("end").getId(), pi1.getRootToken().getNode().getId()); assertTrue(pi1.hasEnded()); } /** * check, that TaskInstances work (TaskInstance reference to Task has to be * adjusted as will) */ public void testTaskInstances() throws Exception { String xmlVersion1 = "<process-definition name='testTaskInstances'>" + " <start-state name='start'>" + " <transition name='path1' to='task1' />" + " <transition name='path2' to='task2' />" + " </start-state>" + " <task-node name='task1'>" + " <task name='theTask1'/>" + " <transition name='end1' to='end' />" + " </task-node>" + " <task-node name='task2'>" + " <task name='theTask2'/>" + " <transition name='end2' to='end' />" + " </task-node>" + " <end-state name='end'/>" + "</process-definition>"; pd1 = ProcessDefinition.parseXmlString(xmlVersion1); jbpmContext.deployProcessDefinition(pd1); // start 2 instances ProcessInstance pi1 = jbpmContext.newProcessInstance("testTaskInstances"); pi1.signal("path1"); ProcessInstance pi2 = jbpmContext.newProcessInstance("testTaskInstances"); pi2.signal("path2"); String xmlVersion2 = "<process-definition name='testTaskInstances'>" + " <start-state name='start'>" + " <transition name='path1' to='task1' />" + " <transition name='path2' to='task2b' />" + " </start-state>" + " <task-node name='task1'>" + " <task name='theTask1'/>" + " <transition name='end1' to='end' />" + " </task-node>" + " <task-node name='task2b'>" + " <task name='theTask2b'/>" + " <transition name='end2b' to='end' />" + " </task-node>" + " <end-state name='end'/>" + "</process-definition>"; pd2 = ProcessDefinition.parseXmlString(xmlVersion2); jbpmContext.deployProcessDefinition(pd2); // process instance 1 can be updated, state names haven't changed new ChangeProcessInstanceVersionCommand().processInstanceId(pi1.getId()) .execute(jbpmContext); // now change all process instances to most current version try { new ChangeProcessInstanceVersionCommand().nodeNameMappingAdd("task2", "task2b") .processName("testTaskInstances") .execute(jbpmContext); // fail because task2 is not mapped fail("Exception expected, saying that theTask2 is missing in new version"); } catch (JbpmException ex) { assert ex.getMessage().indexOf("theTask2") != -1 : ex.getMessage(); } // now supply a mapping for the missing task new ChangeProcessInstanceVersionCommand().nodeNameMappingAdd("task2", "task2b") .taskNameMappingAdd("theTask2", "theTask2b") .processName("testTaskInstances") .execute(jbpmContext); newTransaction(); pi1 = graphSession.loadProcessInstance(pi1.getId()); pi2 = graphSession.loadProcessInstance(pi2.getId()); assertEquals(pd2.getNode("task1").getId(), pi1.getRootToken().getNode().getId()); assertEquals(pd2.getNode("task2b").getId(), pi2.getRootToken().getNode().getId()); TaskInstance ti1 = (TaskInstance) pi1.getTaskMgmtInstance() .getTaskInstances() .iterator() .next(); TaskInstance ti2 = (TaskInstance) pi2.getTaskMgmtInstance() .getTaskInstances() .iterator() .next(); assertEquals(pd2.getTaskMgmtDefinition().getTask("theTask1").getId(), ti1.getTask().getId()); assertEquals(pd2.getTaskMgmtDefinition().getTask("theTask2b").getId(), ti2.getTask() .getId()); ti1.end("end1"); ti2.end("end2b"); assertEquals(pd2.getNode("end").getId(), pi1.getRootToken().getNode().getId()); assertTrue(pi1.hasEnded()); assertEquals(pd2.getNode("end").getId(), pi2.getRootToken().getNode().getId()); assertTrue(pi2.hasEnded()); } /** * test if changing process version works correctly if a timer is included in * the process definition. Important: The timer itself IS NOT changed, so e.g. * used leaving transitions must be still existent */ public void testTimerInState() throws Exception { String xmlVersion1 = "<process-definition name='TestChangeVersion'>" + " <start-state name='start'>" + " <transition to='timer1' />" + " </start-state>" + " <state name='timer1'>" + " <timer name='timer1' duedate='5 seconds' transition='end' />" + " <transition name='end' to='end' />" + " </state>" + " <end-state name='end'/>" + "</process-definition>"; pd1 = ProcessDefinition.parseXmlString(xmlVersion1); jbpmContext.deployProcessDefinition(pd1); // start instance ProcessInstance pi1 = jbpmContext.newProcessInstance("TestChangeVersion"); pi1.signal(); Timer timer = (Timer) session.createQuery("from org.jbpm.job.Timer").uniqueResult(); // check timer assertNotNull("Timer is null", timer); assertEquals("timer1", timer.getName()); assertEquals(pd1.getNode("timer1").getId(), timer.getGraphElement().getId()); String xmlVersion2 = "<process-definition name='TestChangeVersion'>" + " <start-state name='start'>" + " <transition to='timer2' />" + " </start-state>" + " <state name='timer2'>" + " <timer name='timer1' duedate='5 seconds' transition='end1' />" + " <transition name='end' to='end' />" + " </state>" + " <end-state name='end'/>" + "</process-definition>"; pd2 = ProcessDefinition.parseXmlString(xmlVersion2); jbpmContext.deployProcessDefinition(pd2); // change version HashMap nameMap = new HashMap(); nameMap.put("timer1", "timer2"); new ChangeProcessInstanceVersionCommand().nodeNameMapping(nameMap) .processInstanceId(pi1.getId()) .execute(jbpmContext); // load changed stuff newTransaction(); pi1 = graphSession.loadProcessInstance(pi1.getId()); timer = (Timer) session.createQuery("from org.jbpm.job.Timer").uniqueResult(); // and check again assertEquals(pd2.getNode("timer2").getId(), pi1.getRootToken().getNode().getId()); assertEquals("timer1", timer.getName()); assertEquals(pd2.getNode("timer2").getId(), timer.getGraphElement().getId()); timer.execute(jbpmContext); assertEquals(pd2.getNode("end").getId(), pi1.getRootToken().getNode().getId()); assertTrue(pi1.hasEnded()); } public void testTimerInTask() throws Exception { String xmlVersion1 = "<process-definition name='TestChangeVersion'>" + " <start-state name='start'>" + " <transition to='timer1' />" + " </start-state>" + " <task-node name='timer1'>" + " <task name='myTask'>" + " <timer name='timer1' duedate='5 seconds' transition='end' />" + " </task>" + " <transition name='end' to='end' />" + " </task-node>" + " <end-state name='end'/>" + "</process-definition>"; pd1 = ProcessDefinition.parseXmlString(xmlVersion1); jbpmContext.deployProcessDefinition(pd1); // start instance ProcessInstance pi1 = jbpmContext.newProcessInstance("TestChangeVersion"); pi1.signal(); // jbpmContext.getJobSession().deleteJobsForProcessInstance(processInstance); // NOT UNIQUE?! Timer timer = (Timer) session.createQuery("from org.jbpm.job.Timer").uniqueResult(); // check timer assertNotNull("Timer is null", timer); assertEquals("timer1", timer.getName()); assertEquals(pd1.getTaskMgmtDefinition().getTask("myTask").getId(), timer.getGraphElement() .getId()); TaskInstance ti1 = (TaskInstance) pi1.getTaskMgmtInstance() .getTaskInstances() .iterator() .next(); assertEquals(pd1.getTaskMgmtDefinition().getTask("myTask").getId(), ti1.getTask().getId()); String xmlVersion2 = "<process-definition name='TestChangeVersion'>" + " <start-state name='start'>" + " <transition to='timer2' />" + " </start-state>" + " <task-node name='timer2'>" + " <task name='myTask2'>" + " <timer name='timer1' duedate='5 seconds' transition='end' />" + " </task>" + " <transition name='end' to='end' />" + " </task-node>" + " <end-state name='end'/>" + "</process-definition>"; pd2 = ProcessDefinition.parseXmlString(xmlVersion2); jbpmContext.deployProcessDefinition(pd2); // change version HashMap nameMap = new HashMap(); nameMap.put("timer1", "timer2"); nameMap.put("myTask", "myTask2"); new ChangeProcessInstanceVersionCommand().nodeNameMapping(nameMap) .taskNameMapping(nameMap) .processInstanceId(pi1.getId()) .execute(jbpmContext); // load changed stuff newTransaction(); pi1 = graphSession.loadProcessInstance(pi1.getId()); timer = (Timer) session.createQuery("from org.jbpm.job.Timer").uniqueResult(); // and check again assertEquals(pd2.getNode("timer2").getId(), pi1.getRootToken().getNode().getId()); assertEquals("timer1", timer.getName()); assertEquals(pd2.getTaskMgmtDefinition().getTask("myTask2").getId(), timer.getGraphElement().getId()); ti1 = (TaskInstance) pi1.getTaskMgmtInstance().getTaskInstances().iterator().next(); assertEquals(pd2.getTaskMgmtDefinition().getTask("myTask2").getId(), ti1.getTask().getId()); // and go on timer.execute(jbpmContext); assertEquals(pd2.getNode("end").getId(), pi1.getRootToken().getNode().getId()); assertTrue(pi1.hasEnded()); } /* * Asynchronous continuation is not affected by changing the version, because * a {@link Job} only holds {@link ProcessInstance}.id, {@link Token}.id or * {@link TaskInstance}.id. None of them are changed while version changes. */ }