/* * 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.prism.delta; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.function.Function; import javax.xml.namespace.QName; import com.evolveum.midpoint.prism.*; import com.evolveum.midpoint.prism.marshaller.XPathHolder; import com.evolveum.midpoint.prism.path.IdItemPathSegment; import com.evolveum.midpoint.prism.path.ItemPath; import com.evolveum.midpoint.prism.path.ItemPath.CompareResult; import com.evolveum.midpoint.prism.path.ItemPathSegment; import com.evolveum.midpoint.prism.path.NameItemPathSegment; import com.evolveum.midpoint.util.DebugDumpable; import com.evolveum.midpoint.util.DebugUtil; import com.evolveum.midpoint.util.Foreachable; import com.evolveum.midpoint.util.MiscUtil; import com.evolveum.midpoint.util.PrettyPrinter; import com.evolveum.midpoint.util.Processor; import com.evolveum.midpoint.util.QNameUtil; import com.evolveum.midpoint.util.exception.SchemaException; import com.evolveum.prism.xml.ns._public.types_3.ItemDeltaType; import com.evolveum.prism.xml.ns._public.types_3.ModificationTypeType; import org.apache.commons.collections4.CollectionUtils; /** * @author Radovan Semancik * */ public abstract class ItemDelta<V extends PrismValue,D extends ItemDefinition> implements Itemable, DebugDumpable, Visitable, PathVisitable, Foreachable<V>, Serializable { /** * Name of the property */ protected QName elementName; /** * Parent path of the property (path to the property container) */ protected ItemPath parentPath; protected D definition; protected Collection<V> valuesToReplace = null; protected Collection<V> valuesToAdd = null; protected Collection<V> valuesToDelete = null; protected Collection<V> estimatedOldValues = null; transient private PrismContext prismContext; protected ItemDelta(D itemDefinition, PrismContext prismContext) { if (itemDefinition == null) { throw new IllegalArgumentException("Attempt to create item delta without a definition"); } //checkPrismContext(prismContext, itemDefinition); this.prismContext = prismContext; this.elementName = itemDefinition.getName(); this.parentPath = new ItemPath(); this.definition = itemDefinition; } protected ItemDelta(QName elementName, D itemDefinition, PrismContext prismContext) { //checkPrismContext(prismContext, itemDefinition); this.prismContext = prismContext; this.elementName = elementName; this.parentPath = new ItemPath(); this.definition = itemDefinition; } protected ItemDelta(ItemPath parentPath, QName elementName, D itemDefinition, PrismContext prismContext) { //checkPrismContext(prismContext, itemDefinition); ItemPath.checkNoSpecialSymbols(parentPath); this.prismContext = prismContext; this.elementName = elementName; this.parentPath = parentPath; this.definition = itemDefinition; } protected ItemDelta(ItemPath path, D itemDefinition, PrismContext prismContext) { //checkPrismContext(prismContext, itemDefinition); ItemPath.checkNoSpecialSymbols(path); this.prismContext = prismContext; if (path == null) { throw new IllegalArgumentException("Null path specified while creating item delta"); } if (path.isEmpty()) { this.elementName = null; } else { ItemPathSegment last = path.last(); if (!(last instanceof NameItemPathSegment)) { throw new IllegalArgumentException("Invalid delta path "+path+". Delta path must always point to item, not to value"); } this.elementName = ((NameItemPathSegment)last).getName(); this.parentPath = path.allExceptLast(); } this.definition = itemDefinition; } // currently unused; we allow deltas without prismContext, except for some operations (e.g. serialization to ItemDeltaType) // private void checkPrismContext(PrismContext prismContext, ItemDefinition itemDefinition) { // if (prismContext == null) { // throw new IllegalStateException("No prismContext in delta for " + itemDefinition); // } // } public QName getElementName() { return elementName; } public void setElementName(QName elementName) { this.elementName = elementName; } public ItemPath getParentPath() { return parentPath; } public void setParentPath(ItemPath parentPath) { this.parentPath = parentPath; } @Override public ItemPath getPath() { if (getParentPath() == null) { throw new IllegalStateException("No parent path in "+this); } return getParentPath().subPath(elementName); } public D getDefinition() { return definition; } public void setDefinition(D definition) { this.definition = definition; } @Override public void accept(Visitor visitor) { accept(visitor, true); } public void accept(Visitor visitor, boolean includeOldValues) { visitor.visit(this); if (getValuesToAdd() != null) { for (V pval : getValuesToAdd()) { pval.accept(visitor); } } if (getValuesToDelete() != null) { for (V pval : getValuesToDelete()) { pval.accept(visitor); } } if (getValuesToReplace() != null) { for (V pval : getValuesToReplace()) { pval.accept(visitor); } } if (includeOldValues && getEstimatedOldValues() != null) { for (V pval : getEstimatedOldValues()) { pval.accept(visitor); } } } // TODO think if estimated old values have to be visited as well @Override public void accept(Visitor visitor, ItemPath path, boolean recursive) { if (path == null || path.isEmpty()) { if (recursive) { accept(visitor); } else { visitor.visit(this); } } else { IdItemPathSegment idSegment = ItemPath.getFirstIdSegment(path); ItemPath rest = ItemPath.pathRestStartingWithName(path); if (idSegment == null || idSegment.isWildcard()) { // visit all values if (getValuesToAdd() != null) { for (V pval : getValuesToAdd()) { pval.accept(visitor, rest, recursive); } } if (getValuesToDelete() != null) { for (V pval : getValuesToDelete()) { pval.accept(visitor, rest, recursive); } } if (getValuesToReplace() != null) { for (V pval : getValuesToReplace()) { pval.accept(visitor, rest, recursive); } } } else { Long id = idSegment.getId(); acceptSet(getValuesToAdd(), id, visitor, rest, recursive); acceptSet(getValuesToDelete(), id, visitor, rest, recursive); acceptSet(getValuesToReplace(), id, visitor, rest, recursive); } } } private void acceptSet(Collection<V> set, Long id, Visitor visitor, ItemPath rest, boolean recursive) { if (set == null) { return; } for (V pval : set) { if (pval instanceof PrismContainerValue<?>) { PrismContainerValue<?> cval = (PrismContainerValue<?>)pval; if (id == null || id.equals(cval.getId())) { pval.accept(visitor, rest, recursive); } } else { throw new IllegalArgumentException("Attempt to fit container id to "+pval.getClass()); } } } public void applyDefinition(D definition) throws SchemaException { this.definition = definition; if (getValuesToAdd() != null) { for (V pval : getValuesToAdd()) { pval.applyDefinition(definition); } } if (getValuesToDelete() != null) { for (V pval : getValuesToDelete()) { pval.applyDefinition(definition); } } if (getValuesToReplace() != null) { for (V pval : getValuesToReplace()) { pval.applyDefinition(definition); } } } public static void applyDefinition(Collection<? extends ItemDelta> deltas, PrismObjectDefinition definition) throws SchemaException { for (ItemDelta itemDelta : deltas) { ItemPath path = itemDelta.getPath(); ItemDefinition itemDefinition = definition.findItemDefinition(path, ItemDefinition.class); if (itemDefinition == null) { throw new SchemaException("Object type " + definition.getTypeName() + " doesn't contain definition for path " + new XPathHolder(path).getXPathWithDeclarations(false)); } itemDelta.applyDefinition(itemDefinition); } } public boolean hasCompleteDefinition() { return getDefinition() != null; } public PrismContext getPrismContext() { return prismContext; } public abstract Class<? extends Item> getItemClass(); public Collection<V> getValuesToAdd() { return valuesToAdd; } public void clearValuesToAdd() { valuesToAdd = null; } public Collection<V> getValuesToDelete() { return valuesToDelete; } public void clearValuesToDelete() { valuesToDelete = null; } public Collection<V> getValuesToReplace() { return valuesToReplace; } public void clearValuesToReplace() { valuesToReplace = null; } public void addValuesToAdd(Collection<V> newValues) { if (newValues == null) { return; } for (V val : newValues) { addValueToAdd(val); } } public void addValuesToAdd(V... newValues) { for (V val : newValues) { addValueToAdd(val); } } public void addValueToAdd(V newValue) { if (valuesToReplace != null) { throw new IllegalStateException("Delta " + this + " already has values to replace ("+valuesToReplace+"), attempt to add value ("+newValue+") is an error"); } if (valuesToAdd == null) { valuesToAdd = newValueCollection(); } if (PrismValue.containsRealValue(valuesToAdd,newValue)) { return; } valuesToAdd.add(newValue); newValue.setParent(this); newValue.recompute(); } public boolean removeValueToAdd(PrismValue valueToRemove) { return removeValue(valueToRemove, valuesToAdd, false); } public boolean removeValueToDelete(PrismValue valueToRemove) { return removeValue(valueToRemove, valuesToDelete, true); } public boolean removeValueToReplace(PrismValue valueToRemove) { return removeValue(valueToRemove, valuesToReplace, false); } private boolean removeValue(PrismValue valueToRemove, Collection<V> set, boolean toDelete) { boolean removed = false; if (set == null) { return false; } Iterator<V> valuesIterator = set.iterator(); while (valuesIterator.hasNext()) { V existingValue = valuesIterator.next(); // TODO either make equalsRealValue return false if both PCVs have IDs and these IDs are different // TODO or include a special test condition here; see MID-3828 if (existingValue.equalsRealValue(valueToRemove) || toDelete && existingValue.representsSameValue(valueToRemove, false)) { // the same algorithm as when deleting the item value valuesIterator.remove(); removed = true; } } return removed; } public void mergeValuesToAdd(Collection<V> newValues) { if (newValues == null) { return; } for (V val : newValues) { mergeValueToAdd(val); } } public void mergeValuesToAdd(V[] newValues) { if (newValues == null) { return; } for (V val : newValues) { mergeValueToAdd(val); } } public void mergeValueToAdd(V newValue) { if (valuesToReplace != null) { if (!PrismValue.containsRealValue(valuesToReplace, newValue)) { valuesToReplace.add(newValue); newValue.setParent(this); } } else { if (!removeValueToDelete(newValue)) { addValueToAdd(newValue); } } } public void addValuesToDelete(Collection<V> newValues) { if (newValues == null) { return; } for (V val : newValues) { addValueToDelete(val); } } public void addValuesToDelete(V... newValues) { for (V val : newValues) { addValueToDelete(val); } } public void addValueToDelete(V newValue) { if (valuesToReplace != null) { throw new IllegalStateException("Delta " + this + " already has values to replace ("+valuesToReplace+"), attempt to set value to delete ("+newValue+")"); } if (valuesToDelete == null) { valuesToDelete = newValueCollection(); } if (containsEquivalentValue(valuesToDelete, newValue)) { return; } valuesToDelete.add(newValue); newValue.setParent(this); newValue.recompute(); } protected boolean containsEquivalentValue(Collection<V> collection, V value) { if (collection == null) { return false; } for (V colVal: collection) { if (isValueEquivalent(colVal, value)) { return true; } } return false; } protected boolean isValueEquivalent(V a, V b) { return a.equalsRealValue(b); } public void mergeValuesToDelete(Collection<V> newValues) { for (V val : newValues) { mergeValueToDelete(val); } } public void mergeValuesToDelete(V[] newValues) { for (V val : newValues) { mergeValueToDelete(val); } } public void mergeValueToDelete(V newValue) { if (valuesToReplace != null) { removeValueToReplace(newValue); } else { if (!removeValueToAdd(newValue)) { addValueToDelete(newValue); } } } public void resetValuesToAdd() { valuesToAdd = null; } public void resetValuesToDelete() { valuesToDelete = null; } public void resetValuesToReplace() { valuesToReplace = null; } public void setValuesToReplace(Collection<V> newValues) { if (newValues == null) { return; } if (valuesToAdd != null) { throw new IllegalStateException("Delta " + this + " already has values to add ("+valuesToAdd+"), attempt to set value to replace ("+newValues+")"); } if (valuesToDelete != null) { throw new IllegalStateException("Delta " + this + " already has values to delete, attempt to set value to replace"); } if (valuesToReplace == null) { valuesToReplace = newValueCollection(); } else { valuesToReplace.clear(); } for (V val : newValues) { valuesToReplace.add(val); val.setParent(this); val.recompute(); } } public void setValuesToReplace(V... newValues) { if (valuesToAdd != null) { throw new IllegalStateException("Delta " + this + " already has values to add, attempt to set value to replace"); } if (valuesToDelete != null) { throw new IllegalStateException("Delta " + this + " already has values to delete, attempt to set value to replace"); } if (valuesToReplace == null) { valuesToReplace = newValueCollection(); } else { valuesToReplace.clear(); } for (V val : newValues) { valuesToReplace.add(val); val.setParent(this); val.recompute(); } } /** * Sets empty value to replace. This efficiently means removing all values. */ public void setValueToReplace() { if (valuesToReplace == null) { valuesToReplace = newValueCollection(); } else { valuesToReplace.clear(); } } public void setValueToReplace(V newValue) { if (valuesToAdd != null) { throw new IllegalStateException("Delta " + this + " already has values to add, attempt to set value to replace"); } if (valuesToDelete != null) { throw new IllegalStateException("Delta " + this + " already has values to delete, attempt to set value to replace"); } if (valuesToReplace == null) { valuesToReplace = newValueCollection(); } else { valuesToReplace.clear(); } if (newValue != null) { valuesToReplace.add(newValue); newValue.setParent(this); newValue.recompute(); } } public void addValueToReplace(V newValue) { if (valuesToAdd != null) { throw new IllegalStateException("Delta " + this + " already has values to add, attempt to set value to replace"); } if (valuesToDelete != null) { throw new IllegalStateException("Delta " + this + " already has values to delete, attempt to set value to replace"); } if (valuesToReplace == null) { valuesToReplace = newValueCollection(); } if (newValue != null) { valuesToReplace.add(newValue); newValue.setParent(this); newValue.recompute(); } } public void mergeValuesToReplace(Collection<V> newValues) { // No matter what type the delta was before. We are just discarding all the previous // state as the replace that we are applying will overwrite that anyway. valuesToAdd = null; valuesToDelete = null; setValuesToReplace(newValues); } public void mergeValuesToReplace(V[] newValues) { // No matter what type the delta was before. We are just discarding all the previous // state as the replace that we are applying will overwrite that anyway. valuesToAdd = null; valuesToDelete = null; setValuesToReplace(newValues); } public void mergeValueToReplace(V newValue) { // No matter what type the delta was before. We are just discarding all the previous // state as the replace that we are applying will overwrite that anyway. valuesToAdd = null; valuesToDelete = null; setValueToReplace(newValue); } private Collection<V> newValueCollection() { return new ArrayList<V>(); } public boolean isValueToAdd(V value) { return isValueSet(value, false, valuesToAdd); } public boolean isValueToAdd(V value, boolean ignoreMetadata) { return isValueSet(value, ignoreMetadata, valuesToAdd); } public boolean isValueToDelete(V value) { return isValueSet(value, false, valuesToDelete); } public boolean isValueToDelete(V value, boolean ignoreMetadata) { return isValueSet(value, ignoreMetadata, valuesToDelete); } public boolean isValueToReplace(V value) { return isValueSet(value, false, valuesToReplace); } public boolean isValueToReplace(V value, boolean ignoreMetadata) { return isValueSet(value, ignoreMetadata, valuesToReplace); } private boolean isValueSet(V value, boolean ignoreMetadata, Collection<V> set) { if (set == null) { return false; } for (V myVal: set) { if (myVal.equals(value, ignoreMetadata)) { return true; } } return false; } public V getAnyValue() { V anyValue = getAnyValue(valuesToAdd); if (anyValue != null) { return anyValue; } anyValue = getAnyValue(valuesToDelete); if (anyValue != null) { return anyValue; } anyValue = getAnyValue(valuesToReplace); if (anyValue != null) { return anyValue; } return null; } private V getAnyValue(Collection<V> set) { if (set == null || set.isEmpty()) { return null; } return set.iterator().next(); } public boolean isEmpty() { if (valuesToAdd == null && valuesToDelete == null && valuesToReplace == null) { return true; } return false; } // TODO merge with isEmpty public boolean isInFactEmpty() { return CollectionUtils.isEmpty(valuesToAdd) && CollectionUtils.isEmpty(valuesToDelete) && valuesToReplace == null; } public static boolean isEmpty(ItemDeltaType itemDeltaType) { if (itemDeltaType == null) { return true; } if (itemDeltaType.getModificationType() == ModificationTypeType.REPLACE) { return false; } return !itemDeltaType.getValue().isEmpty(); } public boolean addsAnyValue() { return hasAnyValue(valuesToAdd) || hasAnyValue(valuesToReplace); } private boolean hasAnyValue(Collection<V> set) { return (set != null && !set.isEmpty()); } public void foreach(Processor<V> processor) { foreachSet(processor, valuesToAdd); foreachSet(processor, valuesToDelete); foreachSet(processor, valuesToReplace); } private void foreachSet(Processor<V> processor, Collection<V> set) { if (set == null) { return; } for (V val: set) { processor.process(val); } } /** * Returns estimated state of the old value before the delta is applied. * This information is not entirely reliable. The state might change * between the value is read and the delta is applied. This is property * is optional and even if provided it is only for for informational * purposes. * * If this method returns null then it should be interpreted as "I do not know". * In that case the delta has no information about the old values. * If this method returns empty collection then it should be interpreted that * we know that there were no values in this item before the delta was applied. * * @return estimated state of the old value before the delta is applied (may be null). */ public Collection<V> getEstimatedOldValues() { return estimatedOldValues; } public void setEstimatedOldValues(Collection<V> estimatedOldValues) { this.estimatedOldValues = estimatedOldValues; } public void addEstimatedOldValues(Collection<V> newValues) { for (V val : newValues) { addEstimatedOldValue(val); } } public void addEstimatedOldValues(V... newValues) { for (V val : newValues) { addEstimatedOldValue(val); } } public void addEstimatedOldValue(V newValue) { if (estimatedOldValues == null) { estimatedOldValues = newValueCollection(); } if (PrismValue.containsRealValue(estimatedOldValues,newValue)) { return; } estimatedOldValues.add(newValue); newValue.setParent(this); newValue.recompute(); } public void normalize() { normalize(valuesToAdd); normalize(valuesToDelete); normalize(valuesToReplace); } private void normalize(Collection<V> set) { if (set == null) { return; } for (V value : set) { value.normalize(); } } public boolean isReplace() { return (valuesToReplace != null); } public boolean isAdd() { return (valuesToAdd != null && !valuesToAdd.isEmpty()); } public boolean isDelete() { return (valuesToDelete != null && !valuesToDelete.isEmpty()); } public void clear() { valuesToReplace = null; valuesToAdd = null; valuesToDelete = null; } public static <T> PropertyDelta<T> findPropertyDelta(Collection<? extends ItemDelta> deltas, QName propertyName) { return findPropertyDelta(deltas, new ItemPath(propertyName)); } public static <T> PropertyDelta<T> findPropertyDelta(Collection<? extends ItemDelta> deltas, ItemPath parentPath, QName propertyName) { return findPropertyDelta(deltas, new ItemPath(parentPath, propertyName)); } public static <T> PropertyDelta<T> findPropertyDelta(Collection<? extends ItemDelta> deltas, ItemPath propertyPath) { return findItemDelta(deltas, propertyPath, PropertyDelta.class); } public static <X extends Containerable> ContainerDelta<X> findContainerDelta(Collection<? extends ItemDelta> deltas, ItemPath propertyPath) { return findItemDelta(deltas, propertyPath, ContainerDelta.class); } public static <X extends Containerable> ContainerDelta<X> findContainerDelta(Collection<? extends ItemDelta> deltas, QName name) { return findContainerDelta(deltas, new ItemPath(name)); } public static <DD extends ItemDelta> DD findItemDelta(Collection<? extends ItemDelta> deltas, ItemPath propertyPath, Class<DD> deltaType) { if (deltas == null) { return null; } for (ItemDelta<?,?> delta : deltas) { if (deltaType.isAssignableFrom(delta.getClass()) && delta.getPath().equivalent(propertyPath)) { return (DD) delta; } if ((delta instanceof ContainerDelta<?>) && delta.getPath().isSubPath(propertyPath)) { return (DD) ((ContainerDelta)delta).getSubDelta(propertyPath.substract(delta.getPath())); } } return null; } public static Collection<? extends ItemDelta<?,?>> findItemDeltasSubPath(Collection<? extends ItemDelta<?,?>> deltas, ItemPath itemPath) { Collection<ItemDelta<?,?>> foundDeltas = new ArrayList<ItemDelta<?,?>>(); if (deltas == null) { return foundDeltas; } for (ItemDelta<?,?> delta : deltas) { if (itemPath.isSubPath(delta.getPath())) { foundDeltas.add(delta); } } return foundDeltas; } public static <D extends ItemDelta> D findItemDelta(Collection<? extends ItemDelta> deltas, QName itemName, Class<D> deltaType) { return findItemDelta(deltas, new ItemPath(itemName), deltaType); } public static ReferenceDelta findReferenceModification(Collection<? extends ItemDelta> deltas, QName itemName) { return findItemDelta(deltas, itemName, ReferenceDelta.class); } public static <D extends ItemDelta> void removeItemDelta(Collection<? extends ItemDelta> deltas, ItemPath propertyPath, Class<D> deltaType) { if (deltas == null) { return; } Iterator<? extends ItemDelta> deltasIterator = deltas.iterator(); while (deltasIterator.hasNext()) { ItemDelta<?,?> delta = deltasIterator.next(); if (deltaType.isAssignableFrom(delta.getClass()) && delta.getPath().equivalent(propertyPath)) { deltasIterator.remove(); } } } public static <D extends ItemDelta> void removeItemDelta(Collection<? extends ItemDelta> deltas, ItemDelta deltaToRemove) { if (deltas == null) { return; } Iterator<? extends ItemDelta> deltasIterator = deltas.iterator(); while (deltasIterator.hasNext()) { ItemDelta<?,?> delta = deltasIterator.next(); if (delta.equals(deltaToRemove)) { deltasIterator.remove(); } } } /** * Filters out all delta values that are meaningless to apply. E.g. removes all values to add that the property already has, * removes all values to delete that the property does not have, etc. */ public ItemDelta<V,D> narrow(PrismObject<? extends Objectable> object) { return narrow(object, null); } /** * Filters out all delta values that are meaningless to apply. E.g. removes all values to add that the property already has, * removes all values to delete that the property does not have, etc. */ public ItemDelta<V,D> narrow(PrismObject<? extends Objectable> object, Comparator<V> comparator) { Item<V,D> currentItem = (Item<V,D>) object.findItem(getPath()); if (currentItem == null) { if (valuesToDelete != null) { ItemDelta<V,D> clone = clone(); clone.valuesToDelete = null; return clone; } else { // Nothing to narrow return this; } } else { ItemDelta<V,D> clone = clone(); if (clone.valuesToDelete != null) { Iterator<V> iterator = clone.valuesToDelete.iterator(); while (iterator.hasNext()) { V valueToDelete = iterator.next(); if (!currentItem.contains(valueToDelete, true, comparator)) { iterator.remove(); } } if (clone.valuesToDelete.isEmpty()) { clone.valuesToDelete = null; } } if (clone.valuesToAdd != null) { Iterator<V> iterator = clone.valuesToAdd.iterator(); while (iterator.hasNext()) { V valueToDelete = iterator.next(); if (currentItem.contains(valueToDelete, true, comparator)) { iterator.remove(); } } if (clone.valuesToAdd.isEmpty()) { clone.valuesToAdd = null; } } return clone; } } /** * Checks if the delta is redundant w.r.t. current state of the object. * I.e. if it changes the current object state. */ public boolean isRedundant(PrismObject<? extends Objectable> object) { Comparator<V> comparator = new Comparator<V>() { @Override public int compare(V o1, V o2) { if (o1.equalsComplex(o2, true, false)) { return 0; } else { return 1; } } }; return isRedundant(object, comparator); } public boolean isRedundant(PrismObject<? extends Objectable> object, Comparator<V> comparator) { Item<V,D> currentItem = (Item<V,D>) object.findItem(getPath()); if (currentItem == null) { if (valuesToReplace != null) { return valuesToReplace.isEmpty(); } return !hasAnyValue(valuesToAdd); } else { if (valuesToReplace != null) { return MiscUtil.unorderedCollectionCompare(valuesToReplace, currentItem.getValues(), comparator); } ItemDelta<V,D> narrowed = narrow(object, comparator); boolean narrowedNotEmpty = narrowed.hasAnyValue(narrowed.valuesToAdd) || narrowed.hasAnyValue(narrowed.valuesToDelete); return narrowedNotEmpty; } } public void validate() throws SchemaException { validate(null); } public void validate(String contextDescription) throws SchemaException { if (definition == null) { throw new IllegalStateException("Attempt to validate delta without a definition: "+this); } if (definition.isSingleValue()) { if (valuesToAdd != null && valuesToAdd.size() > 1) { throw new SchemaException("Attempt to add "+valuesToAdd.size()+" values to a single-valued item "+getPath() + (contextDescription == null ? "" : " in "+contextDescription) + "; values: "+valuesToAdd); } if (valuesToReplace != null && valuesToReplace.size() > 1) { throw new SchemaException("Attempt to replace "+valuesToReplace.size()+" values to a single-valued item "+getPath() + (contextDescription == null ? "" : " in "+contextDescription) + "; values: "+valuesToReplace); } } if (definition.isMandatory()) { if (valuesToReplace != null && valuesToReplace.isEmpty()) { throw new SchemaException("Attempt to clear all values of a mandatory item "+getPath() + (contextDescription == null ? "" : " in "+contextDescription)); } } } public static void checkConsistence(Collection<? extends ItemDelta> deltas) { checkConsistence(deltas, ConsistencyCheckScope.THOROUGH); } public static void checkConsistence(Collection<? extends ItemDelta> deltas, ConsistencyCheckScope scope) { checkConsistence(deltas, false, false, scope); } public static void checkConsistence(Collection<? extends ItemDelta> deltas, boolean requireDefinition, boolean prohibitRaw, ConsistencyCheckScope scope) { Map<ItemPath,ItemDelta<?,?>> pathMap = new HashMap<>(); for (ItemDelta<?,?> delta : deltas) { delta.checkConsistence(requireDefinition, prohibitRaw, scope); int matches = 0; for (ItemDelta<?,?> other : deltas) { if (other == delta) { matches++; } else if (other.equals(delta)) { throw new IllegalStateException("Duplicate item delta: "+delta+" and "+other); } } if (matches > 1) { throw new IllegalStateException("The delta "+delta+" appears multiple times in the modification list"); } } } public void checkConsistence() { checkConsistence(ConsistencyCheckScope.THOROUGH); } public void checkConsistence(ConsistencyCheckScope scope) { checkConsistence(false, false, scope); } public void checkConsistence(boolean requireDefinition, boolean prohibitRaw, ConsistencyCheckScope scope) { if (scope.isThorough() && parentPath == null) { throw new IllegalStateException("Null parent path in " + this); } if (scope.isThorough() && requireDefinition && definition == null) { throw new IllegalStateException("Null definition in "+this); } if (scope.isThorough() && valuesToReplace != null && (valuesToAdd != null || valuesToDelete != null)) { throw new IllegalStateException( "The delta cannot be both 'replace' and 'add/delete' at the same time"); } assertSetConsistence(valuesToReplace, "replace", requireDefinition, prohibitRaw, scope); assertSetConsistence(valuesToAdd, "add", requireDefinition, prohibitRaw, scope); assertSetConsistence(valuesToDelete, "delete", requireDefinition, prohibitRaw, scope); } private void assertSetConsistence(Collection<V> values, String type, boolean requireDefinitions, boolean prohibitRaw, ConsistencyCheckScope scope) { if (values == null) { return; } // This may be not be 100% correct but we can tolerate it now // if (values.isEmpty()) { // throw new // IllegalStateException("The "+type+" values set in "+this+" is not-null but it is empty"); // } for (V val : values) { if (scope.isThorough()) { if (val == null) { throw new IllegalStateException("Null value in the " + type + " values set in " + this); } if (val.getParent() != this) { throw new IllegalStateException("Wrong parent for " + val + " in " + type + " values set in " + this + ": " + val.getParent()); } } val.checkConsistenceInternal(this, requireDefinitions, prohibitRaw, scope); } } /** * Distributes the replace values of this delta to add and delete with * respect to provided existing values. */ public void distributeReplace(Collection<V> existingValues) { Collection<V> origValuesToReplace = getValuesToReplace(); // We have to clear before we distribute, otherwise there will be replace/add or replace/delete conflict clearValuesToReplace(); if (existingValues != null) { for (V existingVal : existingValues) { if (!isIn(origValuesToReplace, existingVal)) { addValueToDelete((V) existingVal.clone()); } } } for (V replaceVal : origValuesToReplace) { if (!isIn(existingValues, replaceVal) && !isIn(getValuesToAdd(), replaceVal)) { addValueToAdd((V) replaceVal.clone()); } } } private boolean isIn(Collection<V> values, V val) { if (values == null) { return false; } for (V v : values) { if (v.equalsRealValue(val)) { return true; } } return false; } /** * Merge specified delta to this delta. This delta is assumed to be * chronologically earlier, delta provided in the parameter is chronilogically later. */ public void merge(ItemDelta<V,D> deltaToMerge) { if (deltaToMerge.isEmpty()) { return; } if (deltaToMerge.valuesToReplace != null) { mergeValuesToReplace(PrismValue.cloneValues(deltaToMerge.valuesToReplace)); } else { if (deltaToMerge.valuesToAdd != null) { mergeValuesToAdd(PrismValue.cloneValues(deltaToMerge.valuesToAdd)); } if (deltaToMerge.valuesToDelete != null) { mergeValuesToDelete(PrismValue.cloneValues(deltaToMerge.valuesToDelete)); } } // We do not want to clean up the sets during merging (e.g. in removeValue methods) because the set // may become empty and the a values may be added later. So just clean it up when all is done. removeEmptySets(); } private void removeEmptySets() { // Do not remove replace set, even if it is empty. // Empty replace set is not the same as no replace set if (valuesToAdd != null && valuesToAdd.isEmpty()) { valuesToAdd = null; } if (valuesToDelete != null && valuesToDelete.isEmpty()) { valuesToDelete = null; } } /** * Transforms the delta to the simplest (and safest) form. E.g. it will transform add delta for * single-value properties to replace delta. */ public void simplify() { ItemDefinition itemDefinition = getDefinition(); if (itemDefinition == null) { throw new IllegalStateException("Attempt to simplify delta without a definition"); } if (itemDefinition.isSingleValue() && isAdd()) { valuesToReplace = valuesToAdd; valuesToAdd = null; valuesToDelete = null; } } private void cleanupAllTheWayUp(Item<?,?> item) { if (item.isEmpty()) { PrismValue itemParent = item.getParent(); if (itemParent != null) { if (itemParent instanceof PrismContainerValue<?>) { ((PrismContainerValue<?>)itemParent).remove(item); if (itemParent.isEmpty()) { Itemable itemGrandparent = itemParent.getParent(); if (itemGrandparent != null) { if (itemGrandparent instanceof Item<?,?>) { cleanupAllTheWayUp((Item<?,?>)itemGrandparent); } } } } } } } public static void applyTo(Collection<? extends ItemDelta> deltas, PrismContainer propertyContainer) throws SchemaException { for (ItemDelta delta : deltas) { delta.applyTo(propertyContainer); } } public static void applyTo(Collection<? extends ItemDelta> deltas, PrismContainerValue propertyContainerValue) throws SchemaException { for (ItemDelta delta : deltas) { delta.applyTo(propertyContainerValue); } } public static void applyToMatchingPath(Collection<? extends ItemDelta> deltas, PrismContainer propertyContainer) throws SchemaException { for (ItemDelta delta : deltas) { delta.applyToMatchingPath(propertyContainer); } } public void applyTo(PrismContainerValue containerValue) throws SchemaException { ItemPath deltaPath = getPath(); if (ItemPath.isNullOrEmpty(deltaPath)) { throw new IllegalArgumentException("Cannot apply empty-path delta " + this + " directly to a PrismContainerValue " + containerValue); } Item subItem = containerValue.findOrCreateItem(deltaPath, getItemClass(), getDefinition()); applyToMatchingPath(subItem); } public void applyTo(Item item) throws SchemaException { ItemPath itemPath = item.getPath(); ItemPath deltaPath = getPath(); CompareResult compareComplex = itemPath.compareComplex(deltaPath); if (compareComplex == CompareResult.EQUIVALENT) { applyToMatchingPath(item); cleanupAllTheWayUp(item); } else if (compareComplex == CompareResult.SUBPATH) { if (item instanceof PrismContainer<?>) { PrismContainer<?> container = (PrismContainer<?>)item; ItemPath remainderPath = deltaPath.remainder(itemPath); Item subItem = container.findOrCreateItem(remainderPath, getItemClass(), getDefinition()); applyToMatchingPath(subItem); } else { throw new SchemaException("Cannot apply delta "+this+" to "+item+" as delta path is below the item path and the item is not a container"); } } else if (compareComplex == CompareResult.SUPERPATH) { throw new SchemaException("Cannot apply delta "+this+" to "+item+" as delta path is above the item path"); } else if (compareComplex == CompareResult.NO_RELATION) { throw new SchemaException("Cannot apply delta "+this+" to "+item+" as paths do not match"); } } /** * Applies delta to item were path of the delta and path of the item matches (skips path checks). */ public void applyToMatchingPath(Item item) throws SchemaException { if (item == null) { return; } if (item.getDefinition() == null && getDefinition() != null){ item.applyDefinition(getDefinition()); } if (!getItemClass().isAssignableFrom(item.getClass())) { throw new SchemaException("Cannot apply delta "+this+" to "+item+" because the deltas is applicable only to "+getItemClass().getSimpleName()); } if (valuesToReplace != null) { item.replaceAll(PrismValue.cloneCollection(valuesToReplace)); // Application of delta might have removed values therefore leaving empty items. // Those needs to be cleaned-up (removed) as empty item is not a legal state. cleanupAllTheWayUp(item); return; } if (valuesToAdd != null) { if (item.getDefinition() != null && item.getDefinition().isSingleValue()) { item.replaceAll(PrismValue.cloneCollection(valuesToAdd)); } else { for (V valueToAdd : valuesToAdd) { if (!item.containsEquivalentValue(valueToAdd)) { item.add(valueToAdd.clone()); } } } } if (valuesToDelete != null) { item.removeAll(valuesToDelete); } // Application of delta might have removed values therefore leaving empty items. // Those needs to be cleaned-up (removed) as empty item is not a legal state. cleanupAllTheWayUp(item); } public ItemDelta<?,?> getSubDelta(ItemPath path) { return this; } public boolean isApplicableTo(Item item) { if (item == null) { return false; } if (!isApplicableToType(item)) { return false; } // TODO: maybe check path? return true; } protected abstract boolean isApplicableToType(Item item); public static void accept(Collection<? extends ItemDelta> modifications, Visitor visitor, ItemPath path, boolean recursive) { for (ItemDelta modification: modifications) { ItemPath modPath = modification.getPath(); CompareResult rel = modPath.compareComplex(path); if (rel == CompareResult.EQUIVALENT) { modification.accept(visitor, null, recursive); } else if (rel == CompareResult.SUBPATH) { modification.accept(visitor, path.substract(modPath), recursive); } } } /** * Returns the "new" state of the property - the state that would be after * the delta is applied. */ public Item<V,D> getItemNew() throws SchemaException { return getItemNew(null); } /** * Returns the "new" state of the property - the state that would be after * the delta is applied. */ public Item<V,D> getItemNew(Item<V,D> itemOld) throws SchemaException { if (definition == null) { throw new IllegalStateException("No definition in "+this); } Item<V,D> itemNew; if (itemOld == null) { if (isEmpty()) { return null; } itemNew = definition.instantiate(getElementName()); } else { itemNew = itemOld.clone(); } applyTo(itemNew); return itemNew; } public Item<V,D> getItemNewMatchingPath(Item<V,D> itemOld) throws SchemaException { if (definition == null) { throw new IllegalStateException("No definition in "+this); } Item<V,D> itemNew; if (itemOld == null) { if (isEmpty()) { return null; } itemNew = definition.instantiate(getElementName()); } else { itemNew = itemOld.clone(); } applyToMatchingPath(itemNew); return itemNew; } /** * Returns true if the other delta is a complete subset of this delta. * I.e. if all the statements of the other delta are already contained * in this delta. As a consequence it also returns true if the two * deltas are equal. */ public boolean contains(ItemDelta<V,D> other) { return contains(other, PrismConstants.EQUALS_DEFAULT_IGNORE_METADATA, PrismConstants.EQUALS_DEFAULT_IS_LITERAL); } /** * Returns true if the other delta is a complete subset of this delta. * I.e. if all the statements of the other delta are already contained * in this delta. As a consequence it also returns true if the two * deltas are equal. */ public boolean contains(ItemDelta<V,D> other, boolean ignoreMetadata, boolean isLiteral) { if (!this.getPath().equivalent(other.getPath())) { return false; } if (!PrismValue.containsAll(this.valuesToAdd, other.valuesToAdd, ignoreMetadata, isLiteral)) { return false; } if (!PrismValue.containsAll(this.valuesToDelete, other.valuesToDelete, ignoreMetadata, isLiteral)) { return false; } if (!PrismValue.containsAll(this.valuesToReplace, other.valuesToReplace, ignoreMetadata, isLiteral)) { return false; } return true; } public void filterValues(Function<V, Boolean> function) { filterValuesSet(this.valuesToAdd, function); filterValuesSet(this.valuesToDelete, function); filterValuesSet(this.valuesToReplace, function); } private void filterValuesSet(Collection<V> set, Function<V, Boolean> function) { if (set == null) { return; } Iterator<V> iterator = set.iterator(); while (iterator.hasNext()) { Boolean keep = function.apply(iterator.next()); if (keep == null || !keep) { iterator.remove(); } } } public abstract ItemDelta<V,D> clone(); protected void copyValues(ItemDelta<V,D> clone) { clone.definition = this.definition; clone.elementName = this.elementName; clone.parentPath = this.parentPath; clone.valuesToAdd = cloneSet(clone, this.valuesToAdd); clone.valuesToDelete = cloneSet(clone, this.valuesToDelete); clone.valuesToReplace = cloneSet(clone, this.valuesToReplace); clone.estimatedOldValues = cloneSet(clone, this.estimatedOldValues); } private Collection<V> cloneSet(ItemDelta clone, Collection<V> thisSet) { if (thisSet == null) { return null; } Collection<V> clonedSet = newValueCollection(); for (V thisVal : thisSet) { V clonedVal = (V) thisVal.clone(); clonedVal.setParent(clone); clonedSet.add(clonedVal); } return clonedSet; } public static <D extends ItemDelta<?,?>> Collection<D> cloneCollection(Collection<D> orig) { if (orig == null) { return null; } Collection<D> clone = new ArrayList<>(orig.size()); for (D delta: orig) { clone.add((D)delta.clone()); } return clone; } @Deprecated public static <IV extends PrismValue,ID extends ItemDefinition> PrismValueDeltaSetTriple<IV> toDeltaSetTriple(Item<IV,ID> item, ItemDelta<IV,ID> delta, boolean oldValuesValid, boolean newValuesValid) { if (item == null && delta == null) { return null; } if (!oldValuesValid && !newValuesValid) { return null; } if (oldValuesValid && !newValuesValid) { // There were values but they no longer are -> everything to minus set PrismValueDeltaSetTriple<IV> triple = new PrismValueDeltaSetTriple<IV>(); if (item != null) { triple.addAllToMinusSet(item.getValues()); } return triple; } if (item == null && delta != null) { return delta.toDeltaSetTriple(item); } if (delta == null || (!oldValuesValid && newValuesValid)) { PrismValueDeltaSetTriple<IV> triple = new PrismValueDeltaSetTriple<IV>(); if (item != null) { triple.addAllToZeroSet(item.getValues()); } return triple; } return delta.toDeltaSetTriple(item); } public static <IV extends PrismValue,ID extends ItemDefinition> PrismValueDeltaSetTriple<IV> toDeltaSetTriple(Item<IV,ID> item, ItemDelta<IV,ID> delta) { if (item == null && delta == null) { return null; } if (delta == null) { PrismValueDeltaSetTriple<IV> triple = new PrismValueDeltaSetTriple<IV>(); triple.addAllToZeroSet(PrismValue.cloneCollection(item.getValues())); return triple; } return delta.toDeltaSetTriple(item); } public PrismValueDeltaSetTriple<V> toDeltaSetTriple() { return toDeltaSetTriple(null); } public PrismValueDeltaSetTriple<V> toDeltaSetTriple(Item<V,D> itemOld) { PrismValueDeltaSetTriple<V> triple = new PrismValueDeltaSetTriple<V>(); if (isReplace()) { triple.getPlusSet().addAll(PrismValue.cloneCollection(getValuesToReplace())); if (itemOld != null) { triple.getMinusSet().addAll(PrismValue.cloneCollection(itemOld.getValues())); } return triple; } if (isAdd()) { triple.getPlusSet().addAll(PrismValue.cloneCollection(getValuesToAdd())); } if (isDelete()) { triple.getMinusSet().addAll(PrismValue.cloneCollection(getValuesToDelete())); } if (itemOld != null && itemOld.getValues() != null) { for (V itemVal: itemOld.getValues()) { if (!PrismValue.containsRealValue(valuesToDelete, itemVal) && !PrismValue.containsRealValue(valuesToAdd, itemVal)) { triple.getZeroSet().add((V) itemVal.clone()); } } } return triple; } public void assertDefinitions(String sourceDescription) throws SchemaException { assertDefinitions(false, sourceDescription); } public void assertDefinitions(boolean tolarateRawValues, String sourceDescription) throws SchemaException { if (tolarateRawValues && isRaw()) { return; } if (definition == null) { throw new SchemaException("No definition in "+this+" in "+sourceDescription); } assertDefinitions(tolarateRawValues, valuesToAdd, "values to add in "+sourceDescription); assertDefinitions(tolarateRawValues, valuesToReplace, "values to replace in "+sourceDescription); assertDefinitions(tolarateRawValues, valuesToDelete, "values to delete in "+sourceDescription); } private void assertDefinitions(boolean tolarateRawValues, Collection<V> values, String sourceDescription) throws SchemaException { if (values == null) { return; } for(V val: values) { if (val instanceof PrismContainerValue<?>) { PrismContainerValue<?> cval = (PrismContainerValue<?>)val; if (cval.getItems() == null){ continue; } for (Item<?,?> item: cval.getItems()) { item.assertDefinitions(tolarateRawValues, cval.toString()+" in "+sourceDescription); } } } } public boolean isRaw() { Boolean isRaw = MiscUtil.and(isRawSet(valuesToAdd), isRawSet(valuesToReplace), isRawSet(valuesToDelete)); if (isRaw == null) { return false; } return isRaw; } private Boolean isRawSet(Collection<V> set) { if (set == null) { return null; } for (V val: set) { if (!val.isRaw()) { return false; } } return true; } public void revive(PrismContext prismContext) throws SchemaException { this.prismContext = prismContext; reviveSet(valuesToAdd, prismContext); reviveSet(valuesToDelete, prismContext); reviveSet(valuesToReplace, prismContext); } private void reviveSet(Collection<V> set, PrismContext prismContext) throws SchemaException { if (set == null) { return; } for (V val: set) { val.revive(prismContext); } } public void applyDefinition(D itemDefinition, boolean force) throws SchemaException { if (this.definition != null && !force) { return; } this.definition = itemDefinition; applyDefinitionSet(valuesToAdd, itemDefinition, force); applyDefinitionSet(valuesToReplace, itemDefinition, force); applyDefinitionSet(valuesToDelete, itemDefinition, force); } private void applyDefinitionSet(Collection<V> set, ItemDefinition itemDefinition, boolean force) throws SchemaException { if (set == null) { return; } for (V val: set) { val.applyDefinition(itemDefinition, force); } } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((definition == null) ? 0 : definition.hashCode()); result = prime * result + ((elementName == null) ? 0 : elementName.hashCode()); // if equals uses parentPath.equivalent call, we should not use default implementation of parentPath.hashCode //result = prime * result + ((parentPath == null) ? 0 : parentPath.hashCode()); return result; } /** * Deltas are equivalent if they have the same result when * applied to an object. I.e. meta-data and other "decorations" * such as old values are not considered in this comparison. */ public boolean equivalent(ItemDelta other) { if (elementName == null) { if (other.elementName != null) return false; } else if (!QNameUtil.match(elementName, elementName)) return false; if (parentPath == null) { if (other.parentPath != null) return false; } else if (!parentPath.equivalent(other.parentPath)) return false; if (!equivalentSetRealValue(this.valuesToAdd, other.valuesToAdd, false)) return false; if (!equivalentSetRealValue(this.valuesToDelete, other.valuesToDelete, true)) return false; if (!equivalentSetRealValue(this.valuesToReplace, other.valuesToReplace, false)) return false; return true; } public static boolean hasEquivalent(Collection<? extends ItemDelta> col, ItemDelta delta) { for (ItemDelta colItem: col) { if (colItem.equivalent(delta)) { return true; } } return false; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; ItemDelta other = (ItemDelta) obj; if (definition == null) { if (other.definition != null) return false; } else if (!definition.equals(other.definition)) return false; if (elementName == null) { if (other.elementName != null) return false; } else if (!elementName.equals(other.elementName)) return false; if (parentPath == null) { if (other.parentPath != null) return false; } else if (!parentPath.equivalent(other.parentPath)) // or "equals" ? return false; if (!equivalentSetRealValue(this.valuesToAdd, other.valuesToAdd, false)) return false; if (!equivalentSetRealValue(this.valuesToDelete, other.valuesToDelete, true)) // TODO ok? return false; if (!equivalentSetRealValue(this.valuesToReplace, other.valuesToReplace, false)) return false; if (!equivalentSetRealValue(this.estimatedOldValues, other.estimatedOldValues, false)) return false; return true; } private boolean equivalentSetRealValue(Collection<V> thisValue, Collection<V> otherValues, boolean isDelete) { return MiscUtil.unorderedCollectionEquals(thisValue, otherValues, (v1, v2) -> { if (v1 != null && v2 != null) { if (!isDelete || !(v1 instanceof PrismContainerValue) || !(v2 instanceof PrismContainerValue)) { // Here it is questionable if we should consider adding "assignment id=1 (A)" and "assignment id=2 (A)" // - i.e. assignments with the same real value but different identifiers - the same delta. // Historically, we considered it as such. But the question is if it's correct. return v1.equalsRealValue(v2); } else { // But for container values to be deleted, they can be referred to either using IDs or values. // If content is used - but no IDs - the content must be equal. // If IDs are used - and are the same - the content is irrelevant. // The problem is if one side has content with ID, and the other has the same content without ID. // This might have the same or different effect, depending on the content it is applied to. // See MID-3828 return (v1.equalsRealValue(v2) && !differentIds(v1, v2)) || v1.representsSameValue(v2, false); } } else { return false; } }); } private boolean differentIds(PrismValue v1, PrismValue v2) { Long id1 = v1 instanceof PrismContainerValue ? ((PrismContainerValue) v1).getId() : null; Long id2 = v2 instanceof PrismContainerValue ? ((PrismContainerValue) v2).getId() : null; return id1 != null && id2 != null && id1.longValue() != id2.longValue(); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(getClass().getSimpleName()).append("("); sb.append(parentPath).append(" / ").append(PrettyPrinter.prettyPrint(elementName)); if (valuesToReplace != null) { sb.append(", REPLACE"); } if (valuesToAdd != null) { sb.append(", ADD"); } if (valuesToDelete != null) { sb.append(", DELETE"); } sb.append(")"); return sb.toString(); } @Override public String debugDump() { return debugDump(0); } @Override public String debugDump(int indent) { StringBuilder sb = new StringBuilder(); DebugUtil.indentDebugDump(sb, indent); if (DebugUtil.isDetailedDebugDump()) { sb.append(getClass().getSimpleName()).append(":"); } ItemPath path = getPath(); sb.append(path); if (definition != null && DebugUtil.isDetailedDebugDump()) { sb.append(" def"); } if (valuesToReplace != null) { sb.append("\n"); dumpValues(sb, "REPLACE", valuesToReplace, indent + 1); } if (valuesToAdd != null) { sb.append("\n"); dumpValues(sb, "ADD", valuesToAdd, indent + 1); } if (valuesToDelete != null) { sb.append("\n"); dumpValues(sb, "DELETE", valuesToDelete, indent + 1); } if (estimatedOldValues != null) { sb.append("\n"); dumpValues(sb, "OLD", estimatedOldValues, indent + 1); } return sb.toString(); } protected void dumpValues(StringBuilder sb, String label, Collection<V> values, int indent) { DebugUtil.indentDebugDump(sb, indent); sb.append(label).append(": "); if (values == null) { sb.append("(null)"); } else { if (DebugUtil.isDetailedDebugDump()) { for (V value: values) { sb.append("\n"); sb.append(value.debugDump(indent + 1)); } } else { Iterator<V> i = values.iterator(); while (i.hasNext()) { V value = i.next(); sb.append(value.toHumanReadableString()); if (i.hasNext()) { sb.append(", "); } } } } } public static void addAll(Collection<? extends ItemDelta> modifications, Collection<? extends ItemDelta> deltasToAdd) { if (deltasToAdd == null) { return; } for (ItemDelta deltaToAdd: deltasToAdd) { if (!modifications.contains(deltaToAdd)) { ((Collection)modifications).add(deltaToAdd); } } } public static void merge(Collection<? extends ItemDelta> modifications, ItemDelta delta) { for (ItemDelta modification: modifications) { if (modification.getPath().equals(delta.getPath())) { modification.merge(delta); return; } } ((Collection)modifications).add(delta); } public static void mergeAll(Collection<? extends ItemDelta> modifications, Collection<? extends ItemDelta> deltasToMerge) { if (deltasToMerge == null) { return; } for (ItemDelta deltaToMerge: deltasToMerge) { merge(modifications, deltaToMerge); } } public void addToReplaceDelta() { if (isReplace()) { throw new IllegalStateException("Delta is a REPLACE delta, not an ADD one"); } valuesToReplace = valuesToAdd; if (valuesToReplace == null) { valuesToReplace = new ArrayList<>(0); } valuesToAdd = null; } public ItemDelta<V,D> createReverseDelta() { ItemDelta<V,D> reverseDelta = clone(); Collection<V> cloneValuesToAdd = reverseDelta.valuesToAdd; Collection<V> cloneValuesToDelete = reverseDelta.valuesToDelete; Collection<V> cloneValuesToReplace = reverseDelta.valuesToReplace; Collection<V> cloneEstimatedOldValues = reverseDelta.estimatedOldValues; reverseDelta.valuesToAdd = cloneValuesToDelete; reverseDelta.valuesToDelete = cloneValuesToAdd; if (cloneValuesToReplace != null) { reverseDelta.valuesToReplace = cloneEstimatedOldValues; if (reverseDelta.valuesToReplace == null) { // We want to force replace delta here. Otherwise the reverse delta // may look like empty. We do not explicitly have old values here, // so this is a bit tricky and not entirely correct. But we can live // with that for now. reverseDelta.valuesToReplace = new ArrayList<>(0); } reverseDelta.estimatedOldValues = cloneValuesToReplace; } else { // TODO: what about estimatedOldValues here? } return reverseDelta; } public V findValueToAddOrReplace(V value) { V found = findValue(valuesToAdd, value); if (found == null) { found = findValue(valuesToReplace, value); } return found; } private V findValue(Collection<V> values, V value) { if (values == null) { return null; } return values.stream() .filter(v -> !differentIds(v, value) && v.equals(value, true)) .findFirst().orElse(null); } /** * Set origin type to all values and subvalues */ public void setOriginTypeRecursive(final OriginType originType) { accept((visitable) -> { if (visitable instanceof PrismValue) { ((PrismValue)visitable).setOriginType(originType); } }); } }