/** * Copyright (c) 2016 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.controller; import java.util.ArrayList; import java.util.Collection; import java.util.List; import com.evolveum.midpoint.model.api.ModelService; import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.evolveum.midpoint.model.api.ModelExecuteOptions; import com.evolveum.midpoint.model.api.util.MergeDeltas; import com.evolveum.midpoint.model.common.SystemObjectCache; import com.evolveum.midpoint.model.common.expression.Expression; import com.evolveum.midpoint.model.common.expression.ExpressionEvaluationContext; import com.evolveum.midpoint.model.common.expression.ExpressionFactory; import com.evolveum.midpoint.model.common.expression.ExpressionVariables; import com.evolveum.midpoint.model.impl.ModelObjectResolver; import com.evolveum.midpoint.prism.Item; import com.evolveum.midpoint.prism.ItemDefinition; import com.evolveum.midpoint.prism.PrismContainer; import com.evolveum.midpoint.prism.PrismContainerValue; import com.evolveum.midpoint.prism.PrismContext; import com.evolveum.midpoint.prism.PrismObject; import com.evolveum.midpoint.prism.PrismReference; import com.evolveum.midpoint.prism.PrismReferenceValue; import com.evolveum.midpoint.prism.PrismValue; import com.evolveum.midpoint.prism.Visitable; import com.evolveum.midpoint.prism.Visitor; import com.evolveum.midpoint.prism.delta.ItemDelta; import com.evolveum.midpoint.prism.delta.ObjectDelta; import com.evolveum.midpoint.prism.delta.PrismValueDeltaSetTriple; import com.evolveum.midpoint.prism.path.ItemPath; import com.evolveum.midpoint.prism.path.ItemPath.CompareResult; import com.evolveum.midpoint.schema.GetOperationOptions; import com.evolveum.midpoint.schema.ObjectDeltaOperation; import com.evolveum.midpoint.schema.ResourceShadowDiscriminator; import com.evolveum.midpoint.schema.SelectorOptions; import com.evolveum.midpoint.schema.constants.ExpressionConstants; import com.evolveum.midpoint.schema.constants.SchemaConstants; import com.evolveum.midpoint.schema.result.OperationResult; import com.evolveum.midpoint.schema.util.MiscSchemaUtil; import com.evolveum.midpoint.schema.util.ShadowUtil; 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.exception.SystemException; import com.evolveum.midpoint.util.exception.TunnelException; import com.evolveum.midpoint.util.logging.Trace; import com.evolveum.midpoint.util.logging.TraceManager; import com.evolveum.midpoint.xml.ns._public.common.common_3.ExpressionType; import com.evolveum.midpoint.xml.ns._public.common.common_3.FocusType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ItemMergeConfigurationType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ItemRefMergeConfigurationType; import com.evolveum.midpoint.xml.ns._public.common.common_3.MergeConfigurationType; import com.evolveum.midpoint.xml.ns._public.common.common_3.MergeStategyType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectReferenceType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ProjectionMergeConfigurationType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ProjectionMergeSituationType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ShadowDiscriminatorType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ShadowType; import com.evolveum.midpoint.xml.ns._public.common.common_3.SystemConfigurationType; /** * Class responsible for object merging. This acts as a controller * for the merge operation and merge preview. * * @author semancik */ @Component public class ObjectMerger { private static final Trace LOGGER = TraceManager.getTrace(ObjectMerger.class); public static final String SIDE_LEFT = "left"; public static final String SIDE_RIGHT = "right"; @Autowired(required = true) private ModelObjectResolver objectResolver; @Autowired(required = true) private SystemObjectCache systemObjectCache; @Autowired(required = true) private ExpressionFactory expressionFactory; @Autowired(required = true) PrismContext prismContext; // TODO: circular dependency to model controller. Not good. // But cannot fix it right now. TODO: later refactor. // MID-3459 @Autowired(required = true) private ModelService modelController; public <O extends ObjectType> Collection<ObjectDeltaOperation<? extends ObjectType>> mergeObjects(Class<O> type, String leftOid, String rightOid, String mergeConfigurationName, Task task, OperationResult result) throws ObjectNotFoundException, SchemaException, ConfigurationException, ObjectAlreadyExistsException, ExpressionEvaluationException, CommunicationException, PolicyViolationException, SecurityViolationException { MergeDeltas<O> deltas = computeMergeDeltas(type, leftOid, rightOid, mergeConfigurationName, task, result); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Merge {} + {} = (computed deltas)\n{}", leftOid, rightOid, deltas.debugDump(1)); } Collection<ObjectDeltaOperation<? extends ObjectType>> executedDeltas = new ArrayList<>(); LOGGER.trace("Executing right link delta (raw): {}", deltas.getRightLinkDelta()); executeDelta(deltas.getRightLinkDelta(), ModelExecuteOptions.createRaw(), executedDeltas, task, result); LOGGER.trace("Executing left link delta (raw): {}", deltas.getLeftLinkDelta()); executeDelta(deltas.getLeftLinkDelta(), ModelExecuteOptions.createRaw(), executedDeltas, task, result); LOGGER.trace("Executing left object delta: {}", deltas.getLeftObjectDelta()); executeDelta(deltas.getLeftObjectDelta(), null, executedDeltas, task, result); result.computeStatus(); if (result.isSuccess()) { // Do not delete the other object if the execution was not success. // We might need to re-try the merge if it has failed and for that we need the right object. ObjectDelta<O> deleteDelta = ObjectDelta.createDeleteDelta(type, rightOid, prismContext); Collection<ObjectDeltaOperation<? extends ObjectType>> executedDeleteDeltas = modelController.executeChanges(MiscSchemaUtil.createCollection(deleteDelta), null, task, result); executedDeltas.addAll(executedDeleteDeltas); } return executedDeltas; } private <O extends ObjectType> void executeDelta(ObjectDelta<O> objectDelta, ModelExecuteOptions options, Collection<ObjectDeltaOperation<? extends ObjectType>> executedDeltas, Task task, OperationResult result) throws ObjectAlreadyExistsException, ObjectNotFoundException, SchemaException, ExpressionEvaluationException, CommunicationException, ConfigurationException, PolicyViolationException, SecurityViolationException { result.computeStatus(); if (!result.isSuccess()) { return; } if (objectDelta != null && !objectDelta.isEmpty()) { Collection<ObjectDeltaOperation<? extends ObjectType>> deltaExecutedDeltas = modelController.executeChanges(MiscSchemaUtil.createCollection(objectDelta), options, task, result); executedDeltas.addAll(deltaExecutedDeltas); } } public <O extends ObjectType> MergeDeltas<O> computeMergeDeltas(Class<O> type, String leftOid, String rightOid, final String mergeConfigurationName, final Task task, final OperationResult result) throws ObjectNotFoundException, SchemaException, ConfigurationException, ExpressionEvaluationException, CommunicationException, SecurityViolationException { final PrismObject<O> objectLeft = (PrismObject<O>) objectResolver.getObjectSimple(type, leftOid, null, task, result).asPrismObject(); final PrismObject<O> objectRight = (PrismObject<O>) objectResolver.getObjectSimple(type, rightOid, null, task, result).asPrismObject(); PrismObject<SystemConfigurationType> systemConfiguration = systemObjectCache.getSystemConfiguration(result); MergeConfigurationType mergeConfiguration = selectConfiguration(systemConfiguration, mergeConfigurationName); if (mergeConfiguration == null) { throw new ConfigurationException("No merge configuration defined"); } // The "left" object is always the one that will be the result. We will use its OID. final ObjectDelta<O> leftObjectDelta = objectLeft.createModifyDelta(); final ObjectDelta<O> leftLinkDelta = objectLeft.createModifyDelta(); final ObjectDelta<O> rightLinkDelta = objectRight.createModifyDelta(); final List<ItemPath> processedPaths = new ArrayList<>(); computeItemDeltas(leftObjectDelta, objectLeft, objectRight, processedPaths, mergeConfiguration, mergeConfigurationName, task, result); computeDefaultDeltas(leftObjectDelta, objectLeft, objectRight, processedPaths, mergeConfiguration, mergeConfigurationName, task, result); computeProjectionDeltas(leftLinkDelta, rightLinkDelta, objectLeft, objectRight, mergeConfiguration, mergeConfigurationName, task, result); return new MergeDeltas<>(leftObjectDelta, leftLinkDelta, rightLinkDelta); } private <O extends ObjectType> void computeItemDeltas(final ObjectDelta<O> leftObjectDelta, final PrismObject<O> objectLeft, final PrismObject<O> objectRight, final List<ItemPath> processedPaths, MergeConfigurationType mergeConfiguration, final String mergeConfigurationName, final Task task, final OperationResult result) throws SchemaException, ConfigurationException, ExpressionEvaluationException, ObjectNotFoundException { for (ItemRefMergeConfigurationType itemMergeConfig: mergeConfiguration.getItem()) { ItemPath itemPath = itemMergeConfig.getRef().getItemPath(); processedPaths.add(itemPath); ItemDelta itemDelta = mergeItem(objectLeft, objectRight, mergeConfigurationName, itemMergeConfig, itemPath, task, result); LOGGER.trace("Item {} delta: {}", itemPath, itemDelta); if (itemDelta != null && !itemDelta.isEmpty()) { leftObjectDelta.addModification(itemDelta); } } } private <O extends ObjectType> void computeDefaultDeltas(final ObjectDelta<O> leftObjectDelta, final PrismObject<O> objectLeft, final PrismObject<O> objectRight, final List<ItemPath> processedPaths, MergeConfigurationType mergeConfiguration, final String mergeConfigurationName, final Task task, final OperationResult result) throws SchemaException, ConfigurationException, ExpressionEvaluationException, ObjectNotFoundException { final ItemMergeConfigurationType defaultItemMergeConfig = mergeConfiguration.getDefault(); if (defaultItemMergeConfig != null) { try { Visitor visitor = new Visitor() { @Override public void visit(Visitable visitable) { if (!(visitable instanceof Item)) { return; } Item item = (Item)visitable; ItemPath itemPath = item.getPath(); if (itemPath == null || itemPath.isEmpty()) { return; } if (SchemaConstants.PATH_LINK_REF.equivalent(itemPath)) { // Skip. There is a special processing for this. return; } boolean found = false; for (ItemPath processedPath: processedPaths) { // Need to check for super-paths here. // E.g. if we have already processed metadata, we do not want to process // metadata/modifyTimestamp CompareResult compareResult = processedPath.compareComplex(itemPath); if (compareResult == CompareResult.EQUIVALENT || compareResult == CompareResult.SUBPATH) { found = true; break; } } if (found) { return; } processedPaths.add(itemPath); if (item instanceof PrismContainer<?>) { if (item.getDefinition().isSingleValue()) { // Ignore single-valued containers such as extension or activation // we will handle every individual property there. return; } else { // TODO: we may need special handling for multi-value containers // such as assignment } } ItemDelta itemDelta; try { itemDelta = mergeItem(objectLeft, objectRight, mergeConfigurationName, defaultItemMergeConfig, itemPath, task, result); } catch (SchemaException | ConfigurationException | ExpressionEvaluationException | ObjectNotFoundException e) { throw new TunnelException(e); } LOGGER.trace("Item {} delta (default): {}", itemPath, itemDelta); if (itemDelta != null && !itemDelta.isEmpty()) { leftObjectDelta.addModification(itemDelta); } } }; objectLeft.accept(visitor); objectRight.accept(visitor); } catch (TunnelException te) { if (te.getCause() instanceof SchemaException) { throw (SchemaException)te.getCause(); } else if (te.getCause() instanceof ConfigurationException) { throw (ConfigurationException)te.getCause(); } else if (te.getCause() instanceof ExpressionEvaluationException) { throw (ExpressionEvaluationException)te.getCause(); } else if (te.getCause() instanceof ObjectNotFoundException) { throw (ObjectNotFoundException)te.getCause(); } else { throw new SystemException("Unexpected exception: "+te, te); } } } } private <O extends ObjectType> void computeProjectionDeltas(final ObjectDelta<O> leftLinkDelta, ObjectDelta<O> rightLinkDelta, final PrismObject<O> objectLeft, final PrismObject<O> objectRight, MergeConfigurationType mergeConfiguration, final String mergeConfigurationName, final Task task, final OperationResult result) throws SchemaException, ConfigurationException, ExpressionEvaluationException, ObjectNotFoundException, CommunicationException, SecurityViolationException { List<ShadowType> projectionsLeft = getProjections(objectLeft, task, result); List<ShadowType> projectionsRight = getProjections(objectRight, task, result); List<ShadowType> mergedProjections = new ArrayList<>(); List<ShadowType> matchedProjections = new ArrayList<>(); ProjectionMergeConfigurationType defaultProjectionMergeConfig = null; for (ProjectionMergeConfigurationType projectionMergeConfig: mergeConfiguration.getProjection()) { if (projectionMergeConfig.getProjectionDiscriminator() == null && projectionMergeConfig.getSituation() == null) { defaultProjectionMergeConfig = projectionMergeConfig; } else { takeProjections(projectionMergeConfig.getLeft(), mergedProjections, matchedProjections, projectionsLeft, projectionsLeft, projectionsRight, projectionMergeConfig); takeProjections(projectionMergeConfig.getRight(), mergedProjections, matchedProjections, projectionsRight, projectionsLeft, projectionsRight, projectionMergeConfig); } } LOGGER.trace("Merged projections (before default): {}", mergedProjections); LOGGER.trace("Matched projections (before default): {}", matchedProjections); if (defaultProjectionMergeConfig != null) { takeUnmatchedProjections(defaultProjectionMergeConfig.getLeft(), mergedProjections, matchedProjections, projectionsLeft); takeUnmatchedProjections(defaultProjectionMergeConfig.getRight(), mergedProjections, matchedProjections, projectionsRight); } LOGGER.trace("Merged projections: {}", mergedProjections); checkConflict(mergedProjections); for (ShadowType mergedProjection: mergedProjections) { PrismReferenceValue leftLinkRef = findLinkRef(objectLeft, mergedProjection); if (leftLinkRef == null) { PrismReferenceValue linkRefRight = findLinkRef(objectRight, mergedProjection); LOGGER.trace("Moving projection right->left: {}", mergedProjection); addUnlinkDelta(rightLinkDelta, linkRefRight); addLinkDelta(leftLinkDelta, linkRefRight); } else { LOGGER.trace("Projection already at the left: {}", mergedProjection); } } for (PrismReferenceValue leftLinkRef: getLinkRefs(objectLeft)) { if (!hasProjection(mergedProjections, leftLinkRef)) { LOGGER.trace("Removing left projection: {}", leftLinkRef); addUnlinkDelta(leftLinkDelta, leftLinkRef); } else { LOGGER.trace("Left projection stays: {}", leftLinkRef); } } } private <O extends ObjectType> void addLinkDelta(ObjectDelta<O> objectDelta, PrismReferenceValue linkRef) { objectDelta.addModificationAddReference(FocusType.F_LINK_REF, linkRef.clone()); } private <O extends ObjectType> void addUnlinkDelta(ObjectDelta<O> objectDelta, PrismReferenceValue linkRef) { objectDelta.addModificationDeleteReference(FocusType.F_LINK_REF, linkRef.clone()); } private <O extends ObjectType> PrismReferenceValue findLinkRef(PrismObject<O> object, ShadowType projection) { for (PrismReferenceValue linkRef: getLinkRefs(object)) { if (linkRef.getOid().equals(projection.getOid())) { return linkRef; } } return null; } private <O extends ObjectType> List<PrismReferenceValue> getLinkRefs(PrismObject<O> object) { PrismReference ref = object.findReference(FocusType.F_LINK_REF); if (ref == null) { return new ArrayList<>(0); } else { return ref.getValues(); } } private boolean hasProjection(List<ShadowType> mergedProjections, PrismReferenceValue leftLinkRef) { for (ShadowType projection: mergedProjections) { if (projection.getOid().equals(leftLinkRef.getOid())) { return true; } } return false; } private boolean hasProjection(List<ShadowType> mergedProjections, ShadowType candidateProjection) { for (ShadowType projection: mergedProjections) { if (projection.getOid().equals(candidateProjection.getOid())) { return true; } } return false; } private <O extends ObjectType> List<ShadowType> getProjections(PrismObject<O> objectRight, Task task, OperationResult result) throws ObjectNotFoundException, CommunicationException, SchemaException, ConfigurationException, SecurityViolationException { if (!objectRight.canRepresent(FocusType.class)) { return new ArrayList<>(0); } List<ObjectReferenceType> linkRefs = ((FocusType)objectRight.asObjectable()).getLinkRef(); List<ShadowType> projections = new ArrayList<>(linkRefs.size()); for (ObjectReferenceType linkRef: linkRefs) { projections.add(getProjection(linkRef, task, result)); } return projections; } private void takeProjections(MergeStategyType strategy, List<ShadowType> mergedProjections, List<ShadowType> matchedProjections, List<ShadowType> candidateProjections, List<ShadowType> projectionsLeft, List<ShadowType> projectionsRight, ProjectionMergeConfigurationType projectionMergeConfig) { if (LOGGER.isTraceEnabled()) { LOGGER.trace("TAKE: Evaluating situation {}, discriminator: {}", projectionMergeConfig.getSituation(), projectionMergeConfig.getProjectionDiscriminator()); } for (ShadowType candidateProjection: candidateProjections) { if (projectionMatches(candidateProjection, projectionsLeft, projectionsRight, projectionMergeConfig)) { LOGGER.trace("Projection matches {}", candidateProjection); matchedProjections.add(candidateProjection); if (strategy == MergeStategyType.TAKE) { mergedProjections.add(candidateProjection); } else if (strategy == null || strategy == MergeStategyType.IGNORE) { // Nothing to do here } else { throw new UnsupportedOperationException("Merge strategy "+strategy+" is not supported"); } } else { LOGGER.trace("Discriminator does NOT match {}", candidateProjection); } } } private boolean projectionMatches(ShadowType candidateProjection, List<ShadowType> projectionsLeft, List<ShadowType> projectionsRight, ProjectionMergeConfigurationType projectionMergeConfig) { ShadowDiscriminatorType discriminatorType = projectionMergeConfig.getProjectionDiscriminator(); if (discriminatorType != null && !ShadowUtil.matchesPattern(candidateProjection, discriminatorType)) { return false; } ProjectionMergeSituationType situationPattern = projectionMergeConfig.getSituation(); if (situationPattern != null) { ProjectionMergeSituationType projectionSituation = determineSituation(candidateProjection, projectionsLeft, projectionsRight); if (situationPattern != projectionSituation) { return false; } } return true; } private void takeUnmatchedProjections(MergeStategyType strategy, List<ShadowType> mergedProjections, List<ShadowType> matchedProjections, List<ShadowType> candidateProjections) { if (strategy == MergeStategyType.TAKE) { for (ShadowType candidateProjection: candidateProjections) { if (!hasProjection(matchedProjections, candidateProjection)) { mergedProjections.add(candidateProjection); } } } else if (strategy == null || strategy == MergeStategyType.IGNORE) { return; } else { throw new UnsupportedOperationException("Merge strategy "+strategy+" is not supported"); } } private ProjectionMergeSituationType determineSituation(ShadowType candidateProjection, List<ShadowType> projectionsLeft, List<ShadowType> projectionsRight) { boolean matchLeft = hasMatchingProjection(candidateProjection, projectionsLeft); boolean matchRight = hasMatchingProjection(candidateProjection, projectionsRight); if (matchLeft && matchRight) { return ProjectionMergeSituationType.CONFLICT; } else if (matchLeft) { return ProjectionMergeSituationType.EXISTING; } else if (matchRight) { return ProjectionMergeSituationType.MERGEABLE; } else { throw new IllegalStateException("Booom! The universe has imploded."); } } private boolean hasMatchingProjection(ShadowType cprojection, List<ShadowType> projections) { for (ShadowType projection: projections) { if (ShadowUtil.isConflicting(projection, cprojection)) { return true; } } return false; } private void checkConflict(List<ShadowType> projections) throws SchemaException { for (ShadowType projection: projections) { for (ShadowType cprojection: projections) { if (cprojection == projection) { continue; } if (ShadowUtil.isConflicting(projection, cprojection)) { throw new SchemaException("Merge would result in projection conflict between "+projection+" and "+cprojection); } } } } private ShadowType getProjection(ObjectReferenceType linkRef, Task task, OperationResult result) throws ObjectNotFoundException, CommunicationException, SchemaException, ConfigurationException, SecurityViolationException { Collection<SelectorOptions<GetOperationOptions>> options = SelectorOptions.createCollection(GetOperationOptions.createNoFetch()); return objectResolver.getObject(ShadowType.class, linkRef.getOid(), options, task, result); } private <O extends ObjectType, I extends Item> ItemDelta mergeItem(PrismObject<O> objectLeft, PrismObject<O> objectRight, String mergeConfigurationName, ItemMergeConfigurationType itemMergeConfig, ItemPath itemPath, Task task, OperationResult result) throws SchemaException, ConfigurationException, ExpressionEvaluationException, ObjectNotFoundException { I itemLeft = (I) objectLeft.findItem(itemPath); I itemRight = (I) objectRight.findItem(itemPath); if (itemLeft == null && itemRight == null) { return null; } ItemDefinition itemDefinition = null; if (itemLeft != null) { itemDefinition = itemLeft.getDefinition(); } else { itemDefinition = itemRight.getDefinition(); } if (itemDefinition.isOperational()) { // Skip operational attributes. There are automatically computed, // we do not want to modify them explicitly. return null; } Expression<PrismValue, ItemDefinition> valueExpression = null; if (itemMergeConfig.getValueExpression() != null) { ExpressionType expressionType = itemMergeConfig.getValueExpression(); valueExpression = expressionFactory.makeExpression(expressionType, itemDefinition, "value expression for item " + itemPath + " in merge configuration " + mergeConfigurationName, task, result); } ItemDelta itemDelta = itemDefinition.createEmptyDelta(itemPath); MergeStategyType leftStrategy = itemMergeConfig.getLeft(); MergeStategyType rightStrategy = itemMergeConfig.getRight(); if (leftStrategy == null || leftStrategy == MergeStategyType.IGNORE) { if (rightStrategy == null || rightStrategy == MergeStategyType.IGNORE) { // IGNORE both if (itemLeft == null) { return null; } else { itemDelta.setValueToReplace(); return itemDelta; } } else { // IGNORE left, TAKE/EXPRESSION right if (itemRight == null) { itemDelta.setValueToReplace(); } else { Collection<PrismValue> valuesToTake = getValuesToTake(objectLeft, objectRight, SIDE_RIGHT, itemRight, rightStrategy, valueExpression, task, result); itemDelta.setValuesToReplace(valuesToTake); } return itemDelta; } } else { if (rightStrategy == null || rightStrategy == MergeStategyType.IGNORE) { if (leftStrategy == MergeStategyType.TAKE) { // TAKE left, IGNORE right return null; } else { // EXPRESSION left, IGNORE right Collection<PrismValue> valuesToLeave = getValuesToTake(objectLeft, objectRight, SIDE_LEFT, itemLeft, leftStrategy, valueExpression, task, result); List<PrismValue> currentLeftValues = itemLeft.getValues(); Collection<PrismValue> leftValuesToRemove = diffValues(currentLeftValues, valuesToLeave); if (leftValuesToRemove != null && !leftValuesToRemove.isEmpty()) { itemDelta.addValuesToDelete(leftValuesToRemove); return itemDelta; } else { return null; } } } else { // TAKE/EXPRESSION left, TAKE/EXPRESSION right if (itemLeft == null) { Collection<PrismValue> valuesToTake = getValuesToTake(objectLeft, objectRight, SIDE_RIGHT, itemRight, rightStrategy, valueExpression, task, result); itemDelta.addValuesToAdd(valuesToTake); return itemDelta; } else { // We want to add only those values that are not yet there. // E.g. adding assignments that are there can cause unnecessary churn Collection<PrismValue> leftValuesToLeave = getValuesToTake(objectLeft, objectRight, SIDE_LEFT, itemLeft, leftStrategy, valueExpression, task, result); Collection<PrismValue> rightValuesToTake = getValuesToTake(objectLeft, objectRight, SIDE_RIGHT, itemRight, rightStrategy, valueExpression, task, result); for (PrismValue rightValueToTake: rightValuesToTake) { if (!PrismValue.collectionContainsEquivalentValue(leftValuesToLeave, rightValueToTake)) { itemDelta.addValueToAdd(rightValueToTake); } } List<PrismValue> currentLeftValues = itemLeft.getValues(); Collection<PrismValue> leftValuesToRemove = diffValues(currentLeftValues, leftValuesToLeave); if (leftValuesToRemove != null && !leftValuesToRemove.isEmpty()) { itemDelta.addValuesToDelete(leftValuesToRemove); } if (LOGGER.isTraceEnabled()) { LOGGER.trace("Merging item {} T/T case:\n leftValuesToLeave: {}\n rightValuesToTake: {}\n leftValuesToRemove: {}\n itemDelta:\n{}", new Object[]{itemPath, leftValuesToLeave, rightValuesToTake, leftValuesToRemove, itemDelta.debugDump(2)}); } return itemDelta; } } } } private Collection<PrismValue> diffValues(List<PrismValue> currentValues, Collection<PrismValue> valuesToLeave) { if (valuesToLeave == null || valuesToLeave.isEmpty()) { return PrismValue.cloneCollection(currentValues); } Collection<PrismValue> diff = new ArrayList<>(); for (PrismValue currentValue: currentValues) { if (!PrismValue.collectionContainsEquivalentValue(valuesToLeave, currentValue)) { diff.add(currentValue.clone()); } } return diff; } private <O extends ObjectType, I extends Item> Collection<PrismValue> getValuesToTake(PrismObject<O> objectLeft, PrismObject<O> objectRight, String side, I origItem, MergeStategyType strategy, Expression<PrismValue, ItemDefinition> valueExpression, Task task, OperationResult result) throws ConfigurationException, SchemaException, ExpressionEvaluationException, ObjectNotFoundException { if (origItem == null) { return new ArrayList<>(0); } if (strategy == MergeStategyType.TAKE) { return cleanContainerIds(origItem.getClonedValues()); } else if (strategy == MergeStategyType.EXPRESSION) { if (valueExpression == null) { throw new ConfigurationException("Expression strategy specified but no expression present"); } List<PrismValue> origValues = origItem.getValues(); Collection<PrismValue> valuesToTake = new ArrayList<>(origValues.size()); for (PrismValue origValue: origValues) { Collection<PrismValue> expressionOutput = evaluateValueExpression(objectLeft, objectRight, side, origValue, valueExpression, task, result); if (expressionOutput != null) { valuesToTake.addAll(expressionOutput); } } return cleanContainerIds(valuesToTake); } else { throw new ConfigurationException("Unknown strategy "+strategy); } } private Collection<PrismValue> cleanContainerIds(Collection<PrismValue> pvals) { if (pvals == null) { return null; } for (PrismValue pval: pvals) { if (pval instanceof PrismContainerValue<?>) { ((PrismContainerValue)pval).setId(null); } } return pvals; } private <O extends ObjectType> Collection<PrismValue> evaluateValueExpression(PrismObject<O> objectLeft, PrismObject<O> objectRight, String side, PrismValue origValue, Expression<PrismValue, ItemDefinition> valueExpression, Task task, OperationResult result) throws SchemaException, ExpressionEvaluationException, ObjectNotFoundException { ExpressionVariables variables = new ExpressionVariables(); variables.addVariableDefinition(ExpressionConstants.VAR_SIDE, side); variables.addVariableDefinition(ExpressionConstants.VAR_OBJECT_LEFT, side); variables.addVariableDefinition(ExpressionConstants.VAR_OBJECT_RIGHT, side); variables.addVariableDefinition(ExpressionConstants.VAR_INPUT, origValue); variables.addVariableDefinition(ExpressionConstants.VAR_VALUE, origValue); ExpressionEvaluationContext exprContext = new ExpressionEvaluationContext(null, variables, "for value "+origValue, task, result); PrismValueDeltaSetTriple<PrismValue> triple = valueExpression.evaluate(exprContext); if (triple == null) { return null; } return triple.getNonNegativeValues(); } private MergeConfigurationType selectConfiguration( PrismObject<SystemConfigurationType> systemConfiguration, String mergeConfigurationName) throws ConfigurationException { if (StringUtils.isBlank(mergeConfigurationName)) { throw new IllegalArgumentException("Merge configuration name not specified"); } for (MergeConfigurationType mergeConfiguration: systemConfiguration.asObjectable().getMergeConfiguration()) { if (mergeConfigurationName.equals(mergeConfiguration.getName())) { return mergeConfiguration; } } throw new ConfigurationException("Merge configuration with name '"+mergeConfigurationName+"' was not found"); } }