/*
* 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.processors.primary;
import com.evolveum.midpoint.audit.api.AuditEventRecord;
import com.evolveum.midpoint.audit.api.AuditEventStage;
import com.evolveum.midpoint.model.api.context.ModelContext;
import com.evolveum.midpoint.model.api.context.ModelProjectionContext;
import com.evolveum.midpoint.model.api.hooks.HookOperationMode;
import com.evolveum.midpoint.model.impl.lens.LensContext;
import com.evolveum.midpoint.model.impl.lens.LensProjectionContext;
import com.evolveum.midpoint.prism.delta.ObjectDelta;
import com.evolveum.midpoint.schema.ObjectDeltaOperation;
import com.evolveum.midpoint.schema.ObjectTreeDeltas;
import com.evolveum.midpoint.schema.ResourceShadowDiscriminator;
import com.evolveum.midpoint.schema.result.OperationResult;
import com.evolveum.midpoint.task.api.Task;
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.WorkflowException;
import com.evolveum.midpoint.wf.impl.messages.ProcessEvent;
import com.evolveum.midpoint.wf.impl.messages.TaskEvent;
import com.evolveum.midpoint.wf.impl.processes.ProcessInterfaceFinder;
import com.evolveum.midpoint.wf.impl.processors.BaseAuditHelper;
import com.evolveum.midpoint.wf.impl.processors.BaseChangeProcessor;
import com.evolveum.midpoint.wf.impl.processors.BaseConfigurationHelper;
import com.evolveum.midpoint.wf.impl.processors.BaseModelInvocationProcessingHelper;
import com.evolveum.midpoint.wf.impl.processors.primary.aspect.PrimaryChangeAspect;
import com.evolveum.midpoint.wf.impl.tasks.WfTask;
import com.evolveum.midpoint.wf.impl.tasks.WfTaskController;
import com.evolveum.midpoint.wf.impl.tasks.WfTaskCreationInstruction;
import com.evolveum.midpoint.wf.impl.tasks.WfTaskUtil;
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 org.apache.commons.lang.Validate;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.*;
import java.util.stream.Collectors;
import static com.evolveum.midpoint.audit.api.AuditEventStage.REQUEST;
import static com.evolveum.midpoint.model.api.context.ModelState.PRIMARY;
import static com.evolveum.midpoint.wf.impl.processors.primary.PrimaryChangeProcessor.ExecutionMode.*;
/**
* @author mederly
*/
@Component
public class PrimaryChangeProcessor extends BaseChangeProcessor {
private static final Trace LOGGER = TraceManager.getTrace(PrimaryChangeProcessor.class);
@Autowired
private PcpConfigurationHelper pcpConfigurationHelper;
@Autowired
private BaseConfigurationHelper baseConfigurationHelper;
@Autowired
private BaseModelInvocationProcessingHelper baseModelInvocationProcessingHelper;
@Autowired
private BaseAuditHelper baseAuditHelper;
@Autowired
private WfTaskUtil wfTaskUtil;
@Autowired
private WfTaskController wfTaskController;
@Autowired
private PcpRepoAccessHelper pcpRepoAccessHelper;
@Autowired
private ProcessInterfaceFinder processInterfaceFinder;
@Autowired
private MiscDataUtil miscDataUtil;
public static final String UNKNOWN_OID = "?";
private List<PrimaryChangeAspect> allChangeAspects = new ArrayList<>();
public enum ExecutionMode {
ALL_AFTERWARDS, ALL_IMMEDIATELY, MIXED;
}
//region Configuration
// =================================================================================== Configuration
@PostConstruct
public void init() {
baseConfigurationHelper.registerProcessor(this);
}
//endregion
//region Processing model invocation
// =================================================================================== Processing model invocation
@Override
public HookOperationMode processModelInvocation(@NotNull ModelContext<?> context, WfConfigurationType wfConfigurationType,
@NotNull Task taskFromModel, @NotNull OperationResult result)
throws SchemaException, ObjectNotFoundException {
if (context.getState() != PRIMARY || context.getFocusContext() == null) {
return null;
}
PrimaryChangeProcessorConfigurationType processorConfigurationType =
wfConfigurationType != null ? wfConfigurationType.getPrimaryChangeProcessor() : null;
if (processorConfigurationType != null && Boolean.FALSE.equals(processorConfigurationType.isEnabled())) {
LOGGER.debug("Primary change processor is disabled.");
return null;
}
ObjectTreeDeltas objectTreeDeltas = baseModelInvocationProcessingHelper.extractTreeDeltasFromModelContext(context);
if (objectTreeDeltas.isEmpty()) {
return null;
}
// examine the request using process aspects
ObjectTreeDeltas changesBeingDecomposed = objectTreeDeltas.clone();
ModelInvocationContext ctx = new ModelInvocationContext(getPrismContext(), context, wfConfigurationType, taskFromModel);
List<PcpChildWfTaskCreationInstruction> childTaskInstructions = gatherStartInstructions(changesBeingDecomposed, ctx, result);
// start the process(es)
if (childTaskInstructions.isEmpty()) {
LOGGER.trace("There are no workflow processes to be started, exiting.");
return null;
}
return submitTasks(childTaskInstructions, context, changesBeingDecomposed, taskFromModel, wfConfigurationType, result);
}
private List<PcpChildWfTaskCreationInstruction> gatherStartInstructions(@NotNull ObjectTreeDeltas changesBeingDecomposed,
ModelInvocationContext ctx, @NotNull OperationResult result) throws SchemaException, ObjectNotFoundException {
PrimaryChangeProcessorConfigurationType processorConfigurationType =
ctx.wfConfiguration != null ? ctx.wfConfiguration.getPrimaryChangeProcessor() : null;
List<PcpChildWfTaskCreationInstruction> startProcessInstructions = new ArrayList<>();
for (PrimaryChangeAspect aspect : getActiveChangeAspects(processorConfigurationType)) {
if (changesBeingDecomposed.isEmpty()) { // nothing left
break;
}
List<PcpChildWfTaskCreationInstruction> instructions = aspect.prepareTasks(changesBeingDecomposed, ctx, result);
logAspectResult(aspect, instructions, changesBeingDecomposed);
if (instructions != null) {
startProcessInstructions.addAll(instructions);
}
}
return startProcessInstructions;
}
private Collection<PrimaryChangeAspect> getActiveChangeAspects(PrimaryChangeProcessorConfigurationType processorConfigurationType) {
Collection<PrimaryChangeAspect> rv = getAllChangeAspects().stream()
.filter(aspect -> aspect.isEnabled(processorConfigurationType)).collect(Collectors.toList());
return rv;
}
private void logAspectResult(PrimaryChangeAspect aspect, List<? extends WfTaskCreationInstruction> instructions, ObjectTreeDeltas changesBeingDecomposed) {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("\n---[ Aspect {} returned the following process start instructions (count: {}) ]---", aspect.getClass(), instructions == null ? "(null)" : instructions.size());
if (instructions != null) {
for (WfTaskCreationInstruction instruction : instructions) {
LOGGER.trace(instruction.debugDump(0));
}
LOGGER.trace("Remaining delta(s):\n{}", changesBeingDecomposed.debugDump());
}
}
}
private HookOperationMode submitTasks(List<PcpChildWfTaskCreationInstruction> instructions, final ModelContext context,
final ObjectTreeDeltas changesWithoutApproval, Task taskFromModel, WfConfigurationType wfConfigurationType, OperationResult result) {
try {
ExecutionMode executionMode = determineExecutionMode(instructions);
// prepare root task and task0
WfTask rootWfTask = submitRootTask(context, changesWithoutApproval, taskFromModel, executionMode, wfConfigurationType, result);
WfTask wfTask0 = submitTask0(context, changesWithoutApproval, rootWfTask, executionMode, wfConfigurationType, result);
// start the jobs
List<WfTask> wfTasks = new ArrayList<>(instructions.size());
for (PcpChildWfTaskCreationInstruction instruction : instructions) {
if (instruction.startsWorkflowProcess() && instruction.isExecuteApprovedChangeImmediately()) {
// if we want to execute approved changes immediately in this instruction, we have to wait for
// task0 (if there is any) and then to update our model context with the results (if there are any)
// TODO CONSIDER THIS... when OID is no longer transferred
instruction.addHandlersAfterWfProcessAtEnd(WfTaskUtil.WAIT_FOR_TASKS_HANDLER_URI, WfPrepareChildOperationTaskHandler.HANDLER_URI);
}
WfTask wfTask = wfTaskController.submitWfTask(instruction, rootWfTask.getTask(), wfConfigurationType, null, result);
wfTasks.add(wfTask);
}
// all jobs depend on job0 (if there is one)
if (wfTask0 != null) {
for (WfTask wfTask : wfTasks) {
wfTask0.addDependent(wfTask);
}
wfTask0.commitChanges(result);
}
baseModelInvocationProcessingHelper.logJobsBeforeStart(rootWfTask, result);
rootWfTask.startWaitingForSubtasks(result);
return HookOperationMode.BACKGROUND;
} catch (SchemaException|ObjectNotFoundException|ObjectAlreadyExistsException|CommunicationException|ConfigurationException|RuntimeException e) {
LoggingUtils.logUnexpectedException(LOGGER, "Workflow process(es) could not be started", e);
result.recordFatalError("Workflow process(es) could not be started: " + e, e);
return HookOperationMode.ERROR;
// todo rollback - at least close open tasks, maybe stop workflow process instances
}
}
private WfTask submitRootTask(ModelContext context, ObjectTreeDeltas changesWithoutApproval, Task taskFromModel, ExecutionMode executionMode,
WfConfigurationType wfConfigurationType, OperationResult result)
throws SchemaException, ObjectNotFoundException, ObjectAlreadyExistsException {
LensContext lensContextForRootTask = determineLensContextForRootTask(context, changesWithoutApproval, executionMode);
WfTaskCreationInstruction instructionForRoot = baseModelInvocationProcessingHelper.createInstructionForRoot(this, context, taskFromModel, lensContextForRootTask, result);
if (executionMode != ALL_IMMEDIATELY) {
instructionForRoot.setHandlersBeforeModelOperation(WfPrepareRootOperationTaskHandler.HANDLER_URI); // gather all deltas from child objects
}
return baseModelInvocationProcessingHelper.submitRootTask(instructionForRoot, taskFromModel, wfConfigurationType, result);
}
// Child task0 - in modes 2, 3 we have to prepare first child that executes all changes that do not require approval
private WfTask submitTask0(ModelContext context, ObjectTreeDeltas changesWithoutApproval, WfTask rootWfTask, ExecutionMode executionMode,
WfConfigurationType wfConfigurationType, OperationResult result) throws SchemaException, ObjectNotFoundException {
if (changesWithoutApproval != null && !changesWithoutApproval.isEmpty() && executionMode != ALL_AFTERWARDS) {
ModelContext task0context = contextCopyWithDeltasReplaced(context, changesWithoutApproval);
WfTaskCreationInstruction instruction0 = WfTaskCreationInstruction.createModelOnly(rootWfTask.getChangeProcessor(), task0context);
instruction0.setTaskName("Executing changes that do not require approval");
return wfTaskController.submitWfTask(instruction0, rootWfTask, wfConfigurationType, result);
} else {
return null;
}
}
private LensContext determineLensContextForRootTask(ModelContext context, ObjectTreeDeltas changesWithoutApproval, ExecutionMode executionMode) throws SchemaException {
LensContext contextForRootTask;
if (executionMode == ALL_AFTERWARDS) {
contextForRootTask = contextCopyWithDeltasReplaced(context, changesWithoutApproval);
} else if (executionMode == MIXED) {
contextForRootTask = contextCopyWithNoDelta(context);
} else {
contextForRootTask = null;
}
return contextForRootTask;
}
private LensContext contextCopyWithDeltasReplaced(ModelContext context, ObjectTreeDeltas changes) throws SchemaException {
Validate.notNull(changes, "changes");
LensContext contextCopy = ((LensContext) context).clone();
contextCopy.replacePrimaryFocusDelta(changes.getFocusChange());
Map<ResourceShadowDiscriminator, ObjectDelta<ShadowType>> changeMap = changes.getProjectionChangeMap();
Collection<ModelProjectionContext> projectionContexts = contextCopy.getProjectionContexts();
for (ModelProjectionContext projectionContext : projectionContexts) {
ObjectDelta<ShadowType> projectionDelta = changeMap.get(projectionContext.getResourceShadowDiscriminator());
projectionContext.setPrimaryDelta(projectionDelta);
}
return contextCopy;
}
public LensContext contextCopyWithNoDelta(ModelContext context) {
LensContext contextCopy = ((LensContext) context).clone();
contextCopy.replacePrimaryFocusDelta(null);
Collection<LensProjectionContext> projectionContexts = contextCopy.getProjectionContexts();
for (ModelProjectionContext projectionContext : projectionContexts) {
projectionContext.setPrimaryDelta(null);
}
return contextCopy;
}
private ExecutionMode determineExecutionMode(List<PcpChildWfTaskCreationInstruction> instructions) {
ExecutionMode executionMode;
if (shouldAllExecuteImmediately(instructions)) {
executionMode = ALL_IMMEDIATELY;
} else if (shouldAllExecuteAfterwards(instructions)) {
executionMode = ALL_AFTERWARDS;
} else {
executionMode = MIXED;
}
return executionMode;
}
private boolean shouldAllExecuteImmediately(List<PcpChildWfTaskCreationInstruction> startProcessInstructions) {
for (PcpChildWfTaskCreationInstruction instruction : startProcessInstructions) {
if (!instruction.isExecuteApprovedChangeImmediately()) {
return false;
}
}
return true;
}
private boolean shouldAllExecuteAfterwards(List<PcpChildWfTaskCreationInstruction> startProcessInstructions) {
for (PcpChildWfTaskCreationInstruction instruction : startProcessInstructions) {
if (instruction.isExecuteApprovedChangeImmediately()) {
return false;
}
}
return true;
}
//endregion
//region Processing process finish event
@Override
public void onProcessEnd(ProcessEvent event, WfTask wfTask, OperationResult result) throws SchemaException, ObjectAlreadyExistsException, ObjectNotFoundException {
PcpWfTask pcpJob = new PcpWfTask(wfTask);
PrimaryChangeAspect aspect = pcpJob.getChangeAspect();
pcpJob.storeResultingDeltas(aspect.prepareDeltaOut(event, pcpJob, result));
}
//endregion
//region Auditing
@Override
public AuditEventRecord prepareProcessInstanceAuditRecord(WfTask wfTask, AuditEventStage stage, Map<String, Object> variables, OperationResult result) {
AuditEventRecord auditEventRecord = baseAuditHelper.prepareProcessInstanceAuditRecord(wfTask, stage, result);
ObjectTreeDeltas<?> deltas;
try {
if (stage == REQUEST) {
deltas = wfTaskUtil.retrieveDeltasToProcess(wfTask.getTask());
} else {
deltas = wfTaskUtil.retrieveResultingDeltas(wfTask.getTask());
}
} catch (SchemaException e) {
throw new SystemException("Couldn't retrieve delta(s) from task " + wfTask.getTask(), e);
}
if (deltas != null) {
List<ObjectDelta<?>> deltaList = deltas.getDeltaList();
for (ObjectDelta delta : deltaList) {
auditEventRecord.addDelta(new ObjectDeltaOperation(delta));
}
}
return auditEventRecord;
}
@Override
public AuditEventRecord prepareWorkItemCreatedAuditRecord(WorkItemType workItem, TaskEvent taskEvent, WfTask wfTask,
OperationResult result) throws WorkflowException {
AuditEventRecord auditEventRecord = baseAuditHelper.prepareWorkItemCreatedAuditRecord(workItem, wfTask, result);
try {
addDeltasToEventRecord(auditEventRecord,
getWfTaskUtil().retrieveDeltasToProcess(wfTask.getTask()));
} catch (SchemaException e) {
LoggingUtils.logUnexpectedException(LOGGER, "Couldn't retrieve deltas to be put into audit record", e);
}
return auditEventRecord;
}
@Override
public AuditEventRecord prepareWorkItemDeletedAuditRecord(WorkItemType workItem, WorkItemEventCauseInformationType cause,
TaskEvent taskEvent, WfTask wfTask, OperationResult result) throws WorkflowException {
AuditEventRecord auditEventRecord = baseAuditHelper.prepareWorkItemDeletedAuditRecord(workItem, cause,
wfTask, result);
try {
AbstractWorkItemOutputType output = workItem.getOutput();
// TODO - or merge with original deltas?
if (output != null && ApprovalUtils.fromUri(output.getOutcome()) == WorkItemOutcomeType.APPROVE
&& output instanceof WorkItemResultType
&& ((WorkItemResultType) output).getAdditionalDeltas() != null) {
addDeltasToEventRecord(auditEventRecord,
ObjectTreeDeltas.fromObjectTreeDeltasType(((WorkItemResultType) output).getAdditionalDeltas(), getPrismContext()));
}
} catch (SchemaException e) {
LoggingUtils.logUnexpectedException(LOGGER, "Couldn't retrieve deltas to be put into audit record", e);
}
return auditEventRecord;
}
private void addDeltasToEventRecord(AuditEventRecord auditEventRecord, ObjectTreeDeltas<?> deltas) {
if (deltas != null) {
for (ObjectDelta<?> delta : deltas.getDeltaList()) {
auditEventRecord.addDelta(new ObjectDeltaOperation(delta));
}
}
}
//endregion
//region Getters and setters
public Collection<PrimaryChangeAspect> getAllChangeAspects() {
return allChangeAspects;
}
PrimaryChangeAspect getChangeAspect(Map<String, Object> variables) {
String aspectClassName = (String) variables.get(PcpProcessVariableNames.VARIABLE_CHANGE_ASPECT);
return findPrimaryChangeAspect(aspectClassName);
}
public PrimaryChangeAspect findPrimaryChangeAspect(String name) {
// we can search either by bean name or by aspect class name (experience will show what is the better way)
if (getBeanFactory().containsBean(name)) {
return getBeanFactory().getBean(name, PrimaryChangeAspect.class);
}
for (PrimaryChangeAspect w : allChangeAspects) {
if (name.equals(w.getClass().getName())) {
return w;
}
}
throw new IllegalStateException("Aspect " + name + " is not registered.");
}
public void registerChangeAspect(PrimaryChangeAspect changeAspect, boolean first) {
LOGGER.trace("Registering aspect implemented by {}; first={}", changeAspect.getClass(), first);
if (first) {
allChangeAspects.add(0, changeAspect);
} else {
allChangeAspects.add(changeAspect);
}
}
WfTaskUtil getWfTaskUtil() { // ugly hack - used in PcpJob
return wfTaskUtil;
}
//endregion
}