private void ReconcileWithBaseNode(AssetMemberNode assetNode) { if (assetNode?.BaseContent == null || !assetNode.CanOverride) { return; } var baseNode = (AssetMemberNode)assetNode.BaseContent; var localValue = assetNode.Retrieve(); var baseValue = assetNode.BaseContent.Retrieve(); // Reconcile occurs only when the node is not overridden. if (!assetNode.IsContentOverridden()) { assetNode.ResettingOverride = true; // Handle null cases first if (localValue == null || baseValue == null) { if (localValue == null && baseValue != null) { var clonedValue = CloneValueFromBase(baseValue, assetNode); assetNode.Update(clonedValue); } else if (localValue != null /*&& baseValue == null*/) { assetNode.Update(null); } } // Then handle collection and dictionary cases else if (assetNode.Descriptor is CollectionDescriptor || assetNode.Descriptor is DictionaryDescriptor) { // Items to add and to remove are stored in local collections and processed later, since they might affect indices var itemsToRemove = new List <ItemId>(); var itemsToAdd = new SortedList <object, ItemId>(new DefaultKeyComparer()); // Check for item present in the instance and absent from the base. foreach (var index in assetNode.Indices) { // Skip overridden items if (assetNode.IsItemOverridden(index)) { continue; } var itemId = assetNode.IndexToId(index); if (itemId != ItemId.Empty) { // Look if an item with the same id exists in the base. if (!baseNode.HasId(itemId)) { // If not, remove this item from the instance. itemsToRemove.Add(itemId); } } else { // This case should not happen, but if we have an empty id due to corrupted data let's just remove the item. itemsToRemove.Add(itemId); } } // Clean items marked as "override-deleted" that are absent from the base. var ids = CollectionItemIdHelper.GetCollectionItemIds(localValue); foreach (var deletedId in ids.DeletedItems.ToList()) { if (assetNode.BaseContent.Indices.All(x => baseNode.IndexToId(x) != deletedId)) { ids.UnmarkAsDeleted(deletedId); } } // Add item present in the base and missing here, and also update items that have different values between base and instance foreach (var index in assetNode.BaseContent.Indices) { var itemId = baseNode.IndexToId(index); // TODO: What should we do if it's empty? It can happen only from corrupted data // Skip items marked as "override-deleted" if (itemId == ItemId.Empty || assetNode.IsItemDeleted(itemId)) { continue; } Index localIndex; if (!assetNode.TryIdToIndex(itemId, out localIndex)) { // For dictionary, we might have a key collision, if so, we consider that the new value from the base is deleted in the instance. var keyCollision = assetNode.Descriptor is DictionaryDescriptor && (assetNode.ItemReferences?.HasIndex(index) == true || assetNode.Indices.Any(x => index.Equals(x))); // For specific collections (eg. EntityComponentCollection) it might not be possible to add due to other kinds of collisions or invalid value. var itemRejected = !CanUpdate(assetNode, ContentChangeType.CollectionAdd, localIndex, baseNode.Retrieve(index)); // We cannot add the item, let's mark it as deleted. if (keyCollision || itemRejected) { var instanceIds = CollectionItemIdHelper.GetCollectionItemIds(assetNode.Retrieve()); instanceIds.MarkAsDeleted(itemId); } else { // Add it if the key is available for add itemsToAdd.Add(index.Value, itemId); } } else { // If the item is present in both the instance and the base, check if we need to reconcile the value var member = assetNode as IMemberNode; var targetNode = assetNode.ItemReferences?[localIndex]?.TargetNode; // Skip it if it's overridden if (!assetNode.IsItemOverridden(localIndex)) { var localItemValue = assetNode.Retrieve(localIndex); var baseItemValue = baseNode.Retrieve(index); if (ShouldReconcileItem(member, targetNode, localItemValue, baseItemValue, assetNode.ItemReferences != null)) { var clonedValue = CloneValueFromBase(baseItemValue, assetNode); assetNode.Update(clonedValue, localIndex); } } // In dictionaries, the keys might be different between the instance and the base. We need to reconcile them too if (assetNode.Descriptor is DictionaryDescriptor && !assetNode.IsKeyOverridden(localIndex)) { if (ShouldReconcileItem(member, targetNode, localIndex.Value, index.Value, false)) { // Reconcile using a move (Remove + Add) of the key-value pair var clonedIndex = new Index(CloneValueFromBase(index.Value, assetNode)); var localItemValue = assetNode.Retrieve(localIndex); assetNode.Remove(localItemValue, localIndex); assetNode.Add(localItemValue, clonedIndex); ids[clonedIndex.Value] = itemId; } } } } // Process items marked to be removed foreach (var item in itemsToRemove) { var index = assetNode.IdToIndex(item); var value = assetNode.Retrieve(index); assetNode.Remove(value, index); // We're reconciling, so let's hack the normal behavior of marking the removed item as deleted. ids.UnmarkAsDeleted(item); } // Process items marked to be added foreach (var item in itemsToAdd) { var baseIndex = baseNode.IdToIndex(item.Value); var baseItemValue = baseNode.Retrieve(baseIndex); var clonedValue = CloneValueFromBase(baseItemValue, assetNode); if (assetNode.Descriptor is CollectionDescriptor) { // In a collection, we need to find an index that matches the index on the base to maintain order. // To do so, we iterate from the index in the base to zero. var currentBaseIndex = baseIndex.Int - 1; // Initialize the target index to zero, in case we don't find any better index. var localIndex = new Index(0); // Find the first item of the base that also exists (in term of id) in the local node, iterating backward (from baseIndex to 0) while (currentBaseIndex >= 0) { ItemId baseId; // This should not happen since the currentBaseIndex comes from the base. if (!baseNode.TryIndexToId(new Index(currentBaseIndex), out baseId)) { throw new InvalidOperationException("Cannot find an identifier matching the index in the base collection"); } Index sameIndexInInstance; // If we have an matching item, we want to insert right after it if (assetNode.TryIdToIndex(baseId, out sameIndexInInstance)) { localIndex = new Index(sameIndexInInstance.Int + 1); break; } currentBaseIndex--; } assetNode.Restore(clonedValue, localIndex, item.Value); } else { // This case is for dictionary. Key collisions have already been handle at that point so we can directly do the add without further checks. assetNode.Restore(clonedValue, baseIndex, item.Value); } } } // Finally, handle single properties else { var member = assetNode as IMemberNode; var targetNode = assetNode.TargetReference?.TargetNode; if (ShouldReconcileItem(member, targetNode, localValue, baseValue, assetNode.TargetReference != null)) { var clonedValue = CloneValueFromBase(baseValue, assetNode); assetNode.Update(clonedValue); } } assetNode.ResettingOverride = false; } }
public NodeOverride(AssetMemberNode overriddenNode, Index overriddenIndex, OverrideTarget target) { Node = overriddenNode; Index = overriddenIndex; Target = target; }