/* * Copyright (c) 2010-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.lens.projector; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.xml.namespace.QName; import com.evolveum.midpoint.common.refinery.RefinedAssociationDefinition; import com.evolveum.midpoint.prism.*; import com.evolveum.midpoint.prism.delta.ContainerDelta; import com.evolveum.midpoint.prism.match.MatchingRule; import com.evolveum.midpoint.schema.processor.ResourceAttributeContainer; import com.evolveum.midpoint.task.api.Task; import com.evolveum.midpoint.util.*; import com.evolveum.midpoint.xml.ns._public.common.common_3.ShadowAssociationType; import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.evolveum.midpoint.common.refinery.PropertyLimitations; import com.evolveum.midpoint.common.refinery.RefinedAttributeDefinition; import com.evolveum.midpoint.common.refinery.RefinedObjectClassDefinition; import com.evolveum.midpoint.common.refinery.RefinedResourceSchema; import com.evolveum.midpoint.model.api.context.SynchronizationPolicyDecision; import com.evolveum.midpoint.model.common.mapping.PrismValueDeltaSetTripleProducer; import com.evolveum.midpoint.model.impl.lens.ItemValueWithOrigin; import com.evolveum.midpoint.model.impl.lens.LensContext; import com.evolveum.midpoint.model.impl.lens.LensFocusContext; import com.evolveum.midpoint.model.impl.lens.LensProjectionContext; import com.evolveum.midpoint.prism.delta.DeltaSetTriple; import com.evolveum.midpoint.prism.delta.ItemDelta; import com.evolveum.midpoint.prism.delta.ObjectDelta; import com.evolveum.midpoint.prism.delta.PropertyDelta; import com.evolveum.midpoint.prism.match.MatchingRuleRegistry; import com.evolveum.midpoint.prism.path.ItemPath; import com.evolveum.midpoint.provisioning.api.ProvisioningService; import com.evolveum.midpoint.schema.GetOperationOptions; import com.evolveum.midpoint.schema.PointInTimeType; import com.evolveum.midpoint.schema.SelectorOptions; import com.evolveum.midpoint.schema.constants.SchemaConstants; import com.evolveum.midpoint.schema.processor.ObjectClassComplexTypeDefinition; import com.evolveum.midpoint.schema.processor.ResourceAttribute; import com.evolveum.midpoint.schema.processor.ResourceAttributeDefinition; import com.evolveum.midpoint.schema.result.OperationResult; import com.evolveum.midpoint.schema.util.ShadowUtil; import com.evolveum.midpoint.util.exception.CommunicationException; import com.evolveum.midpoint.util.exception.ConfigurationException; import com.evolveum.midpoint.util.exception.ObjectNotFoundException; 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.FocusType; import com.evolveum.midpoint.xml.ns._public.common.common_3.LayerType; import com.evolveum.midpoint.xml.ns._public.common.common_3.MappingStrengthType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectType; import com.evolveum.midpoint.xml.ns._public.common.common_3.PropertyAccessType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ShadowType; /** * Processor that reconciles the computed account and the real account. There * will be some deltas already computed from the other processors. This * processor will compare the "projected" state of the account after application * of the deltas to the actual (real) account with the result of the mappings. * The differences will be expressed as additional "reconciliation" deltas. * * @author lazyman * @author Radovan Semancik */ @Component public class ReconciliationProcessor { @Autowired private ProvisioningService provisioningService; @Autowired PrismContext prismContext; @Autowired private MatchingRuleRegistry matchingRuleRegistry; private static final String PROCESS_RECONCILIATION = ReconciliationProcessor.class.getName() + ".processReconciliation"; private static final Trace LOGGER = TraceManager.getTrace(ReconciliationProcessor.class); <F extends ObjectType> void processReconciliation(LensContext<F> context, LensProjectionContext projectionContext, Task task, OperationResult result) throws SchemaException, ObjectNotFoundException, CommunicationException, ConfigurationException, SecurityViolationException { LensFocusContext<F> focusContext = context.getFocusContext(); if (focusContext == null) { return; } if (!FocusType.class.isAssignableFrom(focusContext.getObjectTypeClass())) { // We can do this only for focal types. return; } processReconciliationFocus(context, projectionContext, task, result); } private <F extends ObjectType> void processReconciliationFocus(LensContext<F> context, LensProjectionContext projCtx, Task task, OperationResult result) throws SchemaException, ObjectNotFoundException, CommunicationException, ConfigurationException, SecurityViolationException { OperationResult subResult = result.createMinorSubresult(PROCESS_RECONCILIATION); try { // Reconcile even if it was not explicitly requested and if we have full shadow // reconciliation is cheap if the shadow is already fetched therefore just do it if (!projCtx.isDoReconciliation() && !projCtx.isFullShadow()) { if (LOGGER.isTraceEnabled()) { LOGGER.trace("Skipping reconciliation of {}: no doReconciliation and no full shadow", projCtx.getHumanReadableName()); } return; } SynchronizationPolicyDecision policyDecision = projCtx.getSynchronizationPolicyDecision(); if (policyDecision != null && (policyDecision == SynchronizationPolicyDecision.DELETE || policyDecision == SynchronizationPolicyDecision.UNLINK)) { if (LOGGER.isTraceEnabled()) { LOGGER.trace("Skipping reconciliation of {}: decision={}", projCtx.getHumanReadableName(), policyDecision); } return; } if (projCtx.getObjectCurrent() == null) { LOGGER.warn("Can't do reconciliation. Account context doesn't contain current version of account."); return; } if (!projCtx.isFullShadow()) { // We need to load the object GetOperationOptions rootOps = GetOperationOptions.createDoNotDiscovery(); rootOps.setPointInTimeType(PointInTimeType.FUTURE); PrismObject<ShadowType> objectOld = provisioningService.getObject(ShadowType.class, projCtx.getOid(), SelectorOptions.createCollection(rootOps), task, result); ShadowType oldShadow = objectOld.asObjectable(); projCtx.determineFullShadowFlag(oldShadow.getFetchResult()); projCtx.setLoadedObject(objectOld); projCtx.recompute(); } if (LOGGER.isTraceEnabled()) { LOGGER.trace("Starting reconciliation of {}", projCtx.getHumanReadableName()); } reconcileAuxiliaryObjectClasses(projCtx); RefinedObjectClassDefinition rOcDef = projCtx.getCompositeObjectClassDefinition(); Map<QName, DeltaSetTriple<ItemValueWithOrigin<PrismPropertyValue<?>,PrismPropertyDefinition<?>>>> squeezedAttributes = projCtx .getSqueezedAttributes(); if (LOGGER.isTraceEnabled()) { LOGGER.trace("Attribute reconciliation processing {}", projCtx.getHumanReadableName()); } reconcileProjectionAttributes(projCtx, squeezedAttributes, rOcDef); Map<QName, DeltaSetTriple<ItemValueWithOrigin<PrismContainerValue<ShadowAssociationType>,PrismContainerDefinition<ShadowAssociationType>>>> squeezedAssociations = projCtx.getSqueezedAssociations(); if (LOGGER.isTraceEnabled()) { LOGGER.trace("Association reconciliation processing {}", projCtx.getHumanReadableName()); } reconcileProjectionAssociations(projCtx, squeezedAssociations, rOcDef, task, result); reconcileMissingAuxiliaryObjectClassAttributes(projCtx); } catch (RuntimeException | SchemaException e) { subResult.recordFatalError(e); throw e; } finally { subResult.computeStatus(); } } private void reconcileAuxiliaryObjectClasses(LensProjectionContext projCtx) throws SchemaException { Map<QName, DeltaSetTriple<ItemValueWithOrigin<PrismPropertyValue<QName>, PrismPropertyDefinition<QName>>>> squeezedAuxiliaryObjectClasses = projCtx.getSqueezedAuxiliaryObjectClasses(); if (squeezedAuxiliaryObjectClasses == null || squeezedAuxiliaryObjectClasses.isEmpty()) { return; } if (LOGGER.isTraceEnabled()) { LOGGER.trace("Auxiliary object class reconciliation processing {}", projCtx.getHumanReadableName()); } PrismObject<ShadowType> shadowNew = projCtx.getObjectNew(); PrismPropertyDefinition<QName> propDef = shadowNew.getDefinition().findPropertyDefinition(ShadowType.F_AUXILIARY_OBJECT_CLASS); DeltaSetTriple<ItemValueWithOrigin<PrismPropertyValue<QName>, PrismPropertyDefinition<QName>>> pvwoTriple = squeezedAuxiliaryObjectClasses.get(ShadowType.F_AUXILIARY_OBJECT_CLASS); Collection<ItemValueWithOrigin<PrismPropertyValue<QName>,PrismPropertyDefinition<QName>>> shouldBePValues = null; if (pvwoTriple == null) { shouldBePValues = new ArrayList<>(); } else { shouldBePValues = pvwoTriple.getNonNegativeValues(); } Collection<PrismPropertyValue<QName>> arePValues = null; PrismProperty<QName> propertyNew = shadowNew.findProperty(ShadowType.F_AUXILIARY_OBJECT_CLASS); if (propertyNew != null) { arePValues = propertyNew.getValues(); } else { arePValues = new HashSet<>(); } ValueMatcher<QName> valueMatcher = ValueMatcher.createDefaultMatcher(DOMUtil.XSD_QNAME, matchingRuleRegistry); boolean auxObjectClassChanged = false; for (ItemValueWithOrigin<PrismPropertyValue<QName>, PrismPropertyDefinition<QName>> shouldBePvwo : shouldBePValues) { QName shouldBeRealValue = shouldBePvwo.getItemValue().getValue(); if (!isInValues(valueMatcher, shouldBeRealValue, arePValues)) { auxObjectClassChanged = true; recordDelta(valueMatcher, projCtx, ItemPath.EMPTY_PATH, propDef, ModificationType.ADD, shouldBeRealValue, shouldBePvwo.getSource(), "it is given"); } } for (PrismPropertyValue<QName> isPValue : arePValues) { if (!isInPvwoValues(valueMatcher, isPValue.getValue(), shouldBePValues)) { auxObjectClassChanged = true; recordDelta(valueMatcher, projCtx, ItemPath.EMPTY_PATH, propDef, ModificationType.DELETE, isPValue.getValue(), null, "it is not given"); } } if (auxObjectClassChanged) { projCtx.recompute(); projCtx.refreshAuxiliaryObjectClassDefinitions(); } } /** * If auxiliary object classes changed, there may still be some attributes that were defined by the aux objectclasses * that were deleted. If these attributes are still around then delete them. Otherwise the delete of the aux object class * may fail. */ private void reconcileMissingAuxiliaryObjectClassAttributes(LensProjectionContext projCtx) throws SchemaException { ObjectDelta<ShadowType> delta = projCtx.getDelta(); if (delta == null) { return; } PropertyDelta<QName> auxOcDelta = delta.findPropertyDelta(ShadowType.F_AUXILIARY_OBJECT_CLASS); if (auxOcDelta == null || auxOcDelta.isEmpty()) { return; } Collection<QName> deletedAuxObjectClassNames = null; PrismObject<ShadowType> objectOld = projCtx.getObjectOld(); if (auxOcDelta.isReplace()) { if (objectOld == null) { return; } PrismProperty<QName> auxOcPropOld = objectOld.findProperty(ShadowType.F_AUXILIARY_OBJECT_CLASS); if (auxOcPropOld == null) { return; } Collection<QName> auxOcsOld = auxOcPropOld.getRealValues(); Set<QName> auxOcsToReplace = PrismPropertyValue.getRealValuesOfCollection(auxOcDelta.getValuesToReplace()); deletedAuxObjectClassNames = new ArrayList<>(auxOcsOld.size()); for (QName auxOcOld: auxOcsOld) { if (!QNameUtil.contains(auxOcsToReplace, auxOcOld)) { deletedAuxObjectClassNames.add(auxOcOld); } } } else { Collection<PrismPropertyValue<QName>> valuesToDelete = auxOcDelta.getValuesToDelete(); if (valuesToDelete == null || valuesToDelete.isEmpty()) { return; } deletedAuxObjectClassNames = PrismPropertyValue.getRealValuesOfCollection(valuesToDelete); } LOGGER.trace("Deleted auxiliary object classes: {}", deletedAuxObjectClassNames); if (deletedAuxObjectClassNames == null || deletedAuxObjectClassNames.isEmpty()) { return; } List<QName> attributesToDelete = new ArrayList<>(); String projHumanReadableName = projCtx.getHumanReadableName(); RefinedResourceSchema refinedResourceSchema = projCtx.getRefinedResourceSchema(); RefinedObjectClassDefinition structuralObjectClassDefinition = projCtx.getStructuralObjectClassDefinition(); Collection<RefinedObjectClassDefinition> auxiliaryObjectClassDefinitions = projCtx.getAuxiliaryObjectClassDefinitions(); for (QName deleteAuxOcName: deletedAuxObjectClassNames) { ObjectClassComplexTypeDefinition auxOcDef = refinedResourceSchema.findObjectClassDefinition(deleteAuxOcName); for (ResourceAttributeDefinition auxAttrDef: auxOcDef.getAttributeDefinitions()) { QName auxAttrName = auxAttrDef.getName(); if (attributesToDelete.contains(auxAttrName)) { continue; } RefinedAttributeDefinition<Object> strucuralAttrDef = structuralObjectClassDefinition.findAttributeDefinition(auxAttrName); if (strucuralAttrDef == null) { boolean found = false; for (RefinedObjectClassDefinition auxiliaryObjectClassDefinition: auxiliaryObjectClassDefinitions) { if (QNameUtil.contains(deletedAuxObjectClassNames, auxiliaryObjectClassDefinition.getTypeName())) { continue; } RefinedAttributeDefinition<Object> existingAuxAttrDef = auxiliaryObjectClassDefinition.findAttributeDefinition(auxAttrName); if (existingAuxAttrDef != null) { found = true; break; } } if (!found) { LOGGER.trace("Removing attribute {} because it is in the deleted object class {} and it is not defined by any current object class for {}", auxAttrName, deleteAuxOcName, projHumanReadableName); attributesToDelete.add(auxAttrName); } } } } LOGGER.trace("Attributes to delete: {}", attributesToDelete); if (attributesToDelete.isEmpty()) { return; } for (QName attrNameToDelete: attributesToDelete) { ResourceAttribute<Object> attrToDelete = ShadowUtil.getAttribute(objectOld, attrNameToDelete); if (attrToDelete == null || attrToDelete.isEmpty()) { continue; } PropertyDelta<Object> attrDelta = attrToDelete.createDelta(); attrDelta.addValuesToDelete(PrismValue.cloneCollection(attrToDelete.getValues())); projCtx.swallowToSecondaryDelta(attrDelta); } } private void reconcileProjectionAttributes( LensProjectionContext projCtx, Map<QName, DeltaSetTriple<ItemValueWithOrigin<PrismPropertyValue<?>,PrismPropertyDefinition<?>>>> squeezedAttributes, RefinedObjectClassDefinition rOcDef) throws SchemaException { PrismObject<ShadowType> shadowNew = projCtx.getObjectNew(); PrismContainer attributesContainer = shadowNew.findContainer(ShadowType.F_ATTRIBUTES); Collection<QName> attributeNames = squeezedAttributes != null ? MiscUtil.union(squeezedAttributes.keySet(), attributesContainer.getValue().getPropertyNames()) : attributesContainer.getValue().getPropertyNames(); for (QName attrName : attributeNames) { reconcileProjectionAttribute(attrName, projCtx, squeezedAttributes, rOcDef, shadowNew, attributesContainer); } } private <T> void reconcileProjectionAttribute(QName attrName, LensProjectionContext projCtx, Map<QName, DeltaSetTriple<ItemValueWithOrigin<PrismPropertyValue<?>,PrismPropertyDefinition<?>>>> squeezedAttributes, RefinedObjectClassDefinition rOcDef, PrismObject<ShadowType> shadowNew, PrismContainer attributesContainer) throws SchemaException { // LOGGER.trace("Attribute reconciliation processing attribute {}",attrName); RefinedAttributeDefinition<T> attributeDefinition = projCtx.findAttributeDefinition(attrName); if (attributeDefinition == null) { String msg = "No definition for attribute " + attrName + " in " + projCtx.getResourceShadowDiscriminator(); throw new SchemaException(msg); } DeltaSetTriple<ItemValueWithOrigin<PrismPropertyValue<T>,PrismPropertyDefinition<T>>> pvwoTriple = squeezedAttributes != null ? (DeltaSetTriple) squeezedAttributes.get(attrName) : null; if (attributeDefinition.isIgnored(LayerType.MODEL)) { LOGGER.trace("Skipping reconciliation of attribute {} because it is ignored", attrName); return; } PropertyLimitations limitations = attributeDefinition.getLimitations(LayerType.MODEL); if (limitations != null) { PropertyAccessType access = limitations.getAccess(); if (access != null) { if (projCtx.isAdd() && (access.isAdd() == null || !access.isAdd())) { LOGGER.trace("Skipping reconciliation of attribute {} because it is non-createable", attrName); return; } if (projCtx.isModify() && (access.isModify() == null || !access.isModify())) { LOGGER.trace("Skipping reconciliation of attribute {} because it is non-updateable", attrName); return; } } } Collection<ItemValueWithOrigin<PrismPropertyValue<T>,PrismPropertyDefinition<T>>> shouldBePValues; if (pvwoTriple == null) { shouldBePValues = new HashSet<>(); } else { shouldBePValues = new HashSet<>(pvwoTriple.getNonNegativeValues()); } // We consider values explicitly requested by user to be among "should be values". addPropValuesFromDelta(shouldBePValues, projCtx.getPrimaryDelta(), attrName); // But we DO NOT take values from sync delta (because they just reflect what's on the resource), // nor from secondary delta (because these got there from mappings). boolean hasStrongShouldBePValue = false; for (ItemValueWithOrigin<? extends PrismPropertyValue<T>,PrismPropertyDefinition<T>> shouldBePValue : shouldBePValues) { if (shouldBePValue.getMapping() != null && shouldBePValue.getMapping().getStrength() == MappingStrengthType.STRONG) { hasStrongShouldBePValue = true; break; } } PrismProperty<T> attribute = attributesContainer.findProperty(attrName); Collection<PrismPropertyValue<T>> arePValues; if (attribute != null) { arePValues = attribute.getValues(); } else { arePValues = new HashSet<>(); } // Too loud :-) // if (LOGGER.isTraceEnabled()) { // StringBuilder sb = new StringBuilder(); // sb.append("Reconciliation\nATTR: ").append(PrettyPrinter.prettyPrint(attrName)); // sb.append("\n Should be:"); // for (ItemValueWithOrigin<?,?> shouldBePValue : shouldBePValues) { // sb.append("\n "); // sb.append(shouldBePValue.getItemValue()); // PrismValueDeltaSetTripleProducer<?, ?> shouldBeMapping = shouldBePValue.getMapping(); // if (shouldBeMapping.getStrength() == MappingStrengthType.STRONG) { // sb.append(" STRONG"); // } // if (shouldBeMapping.getStrength() == MappingStrengthType.WEAK) { // sb.append(" WEAK"); // } // if (!shouldBePValue.isValid()) { // sb.append(" INVALID"); // } // } // sb.append("\n Is:"); // for (PrismPropertyValue<Object> isPVal : arePValues) { // sb.append("\n "); // sb.append(isPVal); // } // LOGGER.trace("{}", sb.toString()); // } ValueMatcher<T> valueMatcher = ValueMatcher.createMatcher(attributeDefinition, matchingRuleRegistry); boolean hasValue = false; for (ItemValueWithOrigin<? extends PrismPropertyValue<T>,PrismPropertyDefinition<T>> shouldBePvwo : shouldBePValues) { PrismValueDeltaSetTripleProducer<?,?> shouldBeMapping = shouldBePvwo.getMapping(); if (shouldBeMapping == null) { continue; } T shouldBeRealValue = shouldBePvwo.getItemValue().getValue(); if (shouldBeMapping.getStrength() != MappingStrengthType.STRONG && (!arePValues.isEmpty() || hasStrongShouldBePValue)) { // weak or normal value and the attribute already has a // value. Skip it. // we cannot override it as it might have been legally // changed directly on the projection resource object LOGGER.trace("Skipping reconciliation of value {} of the attribute {}: the mapping is not strong" , shouldBeRealValue, attributeDefinition.getName().getLocalPart()); continue; } if (!isInValues(valueMatcher, shouldBeRealValue, arePValues)) { if (attributeDefinition.isSingleValue()) { if (hasValue) { throw new SchemaException( "Attempt to set more than one value for single-valued attribute " + attrName + " in " + projCtx.getResourceShadowDiscriminator()); } recordDelta(valueMatcher, projCtx, SchemaConstants.PATH_ATTRIBUTES, attributeDefinition, ModificationType.REPLACE, shouldBeRealValue, shouldBePvwo.getSource(), "it is given by a mapping"); } else { recordDelta(valueMatcher, projCtx, SchemaConstants.PATH_ATTRIBUTES, attributeDefinition, ModificationType.ADD, shouldBeRealValue, shouldBePvwo.getSource(), "it is given by a mapping"); } hasValue = true; } } decideIfTolerate(projCtx, attributeDefinition, arePValues, shouldBePValues, valueMatcher); } private <T> void addPropValuesFromDelta( Collection<ItemValueWithOrigin<PrismPropertyValue<T>, PrismPropertyDefinition<T>>> shouldBePValues, ObjectDelta<ShadowType> delta, QName attrName) { if (delta == null) { return; } List<PrismValue> values = delta.getNewValuesFor(new ItemPath(ShadowType.F_ATTRIBUTES, attrName)); for (PrismValue value : values) { if (value instanceof PrismPropertyValue) { shouldBePValues.add(new ItemValueWithOrigin<>((PrismPropertyValue) value, null, null)); } else if (value != null) { throw new IllegalStateException("Unexpected type of prism value. Expected PPV, got " + value); } } } private void addContainerValuesFromDelta( Collection<ItemValueWithOrigin<PrismContainerValue<ShadowAssociationType>, PrismContainerDefinition<ShadowAssociationType>>> shouldBeCValues, ObjectDelta<ShadowType> delta, QName assocName) { if (delta == null) { return; } List<PrismValue> values = delta.getNewValuesFor(new ItemPath(ShadowType.F_ASSOCIATION)); for (PrismValue value : values) { if (value instanceof PrismContainerValue) { Containerable c = ((PrismContainerValue) value).asContainerable(); if (c instanceof ShadowAssociationType) { ShadowAssociationType assocValue = (ShadowAssociationType) c; if (QNameUtil.match(assocValue.getName(), assocName)) { shouldBeCValues .add(new ItemValueWithOrigin<>((PrismContainerValue<ShadowAssociationType>) value, null, null)); } } else { throw new IllegalStateException("Unexpected type of prism value. Expected PCV<ShadowAssociationType>, got " + value); } } else if (value != null) { throw new IllegalStateException("Unexpected type of prism value. Expected PCV<ShadowAssociationType>, got " + value); } } } private void reconcileProjectionAssociations( LensProjectionContext projCtx, Map<QName, DeltaSetTriple<ItemValueWithOrigin<PrismContainerValue<ShadowAssociationType>, PrismContainerDefinition<ShadowAssociationType>>>> squeezedAssociations, RefinedObjectClassDefinition accountDefinition, Task task, OperationResult result) throws SchemaException, ConfigurationException, ObjectNotFoundException, CommunicationException, SecurityViolationException { PrismObject<ShadowType> shadowNew = projCtx.getObjectNew(); PrismContainer associationsContainer = shadowNew.findContainer(ShadowType.F_ASSOCIATION); Collection<QName> associationNames = squeezedAssociations != null ? MiscUtil.union(squeezedAssociations.keySet(), accountDefinition.getNamesOfAssociations()) : accountDefinition.getNamesOfAssociations(); for (QName assocName : associationNames) { LOGGER.trace("Association reconciliation processing association {}", assocName); RefinedAssociationDefinition associationDefinition = accountDefinition.findAssociationDefinition(assocName); if (associationDefinition == null) { throw new SchemaException("No definition for association " + assocName + " in " + projCtx.getResourceShadowDiscriminator()); } DeltaSetTriple<ItemValueWithOrigin<PrismContainerValue<ShadowAssociationType>,PrismContainerDefinition<ShadowAssociationType>>> cvwoTriple = squeezedAssociations != null ? squeezedAssociations.get(assocName) : null; // note: actually isIgnored is not implemented yet if (associationDefinition.isIgnored()) { LOGGER.trace("Skipping reconciliation of association {} because it is ignored", assocName); continue; } // TODO implement limitations // PropertyLimitations limitations = associationDefinition.getLimitations(LayerType.MODEL); // if (limitations != null) { // PropertyAccessType access = limitations.getAccess(); // if (access != null) { // if (projCtx.isAdd() && (access.isAdd() == null || !access.isAdd())) { // LOGGER.trace("Skipping reconciliation of attribute {} because it is non-createable", // attrName); // continue; // } // if (projCtx.isModify() && (access.isModify() == null || !access.isModify())) { // LOGGER.trace("Skipping reconciliation of attribute {} because it is non-updateable", // attrName); // continue; // } // } // } Collection<ItemValueWithOrigin<PrismContainerValue<ShadowAssociationType>,PrismContainerDefinition<ShadowAssociationType>>> shouldBeCValues; if (cvwoTriple == null) { shouldBeCValues = new HashSet<>(); } else { shouldBeCValues = new HashSet<>(cvwoTriple.getNonNegativeValues()); } // TODO what about equality checks? There will be probably duplicates there. // We consider values explicitly requested by user to be among "should be values". addContainerValuesFromDelta(shouldBeCValues, projCtx.getPrimaryDelta(), assocName); // But we DO NOT take values from sync delta (because they just reflect what's on the resource), // nor from secondary delta (because these got there from mappings). // values in shouldBeCValues are parent-less // to be able to make Containerable out of them, we provide them a (fake) parent // (and we clone them not to mess anything) PrismContainer<ShadowAssociationType> fakeParent = prismContext.getSchemaRegistry().findContainerDefinitionByCompileTimeClass(ShadowAssociationType.class).instantiate(); for (ItemValueWithOrigin<PrismContainerValue<ShadowAssociationType>,PrismContainerDefinition<ShadowAssociationType>> cvwo : shouldBeCValues) { PrismContainerValue<ShadowAssociationType> cvalue = cvwo.getItemValue().clone(); cvalue.setParent(fakeParent); cvwo.setItemValue(cvalue); } boolean hasStrongShouldBeCValue = false; for (ItemValueWithOrigin<PrismContainerValue<ShadowAssociationType>,PrismContainerDefinition<ShadowAssociationType>> shouldBeCValue : shouldBeCValues) { if (shouldBeCValue.getMapping() != null && shouldBeCValue.getMapping().getStrength() == MappingStrengthType.STRONG) { hasStrongShouldBeCValue = true; break; } } Collection<PrismContainerValue<ShadowAssociationType>> areCValues = new HashSet<>(); if (associationsContainer != null) { for (Object o : associationsContainer.getValues()) { PrismContainerValue<ShadowAssociationType> existingAssocValue = (PrismContainerValue<ShadowAssociationType>) o; if (existingAssocValue.getValue().getName().equals(assocName)) { areCValues.add(existingAssocValue); } } } else { areCValues = new HashSet<>(); } // todo comment this logging code out eventually // if (LOGGER.isTraceEnabled()) { // StringBuilder sb = new StringBuilder(); // sb.append("Reconciliation\nASSOCIATION: ").append(PrettyPrinter.prettyPrint(assocName)); // sb.append("\n Should be:"); // for (ItemValueWithOrigin<PrismContainerValue<ShadowAssociationType>,PrismContainerDefinition<ShadowAssociationType>> shouldBeCValue : shouldBeCValues) { // sb.append("\n "); // sb.append(shouldBeCValue.getItemValue()); // PrismValueDeltaSetTripleProducer<?,?> shouldBeMapping = shouldBeCValue.getMapping(); // if (shouldBeMapping != null && shouldBeMapping.getStrength() == MappingStrengthType.STRONG) { // sb.append(" STRONG"); // } // if (shouldBeMapping != null && shouldBeMapping.getStrength() == MappingStrengthType.WEAK) { // sb.append(" WEAK"); // } // if (!shouldBeCValue.isValid()) { // sb.append(" INVALID"); // } // } // sb.append("\n Is:"); // for (PrismContainerValue<ShadowAssociationType> isCVal : areCValues) { // sb.append("\n "); // sb.append(isCVal); // } // LOGGER.trace("{}", sb.toString()); // } ValueMatcher associationValueMatcher = new ValueMatcher(null) { // todo is this correct? [med] @Override public boolean match(Object realA, Object realB) { checkType(realA); checkType(realB); if (realA == null) { return realB == null; } else if (realB == null) { return false; } else { ShadowAssociationType a = (ShadowAssociationType) realA; ShadowAssociationType b = (ShadowAssociationType) realB; checkName(a); checkName(b); if (!a.getName().equals(b.getName())) { return false; } if (a.getShadowRef() != null && a.getShadowRef().getOid() != null && b.getShadowRef() != null && b.getShadowRef().getOid() != null) { return a.getShadowRef().getOid().equals(b.getShadowRef().getOid()); } LOGGER.warn("Comparing association values without shadowRefs: {} and {}", a, b); return false; } } private void checkName(ShadowAssociationType s) { if (s.getName() == null) { throw new IllegalStateException("No name for association " + s); } } @Override public boolean matches(Object realValue, String regex) { throw new UnsupportedOperationException(); } @Override public boolean hasRealValue(PrismProperty property, PrismPropertyValue pValue) { throw new UnsupportedOperationException(); } @Override public boolean isRealValueToAdd(PropertyDelta delta, PrismPropertyValue pValue) { throw new UnsupportedOperationException(); } private void checkType(Object o) { if (o != null && !(o instanceof ShadowAssociationType)) { throw new IllegalStateException("Object is not a ShadowAssociationType, it is " + o.getClass() + " instead"); } } }; for (ItemValueWithOrigin<PrismContainerValue<ShadowAssociationType>,PrismContainerDefinition<ShadowAssociationType>> shouldBeCvwo : shouldBeCValues) { PrismValueDeltaSetTripleProducer<?,?> shouldBeMapping = shouldBeCvwo.getMapping(); if (shouldBeMapping == null) { continue; } if (shouldBeMapping.getStrength() != MappingStrengthType.STRONG && (!areCValues.isEmpty() || hasStrongShouldBeCValue)) { // weak or normal value and the attribute already has a // value. Skip it. // we cannot override it as it might have been legally // changed directly on the projection resource object continue; } ShadowAssociationType shouldBeRealValue = shouldBeCvwo.getItemValue().getValue(); if (shouldBeCvwo.isValid() && !isInAssociationValues(associationValueMatcher, shouldBeRealValue, areCValues)) { recordAssociationDelta(associationValueMatcher, projCtx, associationDefinition, ModificationType.ADD, shouldBeRealValue, shouldBeCvwo.getSource(), "it is given by a mapping"); } } if (LOGGER.isTraceEnabled()) { LOGGER.trace("Before decideIfTolerateAssociation:"); LOGGER.trace("areCValues:\n{}", DebugUtil.debugDump(areCValues)); LOGGER.trace("shouldBeCValues:\n{}", DebugUtil.debugDump(shouldBeCValues)); } decideIfTolerateAssociation(projCtx, associationDefinition, areCValues, shouldBeCValues, associationValueMatcher, task, result); } } private <T> void decideIfTolerate(LensProjectionContext projCtx, RefinedAttributeDefinition<T> attributeDefinition, Collection<PrismPropertyValue<T>> arePValues, Collection<ItemValueWithOrigin<PrismPropertyValue<T>,PrismPropertyDefinition<T>>> shouldBePValues, ValueMatcher<T> valueMatcher) throws SchemaException { for (PrismPropertyValue<T> isPValue : arePValues){ if (matchPattern(attributeDefinition.getTolerantValuePattern(), isPValue, valueMatcher)){ LOGGER.trace("Reconciliation: KEEPING value {} of the attribute {}: match with tolerant value pattern." , isPValue, attributeDefinition.getName().getLocalPart()); continue; } if (matchPattern(attributeDefinition.getIntolerantValuePattern(), isPValue, valueMatcher)){ recordDeleteDelta(isPValue, attributeDefinition, valueMatcher, projCtx, "it has matched with intolerant pattern"); continue; } if (!attributeDefinition.isTolerant()) { if (!isInPvwoValues(valueMatcher, isPValue.getValue(), shouldBePValues)) { recordDeleteDelta(isPValue, attributeDefinition, valueMatcher, projCtx, "it is not given by any mapping and the attribute is not tolerant"); } } } } private void decideIfTolerateAssociation(LensProjectionContext accCtx, RefinedAssociationDefinition assocDef, Collection<PrismContainerValue<ShadowAssociationType>> areCValues, Collection<ItemValueWithOrigin<PrismContainerValue<ShadowAssociationType>, PrismContainerDefinition<ShadowAssociationType>>> shouldBeCValues, ValueMatcher valueMatcher, Task task, OperationResult result) throws SchemaException, SecurityViolationException, CommunicationException, ConfigurationException, ObjectNotFoundException { boolean evaluatePatterns = !assocDef.getTolerantValuePattern().isEmpty() || !assocDef.getIntolerantValuePattern().isEmpty(); MatchingRule<Object> matchingRule = evaluatePatterns ? getMatchingRuleForTargetNamingIdentifier(assocDef) : null; // for each existing value we decide whether to keep it or delete it for (PrismContainerValue<ShadowAssociationType> isCValue : areCValues) { ResourceAttribute<String> targetNamingIdentifier = null; if (evaluatePatterns) { targetNamingIdentifier = getTargetNamingIdentifier(isCValue, task, result); if (targetNamingIdentifier == null) { LOGGER.warn("Couldn't check tolerant/intolerant patterns for {}, as there's no naming identifier for it", isCValue); evaluatePatterns = false; } } String assocNameLocal = assocDef.getName().getLocalPart(); if (evaluatePatterns && matchesAssociationPattern(assocDef.getTolerantValuePattern(), targetNamingIdentifier, matchingRule)) { LOGGER.trace("Reconciliation: KEEPING value {} of association {}: identifier {} matches with tolerant value pattern.", isCValue, assocNameLocal, targetNamingIdentifier); continue; } if (isInCvwoAssociationValues(valueMatcher, isCValue.getValue(), shouldBeCValues)) { LOGGER.trace("Reconciliation: KEEPING value {} of association {}: it is in 'shouldBeCValues'", isCValue, assocNameLocal); continue; } if (evaluatePatterns && matchesAssociationPattern(assocDef.getIntolerantValuePattern(), targetNamingIdentifier, matchingRule)) { recordAssociationDelta(valueMatcher, accCtx, assocDef, ModificationType.DELETE, isCValue.getValue(), null, "identifier " + targetNamingIdentifier + " matches with intolerant pattern"); continue; } if (!assocDef.isTolerant()) { recordAssociationDelta(valueMatcher, accCtx, assocDef, ModificationType.DELETE, isCValue.getValue(), null, "it is not given by any mapping and the association is not tolerant"); } else { LOGGER.trace("Reconciliation: KEEPING value {} of association {}: the association is tolerant and the value" + " was not caught by any intolerantValuePattern", isCValue, assocNameLocal); } } } @NotNull private MatchingRule<Object> getMatchingRuleForTargetNamingIdentifier(RefinedAssociationDefinition associationDefinition) throws SchemaException { RefinedAttributeDefinition<Object> targetNamingAttributeDef = associationDefinition.getAssociationTarget().getNamingAttribute(); if (targetNamingAttributeDef != null) { QName matchingRuleName = targetNamingAttributeDef.getMatchingRuleQName(); return matchingRuleRegistry.getMatchingRule(matchingRuleName, null); } else { throw new IllegalStateException( "Couldn't evaluate tolerant/intolerant value patterns, because naming attribute is not known for " + associationDefinition.getAssociationTarget()); } } private ResourceAttribute<String> getTargetNamingIdentifier( PrismContainerValue<ShadowAssociationType> associationValue, Task task, OperationResult result) throws SchemaException, SecurityViolationException, ObjectNotFoundException, CommunicationException, ConfigurationException { return getIdentifiersForAssociationTarget(associationValue, task, result).getNamingAttribute(); } @NotNull private ResourceAttributeContainer getIdentifiersForAssociationTarget(PrismContainerValue<ShadowAssociationType> isCValue, Task task, OperationResult result) throws CommunicationException, SchemaException, ConfigurationException, SecurityViolationException, ObjectNotFoundException { ResourceAttributeContainer identifiersContainer = ShadowUtil.getAttributesContainer(isCValue, ShadowAssociationType.F_IDENTIFIERS); if (identifiersContainer != null) { return identifiersContainer; } String oid = isCValue.asContainerable().getShadowRef() != null ? isCValue.asContainerable().getShadowRef().getOid() : null; if (oid == null) { // TODO maybe warn/error log would suffice? throw new IllegalStateException("Couldn't evaluate tolerant/intolerant values for association " + isCValue + ", because there are no identifiers and no shadow reference present"); } PrismObject<ShadowType> target; try { GetOperationOptions rootOpt = GetOperationOptions.createPointInTimeType(PointInTimeType.FUTURE); rootOpt.setNoFetch(true); target = provisioningService.getObject(ShadowType.class, oid, SelectorOptions.createCollection(rootOpt), task, result); } catch (ObjectNotFoundException e) { // TODO maybe warn/error log would suffice (also for other exceptions?) throw new ObjectNotFoundException("Couldn't evaluate tolerant/intolerant values for association " + isCValue + ", because the association target object does not exist: " + e.getMessage(), e); } identifiersContainer = ShadowUtil.getAttributesContainer(target); if (identifiersContainer == null) { // TODO maybe warn/error log would suffice? throw new IllegalStateException("Couldn't evaluate tolerant/intolerant values for association " + isCValue + ", because there are no identifiers present, even in the repository object for association target"); } return identifiersContainer; } private <T> void recordDelta(ValueMatcher<T> valueMatcher, LensProjectionContext projCtx, ItemPath parentPath, PrismPropertyDefinition<T> attrDef, ModificationType changeType, T value, ObjectType originObject, String reason) throws SchemaException { ItemDelta existingDelta = null; if (projCtx.getSecondaryDelta() != null) { existingDelta = projCtx.getSecondaryDelta().findItemDelta( new ItemPath(parentPath, attrDef.getName())); } if (LOGGER.isTraceEnabled()) { LOGGER.trace("Reconciliation will {} value of attribute {}: {} because {}", changeType, PrettyPrinter.prettyPrint(attrDef.getName()), value, reason); } PropertyDelta<T> attrDelta = new PropertyDelta<>(parentPath, attrDef.getName(), attrDef, prismContext); PrismPropertyValue<T> pValue = new PrismPropertyValue<>(value, OriginType.RECONCILIATION, originObject); if (changeType == ModificationType.ADD) { attrDelta.addValueToAdd(pValue); } else if (changeType == ModificationType.DELETE) { if (!isToBeDeleted(existingDelta, valueMatcher, value)){ attrDelta.addValueToDelete(pValue); } } else if (changeType == ModificationType.REPLACE) { attrDelta.setValueToReplace(pValue); } else { throw new IllegalArgumentException("Unknown change type " + changeType); } projCtx.swallowToSecondaryDelta(attrDelta); } private <T> void recordDeleteDelta(PrismPropertyValue<T> isPValue, RefinedAttributeDefinition<T> attributeDefinition, ValueMatcher<T> valueMatcher, LensProjectionContext projCtx, String reason) throws SchemaException { recordDelta(valueMatcher, projCtx, SchemaConstants.PATH_ATTRIBUTES, attributeDefinition, ModificationType.DELETE, isPValue.getValue(), null, reason); } private void recordAssociationDelta(ValueMatcher valueMatcher, LensProjectionContext accCtx, RefinedAssociationDefinition assocDef, ModificationType changeType, ShadowAssociationType value, ObjectType originObject, String reason) throws SchemaException { ItemDelta existingDelta = null; if (accCtx.getSecondaryDelta() != null) { existingDelta = accCtx.getSecondaryDelta().findItemDelta(SchemaConstants.PATH_ASSOCIATION); } LOGGER.trace("Reconciliation will {} value of association {}: {} because {}", changeType, assocDef, value, reason); // todo initialize only once PrismContainerDefinition<ShadowAssociationType> associationDefinition = prismContext.getSchemaRegistry().findObjectDefinitionByCompileTimeClass(ShadowType.class) .findContainerDefinition(ShadowType.F_ASSOCIATION); ContainerDelta assocDelta = new ContainerDelta(SchemaConstants.PATH_ASSOCIATION, associationDefinition, prismContext); PrismContainerValue cValue = value.asPrismContainerValue().clone(); cValue.setOriginType(OriginType.RECONCILIATION); cValue.setOriginObject(originObject); if (changeType == ModificationType.ADD) { assocDelta.addValueToAdd(cValue); } else if (changeType == ModificationType.DELETE) { if (!isToBeDeleted(existingDelta, valueMatcher, value)){ LOGGER.trace("Adding association value to delete {} ", cValue); assocDelta.addValueToDelete(cValue); } } else if (changeType == ModificationType.REPLACE) { assocDelta.setValueToReplace(cValue); } else { throw new IllegalArgumentException("Unknown change type " + changeType); } accCtx.swallowToSecondaryDelta(assocDelta); } private <T> boolean isToBeDeleted(ItemDelta existingDelta, ValueMatcher valueMatcher, T value) { LOGGER.trace("Checking existence for DELETE of value {} in existing delta: {}", value, existingDelta); if (existingDelta == null) { return false; } if (existingDelta.getValuesToDelete() == null){ return false; } for (Object isInDeltaValue : existingDelta.getValuesToDelete()) { if (isInDeltaValue instanceof PrismPropertyValue){ PrismPropertyValue isInRealValue = (PrismPropertyValue) isInDeltaValue; if (matchValue(isInRealValue.getValue(), value, valueMatcher)) { LOGGER.trace("Skipping adding value {} to delta for DELETE because it's already there"); return true; } } else if (isInDeltaValue instanceof PrismContainerValue) { PrismContainerValue isInRealValue = (PrismContainerValue) isInDeltaValue; if (matchValue(isInRealValue.asContainerable(), value, valueMatcher)){ LOGGER.trace("Skipping adding value {} to delta for DELETE because it's already there"); return true; } } //TODO: reference delta??? } return false; } private <T> boolean isInValues(ValueMatcher<T> valueMatcher, T shouldBeValue, Collection<PrismPropertyValue<T>> arePValues) { if (arePValues == null || arePValues.isEmpty()) { return false; } for (PrismPropertyValue<T> isPValue : arePValues) { if (matchValue(isPValue.getValue(), shouldBeValue, valueMatcher)) { return true; } } return false; } // todo deduplicate; this was copied not to broke what works now [mederly] private boolean isInAssociationValues(ValueMatcher valueMatcher, ShadowAssociationType shouldBeValue, Collection<PrismContainerValue<ShadowAssociationType>> arePValues) { if (arePValues == null || arePValues.isEmpty()) { return false; } for (PrismContainerValue<ShadowAssociationType> isPValue : arePValues) { if (matchValue(isPValue.getValue(), shouldBeValue, valueMatcher)) { return true; } } return false; } private <T> boolean isInPvwoValues(ValueMatcher<T> valueMatcher, T value, Collection<ItemValueWithOrigin<PrismPropertyValue<T>,PrismPropertyDefinition<T>>> shouldBePvwos) { if (shouldBePvwos == null || shouldBePvwos.isEmpty()) { return false; } for (ItemValueWithOrigin<? extends PrismPropertyValue<T>,PrismPropertyDefinition<T>> shouldBePvwo : shouldBePvwos) { if (!shouldBePvwo.isValid()) { continue; } PrismPropertyValue<T> shouldBePPValue = shouldBePvwo.getItemValue(); T shouldBeValue = shouldBePPValue.getValue(); if (matchValue(value, shouldBeValue, valueMatcher)) { return true; } } return false; } private boolean isInCvwoAssociationValues(ValueMatcher valueMatcher, ShadowAssociationType value, Collection<ItemValueWithOrigin<PrismContainerValue<ShadowAssociationType>,PrismContainerDefinition<ShadowAssociationType>>> shouldBeCvwos) { if (shouldBeCvwos == null || shouldBeCvwos.isEmpty()) { return false; } for (ItemValueWithOrigin<? extends PrismContainerValue<ShadowAssociationType>,PrismContainerDefinition<ShadowAssociationType>> shouldBeCvwo : shouldBeCvwos) { if (!shouldBeCvwo.isValid()) { continue; } PrismContainerValue<ShadowAssociationType> shouldBePCValue = shouldBeCvwo.getItemValue(); ShadowAssociationType shouldBeValue = shouldBePCValue.getValue(); if (matchValue(value, shouldBeValue, valueMatcher)) { return true; } } return false; } private <T> boolean matchValue(T realA, T realB, ValueMatcher<T> valueMatcher) { try { return valueMatcher.match(realA, realB); } catch (SchemaException e) { LOGGER.warn("Value '{}' or '{}' is invalid: {}", realA, realB, e.getMessage(), e); return false; } } private <T> boolean matchPattern(List<String> patterns, PrismPropertyValue<T> isPValue, ValueMatcher<T> valueMatcher) { if (patterns == null || patterns.isEmpty()) { return false; } for (String pattern : patterns) { try { if (valueMatcher.matches(isPValue.getValue(), pattern)) { return true; } } catch (SchemaException e) { LOGGER.warn("Value '{}' is invalid: {}", isPValue.getValue(), e.getMessage(), e); return false; } } return false; } private boolean matchesAssociationPattern(@NotNull List<String> patterns, @NotNull ResourceAttribute<?> identifier, @NotNull MatchingRule<Object> matchingRule) { for (String pattern : patterns) { for (PrismPropertyValue<?> identifierValue : identifier.getValues()) { try { if (identifierValue != null && matchingRule.matchRegex(identifierValue.getRealValue(), pattern)) { return true; } } catch (SchemaException e) { LOGGER.warn("Value '{}' is invalid: {}", identifierValue, e.getMessage(), e); return false; } } } return false; } }