/*
* JBoss, Home of Professional Open Source
* Copyright 2006, 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.jboss.soa.esb.services.jbpm.cmd;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.transaction.Synchronization;
import javax.transaction.Transaction;
import org.apache.log4j.Logger;
import org.jboss.soa.esb.common.TransactionStrategy;
import org.jboss.soa.esb.services.jbpm.Mapping;
import org.jbpm.JbpmContext;
import org.jbpm.JbpmException;
import org.jbpm.command.SignalCommand;
import org.jbpm.context.exe.ContextInstance;
import org.jbpm.graph.def.Action;
import org.jbpm.graph.def.ActionHandler;
import org.jbpm.graph.def.ProcessDefinition;
import org.jbpm.graph.exe.ExecutionContext;
import org.jbpm.graph.exe.ProcessInstance;
import org.jbpm.graph.exe.Token;
import org.jbpm.instantiation.Delegation;
import org.jbpm.job.ExecuteActionJob;
import org.jbpm.job.executor.JobExecutor;
import org.jbpm.msg.MessageService;
import org.jbpm.svc.Services;
/**
* Handle the asynchronous signalling of a process instance task.
*
* @author <a href='kevin.conner@jboss.com'>Kevin Conner</a>
*/
class AsyncProcessSignal
{
/**
* The logger for this class.
*/
private static Logger logger = Logger.getLogger(AsyncProcessSignal.class);
/**
* The name of the ESB asynchronous signal action.
*/
private static final String ESB_ASYNC_SIGNAL_ACTION_NAME = "ESB_ASYNC_SIGNAL_ACTION" ;
/**
* The base name of the ESB asynchronous signal context variable.
*/
private static final String ESB_ASYNC_SIGNAL_VARIABLE_NAME = "ESB_ASYNC_SIGNAL_VARIABLE_" ;
/**
* The name of the ESB asynchronous signal transition variable.
*/
private static final String ESB_ASYNC_SIGNAL_TRANSITION_VARIABLE_NAME = ESB_ASYNC_SIGNAL_VARIABLE_NAME + "TRANSITION_" ;
/**
* The name of the ESB asynchronous signal actor variable.
*/
private static final String ESB_ASYNC_SIGNAL_ACTOR_VARIABLE_NAME = ESB_ASYNC_SIGNAL_VARIABLE_NAME + "ACTOR_" ;
/**
* The name of the ESB asynchronous signal variable count.
*/
private static final String ESB_ASYNC_SIGNAL_VARIABLE_COUNT = ESB_ASYNC_SIGNAL_VARIABLE_NAME + "COUNT_" ;
/**
* The name of the ESB asynchronous signal variable names.
*/
private static final String ESB_ASYNC_SIGNAL_VARIABLE_NAMES = ESB_ASYNC_SIGNAL_VARIABLE_NAME + "NAME_" ;
/**
* Map of active synchronisations.
*/
private static final ConcurrentHashMap<Transaction, Synchronization> SYNCHRONISATIONS = new ConcurrentHashMap<Transaction, Synchronization>() ;
/**
* Create an asynchronous signal job for the specified token and transition.
* @param token The token to signal.
* @param transitionName The transition to signal or null if the default transition is to be used.
* @param actor The actor to use.
* @param globalProcessScope The default process scope to use.
* @param variables Any variables to update.
*/
static void createSignalJob(final JbpmContext jbpmContext, final Token token, final String transitionName, final String actor,
final boolean defaultProcessScope, final Map<Mapping, Object> variables)
{
final boolean isDebugEnabled = logger.isDebugEnabled() ;
final long tokenId = token.getId() ;
final ProcessInstance processInstance = token.getProcessInstance() ;
final long processInstanceId = processInstance.getId() ;
final ContextInstance contextInstance = processInstance.getContextInstance() ;
if (isDebugEnabled)
{
logger.debug("Locking token id " + tokenId + " from process instance " + processInstanceId) ;
}
token.lock(ESB_ASYNC_SIGNAL_ACTION_NAME);
final String transitionVariableName = ESB_ASYNC_SIGNAL_TRANSITION_VARIABLE_NAME ;
setVariable(contextInstance, token, transitionVariableName, transitionName) ;
final String actorVariableName = ESB_ASYNC_SIGNAL_ACTOR_VARIABLE_NAME ;
setVariable(contextInstance, token, actorVariableName, actor) ;
final int numVariables = (variables == null ? 0 : variables.size()) ;
if (numVariables > 0)
{
int count = 0 ;
final Iterator<Map.Entry<Mapping, Object>> variableEntryIter = variables.entrySet().iterator() ;
do
{
final Map.Entry<Mapping, Object> variableEntry = variableEntryIter.next() ;
final Mapping mapping = (Mapping)variableEntry.getKey() ;
final String name = mapping.getBpm() ;
setVariable(contextInstance, token, name, variableEntry.getValue()) ;
final Boolean isProcessScope = mapping.getIsProcessScope() ;
final boolean setScope = (isProcessScope == null ? defaultProcessScope : isProcessScope.booleanValue()) ;
if (setScope)
{
setVariable(contextInstance, token, ESB_ASYNC_SIGNAL_VARIABLE_NAMES + (count++), name) ;
}
} while (variableEntryIter.hasNext()) ;
setVariable(contextInstance, token, ESB_ASYNC_SIGNAL_VARIABLE_COUNT, Integer.toString(count)) ;
}
else
{
setVariable(contextInstance, token, ESB_ASYNC_SIGNAL_VARIABLE_COUNT, String.valueOf(0)) ;
}
final ExecuteActionJob signalJob = new ExecuteActionJob(token) ;
signalJob.setAction(getAsyncSignalAction(token)) ;
signalJob.setDueDate(new Date()) ;
signalJob.setSuspended(token.isSuspended()) ;
if (isDebugEnabled)
{
logger.debug("Sending " + (token.isSuspended() ? "suspended " : "") +"signal task to message service for token id " + tokenId + " from process instance " + processInstanceId) ;
}
final MessageService messageService = (MessageService)Services.getCurrentService(Services.SERVICENAME_MESSAGE, true) ;
messageService.send(signalJob) ;
if (isDebugEnabled)
{
logger.debug("Sent signal task to message service for token id " + tokenId + " from process instance " + processInstanceId) ;
}
final JobExecutor jobExecutor = jbpmContext.getJbpmConfiguration().getJobExecutor() ;
if (jobExecutor != null)
{
final TransactionStrategy transactionStrategy = TransactionStrategy.getTransactionStrategy(true) ;
try
{
if (transactionStrategy.isActive())
{
final Transaction transaction = (Transaction)transactionStrategy.getTransaction() ;
if ((transaction != null) && !SYNCHRONISATIONS.containsKey(transaction))
{
final Synchronization synch = new JobNotifierSynchronisation(transaction, jobExecutor) ;
transaction.registerSynchronization(synch) ;
SYNCHRONISATIONS.put(transaction, synch) ;
}
}
}
catch (final Exception ex)
{
if (logger.isDebugEnabled())
{
logger.debug("Failed to register synchronization", ex) ;
}
}
}
}
/**
* Set the context instance variable.
* @param contextInstance The context instance.
* @param token The current token.
* @param name The variable name
* @param value The variable value
*/
private static void setVariable(final ContextInstance contextInstance, final Token token, final String name, final Object value)
{
if (value != null)
{
contextInstance.setVariableLocally(name, value, token) ;
}
}
/**
* Locate the asynchronous signal action associated with the process instance.
* @param token The token to signal.
* @return The action used for asynchronous signalling.
* @throws JbpmException For errors creating or locating the action.
*/
private static Action getAsyncSignalAction(final Token token)
throws JbpmException
{
final ProcessInstance processInstance = token.getProcessInstance() ;
final ProcessDefinition processDefinition = token.getProcessInstance().getProcessDefinition() ;
if (processDefinition == null)
{
throw new JbpmException("Could not locate process definition for process instance: " + processInstance.getId()) ;
}
final Action currentAction = token.getProcessInstance().getProcessDefinition().getAction(ESB_ASYNC_SIGNAL_ACTION_NAME) ;
if (currentAction!= null)
{
return currentAction ;
}
if (logger.isDebugEnabled())
{
logger.debug("Creating Callback action for process definition: " + processDefinition.getName() + ", id: " + processDefinition.getId()) ;
}
final Delegation delegation = new Delegation(AsyncSignalAction.class.getName()) ;
delegation.setConfigType("constructor") ;
final Action newAction = new Action(delegation) ;
newAction.setName(ESB_ASYNC_SIGNAL_ACTION_NAME) ;
processDefinition.addAction(newAction) ;
return newAction ;
}
/**
* The task for asynchronously signalling the token.
* @author <a href='kevin.conner@jboss.com'>Kevin Conner</a>
*/
private static class AsyncSignalAction implements ActionHandler
{
/**
* Serial version UID for this action.
*/
private static final long serialVersionUID = -1462271955979788905L;
/**
* Construct the signal job.
* @param configuration The configuration.
*/
AsyncSignalAction(final String configuration)
throws JbpmException
{
}
/**
* Process the background signal task.
* @param executionContext The current execution context.
*/
public void execute(final ExecutionContext executionContext)
throws Exception
{
final boolean isDebugEnabled = logger.isDebugEnabled() ;
final Token token = executionContext.getToken() ;
final long tokenId = token.getId() ;
if (isDebugEnabled)
{
logger.debug("Unlocking token id " + tokenId + " from process instance " +
token.getProcessInstance().getId()) ;
}
token.unlock(ESB_ASYNC_SIGNAL_ACTION_NAME) ;
if (isDebugEnabled)
{
logger.debug("Signaling task " + tokenId + " from process instance " +
token.getProcessInstance().getId()) ;
}
final ProcessInstance processInstance = token.getProcessInstance() ;
final ContextInstance contextInstance = processInstance.getContextInstance() ;
final JbpmContext jbpmContext = executionContext.getJbpmContext() ;
final String transitionName = (String)removeVariableLocally(jbpmContext, contextInstance, ESB_ASYNC_SIGNAL_TRANSITION_VARIABLE_NAME, token) ;
final String actor = (String)removeVariableLocally(jbpmContext, contextInstance, ESB_ASYNC_SIGNAL_ACTOR_VARIABLE_NAME, token) ;
final String origActor = jbpmContext.getActorId() ;
final int variableCount = Integer.parseInt((String)removeVariableLocally(jbpmContext, contextInstance, ESB_ASYNC_SIGNAL_VARIABLE_COUNT, token)) ;
for(int count = 0 ; count < variableCount ; count++)
{
final String name = (String)removeVariableLocally(jbpmContext, contextInstance, ESB_ASYNC_SIGNAL_VARIABLE_NAMES + count, token) ;
final Object value = removeVariableLocally(jbpmContext, contextInstance, name, token) ;
contextInstance.setVariable(name, value) ;
}
try
{
if (actor != null)
{
jbpmContext.setActorId(actor) ;
}
final SignalCommand signalCommand = new SignalCommand(tokenId, transitionName) ;
signalCommand.execute(jbpmContext) ;
}
finally
{
jbpmContext.setActorId(origActor) ;
}
if (isDebugEnabled)
{
logger.debug("Signalled task " + tokenId + " from process instance " +
token.getProcessInstance().getId()) ;
}
}
private Object removeVariableLocally(final JbpmContext jbpmContext, final ContextInstance contextInstance, final String name, final Token token)
{
final Object value = contextInstance.getVariableLocally(name, token) ;
if (value != null)
{
contextInstance.deleteVariable(name, token) ;
}
return value ;
}
}
/**
* Synchronisation to notify the job executor.
* @author kevin
*/
private static final class JobNotifierSynchronisation implements Synchronization
{
/**
* The associated transaction.
*/
private Transaction transaction ;
/**
* The current job executor.
*/
private final JobExecutor jobExecutor ;
/**
* Create the notifier synchronisation.
* @param transaction The current transaction.
* @param jobExecutor The current job executor.
*/
public JobNotifierSynchronisation(final Transaction transaction, final JobExecutor jobExecutor)
{
this.transaction = transaction ;
this.jobExecutor = jobExecutor ;
}
/**
* The before completion notification.
*/
public void beforeCompletion()
{
}
/**
* The after completion notification.
* @param status The status of the transaction.
*/
public void afterCompletion(final int status)
{
SYNCHRONISATIONS.remove(transaction) ;
synchronized(jobExecutor)
{
jobExecutor.notify() ;
}
}
}
}