/* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.activiti.cdi; import java.io.Serializable; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import javax.enterprise.context.Conversation; import javax.enterprise.context.RequestScoped; import javax.enterprise.inject.Instance; import javax.enterprise.inject.Produces; import javax.inject.Inject; import javax.inject.Named; import org.activiti.cdi.annotation.BusinessProcessScoped; import org.activiti.cdi.annotation.ProcessInstanceId; import org.activiti.cdi.annotation.TaskId; import org.activiti.cdi.impl.context.BusinessProcessAssociationManager; import org.activiti.cdi.impl.context.CachingBeanStore; import org.activiti.engine.ActivitiException; import org.activiti.engine.ProcessEngine; import org.activiti.engine.RuntimeService; import org.activiti.engine.TaskService; import org.activiti.engine.impl.context.Context; import org.activiti.engine.impl.persistence.entity.ExecutionEntity; import org.activiti.engine.repository.ProcessDefinition; import org.activiti.engine.runtime.Execution; import org.activiti.engine.runtime.ProcessInstance; import org.activiti.engine.task.Task; /** * Bean supporting contextual business process management. This allows us to * implement a unit of work, in which a particular CDI scope (Conversation / * Request / Thread) is associated with a particular Execution / ProcessInstance * or Task. * <p /> * The protocol is that we <em>associate</em> the {@link BusinessProcess} bean * with a particular Execution / Task, then perform some changes (retrieve / set process * variables) and then end the unit of work. This bean makes sure that our changes are * only "flushed" to the process engine when we successfully complete the unit of work. * <p /> * A typical usage scenario might look like this:<br /> * <strong>1st unit of work ("process instantiation"):</strong> * <pre> * conversation.begin(); * ... * businessProcess.setVariable("billingId", "1"); // setting variables before starting the process * businessProcess.startProcessByKey("billingProcess"); * conversation.end(); * </pre> * <strong>2nd unit of work ("perform a user task"):</strong> * <pre> * conversation.begin(); * businessProcess.startTask(id); // now we have associated a task with the current conversation * ... // this allows us to retrieve and change process variables * // and @BusinessProcessScoped beans * businessProcess.setVariable("billingDetails", "someValue"); // these changes are cached in the conversation * ... * businessProcess.completeTask(); // now all changed process variables are flushed * conversation.end(); * </pre> * <p /> * <strong>NOTE:</strong> in the absence of a conversation, (non faces request, i.e. when processing a JAX-RS, * JAX-WS, JMS, remote EJB or plain Servlet requests), the {@link BusinessProcess} bean associates with the * current Request (see {@link RequestScoped @RequestScoped}). * <p /> * <strong>NOTE:</strong> in the absence of a request, ie. when the activiti JobExecutor accesses * {@link BusinessProcessScoped @BusinessProcessScoped} beans, the execution is associated with the * current thread. * * @author Daniel Meyer * */ @Named public class BusinessProcess implements Serializable { private static final long serialVersionUID = 1L; private static Logger logger = Logger.getLogger(BusinessProcess.class.getName()); @Inject private ProcessEngine processEngine; @Inject private BusinessProcessAssociationManager associationManager; @Inject private Instance<Conversation> conversationInstance; public ProcessInstance startProcessById(String processDefinitionId) { ProcessInstance instance = processEngine.getRuntimeService().startProcessInstanceById(processDefinitionId, getBeanStore().getAll()); associate(instance.getProcessInstanceId()); return instance; } public ProcessInstance startProcessById(String processDefinitionId, Map<String, Object> variables) { getBeanStore().putAll(variables); ProcessInstance instance = processEngine.getRuntimeService().startProcessInstanceById(processDefinitionId, getBeanStore().getAll()); associate(instance.getProcessInstanceId()); return instance; } public ProcessInstance startProcessByKey(String key) { ProcessInstance instance = processEngine.getRuntimeService().startProcessInstanceByKey(key, getBeanStore().getAll()); associate(instance.getProcessInstanceId()); return instance; } public ProcessInstance startProcessByKey(String key, Map<String, Object> variables) { getBeanStore().putAll(variables); ProcessInstance instance = processEngine.getRuntimeService().startProcessInstanceByKey(key, getBeanStore().getAll()); associate(instance.getProcessInstanceId()); return instance; } public ProcessInstance startProcessByName(String string) { ProcessDefinition definition = processEngine.getRepositoryService().createProcessDefinitionQuery().processDefinitionName(string).singleResult(); if (definition == null) { logger.log(Level.SEVERE, "No process definition found for name: " + string); throw new ActivitiException("No process definition found for name: " + string); } ProcessInstance instance = processEngine.getRuntimeService().startProcessInstanceById(definition.getId(), getBeanStore().getAll()); associate(instance.getProcessInstanceId()); return instance; } public ProcessInstance startProcessByName(String string, Map<String, Object> variables) { ProcessDefinition definition = processEngine.getRepositoryService().createProcessDefinitionQuery().processDefinitionName(string).singleResult(); if (definition == null) { logger.log(Level.SEVERE, "No process definition found for name: " + string); throw new ActivitiException("No process definition found for name: " + string); } getBeanStore().putAll(variables); ProcessInstance instance = processEngine.getRuntimeService().startProcessInstanceById(definition.getId(), getBeanStore().getAll()); associate(instance.getProcessInstanceId()); return instance; } /** * Associate with the provided execution. This starts a unit of work. * * @param executionId * the id of the execution to associate with. * @throw ActivitiCdiException * if no such execution exists */ public void associateExecutionById(String executionId) { associate(executionId); try { getExecution(); } catch (ActivitiException e) { associationManager.disAssociate(); throw new ActivitiCdiException("Cannot resume process: no execution with id '" + executionId + "' found."); } if (logger.isLoggable(Level.FINE)) { logger.fine("Resumig Execution[" + executionId + "]."); } } /** * returns true if an {@link Execution} is associated. * * @see #associateExecutionById(String) */ public boolean isAssociated() { return associationManager.getExecutionId() != null; } /** * Signals the current execution, see {@link RuntimeService#signal(String)} * <p/> * Ends the current unit of work (flushes changes to process variables set * using {@link #setVariable(String, Object)} or made on * {@link BusinessProcessScoped @BusinessProcessScoped} beans). * * @throws ActivitiCdiException * if no execution is currently associated * @throws ActivitiException * if the activiti command fails */ public void signalExecution() { assertAssociated(); processEngine.getRuntimeService().signal(associationManager.getExecutionId(), associationManager.getBeanStore().getAllAndClear()); associationManager.disAssociate(); } /** * @see #signalExecution() * * In addition, this method allows to end the current conversation */ public void signalExecution(boolean endConversation) { signalExecution(); if(endConversation) { conversationInstance.get().end(); } } // ------------------------------------- /** * Associates the task with the provided taskId with the current conversation. * <p/> * * @param taskId * the id of the task * * @return the resumed task * * @throws ActivitiCdiException * if no such task is found */ public Task startTask(String taskId) { Task currentTask = associationManager.getTask(); if(currentTask != null && currentTask.getId().equals(taskId)) { return currentTask; } Task task = processEngine.getTaskService().createTaskQuery().taskId(taskId).singleResult(); if(task == null) { throw new ActivitiCdiException("Cannot resume task with id '"+taskId+"', no such task."); } associationManager.setTask(task); associateExecutionById(task.getExecutionId()); return task; } /** * @see #startTask(String) * * this method allows to start a conversation if no conversation is active */ public Task startTask(String taskId, boolean beginConversation) { Conversation conversation = conversationInstance.get(); if(conversation.isTransient()) { conversation.begin(); } return startTask(taskId); } /** * Completes the current UserTask, see {@link TaskService#complete(String)} * <p/> * Ends the current unit of work (flushes changes to process variables set * using {@link #setVariable(String, Object)} or made on * {@link BusinessProcessScoped @BusinessProcessScoped} beans). * * @throws ActivitiCdiException * if no task is currently associated * @throws ActivitiException * if the activiti command fails */ public void completeTask() { assertTaskAssociated(); processEngine.getTaskService().complete(getTask().getId(), associationManager.getBeanStore().getAllAndClear()); associationManager.disAssociate(); } /** * @see BusinessProcess#completeTask() * * In addition this allows to end the current conversation. * */ public void completeTask(boolean endConversation) { completeTask(); if(endConversation) { conversationInstance.get().end(); } } public boolean isTaskAssociated() { return associationManager.getTask() != null; } // ------------------------------------------------- /** * @param variableName * the name of the process variable for which the value is to be * retrieved * @return the value of the provided process variable or 'null' if no such * variable is set */ @SuppressWarnings("unchecked") public <T> T getVariable(String variableName) { resumeExecutionFromContext(); Object value = null; if (!isAssociated() || getBeanStore().holdsValue(variableName)) { value = getBeanStore().getContextualInstance(variableName); } else { if(Context.getCommandContext() != null) { value = Context.getExecutionContext().getExecution().getVariable(variableName); }else { value = processEngine.getRuntimeService().getVariable(associationManager.getExecutionId(), variableName); } // cache the value in the bean store. getBeanStore().put(variableName, value); } setVariable(variableName, value); if (value == null) { return null; } else { return (T) value; } } /** * Set a value for a process variable. * <p /> * * <strong>NOTE:</strong> If no execution is currently associated, * the value is temporarily cached and flushed to the process instance * at the end of the unit of work * * @param variableName * the name of the process variable for which a value is to be set * @param value * the value to be set * */ public void setVariable(String variableName, Object value) { resumeExecutionFromContext(); // write through to the execution, if participating in a command: if(Context.getCommandContext() != null) { ExecutionEntity execution = Context.getExecutionContext().getExecution(); if(execution != null) { execution.setVariable(variableName, value); } } getBeanStore().put(variableName, value); } // ----------------------------------- Getters / Setters and Producers /** * @see #startTask(String) */ public void setTask(Task task) { startTask(task.getId()); } /** * @see #startTask(String) */ public void setTaskId(String taskId) { startTask(taskId); } /** * @see #associateExecutionById(String) */ public void setExecution(Execution execution) { associate(execution.getId()); } /** * @see #associateExecutionById(String) */ public void setExecutionId(String executionId) { associate(executionId); } /** * Returns the id of the currently associated process instance or 'null' */ /* Also makes the processId available for injection */ @Produces @Named("processInstanceId") @ProcessInstanceId public String getProcessInstanceId() { ProcessInstance processInstance = getProcessInstance(); if(processInstance != null) { return processInstance.getId(); } return null; } /** * Returns the id of the task associated with the current conversation or 'null'. */ /* Also makes the taskId available for injection */ @Produces @Named("taskId") @TaskId public String getTaskId() { Task task = getTask(); if(task != null) { return task.getId(); } return null; } /** * Returns the currently associated {@link Task} or 'null' * * @throws ActivitiCdiException * if no {@link Task} is associated. Use {@link #isTaskAssociated()} * to check whether an association exists. * */ /* Also makes the current Task available for injection */ @Produces @Named public Task getTask() { return associationManager.getTask(); } /** * Returns the currently associated execution or 'null' * */ /* Also makes the current Execution available for injection */ @Produces @Named public Execution getExecution() { // participate in current command: if(Context.getCommandContext() != null) { ExecutionEntity execution = Context.getExecutionContext().getExecution(); if(execution != null) { return execution; } } if(isAssociated()) { return processEngine.getRuntimeService() .createExecutionQuery() .executionId(associationManager.getExecutionId()) .singleResult(); } return null; } /** * @see #getExecution() */ @Produces @Named public String getExecutionId() { return associationManager.getExecutionId(); } /** * Returns the {@link ProcessInstance} currently associated or 'null' * * @throws ActivitiCdiException * if no {@link Execution} is associated. Use * {@link #isAssociated()} to check whether an association exists. */ /* Also makes the current ProcessInstance available for injection */ @Produces @Named public ProcessInstance getProcessInstance() { // participate in current command: if(Context.getCommandContext() != null) { ExecutionEntity processInstance = Context.getExecutionContext().getProcessInstance(); if(processInstance != null) { return processInstance; } } Execution execution = getExecution(); if(execution != null){ return processEngine .getRuntimeService() .createProcessInstanceQuery() .processInstanceId(execution.getProcessInstanceId()) .singleResult(); } return null; } // internal implementation ////////////////////////////////////////////////////////// protected void assertAssociated() { resumeExecutionFromContext(); if (associationManager.getExecutionId() == null) { throw new ActivitiCdiException("No execution associated. Call busniessProcess.associateExecutionById() or businessProcess.startTask() first."); } } protected void assertTaskAssociated() { if (associationManager.getTask() == null) { throw new ActivitiCdiException("No task associated. Call businessProcess.startTask() first."); } } protected void associate(String executionId) { associationManager.associate(executionId); } protected CachingBeanStore getBeanStore() { return associationManager.getBeanStore(); } /** * if no association exists, we try to resume an execution form the activiti * execution context (possible if we participate in a command) */ protected void resumeExecutionFromContext() { if (associationManager.getExecutionId() != null) { return; } if (Context.getCommandContext() != null) { Execution execution = Context.getExecutionContext().getExecution(); if (execution != null) { associationManager.associate(execution.getId()); } } } }