Index: Ringtoets/Common/src/Ringtoets.Common.Data/Ringtoets.Common.Data.csproj =================================================================== diff -u -rb1537a16c5961d9f66d5564c215f4bee59294f82 -r92d8502b4ffd316c6387a0b1378e4d097c43c58d --- Ringtoets/Common/src/Ringtoets.Common.Data/Ringtoets.Common.Data.csproj (.../Ringtoets.Common.Data.csproj) (revision b1537a16c5961d9f66d5564c215f4bee59294f82) +++ Ringtoets/Common/src/Ringtoets.Common.Data/Ringtoets.Common.Data.csproj (.../Ringtoets.Common.Data.csproj) (revision 92d8502b4ffd316c6387a0b1378e4d097c43c58d) @@ -100,6 +100,7 @@ + Index: Ringtoets/Common/src/Ringtoets.Common.Data/UpdateDataStrategies/UpdateDataStrategyBase.cs =================================================================== diff -u --- Ringtoets/Common/src/Ringtoets.Common.Data/UpdateDataStrategies/UpdateDataStrategyBase.cs (revision 0) +++ Ringtoets/Common/src/Ringtoets.Common.Data/UpdateDataStrategies/UpdateDataStrategyBase.cs (revision 92d8502b4ffd316c6387a0b1378e4d097c43c58d) @@ -0,0 +1,174 @@ +// Copyright (C) Stichting Deltares 2016. 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.FailureMechanism; + +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 feature of the target data that should be validated on for the uniqueness of elements. + /// The failure mechanism in which the target collection should be updated. + public abstract class UpdateDataStrategyBase + where TTargetData : class + where TFeature : class + where TFailureMechanism : IFailureMechanism + { + protected TFailureMechanism failureMechanism; + private readonly IEqualityComparer equalityComparer; + + /// + /// Instantiates a object. + /// + /// The failure mechanism which needs to be updated. + /// The comparer which should be used to determine when two objects are equal + /// Thrown when any input parameter is null. + protected UpdateDataStrategyBase(TFailureMechanism failureMechanism, IEqualityComparer equalityComparer) + { + if (failureMechanism == null) + { + throw new ArgumentNullException(nameof(failureMechanism)); + } + if (equalityComparer == null) + { + throw new ArgumentNullException(nameof(equalityComparer)); + } + + this.equalityComparer = equalityComparer; + this.failureMechanism = failureMechanism; + } + + /// + /// Updates the (dependent) objects with new data from the imported data. + /// + /// Objects that need to be updated. + /// The data that was imported. + /// An with affected objects. + /// Thrown when duplicate items are found. + protected abstract IEnumerable UpdateData(IEnumerable objectsToUpdate, + IEnumerable importedDataCollection); + + /// + /// Removes the objects and their dependent data. + /// + /// The objects that are removed. + /// An with affected objects. + protected abstract IEnumerable RemoveData(IEnumerable removedObjects); + + /// + /// Updates the items and their associated data within the target collection with the data contained + /// in the imported data collection. + /// + /// The target collection that needs to be updated. + /// The imported data collection that is used to update + /// the . + /// The source file path. + /// A of affected objects. + /// Thrown when any of the input parameters are null. + /// Thrown when duplicate items are being added to the + /// . + /// Thrown when duplicate items are found during the + /// update of the items to be updatd in the . + protected IEnumerable UpdateTargetCollectionData(ObservableUniqueItemCollectionWithSourcePath targetDataCollection, + IEnumerable importedDataCollection, + string sourceFilePath) + { + if (targetDataCollection == null) + { + throw new ArgumentNullException(nameof(targetDataCollection)); + } + if (importedDataCollection == null) + { + throw new ArgumentNullException(nameof(importedDataCollection)); + } + if (sourceFilePath == null) + { + throw new ArgumentNullException(nameof(sourceFilePath)); + } + + return ModifyDataCollection(targetDataCollection, importedDataCollection, sourceFilePath); + } + + /// + /// Identifies which models 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 target data collection which needs to be updated. + /// 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 + /// . + /// Thrown when duplicate items are found during the + /// update of the items to be updatd in the . + private IEnumerable ModifyDataCollection(ObservableUniqueItemCollectionWithSourcePath targetDataCollection, + IEnumerable importedDataCollection, + string sourceFilePath) + { + TTargetData[] importedObjects = importedDataCollection.ToArray(); + TTargetData[] addedObjects = GetAddedObjects(targetDataCollection, importedObjects).ToArray(); + TTargetData[] removedObjects = GetRemovedObjects(targetDataCollection, importedObjects).ToArray(); + TTargetData[] updatedObjects = GetUpdatedObjects(targetDataCollection, importedObjects).ToArray(); + + var affectedObjects = new List(); + if (addedObjects.Any()) + { + affectedObjects.Add(targetDataCollection); + } + affectedObjects.AddRange(UpdateData(targetDataCollection, importedObjects)); + affectedObjects.AddRange(RemoveData(removedObjects)); + + targetDataCollection.Clear(); + targetDataCollection.AddRange(addedObjects.Union(updatedObjects), sourceFilePath); + + return affectedObjects.Distinct(new ReferenceEqualityComparer()); + } + + private IEnumerable GetRemovedObjects(IEnumerable existingCollection, IEnumerable importedDataOjects) + { + return existingCollection.Except(importedDataOjects, equalityComparer); + } + + private IEnumerable GetUpdatedObjects(IEnumerable existingCollection, IEnumerable importedDataObjects) + { + return existingCollection.Intersect(importedDataObjects, equalityComparer); + } + + private IEnumerable GetAddedObjects(IEnumerable existingCollection, IEnumerable importedDataObjects) + { + return importedDataObjects.Where(source => !existingCollection.Contains(source, equalityComparer)); + } + } +} \ No newline at end of file Index: Ringtoets/Common/test/Ringtoets.Common.Data.Test/Ringtoets.Common.Data.Test.csproj =================================================================== diff -u -rb1537a16c5961d9f66d5564c215f4bee59294f82 -r92d8502b4ffd316c6387a0b1378e4d097c43c58d --- Ringtoets/Common/test/Ringtoets.Common.Data.Test/Ringtoets.Common.Data.Test.csproj (.../Ringtoets.Common.Data.Test.csproj) (revision b1537a16c5961d9f66d5564c215f4bee59294f82) +++ Ringtoets/Common/test/Ringtoets.Common.Data.Test/Ringtoets.Common.Data.Test.csproj (.../Ringtoets.Common.Data.Test.csproj) (revision 92d8502b4ffd316c6387a0b1378e4d097c43c58d) @@ -95,6 +95,7 @@ + Index: Ringtoets/Common/test/Ringtoets.Common.Data.Test/UpdateDataStrategies/UpdateDataStrategyBaseTest.cs =================================================================== diff -u --- Ringtoets/Common/test/Ringtoets.Common.Data.Test/UpdateDataStrategies/UpdateDataStrategyBaseTest.cs (revision 0) +++ Ringtoets/Common/test/Ringtoets.Common.Data.Test/UpdateDataStrategies/UpdateDataStrategyBaseTest.cs (revision 92d8502b4ffd316c6387a0b1378e4d097c43c58d) @@ -0,0 +1,266 @@ +// Copyright (C) Stichting Deltares 2016. 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 NUnit.Framework; +using Ringtoets.Common.Data.TestUtil; +using Ringtoets.Common.Data.UpdateDataStrategies; + +namespace Ringtoets.Common.Data.Test.UpdateDataStrategies +{ + [TestFixture] + public class UpdateDataStrategyBaseTest + { + private readonly Func getUniqueFeature = item => item.Name; + private const string typeDescriptor = "TestItem"; + private const string featureDescription = "naam"; + + [Test] + public void DefaultConstructor_FailureMechanismNull_ThrowsArgumentNullException() + { + // Call + TestDelegate call = () => new ConcreteUpdateDataStrategy(null); + + // Assert + string paramName = Assert.Throws(call).ParamName; + Assert.AreEqual("failureMechanism", paramName); + } + + [Test] + public void DefaultConstructor_EqualityComparerNull_ThrowsArgumentNullException() + { + // Call + TestDelegate call = () => new ConcreteUpdateDataStrategy(new TestFailureMechanism(), null); + + // Assert + string paramName = Assert.Throws(call).ParamName; + Assert.AreEqual("equalityComparer", paramName); + } + + [Test] + public void DefaultConstructor_FailureMechanismNotNull_DoesNotThrowException() + { + // Call + TestDelegate call = () => new ConcreteUpdateDataStrategy(new TestFailureMechanism()); + + // Assert + Assert.DoesNotThrow(call); + } + + [Test] + public void UpdateTargetCollectionData_TargetCollectionNull_ThrowsArgumentNullException() + { + // Setup + var strategy = new ConcreteUpdateDataStrategy(new TestFailureMechanism()); + + // Call + TestDelegate call = () => strategy.ConcreteUpdateData(null, Enumerable.Empty(), string.Empty); + + // Assert + string paramName = Assert.Throws(call).ParamName; + Assert.AreEqual("targetDataCollection", paramName); + } + + [Test] + public void UpdateTargetCollectionData_ImportedDataCollectionNull_ThrowsArgumentNullException() + { + // Setup + var strategy = new ConcreteUpdateDataStrategy(new TestFailureMechanism()); + var collection = new ObservableUniqueItemCollectionWithSourcePath( + getUniqueFeature, typeDescriptor, featureDescription); + + // Call + TestDelegate call = () => strategy.ConcreteUpdateData(collection, null, string.Empty); + + // Assert + string paramName = Assert.Throws(call).ParamName; + Assert.AreEqual("importedDataCollection", paramName); + } + + [Test] + public void UpdateTargetCollectionData_SourceFilePathNull_ThrowsArgumentNullException() + { + // Setup + var strategy = new ConcreteUpdateDataStrategy(new TestFailureMechanism()); + var collection = new ObservableUniqueItemCollectionWithSourcePath( + getUniqueFeature, typeDescriptor, featureDescription); + + // Call + TestDelegate call = () => strategy.ConcreteUpdateData(collection, Enumerable.Empty(), null); + + // Assert + string paramName = Assert.Throws(call).ParamName; + Assert.AreEqual("sourceFilePath", paramName); + } + + [Test] + public void UpdateTargetCollectionData_WithCollectionAndImportedDataEmpty_ClearsTargetCollection() + { + // Setup + var collection = new ObservableUniqueItemCollectionWithSourcePath( + getUniqueFeature, typeDescriptor, featureDescription); + const string filePath = "path"; + collection.AddRange(new[] + { + new TestItem("Name A"), + new TestItem("Name B") + }, filePath); + + var strategy = new ConcreteUpdateDataStrategy(new TestFailureMechanism()); + + // Call + strategy.ConcreteUpdateData(collection, Enumerable.Empty(), filePath); + + // Assert + CollectionAssert.IsEmpty(collection); + Assert.AreEqual(filePath, collection.SourcePath); + } + + [Test] + public void UpdateTargetCollectionData_Call_CallsFunctions() + { + // Setup + var collection = new ObservableUniqueItemCollectionWithSourcePath( + getUniqueFeature, typeDescriptor, featureDescription); + + var strategy = new ConcreteUpdateDataStrategy(new TestFailureMechanism()); + + // Call + strategy.ConcreteUpdateData(collection, Enumerable.Empty(), "path"); + + // Assert + Assert.IsTrue(strategy.IsUpdateDataCalled); + Assert.IsTrue(strategy.IsRemoveDataCalled); + } + + [Test] + public void UpdateTargetCollectionData_WithEmptyCollectionAndImportedDataCollectionNotEmpty_AddsNewItems() + { + // Setup + var collection = new ObservableUniqueItemCollectionWithSourcePath( + getUniqueFeature, typeDescriptor, featureDescription); + var importedCollection = new[] + { + new TestItem("Name A"), + new TestItem("Name B") + }; + + var strategy = new ConcreteUpdateDataStrategy(new TestFailureMechanism()); + + // Call + IEnumerable affectedObjects = strategy.ConcreteUpdateData(collection, importedCollection, "path"); + + // Assert + CollectionAssert.AreEqual(importedCollection, collection); + CollectionAssert.Contains(affectedObjects, collection); + Assert.AreEqual("path", collection.SourcePath); + } + + [Test] + public void UpdateTargetCollectionData_ImportedDataContainsDuplicateData_ThrowsArgumentException() + { + // Setup + var collection = new ObservableUniqueItemCollectionWithSourcePath( + getUniqueFeature, typeDescriptor, featureDescription); + + const string duplicateName = "Duplicate Name"; + var importedCollection = new[] + { + new TestItem(duplicateName), + new TestItem(duplicateName) + }; + + var strategy = new ConcreteUpdateDataStrategy(new TestFailureMechanism()); + + // Call + TestDelegate call = () => strategy.ConcreteUpdateData(collection, importedCollection, "path"); + + // Assert + var exception = Assert.Throws(call); + string message = $"{typeDescriptor} moeten een unieke {featureDescription} hebben. Gevonden dubbele elementen: {duplicateName}."; + Assert.AreEqual(message, exception.Message); + + CollectionAssert.IsEmpty(collection); + } + + private class ConcreteUpdateDataStrategy : UpdateDataStrategyBase + { + public bool IsUpdateDataCalled { get; private set; } + public bool IsRemoveDataCalled { get; private set; } + + public ConcreteUpdateDataStrategy(TestFailureMechanism failureMechanism, IEqualityComparer comparer) + : base(failureMechanism, comparer) {} + + public ConcreteUpdateDataStrategy(TestFailureMechanism failureMechanism) + : base(failureMechanism, new NameComparer()) {} + + public IEnumerable ConcreteUpdateData(ObservableUniqueItemCollectionWithSourcePath targetCollection, + IEnumerable importedDataCollection, + string sourceFilePath) + { + return UpdateTargetCollectionData(targetCollection, importedDataCollection, sourceFilePath); + } + + protected override IEnumerable UpdateData(IEnumerable objectsToUpdate, IEnumerable importedDataCollection) + { + IsUpdateDataCalled = true; + return Enumerable.Empty(); + } + + protected override IEnumerable RemoveData(IEnumerable removedObjects) + { + IsRemoveDataCalled = true; + return Enumerable.Empty(); + } + + private class NameComparer : IEqualityComparer + { + public bool Equals(TestItem x, TestItem y) + { + return x.Name == y.Name; + } + + public int GetHashCode(TestItem obj) + { + return obj.Name.GetHashCode(); + } + } + } + + private class TestItem + { + public string Name { get; } + + public TestItem(string name) + { + Name = name; + } + + public override string ToString() + { + return Name; + } + } + } +} \ No newline at end of file