package org.jbpm.command;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Query;
import org.jbpm.JbpmException;
import org.jbpm.graph.def.Action;
import org.jbpm.graph.def.GraphElement;
import org.jbpm.graph.def.Node;
import org.jbpm.graph.def.NodeCollection;
import org.jbpm.graph.def.ProcessDefinition;
import org.jbpm.graph.def.SuperState;
import org.jbpm.graph.def.Transition;
import org.jbpm.graph.exe.ProcessInstance;
import org.jbpm.graph.exe.Token;
import org.jbpm.graph.log.ActionLog;
import org.jbpm.graph.log.NodeLog;
import org.jbpm.graph.log.SignalLog;
import org.jbpm.graph.log.TransitionLog;
import org.jbpm.graph.node.ProcessState;
import org.jbpm.graph.node.TaskNode;
import org.jbpm.job.Job;
import org.jbpm.job.Timer;
import org.jbpm.logging.log.ProcessLog;
import org.jbpm.taskmgmt.def.Task;
import org.jbpm.taskmgmt.exe.TaskInstance;
/**
* Modificat per Límit per evitar el problema de nodes amb el nom que conté un
* caràcter '/'.
*
* <b>THIS COMMAND IS NOT YET STABLE, BUT FEEL FREE TO TEST :-)</b><br>
*
* Status update: Still not complete, but refactored and added simple test cases:
* {@link ChangeProcessInstanceVersionCommandTest}.<br>
*
* Change the version of a running process instance.
*
* This works only, if the current node is also available in the new
* version of the process definition or a name mapping has to be provided.<br>
*
* <b>Currently known limitations:</b>
* <ul>
* <li> {@link Task}s cannot move "into" another node. If an active
* {@link TaskInstance} exists, the {@link Task} definition must exist in the
* {@link TaskNode} with the same (or mapped) name. Otherwise the right node
* cannot be found easily because it may be ambiguous.</li>
* <li> Sub processes aren't yet tested. Since the {@link ProcessState} is
* a {@link Node} like any other, it should work anyway.</li>
* <li> Can have <b>negative impact on referential integrity</b>! Because
* one {@link ProcessInstance} can have {@link ProcessLog}s point to
* old {@link ProcessDefinition}s. Hence, delete a {@link ProcessDefinition}
* may not work and throw an Exception (Integrity constraint violation)</li>
* <li> In combination with ESB the ESB uses {@link Token}.id <b>and</b> {@link Node}.id
* as correlation identifier. After changing the version of a {@link ProcessInstance}
* the {@link Node}.id has changed, so a signal from ESB will result in an exception
* and has to be corrected manually.</li>
* </ul>
*
* @author Bernd Ruecker (bernd.ruecker@camunda.com)
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public class ChangeProcessInstanceVersionCommand extends AbstractProcessInstanceBaseCommand {
private static final long serialVersionUID = 2277080393930008224L;
/**
* new version of process, if <=0, the latest process definition is used
*/
private int newVersion = -1;
private static final Log log = LogFactory.getLog(ChangeProcessInstanceVersionCommand.class);
/**
* the map configures for every node-name in the old process definition (as
* key) which node-name to use in the new process definition.
*
* if a node is not mentioned in this Map, old node name = new node name is
* applied
*/
private Map<String, String> nodeNameMapping = new HashMap<String, String>();
private Map<String, String> taskNameMapping = new HashMap<String, String>();
public ChangeProcessInstanceVersionCommand() {
}
public ChangeProcessInstanceVersionCommand(long processId, int newVersion) {
super();
super.setProcessInstanceId(processId);
this.newVersion = newVersion;
}
public String getAdditionalToStringInformation() {
return ";newVersion=" + newVersion;
}
private ProcessDefinition loadNewProcessDefinition(String processName) {
if (newVersion <= 0)
return getJbpmContext().getGraphSession().findLatestProcessDefinition(processName);
else
return getJbpmContext().getGraphSession().findProcessDefinition(processName, newVersion);
}
public ProcessInstance execute(ProcessInstance pi) {
ProcessDefinition oldDef = pi.getProcessDefinition();
ProcessDefinition newDef = loadNewProcessDefinition(oldDef.getName());
if (newDef == null) {
throw new JbpmException(
"Process definition " + oldDef.getName() + " in version " + newVersion + " not found.");
}
log.debug("Start changing process id " + pi.getId() + " from version " + pi.getProcessDefinition().getVersion()
+ " to new version " + newDef.getVersion());
pi.setProcessDefinition(newDef);
changeTokenVersion(pi.getRootToken());
log.debug("process id " + pi.getId() + " changed to version " + pi.getProcessDefinition().getVersion());
return pi;
}
private ProcessDefinition getNewProcessDefinition(Token t) {
return t.getProcessInstance().getProcessDefinition();
}
private void changeTokenVersion(Token token) {
ProcessDefinition newDef = getNewProcessDefinition(token);
log.debug("change token id " + token.getId() + " to new version " + newDef.getVersion());
// change node reference on token (current node)
Node oldNode = token.getNode();
Node newNode = findReplacementNode(newDef, oldNode);
token.setNode(newNode);
// Change timers too!
adjustTimersForToken(token);
// change tasks
adjustTaskInstancesForToken(token);
// change children recursively
if (token.getChildren() != null) {
Iterator<Token> tokenIter = token.getChildren().values().iterator();
while (tokenIter.hasNext()) {
changeTokenVersion(tokenIter.next());
}
}
// change log references
adjustLogsForToken(newDef, token);
}
private void adjustLogsForToken(ProcessDefinition newDef, Token token) {
Map<Long, Transition> transicions = new HashMap<Long, Transition>();
List<TransitionLog> transiotionLogs = getTransitionLogs(token);
for (TransitionLog log : transiotionLogs) {
if (log.getTransition() != null) {
Transition oldTransition = log.getTransition();
Transition newTransition = transicions.get(oldTransition.getId());
if (newTransition == null) {
newTransition = findReplacementTransition(newDef, oldTransition);
transicions.put(oldTransition.getId(), newTransition);
}
setLogTransition(log, newTransition);
}
}
List<SignalLog> signalLogs = getSignalLogs(token);
for (SignalLog log : signalLogs) {
if (log.getTransition() != null) {
Transition oldTransition = log.getTransition();
Transition newTransition = transicions.get(oldTransition.getId());
if (newTransition == null) {
newTransition = findReplacementTransition(newDef, oldTransition);
transicions.put(oldTransition.getId(), newTransition);
}
setLogSignal(log, newTransition);
}
}
List<NodeLog> nodeLogs = getNodeLogs(token);
for (NodeLog log : nodeLogs) {
if (log.getNode() != null) {
Node oldNode = log.getNode();
Node newNode = findReplacementNode(newDef, oldNode);
setLogNode(log, newNode);
}
}
List<ActionLog> actionLogs = getActionLogs(token);
for (ActionLog log : actionLogs) {
if (log.getAction() != null) {
Action oldAction = log.getAction();
if (oldAction.getProcessDefinition() != null) {
Action newAction = findReplacementAction(newDef, oldAction);
log.setAction(newAction);
}
}
}
}
private List<TransitionLog> getTransitionLogs(Token t) {
Query q = getJbpmContext().getSession().createQuery(
"select tl " +
"from org.jbpm.graph.log.TransitionLog as tl " +
"where tl.token.processInstance = :processInstance");
q.setEntity("processInstance", t.getProcessInstance());
return q.list();
}
private List<SignalLog> getSignalLogs(Token t) {
Query q = getJbpmContext().getSession().createQuery(
"select sl " +
"from org.jbpm.graph.log.SignalLog as sl " +
"where sl.token.processInstance = :processInstance");
q.setEntity("processInstance", t.getProcessInstance());
return q.list();
}
private List<NodeLog> getNodeLogs(Token t) {
Query q = getJbpmContext().getSession().createQuery(
"select nl " +
"from org.jbpm.graph.log.NodeLog as nl " +
"where nl.token.processInstance = :processInstance");
q.setEntity("processInstance", t.getProcessInstance());
return q.list();
}
private List<ActionLog> getActionLogs(Token t) {
Query q = getJbpmContext().getSession().createQuery(
"select al " +
"from org.jbpm.graph.log.ActionLog as al " +
"where al.token.processInstance = :processInstance");
q.setEntity("processInstance", t.getProcessInstance());
return q.list();
}
private void setLogTransition(TransitionLog log, Transition transition) {
Query updateQuery = getJbpmContext().getSession().createQuery(
"update org.jbpm.graph.log.TransitionLog set "
+ "transition = :transition, "
+ "sourceNode = :nodeFrom, "
+ "destinationNode = :nodeTo "
+ "where id = :logId");
updateQuery.setEntity("transition", transition);
updateQuery.setEntity("nodeFrom", transition.getFrom());
updateQuery.setEntity("nodeTo", transition.getTo());
updateQuery.setLong("logId", log.getId());
updateQuery.executeUpdate();
}
private void setLogSignal(SignalLog log, Transition transition) {
Query updateQuery = getJbpmContext().getSession().createQuery(
"update org.jbpm.graph.log.SignalLog set "
+ "transition = :transition "
+ "where id = :logId");
updateQuery.setEntity("transition", transition);
updateQuery.setLong("logId", log.getId());
updateQuery.executeUpdate();
}
private void setLogNode(NodeLog log, Node node) {
Query updateQuery = getJbpmContext().getSession().createQuery(
"update org.jbpm.graph.log.NodeLog set "
+ "node = :node "
+ "where id = :logId");
updateQuery.setEntity("node", node);
updateQuery.setLong("logId", log.getId());
updateQuery.executeUpdate();
}
private void adjustTaskInstancesForToken(Token token) {
ProcessDefinition newDef = getNewProcessDefinition(token);
Iterator<TaskInstance> iter = getTasksForToken(token).iterator();
while (iter.hasNext()) {
TaskInstance ti = iter.next();
// find new task
Task oldTask = ti.getTask();
Node oldNode = oldTask.getTaskNode();
Task newTask = findReplacementTask(newDef, oldNode, oldTask);
ti.setTask(newTask);
// Canviam el TaskMngmtInstance per a associar-lo a la nova definició de procés
ti.getTaskMgmtInstance().setTaskMgmtDefinition(newDef.getTaskMgmtDefinition());
log.debug("change dependent task-instance with id " + oldTask.getId());
// // Canviam el processDefinition dels tasksMgmtInstance dels tasksInstance de la versió que s'intenta esborrar a la de la tasca.
// TaskMgmtDefinition tmd = ti.getTaskMgmtInstance().getTaskMgmtDefinition();
// tmd.setProcessDefinition(newDef);
// if (tmd.getStartTask() != null) {
// tmd.setStartTask(newDef.getTaskMgmtDefinition().getStartTask());
// }
// getJbpmContext().getSession().save(tmd);
}
}
private void adjustTimersForToken(Token token) {
ProcessDefinition newDef = getNewProcessDefinition(token);
List<Job> jobs = getJbpmContext().getJobSession().findJobsByToken(token);
for (Job job : jobs) {
if (job instanceof Timer) {
// check all timers if connected to a GraphElement
Timer timer = (Timer) job;
if (timer.getGraphElement() != null) {
// and change the reference (take name mappings into
// account!)
if (timer.getGraphElement() instanceof Task) {
// change to new task definition
Task oldTask = (Task) timer.getGraphElement();
TaskNode oldNode = oldTask.getTaskNode();
timer.setGraphElement(findReplacementTask(newDef, oldNode, oldTask));
} else {
// change to new node
GraphElement oldNode = timer.getGraphElement();
// TODO: What with other GraphElements?
timer.setGraphElement(findReplacementNode(newDef, oldNode));
}
}
}
}
}
private Node findReplacementNode(ProcessDefinition newDef, GraphElement oldNode) {
String name = getReplacementNodeName(oldNode);
log.debug("get replacement for node with name '" + name + "'");
// Node newNode = newDef.findNode(name);
Node newNode = findNode(newDef, name);
if (newNode == null) {
throw new JbpmException("node with name '" + name + "' not found in new process definition");
}
return newNode;
}
private Task findReplacementTask(ProcessDefinition newDef, Node oldNode, Task oldTask) {
String replacementTaskName = getReplacementTaskName(oldTask);
Node newTaskNode = findReplacementNode(newDef, oldNode);
Query q = getJbpmContext().getSession().getNamedQuery("TaskMgmtSession.findTaskForNode");
q.setString("taskName", replacementTaskName);
q.setLong("taskNodeId", newTaskNode.getId());
Task newTask = (Task) q.uniqueResult();
if (newTask == null) {
throw new JbpmException("Task '" + replacementTaskName + "' for node '" + newTaskNode.getName()
+ "' not found in new process definition");
}
return newTask;
}
private Transition findReplacementTransition(ProcessDefinition newDef, Transition oldTransition) {
Transition newTransition = null;
Node newNodeFrom = findReplacementNode(newDef, oldTransition.getFrom());
if (newNodeFrom != null) {
Node newNodeTo = findReplacementNode(newDef, oldTransition.getTo());
if (newNodeTo != null) {
Query q = getJbpmContext().getSession().createQuery(
"from org.jbpm.graph.def.Transition t "
+ "where t.from = :nodeFrom "
+ " and t.to = :nodeTo");
q.setEntity("nodeFrom", newNodeFrom);
q.setEntity("nodeTo", newNodeTo);
List<Transition> transicions = (List<Transition>)q.list();
if (!transicions.isEmpty()) {
if (transicions.size() == 1)
newTransition = transicions.get(0);
else {
for (Transition t: transicions) {
if (oldTransition.getName() == null) {
if (t.getName() == null)
newTransition = t;
} else if (t.getName() != null && t.getName().equals(oldTransition.getName())) {
newTransition = t;
break;
}
}
}
}
}
}
if (newTransition == null) {
throw new JbpmException("Transition for nodes from='" + oldTransition.getFrom().getName() +
"' and to='" + oldTransition.getTo().getName() + "' not found in new process definition");
}
return newTransition;
}
private Action findReplacementAction(ProcessDefinition newDef, Action oldAction) {
Action newAction = null;
if (oldAction.getName() != null) {
newAction = newDef.getAction(oldAction.getName());
}
if (newAction == null) {
throw new JbpmException("Action '" + oldAction.getName() + "' not found in new process definition");
}
return newAction;
}
/**
* @param oldNode
* @return the name of the new node (given in the map or return default
* value, which is the old node name)
*/
private String getReplacementNodeName(GraphElement oldNode) {
String oldName = (oldNode instanceof Node ? ((Node) oldNode).getFullyQualifiedName() : oldNode.getName());
if (nodeNameMapping.containsKey(oldName)) {
return (String) nodeNameMapping.get(oldName);
}
// return new node name = old node name as default
return oldName;
}
private String getReplacementTaskName(Task oldTask) {
String oldName = oldTask.getName();
if (taskNameMapping.containsKey(oldName)) {
return (String) taskNameMapping.get(oldName);
}
// return new node name = old node name as default
return oldName;
}
public static Node findNode(NodeCollection nodeCollection, String hierarchicalName) {
Node node = null;
if ((hierarchicalName != null) && (!"".equals(hierarchicalName.trim()))) {
// Afegit per evitar el problema de nodes amb el nom que
// conté un caràcter '/'.
node = nodeCollection.getNode(hierarchicalName);
if (node != null) {
return node;
}
//
if ((hierarchicalName.startsWith("/")) && (nodeCollection instanceof SuperState)) {
nodeCollection = ((SuperState) nodeCollection).getProcessDefinition();
}
StringTokenizer tokenizer = new StringTokenizer(hierarchicalName, "/");
while (tokenizer.hasMoreElements()) {
String namePart = tokenizer.nextToken();
if ("..".equals(namePart)) {
if (nodeCollection instanceof ProcessDefinition) {
throw new JbpmException("couldn't find node '" + hierarchicalName
+ "' because of a '..' on the process definition.");
}
nodeCollection = (NodeCollection) ((GraphElement) nodeCollection).getParent();
} else if (tokenizer.hasMoreElements()) {
nodeCollection = (NodeCollection) nodeCollection.getNode(namePart);
} else {
node = nodeCollection.getNode(namePart);
}
}
}
return node;
}
/**
* We may still have open tasks, even though their parent tokens have been
* ended. So we'll simply get all tasks from this process instance and
* cancel them if they are still active.
*
*/
private List getTasksForToken(Token token) {
Query query = getJbpmContext().getSession().getNamedQuery("TaskMgmtSession.findTaskInstancesByTokenId");
query.setLong("tokenId", token.getId());
return query.list();
}
public Map getNodeNameMapping() {
return nodeNameMapping;
}
public void setNodeNameMapping(Map<String, String> nameMapping) {
if (nameMapping == null) {
this.nodeNameMapping = new HashMap<String, String>();
} else {
this.nodeNameMapping = nameMapping;
}
}
public int getNewVersion() {
return newVersion;
}
public void setNewVersion(int newVersion) {
this.newVersion = newVersion;
}
public Map getTaskNameMapping() {
return taskNameMapping;
}
public void setTaskNameMapping(Map<String, String> nameMapping) {
if (nameMapping == null) {
this.taskNameMapping = new HashMap<String, String>();
} else {
this.taskNameMapping = nameMapping;
}
}
/**
* @deprecated use getProcessInstanceId instead
*/
public long getProcessId() {
if (getProcessInstanceIds() != null && getProcessInstanceIds().length > 0)
return getProcessInstanceIds()[0];
else
return 0;
}
/**
* @deprecated use setProcessInstanceId instead
*/
public void setProcessId(long processId) {
super.setProcessInstanceId(processId);
}
/**
* @deprecated use getNodeNameMapping instead
*/
public Map getNameMapping() {
return getNodeNameMapping();
}
/**
* @deprecated use setNodeNameMapping instead
*/
public void setNameMapping(Map nameMapping) {
setNodeNameMapping(nameMapping);
}
// methods for fluent programming
public ChangeProcessInstanceVersionCommand nodeNameMapping(Map<String, String> nameMapping) {
setNodeNameMapping(nameMapping);
return this;
}
public ChangeProcessInstanceVersionCommand newVersion(int newVersion) {
setNewVersion(newVersion);
return this;
}
public ChangeProcessInstanceVersionCommand taskNameMapping(Map<String, String> nameMapping) {
setTaskNameMapping(nameMapping);
return this;
}
public ChangeProcessInstanceVersionCommand nodeNameMappingAdd(String oldNodeName, String newNodeName) {
if (nodeNameMapping == null) {
this.nodeNameMapping = new HashMap<String, String>();
}
this.nodeNameMapping.put(oldNodeName, newNodeName);
return this;
}
public ChangeProcessInstanceVersionCommand taskNameMappingAdd(String oldTaskName, String newNodeName) {
if (taskNameMapping == null) {
this.taskNameMapping = new HashMap<String, String>();
}
this.taskNameMapping.put(oldTaskName, newNodeName);
return this;
}
}