/* * Copyright (c) 2010-2017 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.model.impl.lens.projector; import static com.evolveum.midpoint.model.api.ProgressInformation.ActivityType.PROJECTOR; import static com.evolveum.midpoint.model.api.ProgressInformation.StateType.ENTERING; import static com.evolveum.midpoint.schema.internals.InternalsConfig.consistencyChecks; import java.util.List; import javax.xml.datatype.XMLGregorianCalendar; import com.evolveum.midpoint.model.api.ProgressInformation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.evolveum.midpoint.common.Clock; import com.evolveum.midpoint.model.api.context.SynchronizationPolicyDecision; import com.evolveum.midpoint.model.impl.lens.LensContext; import com.evolveum.midpoint.model.impl.lens.LensProjectionContext; import com.evolveum.midpoint.model.impl.lens.LensUtil; import com.evolveum.midpoint.model.impl.lens.projector.credentials.ProjectionCredentialsProcessor; import com.evolveum.midpoint.prism.xml.XmlTypeConverter; import com.evolveum.midpoint.schema.ResourceShadowDiscriminator; import com.evolveum.midpoint.schema.result.OperationResult; import com.evolveum.midpoint.schema.result.OperationResultStatus; import com.evolveum.midpoint.schema.util.ExceptionUtil; import com.evolveum.midpoint.task.api.Task; import com.evolveum.midpoint.util.exception.CommunicationException; import com.evolveum.midpoint.util.exception.ConfigurationException; import com.evolveum.midpoint.util.exception.ExpressionEvaluationException; import com.evolveum.midpoint.util.exception.ObjectAlreadyExistsException; import com.evolveum.midpoint.util.exception.ObjectNotFoundException; import com.evolveum.midpoint.util.exception.PolicyViolationException; import com.evolveum.midpoint.util.exception.SchemaException; import com.evolveum.midpoint.util.exception.SecurityViolationException; import com.evolveum.midpoint.util.logging.Trace; import com.evolveum.midpoint.util.logging.TraceManager; import com.evolveum.midpoint.xml.ns._public.common.common_3.ErrorSelectorType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectType; import com.evolveum.midpoint.xml.ns._public.common.common_3.PartialProcessingOptionsType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ResourceType; /** * Projector recomputes the context. It takes the context with a few basic data as input. It uses all the policies * and mappings to derive all the other data. E.g. a context with only a user (primary) delta. It applies user template, * outbound mappings and the inbound mappings and then inbound and outbound mappings of other accounts and so on until * all the data are computed. The output is the original context with all the computed delta. * * Primary deltas are in the input, secondary deltas are computed in projector. Projector "projects" primary deltas to * the secondary deltas of user and accounts. * * Projector does NOT execute the deltas. It only recomputes the context. It may read a lot of objects (user, accounts, policies). * But it does not change any of them. * * @author Radovan Semancik * */ @Component public class Projector { private static final String OPERATION_PROJECT_PROJECTION = Projector.class.getName() + ".projectProjection"; @Autowired private ContextLoader contextLoader; @Autowired private FocusProcessor focusProcessor; @Autowired private AssignmentProcessor assignmentProcessor; @Autowired private ProjectionValuesProcessor projectionValuesProcessor; @Autowired private ReconciliationProcessor reconciliationProcessor; @Autowired private ProjectionCredentialsProcessor projectionCredentialsProcessor; @Autowired private ActivationProcessor activationProcessor; @Autowired private DependencyProcessor dependencyProcessor; @Autowired private Clock clock; private static final Trace LOGGER = TraceManager.getTrace(Projector.class); /** * Runs one projection wave, starting at current execution wave. */ public <F extends ObjectType> void project(LensContext<F> context, String activityDescription, Task task, OperationResult parentResult) throws SchemaException, PolicyViolationException, ExpressionEvaluationException, ObjectNotFoundException, ObjectAlreadyExistsException, CommunicationException, ConfigurationException, SecurityViolationException { projectInternal(context, activityDescription, true, false, task, parentResult); } /** * Resumes projection at current projection wave. */ public <F extends ObjectType> void resume(LensContext<F> context, String activityDescription, Task task, OperationResult parentResult) throws SchemaException, PolicyViolationException, ExpressionEvaluationException, ObjectNotFoundException, ObjectAlreadyExistsException, CommunicationException, ConfigurationException, SecurityViolationException { if (context.getProjectionWave() != context.getExecutionWave()) { throw new IllegalStateException("Projector.resume called in illegal wave state: execution wave = " + context.getExecutionWave() + ", projection wave = " + context.getProjectionWave()); } if (!context.isFresh()) { throw new IllegalStateException("Projector.resume called on non-fresh context"); } projectInternal(context, activityDescription, false, false, task, parentResult); } /** * Executes projector from current execution wave to the last computed wave. * Useful for change preview. */ public <F extends ObjectType> void projectAllWaves(LensContext<F> context, String activityDescription, Task task, OperationResult parentResult) throws SchemaException, PolicyViolationException, ExpressionEvaluationException, ObjectNotFoundException, ObjectAlreadyExistsException, CommunicationException, ConfigurationException, SecurityViolationException { projectInternal(context, activityDescription, true, true, task, parentResult); } private <F extends ObjectType> void projectInternal(LensContext<F> context, String activityDescription, boolean fromStart, boolean allWaves, Task task, OperationResult parentResult) throws SchemaException, PolicyViolationException, ExpressionEvaluationException, ObjectNotFoundException, ObjectAlreadyExistsException, CommunicationException, ConfigurationException, SecurityViolationException { context.checkAbortRequested(); if (context.getDebugListener() != null) { context.getDebugListener().beforeProjection(context); } // Read the time at the beginning so all processors have the same notion of "now" // this provides nicer unified timestamp that can be used in equality checks in tests and also for // troubleshooting XMLGregorianCalendar now = clock.currentTimeXMLGregorianCalendar(); String traceTitle = fromStart ? "projector start" : "projector resume"; LensUtil.traceContext(LOGGER, activityDescription, traceTitle, false, context, false); if (consistencyChecks) context.checkConsistence(); if (fromStart) { context.normalize(); context.resetProjectionWave(); } OperationResult result = parentResult.createSubresult(Projector.class.getName() + ".project"); result.addParam("fromStart", fromStart); result.addContext("projectionWave", context.getProjectionWave()); result.addContext("executionWave", context.getExecutionWave()); PartialProcessingOptionsType partialProcessingOptions = context.getPartialProcessingOptions(); // Projector is using a set of "processors" to do parts of its work. The processors will be called in sequence // in the following code. try { context.reportProgress(new ProgressInformation(PROJECTOR, ENTERING)); if (fromStart) { LensUtil.partialExecute("load", () -> { contextLoader.load(context, activityDescription, task, result); // Set the "fresh" mark now so following consistency check will be stricter context.setFresh(true); if (consistencyChecks) context.checkConsistence(); }, partialProcessingOptions::getLoad, result); } // For now let's pretend to do just one wave. The maxWaves number will be corrected in the // first wave when dependencies are sorted out for the first time. int maxWaves = context.getExecutionWave() + 1; // Start the waves .... LOGGER.trace("WAVE: Starting the waves."); boolean firstWave = true; while ((allWaves && context.getProjectionWave() < maxWaves) || (!allWaves && context.getProjectionWave() <= context.getExecutionWave())) { boolean inFirstWave = firstWave; firstWave = false; // in order to not forget to reset it ;) context.checkAbortRequested(); LOGGER.trace("WAVE {} (maxWaves={}, executionWave={})", context.getProjectionWave(), maxWaves, context.getExecutionWave()); //just make sure everything is loaded and set as needed dependencyProcessor.preprocessDependencies(context); // Process the focus-related aspects of the context. That means inbound, focus activation, // object template and assignments. LensUtil.partialExecute("focus", () -> { focusProcessor.processFocus(context, activityDescription, now, task, result); context.recomputeFocus(); if (consistencyChecks) context.checkConsistence(); }, partialProcessingOptions::getFocus, result); LensUtil.traceContext(LOGGER, activityDescription, "focus processing", false, context, false); LensUtil.checkContextSanity(context, "focus processing", result); // Process activation of all resources, regardless of the waves. This is needed to properly // sort projections to waves as deprovisioning will reverse the dependencies. And we know whether // a projection is provisioned or deprovisioned only after the activation is processed. if (fromStart && inFirstWave) { LOGGER.trace("Processing activation for all contexts"); for (LensProjectionContext projectionContext: context.getProjectionContexts()) { if (projectionContext.getSynchronizationPolicyDecision() == SynchronizationPolicyDecision.BROKEN || projectionContext.getSynchronizationPolicyDecision() == SynchronizationPolicyDecision.IGNORE) { continue; } activationProcessor.processActivation(context, projectionContext, now, task, result); projectionContext.recompute(); } assignmentProcessor.removeIgnoredContexts(context); // TODO move implementation of this method elsewhere; but it has to be invoked here, as activationProcessor sets the IGNORE flag } LensUtil.traceContext(LOGGER, activityDescription, "projection activation of all resources", true, context, true); if (consistencyChecks) context.checkConsistence(); dependencyProcessor.sortProjectionsToWaves(context); maxWaves = dependencyProcessor.computeMaxWaves(context); LOGGER.trace("Continuing wave {}, maxWaves={}", context.getProjectionWave(), maxWaves); for (LensProjectionContext projectionContext: context.getProjectionContexts()) { LensUtil.partialExecute("projection "+projectionContext.getHumanReadableName(), () -> projectProjection(context, projectionContext, partialProcessingOptions, now, activityDescription, task, result), partialProcessingOptions::getProjection); // TODO: make this condition more complex in the future. We may want the ability // to select only some projections to process } // if there exists some conflicting projection contexts, add them to the context so they will be recomputed in the next wave.. addConflictingContexts(context); if (consistencyChecks) context.checkConsistence(); context.incrementProjectionWave(); } LOGGER.trace("WAVE: Stopping the waves. There was {} waves", context.getProjectionWave()); // We can do this only when computation of all the waves is finished. Before that we do not know // activation of every account and therefore cannot decide what is OK and what is not dependencyProcessor.checkDependenciesFinal(context, result); if (consistencyChecks) context.checkConsistence(); computeResultStatus(now, result); } catch (SchemaException | PolicyViolationException | ExpressionEvaluationException | ObjectAlreadyExistsException | ObjectNotFoundException | CommunicationException | ConfigurationException | SecurityViolationException e) { recordFatalError(e, now, result); throw e; } catch (RuntimeException e) { recordFatalError(e, now, result); // This should not normally happen unless there is something really bad or there is a bug. // Make sure that it is logged. LOGGER.error("Runtime error in projector: {}", e.getMessage(), e); throw e; } finally { if (context.getDebugListener() != null) { context.getDebugListener().afterProjection(context); } context.reportProgress(new ProgressInformation(PROJECTOR, result)); } } private <F extends ObjectType> void projectProjection(LensContext<F> context, LensProjectionContext projectionContext, PartialProcessingOptionsType partialProcessingOptions, XMLGregorianCalendar now, String activityDescription, Task task, OperationResult parentResult) throws ObjectNotFoundException, CommunicationException, SchemaException, ConfigurationException, SecurityViolationException, PolicyViolationException, ExpressionEvaluationException, ObjectAlreadyExistsException { if (projectionContext.getWave() != context.getProjectionWave()) { // Let's skip accounts that do not belong into this wave. return; } String projectionDesc = getProjectionDesc(projectionContext); OperationResult result = parentResult.createMinorSubresult(OPERATION_PROJECT_PROJECTION); result.addParam(OperationResult.PARAM_PROJECTION, projectionDesc); try { context.checkAbortRequested(); if (projectionContext.getSynchronizationPolicyDecision() == SynchronizationPolicyDecision.BROKEN || projectionContext.getSynchronizationPolicyDecision() == SynchronizationPolicyDecision.IGNORE) { result.recordStatus(OperationResultStatus.NOT_APPLICABLE, "Skipping projection because it is "+projectionContext.getSynchronizationPolicyDecision()); return; } if (projectionContext.isThombstone()) { result.recordStatus(OperationResultStatus.NOT_APPLICABLE, "Skipping projection because it is a thombstone"); return; } LOGGER.trace("WAVE {} PROJECTION {}", context.getProjectionWave(), projectionDesc); // Some projections may not be loaded at this point, e.g. high-order dependency projections contextLoader.makeSureProjectionIsLoaded(context, projectionContext, task, result); if (consistencyChecks) context.checkConsistence(); if (!dependencyProcessor.checkDependencies(context, projectionContext, result)) { result.recordStatus(OperationResultStatus.NOT_APPLICABLE, "Skipping projection because it has unsatisfied dependencies"); return; } // TODO: decide if we need to continue LensUtil.partialExecute("projectionValues", () -> { // This is a "composite" processor. it contains several more processor invocations inside projectionValuesProcessor.process(context, projectionContext, activityDescription, task, result); if (consistencyChecks) context.checkConsistence(); projectionContext.recompute(); if (consistencyChecks) context.checkConsistence(); }, partialProcessingOptions::getProjectionValues); LensUtil.partialExecute("projectionCredentials", () -> { projectionCredentialsProcessor.processProjectionCredentials(context, projectionContext, now, task, result); if (consistencyChecks) context.checkConsistence(); projectionContext.recompute(); LensUtil.traceContext(LOGGER, activityDescription, "projection values and credentials of "+projectionDesc, false, context, true); if (consistencyChecks) context.checkConsistence(); }, partialProcessingOptions::getProjectionCredentials); LensUtil.partialExecute("projectionReconciliation", () -> { reconciliationProcessor.processReconciliation(context, projectionContext, task, result); projectionContext.recompute(); LensUtil.traceContext(LOGGER, activityDescription, "projection reconciliation of "+projectionDesc, false, context, false); if (consistencyChecks) context.checkConsistence(); }, partialProcessingOptions::getProjectionReconciliation); LensUtil.partialExecute("projectionLifecycle", () -> { activationProcessor.processLifecycle(context, projectionContext, now, task, result); if (consistencyChecks) context.checkConsistence(); projectionContext.recompute(); // LensUtil.traceContext(LOGGER, activityDescription, "projection lifecycle of "+projectionDesc, false, context, false); if (consistencyChecks) context.checkConsistence(); }, partialProcessingOptions::getProjectionLifecycle); result.recordSuccess(); } catch (ObjectNotFoundException | CommunicationException | SchemaException | ConfigurationException | SecurityViolationException | PolicyViolationException | ExpressionEvaluationException | ObjectAlreadyExistsException | RuntimeException | Error e) { result.recordFatalError(e); ResourceType resourceType = projectionContext.getResource(); if (resourceType == null) { throw e; } else { ErrorSelectorType errorSelector = null; if (resourceType.getConsistency() != null) { errorSelector = resourceType.getConsistency().getConnectorErrorCriticality(); } if (errorSelector == null) { if (e instanceof CommunicationException) { // Just continue evaluation. The error is recorded in the result. // The consistency mechanism has (most likely) already done the best. // We cannot do any better. } else { throw e; } } else { if (ExceptionUtil.isSelected(errorSelector, e)) { throw e; } else { // Just continue evaluation. The error is recorded in the result. } } } } } private String getProjectionDesc(LensProjectionContext projectionContext) { if (projectionContext.getResource() != null) { return projectionContext.getResource() + "("+projectionContext.getResourceShadowDiscriminator().getIntent()+")"; } else { ResourceShadowDiscriminator discr = projectionContext.getResourceShadowDiscriminator(); if (discr != null) { return projectionContext.getResourceShadowDiscriminator().toString(); } else { return "(UNKNOWN)"; } } } private <F extends ObjectType> void addConflictingContexts(LensContext<F> context) { List<LensProjectionContext> conflictingContexts = context.getConflictingProjectionContexts(); if (conflictingContexts != null && !conflictingContexts.isEmpty()){ for (LensProjectionContext conflictingContext : conflictingContexts){ LOGGER.trace("Adding conflicting projection context {}", conflictingContext.getHumanReadableName()); context.addProjectionContext(conflictingContext); } context.clearConflictingProjectionContexts(); } } private void recordFatalError(Exception e, XMLGregorianCalendar projectoStartTimestampCal, OperationResult result) { result.recordFatalError(e); result.cleanupResult(e); if (LOGGER.isDebugEnabled()) { long projectoStartTimestamp = XmlTypeConverter.toMillis(projectoStartTimestampCal); long projectorEndTimestamp = clock.currentTimeMillis(); LOGGER.debug("Projector failed: {}. Etime: {} ms", e.getMessage(), (projectorEndTimestamp - projectoStartTimestamp)); } } private void computeResultStatus(XMLGregorianCalendar projectoStartTimestampCal, OperationResult result) { boolean hasProjectionErrror = false; OperationResultStatus finalStatus = OperationResultStatus.SUCCESS; String message = null; for (OperationResult subresult: result.getSubresults()) { if (subresult.isNotApplicable() || subresult.isSuccess()) { continue; } if (subresult.isHandledError()) { if (finalStatus == OperationResultStatus.SUCCESS) { finalStatus = OperationResultStatus.HANDLED_ERROR; } continue; } if (subresult.isError()) { message = subresult.getMessage(); if (OPERATION_PROJECT_PROJECTION.equals(subresult.getOperation())) { hasProjectionErrror = true; } else { if (finalStatus != OperationResultStatus.FATAL_ERROR) { finalStatus = subresult.getStatus(); } } } } if (finalStatus != OperationResultStatus.FATAL_ERROR && hasProjectionErrror) { finalStatus = OperationResultStatus.PARTIAL_ERROR; } result.setStatus(finalStatus); result.setMessage(message); result.cleanupResult(); if (LOGGER.isDebugEnabled()) { long projectoStartTimestamp = XmlTypeConverter.toMillis(projectoStartTimestampCal); long projectorEndTimestamp = clock.currentTimeMillis(); LOGGER.trace("Projector finished ({}). Etime: {} ms", result.getStatus(), (projectorEndTimestamp - projectoStartTimestamp)); } } }