/*
* JBoss, Home of Professional Open Source
* Copyright 2005, JBoss Inc., and individual contributors as indicated
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jbpm.graph.exe;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Query;
import org.jbpm.JbpmContext;
import org.jbpm.JbpmException;
import org.jbpm.db.JobSession;
import org.jbpm.graph.def.Event;
import org.jbpm.graph.def.Identifiable;
import org.jbpm.graph.def.Node;
import org.jbpm.graph.def.ProcessDefinition;
import org.jbpm.graph.def.Transition;
import org.jbpm.graph.log.SignalLog;
import org.jbpm.graph.log.TokenCreateLog;
import org.jbpm.graph.log.TokenEndLog;
import org.jbpm.jpdl.el.impl.JbpmExpressionEvaluator;
import org.jbpm.logging.exe.LoggingInstance;
import org.jbpm.logging.log.CompositeLog;
import org.jbpm.logging.log.ProcessLog;
import org.jbpm.svc.Services;
import org.jbpm.taskmgmt.exe.TaskMgmtInstance;
import org.jbpm.util.Clock;
import org.jbpm.util.EqualsUtil;
import net.conselldemallorca.helium.jbpm3.integracio.Jbpm3HeliumBridge;
import net.conselldemallorca.helium.v3.core.api.dto.ExpedientDto;
/**
* represents one path of execution and maintains a pointer to a node in the
* {@link org.jbpm.graph.def.ProcessDefinition}. Most common way to get a hold of the token objects is with
* {@link ProcessInstance#getRootToken()} or {@link org.jbpm.graph.exe.ProcessInstance#findToken(String)}.
*/
public class Token implements Identifiable, Serializable
{
private static final long serialVersionUID = 1L;
long id = 0;
int version = 0;
protected String name = null;
protected Date start = null;
protected Date end = null;
protected Node node = null;
protected Date nodeEnter = null;
protected ProcessInstance processInstance = null;
protected Token parent = null;
protected Map<String,Token> children = null;
protected List comments = null;
protected ProcessInstance subProcessInstance = null;
protected int nextLogIndex = 0;
boolean isAbleToReactivateParent = true;
boolean isTerminationImplicit = false;
boolean isSuspended = false;
String lock = null;
// constructors
// ////////////////////
///////////////////////////////////////////////////////
public Token()
{
}
/**
* creates a root token.
*/
public Token(ProcessInstance processInstance)
{
this.start = Clock.getCurrentTime();
this.processInstance = processInstance;
this.node = processInstance.getProcessDefinition().getStartState();
this.isTerminationImplicit = processInstance.getProcessDefinition().isTerminationImplicit();
// optimization: assigning an id is not necessary since the process instance will be saved shortly.
// Services.assignId(this);
}
/**
* creates a child token.
*/
public Token(Token parent, String name)
{
this.start = Clock.getCurrentTime();
this.processInstance = parent.getProcessInstance();
this.name = name;
this.node = parent.getNode();
this.parent = parent;
parent.addChild(this);
this.isTerminationImplicit = parent.isTerminationImplicit();
parent.addLog(new TokenCreateLog(this));
// assign an id to this token before events get fired
Services.assignId(this);
}
// operations
// ///////////////////////////////////////////////////////////////////////////
void addChild(Token token)
{
if (children == null)
{
children = new HashMap<String,Token>();
}
children.put(token.getName(), token);
}
/**
* provides a signal to the token. this method activates this token and leaves the current state over the default
* transition.
*/
public void signal()
{
if (node == null)
{
throw new JbpmException("token '" + this + "' can't be signalled cause it is currently not positioned in a node");
}
if (node.getDefaultLeavingTransition() == null)
{
throw new JbpmException("couldn't signal token '" + this + "' : node '" + node + "' doesn't have a default transition");
}
signal(node.getDefaultLeavingTransition(), new ExecutionContext(this));
}
/**
* Provides a signal to the token.
* This leave the current state over the given transition name.
*/
public void signal(String transitionName)
{
if (node == null)
throw new JbpmException("token '" + this + "' can't be signalled cause it is currently not positioned in a node");
Transition leavingTransition = node.getLeavingTransition(transitionName);
if (leavingTransition == null)
{
// Fall back to the name of the target node
for (Transition auxTrans : node.getLeavingTransitions())
{
if (transitionName.equals(auxTrans.getTo().getName()))
{
leavingTransition = auxTrans;
break;
}
}
}
if (leavingTransition == null)
throw new JbpmException("transition '" + transitionName + "' does not exist on " + node);
signal(leavingTransition, new ExecutionContext(this));
}
/**
* provides a signal to the token. this leave the current state over the given transition name.
*/
public void signal(Transition transition)
{
signal(transition, new ExecutionContext(this));
}
void signal(ExecutionContext executionContext)
{
signal(node.getDefaultLeavingTransition(), executionContext);
}
void signal(Transition transition, ExecutionContext executionContext)
{
if (transition == null)
{
throw new JbpmException("couldn't signal without specifying a leaving transition : transition is null");
}
if (executionContext == null)
{
throw new JbpmException("couldn't signal without an execution context: executionContext is null");
}
if (isSuspended)
{
throw new JbpmException("can't signal token '" + name + "' (" + id + "): it is suspended");
}
if (isLocked())
{
throw new JbpmException("this token is locked by " + lock);
}
if (isRoot() && processInstance.getExpedient() == null) {
ProcessInstance processInstanceSuperior = processInstance;
while (processInstanceSuperior.getSuperProcessToken() != null) {
processInstanceSuperior = processInstanceSuperior.getSuperProcessToken().getProcessInstance();
}
Query query = executionContext.getJbpmContext().getSession().createQuery(
"from " +
" org.jbpm.graph.exe.ProcessInstanceExpedient exp " +
"where " +
" exp.processInstanceId = :processInstanceId");
query.setParameter(
"processInstanceId",
new Long(processInstanceSuperior.getId()).toString());
ProcessInstanceExpedient expedient = (ProcessInstanceExpedient)query.uniqueResult();
processInstance.setExpedient(expedient);
}
startCompositeLog(new SignalLog(transition));
try
{
// fire the event before-signal
Node signalNode = node;
signalNode.fireEvent(Event.EVENTTYPE_BEFORE_SIGNAL, executionContext);
// start calculating the next state
node.leave(executionContext, transition);
// if required, check if this token is implicitly terminated
checkImplicitTermination();
// fire the event after-signal
signalNode.fireEvent(Event.EVENTTYPE_AFTER_SIGNAL, executionContext);
}
finally
{
endCompositeLog();
}
}
/**
* a set of all the leaving transitions on the current node for which the condition expression resolves to true.
*/
public Set getAvailableTransitions()
{
Set availableTransitions = new HashSet();
if (node != null)
{
addAvailableTransitionsOfNode(node, availableTransitions);
}
return availableTransitions;
}
/**
* adds available transitions of that node to the Set and after that calls itself recursivly for the SuperSate of the
* Node if it has a super state
*/
private void addAvailableTransitionsOfNode(Node currentNode, Set availableTransitions)
{
List leavingTransitions = currentNode.getLeavingTransitions();
if (leavingTransitions != null)
{
Iterator iter = leavingTransitions.iterator();
while (iter.hasNext())
{
Transition transition = (Transition)iter.next();
String conditionExpression = transition.getCondition();
if (conditionExpression != null)
{
Object result = JbpmExpressionEvaluator.evaluate(conditionExpression, new ExecutionContext(this));
if ((result instanceof Boolean) && (((Boolean)result).booleanValue()))
{
availableTransitions.add(transition);
}
}
else
{
availableTransitions.add(transition);
}
}
}
if (currentNode.getSuperState() != null)
{
addAvailableTransitionsOfNode(currentNode.getSuperState(), availableTransitions);
}
}
/**
* ends this token and all of its children (if any). this is the last active (=not-ended) child of a parent token, the
* parent token will be ended as well and that verification will continue to propagate.
*/
public void end()
{
end(true);
}
/**
* ends this token with optional parent ending verification.
*
* @param verifyParentTermination specifies if the parent token should be checked for termination. if
* verifyParentTermination is set to true and this is the last non-ended child of a parent token, the parent
* token will be ended as well and the verification will continue to propagate.
*/
public void end(boolean verifyParentTermination)
{
// if not already ended
if (end == null)
{
// ended tokens cannot reactivate parents
isAbleToReactivateParent = false;
// set the end date
// the end date is also the flag that indicates that this token has ended.
this.end = Clock.getCurrentTime();
// end all this token's children
if (children != null)
{
Iterator iter = children.values().iterator();
while (iter.hasNext())
{
Token child = (Token)iter.next();
if (!child.hasEnded())
{
child.end();
}
}
}
if (subProcessInstance != null)
{
subProcessInstance.end();
}
// only log the end of child-tokens. the process instance logs replace the root token logs.
if (parent != null)
{
// add a log
parent.addLog(new TokenEndLog(this));
}
// if there are tasks associated to this token, remove signaling capabilities
TaskMgmtInstance taskMgmtInstance = (processInstance != null ? processInstance.getTaskMgmtInstance() : null);
if (taskMgmtInstance != null)
{
taskMgmtInstance.removeSignalling(this);
}
if (verifyParentTermination)
{
// if this is the last active token of the parent,
// the parent needs to be ended as well
notifyParentOfTokenEnd();
}
}
}
// comments /////////////////////////////////////////////////////////////////
public void addComment(String message)
{
addComment(new Comment(message));
}
public void addComment(Comment comment)
{
if (comments == null)
comments = new ArrayList();
comments.add(comment);
comment.setToken(this);
}
public List getComments()
{
return comments;
}
// operations helper methods ////////////////////////////////////////////////
/**
* notifies a parent that one of its nodeMap has ended.
*/
void notifyParentOfTokenEnd()
{
if (isRoot())
{
processInstance.end();
}
else
{
if (!parent.hasActiveChildren())
{
parent.end();
}
}
}
/**
* tells if this token has child tokens that have not yet ended.
*/
public boolean hasActiveChildren()
{
boolean foundActiveChildToken = false;
// try and find at least one child token that is
// still active (= not ended)
if (children != null)
{
Iterator iter = children.values().iterator();
while ((iter.hasNext()) && (!foundActiveChildToken))
{
Token child = (Token)iter.next();
if (!child.hasEnded())
{
foundActiveChildToken = true;
}
}
}
return foundActiveChildToken;
}
// log convenience methods //////////////////////////////////////////////////
/**
* convenience method for adding a process log.
*/
public void addLog(ProcessLog processLog)
{
LoggingInstance li = (LoggingInstance)processInstance.getInstance(LoggingInstance.class);
if (li != null)
{
processLog.setToken(this);
li.addLog(processLog);
}
}
/**
* convenience method for starting a composite log. When you add composite logs, make sure you put the
* {@link #endCompositeLog()} in a finally block.
*/
public void startCompositeLog(CompositeLog compositeLog)
{
LoggingInstance li = (LoggingInstance)processInstance.getInstance(LoggingInstance.class);
if (li != null)
{
compositeLog.setToken(this);
li.startCompositeLog(compositeLog);
}
}
/**
* convenience method for ending a composite log. Make sure you put this in a finally block.
*/
public void endCompositeLog()
{
LoggingInstance li = (LoggingInstance)processInstance.getInstance(LoggingInstance.class);
if (li != null)
{
li.endCompositeLog();
}
}
// various information extraction methods ///////////////////////////////////
public String toString()
{
return "Token(" + getFullName() + ")";
}
public boolean hasEnded()
{
return (end != null);
}
public boolean isRoot()
{
return (parent == null);
}
public boolean hasParent()
{
return (parent != null);
}
public boolean hasChild(String name)
{
return (children != null ? children.containsKey(name) : false);
}
public Token getChild(String name)
{
Token child = null;
if (children != null)
{
child = (Token)children.get(name);
}
return child;
}
public String getFullName()
{
if (parent == null)
return "/";
if (parent.getParent() == null)
return "/" + name;
return parent.getFullName() + "/" + name;
}
public List getChildrenAtNode(Node aNode)
{
List foundChildren = new ArrayList();
getChildrenAtNode(aNode, foundChildren);
return foundChildren;
}
void getChildrenAtNode(Node aNode, List foundTokens)
{
if (aNode.equals(node))
{
foundTokens.add(this);
}
else if (children != null && !children.isEmpty())
{
for (Iterator it = children.values().iterator(); it.hasNext();)
{
Token aChild = (Token)it.next();
aChild.getChildrenAtNode(aNode, foundTokens);
}
}
}
public void collectChildrenRecursively(List tokens)
{
if (children != null)
{
Iterator iter = children.values().iterator();
while (iter.hasNext())
{
Token child = (Token)iter.next();
tokens.add(child);
child.collectChildrenRecursively(tokens);
}
}
}
public Token findToken(String relativeTokenPath)
{
if (relativeTokenPath == null)
return null;
String path = relativeTokenPath.trim();
if (("".equals(path)) || (".".equals(path)))
{
return this;
}
if ("..".equals(path))
{
return parent;
}
if (path.startsWith("/"))
{
Token root = processInstance.getRootToken();
return root.findToken(path.substring(1));
}
if (path.startsWith("./"))
{
return findToken(path.substring(2));
}
if (path.startsWith("../"))
{
if (parent != null)
{
return parent.findToken(path.substring(3));
}
return null;
}
int slashIndex = path.indexOf('/');
if (slashIndex == -1)
{
return (Token)(children != null ? children.get(path) : null);
}
Token token = null;
String name = path.substring(0, slashIndex);
token = (Token)children.get(name);
if (token != null)
{
return token.findToken(path.substring(slashIndex + 1));
}
return null;
}
public Map getActiveChildren()
{
Map activeChildren = new HashMap();
if (children != null)
{
Iterator iter = children.entrySet().iterator();
while (iter.hasNext())
{
Map.Entry entry = (Map.Entry)iter.next();
Token child = (Token)entry.getValue();
if (!child.hasEnded())
{
String childName = (String)entry.getKey();
activeChildren.put(childName, child);
}
}
}
return activeChildren;
}
public void checkImplicitTermination()
{
if (isTerminationImplicit && node.hasNoLeavingTransitions())
{
end();
if (processInstance.isTerminatedImplicitly())
{
processInstance.end();
}
}
}
public boolean isTerminatedImplicitly()
{
if (end != null)
return true;
Map leavingTransitions = node.getLeavingTransitionsMap();
if ((leavingTransitions != null) && (leavingTransitions.size() > 0))
{
// ok: found a non-terminated token
return false;
}
// loop over all active child tokens
Iterator iter = getActiveChildren().values().iterator();
while (iter.hasNext())
{
Token child = (Token)iter.next();
if (!child.isTerminatedImplicitly())
{
return false;
}
}
// if none of the above, this token is terminated implicitly
return true;
}
public int nextLogIndex()
{
return nextLogIndex++;
}
/**
* suspends a process execution.
*/
public void suspend()
{
isSuspended = true;
suspendJobs();
suspendTaskInstances();
// propagate to child tokens
if (children != null)
{
Iterator iter = children.values().iterator();
while (iter.hasNext())
{
Token child = (Token)iter.next();
child.suspend();
}
}
}
void suspendJobs()
{
JbpmContext jbpmContext = JbpmContext.getCurrentJbpmContext();
JobSession jobSession = (jbpmContext != null ? jbpmContext.getJobSession() : null);
if (jobSession != null)
{
jobSession.suspendJobs(this);
}
}
void suspendTaskInstances()
{
TaskMgmtInstance taskMgmtInstance = (processInstance != null ? processInstance.getTaskMgmtInstance() : null);
if (taskMgmtInstance != null)
{
taskMgmtInstance.suspend(this);
}
}
/**
* resumes a process execution.
*/
public void resume()
{
isSuspended = false;
resumeJobs();
resumeTaskInstances();
// propagate to child tokens
if (children != null)
{
Iterator iter = children.values().iterator();
while (iter.hasNext())
{
Token child = (Token)iter.next();
child.resume();
}
}
}
void resumeJobs()
{
JbpmContext jbpmContext = JbpmContext.getCurrentJbpmContext();
JobSession jobSession = (jbpmContext != null ? jbpmContext.getJobSession() : null);
if (jobSession != null)
{
jobSession.resumeJobs(this);
}
}
void resumeTaskInstances()
{
TaskMgmtInstance taskMgmtInstance = (processInstance != null ? processInstance.getTaskMgmtInstance() : null);
if (taskMgmtInstance != null)
{
taskMgmtInstance.resume(this);
}
}
// equals ///////////////////////////////////////////////////////////////////
// hack to support comparing hibernate proxies against the real objects
// since this always falls back to ==, we don't need to overwrite the hashcode
public boolean equals(Object o)
{
return EqualsUtil.equals(this, o);
}
public ProcessInstance createSubProcessInstance(ProcessDefinition subProcessDefinition)
{
// create the new sub process instance
subProcessInstance = new ProcessInstance(subProcessDefinition);
// bind the subprocess to the super-process-token
setSubProcessInstance(subProcessInstance);
subProcessInstance.setSuperProcessToken(this);
// make sure the process gets saved during super process save
processInstance.addCascadeProcessInstance(subProcessInstance);
return subProcessInstance;
}
/**
* locks a process instance for further execution. A locked token cannot continue execution. This is a non-persistent
* operation. This is used to prevent tokens being propagated during the execution of actions.
*
* @see #unlock(String)
*/
public void lock(String lockOwnerId)
{
if (lockOwnerId == null)
{
throw new JbpmException("can't lock with null value for the lockOwnerId");
}
if ((lock != null) && (!lock.equals(lockOwnerId)))
{
throw new JbpmException("token '" + id + "' can't be locked by '" + lockOwnerId + "' cause it's already locked by '" + lock + "'");
}
log.debug("token[" + id + "] is locked by " + lockOwnerId);
lock = lockOwnerId;
}
/**
* @see #lock(String)
*/
public void unlock(String lockOwnerId)
{
if (lock == null)
{
log.warn("lock owner '" + lockOwnerId + "' tries to unlock token '" + id + "' which is not locked");
}
else if (!lock.equals(lockOwnerId))
{
throw new JbpmException("'" + lockOwnerId + "' can't unlock token '" + id + "' because it was already locked by '" + lock + "'");
}
log.debug("token[" + id + "] is unlocked by " + lockOwnerId);
lock = null;
}
/**
* force unlocking the token, even if the owner is not known. In some
* use cases (e.g. in the jbpm esb integration) the lock is persistent,
* so a state can be reached where the client needs a possibility to force
* unlock of a token without knowing the owner.
*
* See https://jira.jboss.org/jira/browse/JBPM-1888
*/
public void foreUnlock()
{
if (lock == null)
{
log.warn("Unlock of token '" + id + "' forced, but it is not locked");
}
log.debug("Foce unlock of token[" + id + "] which was locked by " + lock);
lock = null;
}
/**
* return the current lock owner of the token
*
* See https://jira.jboss.org/jira/browse/JBPM-1888
*/
public String getLockOwner() {
return lock;
}
public boolean isLocked()
{
return lock != null;
}
// getters and setters //////////////////////////////////////////////////////
public long getId()
{
return id;
}
public Date getStart()
{
return start;
}
public Date getEnd()
{
return end;
}
public String getName()
{
return name;
}
public ProcessInstance getProcessInstance()
{
return processInstance;
}
public Map<String,Token> getChildren()
{
return children;
}
public Node getNode()
{
return node;
}
public void setNode(Node node)
{
this.node = node;
}
public Token getParent()
{
return parent;
}
public void setParent(Token parent)
{
this.parent = parent;
}
public void setProcessInstance(ProcessInstance processInstance)
{
this.processInstance = processInstance;
}
public ProcessInstance getSubProcessInstance()
{
return subProcessInstance;
}
public Date getNodeEnter()
{
return nodeEnter;
}
public void setNodeEnter(Date nodeEnter)
{
this.nodeEnter = nodeEnter;
}
public boolean isAbleToReactivateParent()
{
return isAbleToReactivateParent;
}
public void setAbleToReactivateParent(boolean isAbleToReactivateParent)
{
this.isAbleToReactivateParent = isAbleToReactivateParent;
}
public boolean isTerminationImplicit()
{
return isTerminationImplicit;
}
public void setTerminationImplicit(boolean isTerminationImplicit)
{
this.isTerminationImplicit = isTerminationImplicit;
}
public boolean isSuspended()
{
return isSuspended;
}
public void setSubProcessInstance(ProcessInstance subProcessInstance)
{
this.subProcessInstance = subProcessInstance;
}
private static final Log log = LogFactory.getLog(Token.class);
}