// Copyright (C) Stichting Deltares 2017. All rights reserved. // // This file is part of Ringtoets. // // Ringtoets is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . // // All names, logos, and references to "Deltares" are registered trademarks of // Stichting Deltares and remain full property of Stichting Deltares at all times. // All rights reserved. using System; using System.Collections.Generic; using System.Linq; using Core.Common.Base; using Core.Common.Utils; using Ringtoets.Common.Data.Exceptions; using Ringtoets.Common.Data.FailureMechanism; using Ringtoets.Common.Data.Properties; namespace Ringtoets.Common.Data.UpdateDataStrategies { /// /// Strategy for updating the current collection and dependent data with imported data: /// /// Adds imported items that are not part of the current collection. /// Removes items that are part of the current collection, but are not part of the imported item collection. /// Updates the items that are part of the current collection and are part of the imported item collection. /// /// /// The target data type. /// The failure mechanism in which the target collection should be updated. public abstract class UpdateDataStrategyBase where TTargetData : Observable where TFailureMechanism : IFailureMechanism { protected readonly TFailureMechanism FailureMechanism; private readonly IEqualityComparer equalityComparer; private readonly ObservableUniqueItemCollectionWithSourcePath targetCollection; /// /// Creates a new instance of object. /// /// The failure mechanism which needs to be updated. /// The collection to add the updated objects to. /// The comparer which should be used to determine when two objects are equal. /// Thrown when any input parameter is null. protected UpdateDataStrategyBase( TFailureMechanism failureMechanism, ObservableUniqueItemCollectionWithSourcePath targetCollection, IEqualityComparer equalityComparer) { if (failureMechanism == null) { throw new ArgumentNullException(nameof(failureMechanism)); } if (equalityComparer == null) { throw new ArgumentNullException(nameof(equalityComparer)); } if (targetCollection == null) { throw new ArgumentNullException(nameof(targetCollection)); } this.equalityComparer = equalityComparer; this.targetCollection = targetCollection; FailureMechanism = failureMechanism; } /// /// Updates the unequal object and its dependent data with data from the imported data. /// /// Object that needs to be updated. /// The object to update from. /// An with affected objects. /// Thrown when duplicate items are found. protected abstract IEnumerable UpdateObjectAndDependentData(TTargetData objectToUpdate, TTargetData objectToUpdateFrom); /// /// Removes the objects and their dependent data. /// /// The object that is removed. /// An with affected objects. protected abstract IEnumerable RemoveObjectAndDependentData(TTargetData removedObject); /// /// Updates the items and their associated data within the target collection with the data contained /// in the imported data collection. /// /// The imported data collection that is used to update /// the . /// The source file path. /// A of affected objects. /// Thrown when an error occurred while updating the data. /// Thrown when contains /// null elements. protected IEnumerable UpdateTargetCollectionData(IEnumerable importedDataCollection, string sourceFilePath) { if (importedDataCollection == null) { throw new ArgumentNullException(nameof(importedDataCollection)); } if (sourceFilePath == null) { throw new ArgumentNullException(nameof(sourceFilePath)); } return ModifyDataCollection(importedDataCollection, sourceFilePath); } /// /// Identifies which items were changed, removed and added to the target collection /// when compared with the imported data and performs the necessary operations for /// the dependent data of the affected elements. /// /// The imported data collection which is used to update /// the . /// The source file path. /// A with affected objects. /// Thrown when: /// /// duplicate items are being added to the . /// duplicate items are found in the . /// /// /// Thrown when /// is null. /// Thrown when contains /// null elements. private IEnumerable ModifyDataCollection(IEnumerable importedDataCollection, string sourceFilePath) { TTargetData[] importedObjects = importedDataCollection.ToArray(); var modification = new CollectionModification(targetCollection, importedObjects, equalityComparer); var affectedObjects = new List(); if (modification.HasUpdates()) { affectedObjects.Add(targetCollection); } affectedObjects.AddRange(UpdateData(modification.ObjectsToBeUpdated.Values, importedObjects)); affectedObjects.AddRange(RemoveData(modification.ObjectsToBeRemoved)); targetCollection.Clear(); targetCollection.AddRange(modification.GetModifiedCollection(), sourceFilePath); return affectedObjects.Distinct(new ReferenceEqualityComparer()); } /// /// Updates all the objects and their dependent data that needs to be /// updated with data from the imported data collection. /// /// The objects that need to be updated. /// The data to update from. /// A of affected items. /// Thrown when the imported /// contains duplicate items. private IEnumerable UpdateData(IEnumerable objectsToUpdate, IEnumerable importedDataCollection) { var affectedObjects = new List(); if (importedDataCollection.Count() != importedDataCollection.Distinct(equalityComparer).Count()) { throw new UpdateDataException(Resources.UpdateDataStrategyBase_UpdateTargetCollectionData_Imported_data_must_contain_unique_items); } foreach (TTargetData objectToUpdate in objectsToUpdate) { TTargetData objectToUpdateFrom = importedDataCollection.Single(importedObject => equalityComparer.Equals(importedObject, objectToUpdate)); if (IsUpdateObjectDataUnequal(objectToUpdate, objectToUpdateFrom)) { affectedObjects.Add(objectToUpdate); affectedObjects.AddRange(UpdateObjectAndDependentData(objectToUpdate, objectToUpdateFrom)); } } return affectedObjects; } private static bool IsUpdateObjectDataUnequal(TTargetData objectToUpdate, TTargetData objectToUpdateFrom) { return !objectToUpdate.Equals(objectToUpdateFrom); } /// /// Removes all the objects and their dependent data. /// /// The objects that need to be removed. /// A of affected items. private IEnumerable RemoveData(IEnumerable objectsToRemove) { var affectedObjects = new List(); foreach (TTargetData objectToRemove in objectsToRemove) { affectedObjects.AddRange(RemoveObjectAndDependentData(objectToRemove)); } return affectedObjects; } /// /// Inner class for obtaining the modifications of an update action. /// private class CollectionModification { /// /// Creates a new instance of . /// /// The current collection of objects. /// The collection containing objects that were imported /// and thus could contain updates for the existing objects. /// The comparer to test whether elements in /// have a matching element in /// . /// Thrown if any parameter is null. /// Thrown when /// or contains a null element. public CollectionModification(IEnumerable existingObjects, IEnumerable updatedObjects, IEqualityComparer equalityComparer) { if (existingObjects == null) { throw new ArgumentNullException(nameof(existingObjects)); } if (updatedObjects == null) { throw new ArgumentNullException(nameof(updatedObjects)); } if (equalityComparer == null) { throw new ArgumentNullException(nameof(equalityComparer)); } if (existingObjects.Contains(null)) { throw new ArgumentException(@"Cannot determine modifications from a collection which contain null.", nameof(existingObjects)); } if (updatedObjects.Contains(null)) { throw new ArgumentException(@"Cannot determine modifications with a collection which contain null.", nameof(updatedObjects)); } TTargetData[] existingArray = existingObjects.ToArray(); var index = 0; foreach (TTargetData importedObject in updatedObjects) { int existingObjectIndex = FindIndex(existingArray, importedObject, equalityComparer); if (existingObjectIndex > -1) { ObjectsToBeUpdated.Add(index, existingArray[existingObjectIndex]); existingArray[existingObjectIndex] = null; } else { ObjectsToBeAdded.Add(index, importedObject); } index++; } ObjectsToBeRemoved = existingArray.Where(e => e != null); } /// /// Gets the objects that were updated. /// public Dictionary ObjectsToBeUpdated { get; } = new Dictionary(); /// /// Gets the objects that were removed. /// public IEnumerable ObjectsToBeRemoved { get; } /// /// Gets a collection of updated objects from the existing object collection and the /// added objects from the imported object collection in the same order that was /// found in the imported object collection. /// /// An ordered collection of updated and added elements. public IEnumerable GetModifiedCollection() { KeyValuePair[] remainingObjects = ObjectsToBeUpdated.Union(ObjectsToBeAdded).ToArray(); var orderedObjects = new TTargetData[remainingObjects.Length]; foreach (KeyValuePair remainingObject in remainingObjects) { orderedObjects[remainingObject.Key] = remainingObject.Value; } return orderedObjects; } /// /// Finds out whether there was a difference between the existing and the imported /// object collections. /// /// true if there were any updates, false otherwise. public bool HasUpdates() { return ObjectsToBeRemoved.Any() || ObjectsToBeAdded.Any() || ObjectsToBeUpdated.Any(); } private Dictionary ObjectsToBeAdded { get; } = new Dictionary(); private static int FindIndex(TTargetData[] collectionToLookIn, TTargetData objectToFind, IEqualityComparer equalityComparer) { for (var i = 0; i < collectionToLookIn.Length; i++) { TTargetData targetData = collectionToLookIn[i]; if (targetData != null && equalityComparer.Equals(targetData, objectToFind)) { return i; } } return -1; } } } }