/* * Copyright (c) 2010-2013 Evolveum * * 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 com.evolveum.midpoint.wf.impl.tasks; import com.evolveum.midpoint.audit.api.AuditEventRecord; import com.evolveum.midpoint.audit.api.AuditEventStage; import com.evolveum.midpoint.audit.api.AuditService; import com.evolveum.midpoint.common.Clock; import com.evolveum.midpoint.prism.PrismContext; import com.evolveum.midpoint.prism.xml.XmlTypeConverter; import com.evolveum.midpoint.schema.constants.SchemaConstants; import com.evolveum.midpoint.schema.result.OperationResult; import com.evolveum.midpoint.schema.util.ObjectTypeUtil; import com.evolveum.midpoint.schema.util.WfContextUtil; import com.evolveum.midpoint.security.api.MidPointPrincipal; import com.evolveum.midpoint.security.api.SecurityUtil; import com.evolveum.midpoint.task.api.Task; import com.evolveum.midpoint.task.api.TaskManager; import com.evolveum.midpoint.util.exception.*; import com.evolveum.midpoint.util.logging.LoggingUtils; import com.evolveum.midpoint.util.logging.Trace; import com.evolveum.midpoint.util.logging.TraceManager; import com.evolveum.midpoint.wf.api.*; import com.evolveum.midpoint.wf.impl.WfConfiguration; import com.evolveum.midpoint.wf.impl.activiti.ActivitiInterface; import com.evolveum.midpoint.wf.impl.messages.*; import com.evolveum.midpoint.wf.impl.processes.ProcessInterfaceFinder; import com.evolveum.midpoint.wf.impl.processes.ProcessMidPointInterface; import com.evolveum.midpoint.wf.impl.processes.common.ActivitiUtil; import com.evolveum.midpoint.wf.impl.processes.common.CommonProcessVariableNames; import com.evolveum.midpoint.wf.impl.processes.common.WfTimedActionTriggerHandler; import com.evolveum.midpoint.wf.impl.processes.itemApproval.MidpointUtil; import com.evolveum.midpoint.wf.impl.processors.ChangeProcessor; import com.evolveum.midpoint.wf.impl.processors.primary.PcpWfTask; import com.evolveum.midpoint.wf.impl.processors.primary.PrimaryChangeProcessor; import com.evolveum.midpoint.wf.impl.util.MiscDataUtil; import com.evolveum.midpoint.wf.util.ApprovalUtils; import com.evolveum.midpoint.xml.ns._public.common.common_3.*; import com.evolveum.prism.xml.ns._public.types_3.ObjectDeltaType; import org.apache.commons.lang.BooleanUtils; import org.apache.commons.lang.Validate; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.xml.datatype.Duration; import java.util.*; import static com.evolveum.midpoint.task.api.TaskExecutionStatus.WAITING; /** * Manages everything related to a activiti process instance, including the task that monitors that process instance. * * This class provides a facade over ugly mess of code managing activiti + task pair describing a workflow process instance. * * @author mederly */ @Component public class WfTaskController { private static final Trace LOGGER = TraceManager.getTrace(WfTaskController.class); public static final long TASK_START_DELAY = 5000L; public static final long COMPLETION_TRIGGER_EQUALITY_THRESHOLD = 10000L; private static final Object DOT_CLASS = WfTaskController.class.getName() + "."; private Set<ProcessListener> processListeners = new HashSet<>(); private Set<WorkItemListener> workItemListeners = new HashSet<>(); @Autowired private WfTaskUtil wfTaskUtil; @Autowired private TaskManager taskManager; @Autowired private ActivitiInterface activitiInterface; @Autowired private AuditService auditService; @Autowired private MiscDataUtil miscDataUtil; @Autowired private ProcessInterfaceFinder processInterfaceFinder; @Autowired private WfConfiguration wfConfiguration; @Autowired private PrismContext prismContext; @Autowired private Clock clock; //region Job creation & re-creation /** * Creates a background task, just as prescribed by the task creation instruction. * @param instruction the job creation instruction * @param parentWfTask the job that will be the parent of newly created one; it may be null * @param wfConfigurationType */ public WfTask submitWfTask(WfTaskCreationInstruction instruction, WfTask parentWfTask, WfConfigurationType wfConfigurationType, OperationResult result) throws SchemaException, ObjectNotFoundException { return submitWfTask(instruction, parentWfTask.getTask(), wfConfigurationType, null, result); } /** * As before, but this time we know only the parent task (not a job). * @param instruction the job creation instruction * @param parentTask the task that will be the parent of the task of newly created job; it may be null * @param wfConfigurationType */ public WfTask submitWfTask(WfTaskCreationInstruction instruction, Task parentTask, WfConfigurationType wfConfigurationType, String channelOverride, OperationResult result) throws SchemaException, ObjectNotFoundException { if (LOGGER.isTraceEnabled()) { LOGGER.trace("Processing start instruction:\n{}", instruction.debugDump()); } Task task = submitTask(instruction, parentTask, wfConfigurationType, channelOverride, result); WfTask wfTask = recreateWfTask(task, instruction.getChangeProcessor()); if (!instruction.isNoProcess()) { startWorkflowProcessInstance(wfTask, instruction, result); } return wfTask; } /** * Re-creates a job, based on existing task information. * * @param task a task from task-processInstance pair * @return recreated job */ public WfTask recreateWfTask(Task task) { return recreateWfTask(task, wfTaskUtil.getChangeProcessor(task)); } public WfTask recreateWfTask(Task task, ChangeProcessor changeProcessor) { String processInstanceId = wfTaskUtil.getProcessId(task); if (changeProcessor instanceof PrimaryChangeProcessor) { return new PcpWfTask(this, task, processInstanceId, changeProcessor); } else { return new WfTask(this, task, processInstanceId, changeProcessor); } } /** * Re-creates a child job, knowing the task and the parent job. * * @param subtask a task from task-processInstance pair * @param parentWfTask the parent job * @return recreated job */ public WfTask recreateChildWfTask(Task subtask, WfTask parentWfTask) { return new WfTask(this, subtask, wfTaskUtil.getProcessId(subtask), parentWfTask.getChangeProcessor()); } /** * Re-creates a root job, based on existing task information. Does not try to find the wf process instance. */ public WfTask recreateRootWfTask(Task task) { return new WfTask(this, task, wfTaskUtil.getChangeProcessor(task)); } //endregion //region Working with midPoint tasks private Task submitTask(WfTaskCreationInstruction instruction, Task parentTask, WfConfigurationType wfConfigurationType, String channelOverride, OperationResult result) throws SchemaException, ObjectNotFoundException { Task wfTask = instruction.createTask(this, parentTask, wfConfigurationType); if (channelOverride != null) { wfTask.setChannel(channelOverride); } if (LOGGER.isTraceEnabled()) { LOGGER.trace("Switching workflow root or child task to background:\n{}", wfTask.debugDump()); } taskManager.switchToBackground(wfTask, result); return wfTask; } /** * Beware, in order to make the change permanent, it is necessary to call commitChanges on * "executesFirst". It is advisable not to modify underlying tasks between 'addDependency' * and 'commitChanges' because of the savePendingModifications() mechanism that is used here. * * @param executesFirst * @param executesSecond */ public void addDependency(WfTask executesFirst, WfTask executesSecond) { Validate.notNull(executesFirst.getTask()); Validate.notNull(executesSecond.getTask()); LOGGER.trace("Setting dependency of {} on 'task0' {}", executesSecond, executesFirst); executesFirst.getTask().addDependent(executesSecond.getTask().getTaskIdentifier()); } public void resumeTask(WfTask wfTask, OperationResult result) throws SchemaException, ObjectNotFoundException { taskManager.resumeTask(wfTask.getTask(), result); } public void unpauseTask(WfTask wfTask, OperationResult result) throws SchemaException, ObjectNotFoundException { taskManager.unpauseTask(wfTask.getTask(), result); } //endregion //region Working with Activiti process instances private void startWorkflowProcessInstance(WfTask wfTask, WfTaskCreationInstruction<?,?> instruction, OperationResult parentResult) { OperationResult result = parentResult.createSubresult(DOT_CLASS + "startWorkflowProcessInstance"); try { LOGGER.trace("startWorkflowProcessInstance starting; instruction = {}", instruction); Task task = wfTask.getTask(); StartProcessCommand spc = new StartProcessCommand(); spc.setProcessName(instruction.getProcessName()); spc.setProcessInstanceName(instruction.getProcessInstanceName()); spc.setSendStartConfirmation(instruction.isSendStartConfirmation()); spc.setVariablesFrom(instruction.getAllProcessVariables()); spc.addVariable(CommonProcessVariableNames.VARIABLE_MIDPOINT_TASK_OID, task.getOid()); spc.setProcessOwner(task.getOwner().getOid()); activitiInterface.startActivitiProcessInstance(spc, task, result); auditProcessStart(wfTask, spc.getVariables(), result); notifyProcessStart(wfTask, result); } catch (SchemaException|RuntimeException|ObjectNotFoundException|ObjectAlreadyExistsException e) { LoggingUtils.logUnexpectedException(LOGGER, "Couldn't send a request to start a process instance to workflow management system", e); result.recordFatalError("Couldn't send a request to start a process instance to workflow management system: " + e.getMessage(), e); throw new SystemException("Workflow process instance creation could not be requested", e); } finally { result.computeStatusIfUnknown(); } LOGGER.trace("startWorkflowProcessInstance finished"); } public void onProcessEvent(ProcessEvent event, Task task, OperationResult result) throws ObjectAlreadyExistsException, ObjectNotFoundException, SchemaException { WfTask wfTask = recreateWfTask(task); LOGGER.trace("Updating instance state and activiti process instance ID in task {}", task); if (wfTask.getProcessInstanceId() == null) { wfTask.setWfProcessId(event.getPid()); } Map<String, Object> variables = event.getVariables(); // update state description ProcessMidPointInterface pmi = processInterfaceFinder.getProcessInterface(variables); wfTask.setProcessInstanceStageInformation(pmi.getStageNumber(variables), pmi.getStageCount(variables), pmi.getStageName(variables), pmi.getStageDisplayName(variables)); wfTask.commitChanges(result); if (event instanceof ProcessFinishedEvent || !event.isRunning()) { onProcessFinishedEvent(event, wfTask, result); } } private void onProcessFinishedEvent(ProcessEvent event, WfTask wfTask, OperationResult result) throws ObjectAlreadyExistsException, ObjectNotFoundException, SchemaException { LOGGER.trace("onProcessFinishedEvent starting"); LOGGER.trace("Calling onProcessEnd on {}", wfTask.getChangeProcessor()); wfTask.getChangeProcessor().onProcessEnd(event, wfTask, result); wfTask.setProcessInstanceEndTimestamp(); wfTask.setOutcome(event.getOutcome()); wfTask.commitChanges(result); auditProcessEnd(wfTask, event, result); notifyProcessEnd(wfTask, result); // passive tasks can be 'let go' at this point if (wfTask.getTaskExecutionStatus() == WAITING) { wfTask.computeTaskResultIfUnknown(result); wfTask.removeCurrentTaskHandlerAndUnpause(result); // removes WfProcessInstanceShadowTaskHandler } LOGGER.trace("onProcessFinishedEvent done"); } private ChangeProcessor getChangeProcessor(Map<String,Object> variables) { String cpName = (String) variables.get(CommonProcessVariableNames.VARIABLE_CHANGE_PROCESSOR); Validate.notNull(cpName, "Change processor is not defined among process instance variables"); return wfConfiguration.findChangeProcessor(cpName); } private ChangeProcessor getChangeProcessor(TaskEvent taskEvent) { return getChangeProcessor(taskEvent.getVariables()); } //endregion //region Processing work item (task) events // workItem contains taskRef, assignee, candidates resolved (if possible) // workItem can be freely modified (e.g. by overriding result, etc.) @SuppressWarnings("unchecked") public void onTaskEvent(WorkItemType workItem, TaskEvent taskEvent, OperationResult result) throws WorkflowException, SchemaException { final TaskType shadowTaskType = WfContextUtil.getTask(workItem); if (shadowTaskType == null) { LOGGER.warn("No task in workItem " + workItem + ", audit and notifications couldn't be performed."); return; } final Task shadowTask = taskManager.createTaskInstance(shadowTaskType.asPrismObject(), result); final WfTask wfTask = recreateWfTask(shadowTask); // auditing & notifications & event if (taskEvent instanceof TaskCreatedEvent) { AuditEventRecord auditEventRecord = getChangeProcessor(taskEvent).prepareWorkItemCreatedAuditRecord(workItem, taskEvent, wfTask, result); auditService.audit(auditEventRecord, wfTask.getTask()); try { notifyWorkItemCreated(workItem.getOriginalAssigneeRef(), workItem, wfTask, result); if (workItem.getAssigneeRef() != null) { WorkItemAllocationChangeOperationInfo operationInfo = new WorkItemAllocationChangeOperationInfo(null, Collections.emptyList(), workItem.getAssigneeRef()); notifyWorkItemAllocationChangeNewActors(workItem, operationInfo, null, wfTask.getTask(), result); } } catch (SchemaException e) { LoggingUtils.logUnexpectedException(LOGGER, "Couldn't send notification about work item create event", e); } } else if (taskEvent instanceof TaskDeletedEvent) { // this might be cancellation because of: // (1) user completion of this task // (2) timed completion of this task // (3) user completion of another task // (4) timed completion of another task // (5) process stop/deletion // // Actually, when the source is (4) timed completion of another task, it is quite probable that this task // would be closed for the same reason. For a user it would be misleading if we would simply view this task // as 'cancelled', while, in fact, it is e.g. approved/rejected because of a timed action. WorkItemOperationKindType operationKind = BooleanUtils.isTrue(ActivitiUtil.getVariable(taskEvent.getVariables(), CommonProcessVariableNames.VARIABLE_WORK_ITEM_WAS_COMPLETED, Boolean.class, prismContext)) ? WorkItemOperationKindType.COMPLETE : WorkItemOperationKindType.CANCEL; WorkItemEventCauseInformationType cause = ActivitiUtil.getVariable(taskEvent.getVariables(), CommonProcessVariableNames.VARIABLE_CAUSE, WorkItemEventCauseInformationType.class, prismContext); boolean genuinelyCompleted = operationKind == WorkItemOperationKindType.COMPLETE; MidPointPrincipal user; try { user = SecurityUtil.getPrincipal(); } catch (SecurityViolationException e) { throw new SystemException("Couldn't determine current user: " + e.getMessage(), e); } ObjectReferenceType userRef = user != null ? user.toObjectReference() : workItem.getPerformerRef(); // partial fallback if (!genuinelyCompleted) { TaskType task = wfTask.getTask().getTaskPrismObject().asObjectable(); int foundTimedActions = 0; for (TriggerType trigger : task.getTrigger()) { if (!WfTimedActionTriggerHandler.HANDLER_URI.equals(trigger.getHandlerUri())) { continue; } String workItemId = ObjectTypeUtil.getExtensionItemRealValue(trigger.getExtension(), SchemaConstants.MODEL_EXTENSION_WORK_ITEM_ID); if (!taskEvent.getTaskId().equals(workItemId)) { continue; } Duration timeBeforeAction = ObjectTypeUtil.getExtensionItemRealValue(trigger.getExtension(), SchemaConstants.MODEL_EXTENSION_TIME_BEFORE_ACTION); if (timeBeforeAction != null) { continue; } WorkItemActionsType actions = ObjectTypeUtil.getExtensionItemRealValue(trigger.getExtension(), SchemaConstants.MODEL_EXTENSION_WORK_ITEM_ACTIONS); if (actions == null || actions.getComplete() == null) { continue; } long diff = XmlTypeConverter.toMillis(trigger.getTimestamp()) - clock.currentTimeMillis(); if (diff >= COMPLETION_TRIGGER_EQUALITY_THRESHOLD) { continue; } CompleteWorkItemActionType completeAction = actions.getComplete(); operationKind = WorkItemOperationKindType.COMPLETE; cause = new WorkItemEventCauseInformationType(); cause.setType(WorkItemEventCauseTypeType.TIMED_ACTION); cause.setName(completeAction.getName()); cause.setDisplayName(completeAction.getDisplayName()); foundTimedActions++; WorkItemResultType workItemOutput = new WorkItemResultType(); workItemOutput.setOutcome(completeAction.getOutcome() != null ? completeAction.getOutcome() : SchemaConstants.MODEL_APPROVAL_OUTCOME_REJECT); workItem.setOutput(workItemOutput); } if (foundTimedActions > 1) { LOGGER.warn("Multiple 'work item complete' timed actions ({}) for {}: {}", foundTimedActions, ObjectTypeUtil.toShortString(task), task.getTrigger()); } } // We don't pass userRef (initiator) to the audit method. It does need the whole object (not only the reference), // so it fetches it directly from the security enforcer (logged-in user). This could change in the future. AuditEventRecord auditEventRecord = getChangeProcessor(taskEvent) .prepareWorkItemDeletedAuditRecord(workItem, cause, taskEvent, wfTask, result); auditService.audit(auditEventRecord, wfTask.getTask()); try { WorkItemAllocationChangeOperationInfo operationInfo = new WorkItemAllocationChangeOperationInfo(operationKind, workItem.getAssigneeRef(), null); WorkItemOperationSourceInfo sourceInfo = new WorkItemOperationSourceInfo(userRef, cause, null); if (workItem.getAssigneeRef().isEmpty()) { notifyWorkItemDeleted(null, workItem, operationInfo, sourceInfo, wfTask, result); } else { for (ObjectReferenceType assignee : workItem.getAssigneeRef()) { notifyWorkItemDeleted(assignee, workItem, operationInfo, sourceInfo, wfTask, result); } } notifyWorkItemAllocationChangeCurrentActors(workItem, operationInfo, sourceInfo, null, wfTask.getTask(), result); } catch (SchemaException e) { LoggingUtils.logUnexpectedException(LOGGER, "Couldn't audit work item complete event", e); } AbstractWorkItemOutputType output = workItem.getOutput(); if (genuinelyCompleted || output != null) { WorkItemCompletionEventType event = new WorkItemCompletionEventType(); ActivitiUtil.fillInWorkItemEvent(event, user, taskEvent.getTaskId(), taskEvent.getVariables(), prismContext); event.setCause(cause); event.setOutput(output); ObjectDeltaType additionalDelta = output instanceof WorkItemResultType && ((WorkItemResultType) output).getAdditionalDeltas() != null ? ((WorkItemResultType) output).getAdditionalDeltas().getFocusPrimaryDelta() : null; MidpointUtil.recordEventInTask(event, additionalDelta, wfTask.getTask().getOid(), result); } MidpointUtil.removeTriggersForWorkItem(wfTask.getTask(), taskEvent.getTaskId(), result); } } //endregion //region Auditing and notifications private void auditProcessStart(WfTask wfTask, Map<String, Object> variables, OperationResult result) { auditProcessStartEnd(wfTask, AuditEventStage.REQUEST, variables, result); } private void auditProcessEnd(WfTask wfTask, ProcessEvent event, OperationResult result) { auditProcessStartEnd(wfTask, AuditEventStage.EXECUTION, event.getVariables(), result); } private void auditProcessStartEnd(WfTask wfTask, AuditEventStage stage, Map<String, Object> variables, OperationResult result) { AuditEventRecord auditEventRecord = wfTask.getChangeProcessor().prepareProcessInstanceAuditRecord(wfTask, stage, variables, result); auditService.audit(auditEventRecord, wfTask.getTask()); } private void notifyProcessStart(WfTask wfTask, OperationResult result) throws SchemaException { for (ProcessListener processListener : processListeners) { processListener.onProcessInstanceStart(wfTask.getTask(), result); } } private void notifyProcessEnd(WfTask wfTask, OperationResult result) throws SchemaException { for (ProcessListener processListener : processListeners) { processListener.onProcessInstanceEnd(wfTask.getTask(), result); } } private void notifyWorkItemCreated(ObjectReferenceType originalAssigneeRef, WorkItemType workItem, WfTask wfTask, OperationResult result) throws SchemaException { for (WorkItemListener workItemListener : workItemListeners) { workItemListener.onWorkItemCreation(originalAssigneeRef, workItem, wfTask.getTask(), result); } } private void notifyWorkItemDeleted(ObjectReferenceType assignee, WorkItemType workItem, WorkItemOperationInfo operationInfo, WorkItemOperationSourceInfo sourceInfo, WfTask wfTask, OperationResult result) throws SchemaException { for (WorkItemListener workItemListener : workItemListeners) { workItemListener.onWorkItemDeletion(assignee, workItem, operationInfo, sourceInfo, wfTask.getTask(), result); } } public void notifyWorkItemAllocationChangeCurrentActors(WorkItemType workItem, @NotNull WorkItemAllocationChangeOperationInfo operationInfo, WorkItemOperationSourceInfo sourceInfo, Duration timeBefore, Task wfTask, OperationResult result) throws SchemaException { for (WorkItemListener workItemListener : workItemListeners) { workItemListener.onWorkItemAllocationChangeCurrentActors(workItem, operationInfo, sourceInfo, timeBefore, wfTask, result); } } public void notifyWorkItemAllocationChangeNewActors(WorkItemType workItem, @NotNull WorkItemAllocationChangeOperationInfo operationInfo, @Nullable WorkItemOperationSourceInfo sourceInfo, Task wfTask, OperationResult result) throws SchemaException { for (WorkItemListener workItemListener : workItemListeners) { workItemListener.onWorkItemAllocationChangeNewActors(workItem, operationInfo, sourceInfo, wfTask, result); } } public void notifyWorkItemCustom(@Nullable ObjectReferenceType assignee, WorkItemType workItem, WorkItemEventCauseInformationType cause, Task wfTask, @NotNull WorkItemNotificationActionType notificationAction, OperationResult result) throws SchemaException { for (WorkItemListener workItemListener : workItemListeners) { workItemListener.onWorkItemCustomEvent(assignee, workItem, notificationAction, cause, wfTask, result); } } public void registerProcessListener(ProcessListener processListener) { LOGGER.trace("Registering process listener {}", processListener); processListeners.add(processListener); } public void registerWorkItemListener(WorkItemListener workItemListener) { LOGGER.trace("Registering work item listener {}", workItemListener); workItemListeners.add(workItemListener); } //endregion //region Getters and setters public WfTaskUtil getWfTaskUtil() { return wfTaskUtil; } public MiscDataUtil getMiscDataUtil() { return miscDataUtil; } public PrismContext getPrismContext() { return prismContext; } public TaskManager getTaskManager() { return taskManager; } public WfConfiguration getWfConfiguration() { return wfConfiguration; } //endregion }