diff --git a/CDP4Orm/Dao/BaseDao.cs b/CDP4Orm/Dao/BaseDao.cs index 8c2b23da..28138354 100644 --- a/CDP4Orm/Dao/BaseDao.cs +++ b/CDP4Orm/Dao/BaseDao.cs @@ -52,6 +52,16 @@ public abstract class BaseDao /// public ICommandLogger CommandLogger { get; set; } + /// + /// The of the current + /// + private DateTime currentTransactionDatetime; + + /// + /// The for which was retrieved + /// + private NpgsqlTransaction currentTransactionDataTimeTransaction; + /// /// Execute additional logic before each update function call. /// @@ -81,6 +91,9 @@ public abstract class BaseDao /// public virtual bool BeforeUpdate(NpgsqlTransaction transaction, string partition, Thing thing, Thing container, out bool isHandled, Dictionary valueTypeDictionaryAdditions) { + var transactionDateTime = this.GetTransactionDateTime(transaction); + thing.ModifiedOn = transactionDateTime; + isHandled = false; return true; } @@ -187,6 +200,8 @@ public virtual bool AfterDelete(bool deleteResult, NpgsqlTransaction transaction /// public virtual bool BeforeWrite(NpgsqlTransaction transaction, string partition, Thing thing, Thing container, out bool isHandled, Dictionary valueTypeDictionaryAdditions) { + thing.ModifiedOn = this.GetTransactionDateTime(transaction); + isHandled = false; return true; } @@ -288,6 +303,29 @@ protected void DeleteAll(NpgsqlTransaction transaction, string partition, string } } + /// + /// Returns the current transaction time from the server + /// + /// + /// + private DateTime GetTransactionDateTime(NpgsqlTransaction transaction) + { + if (transaction != null && this.currentTransactionDataTimeTransaction != transaction) + { + this.currentTransactionDataTimeTransaction = transaction; + + using (var command = new NpgsqlCommand( + $"SELECT * FROM \"SiteDirectory\".\"get_transaction_time\"();", + transaction.Connection, + transaction)) + { + this.currentTransactionDatetime = (DateTime)command.ExecuteScalar(); + } + } + + return this.currentTransactionDatetime; + } + /// /// Instantiates a from the content of a /// diff --git a/CDP4WebServer/CDP4WebServer.csproj b/CDP4WebServer/CDP4WebServer.csproj index a0289fe8..45534612 100644 --- a/CDP4WebServer/CDP4WebServer.csproj +++ b/CDP4WebServer/CDP4WebServer.csproj @@ -5,7 +5,7 @@ Exe RHEA System S.A. CDP4WebServer-CE - 6.1.0 + 6.1.1 CDP4 Services Host Copyright © RHEA System S.A. Sam, Merlin, Alex, Naron, Alexander @@ -16,6 +16,8 @@ + + diff --git a/CDP4WebServer/Startup.cs b/CDP4WebServer/Startup.cs index 94a1be8e..71c36ff5 100644 --- a/CDP4WebServer/Startup.cs +++ b/CDP4WebServer/Startup.cs @@ -1,11 +1,33 @@ // -------------------------------------------------------------------------------------------------------------------- // -// Copyright (c) 2016 RHEA System S.A. All rights reserverd +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Alex Vorobiev, Alexander van Delft. +// +// This file is part of CDP4 Web Services Community Edition. +// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// This is an auto-generated class. Any manual changes to this file will be overwritten! +// +// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The CDP4 Web Services Community Edition 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 +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . // // -------------------------------------------------------------------------------------------------------------------- namespace CDP4WebServer { + using Hangfire; + using Hangfire.MemoryStorage; + using Nancy; using Nancy.Owin; @@ -24,6 +46,10 @@ public class Startup /// public void Configuration(IAppBuilder app) { + GlobalConfiguration.Configuration.UseMemoryStorage(); + app.UseHangfireDashboard("/hangfire"); + app.UseHangfireServer(); + app.UseNancy(options => options.PassThroughWhenStatusCodesAre(HttpStatusCode.NotFound)); } } diff --git a/CDP4WebServices.API.Tests/ChangeLog/ChangeLogTestFixture.cs b/CDP4WebServices.API.Tests/ChangeLog/ChangeLogTestFixture.cs new file mode 100644 index 00000000..43d2cb54 --- /dev/null +++ b/CDP4WebServices.API.Tests/ChangeLog/ChangeLogTestFixture.cs @@ -0,0 +1,2085 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Merlin Bieze, Alex Vorobiev, Naron Phou, Alexander van Delft. +// +// This file is part of CDP4 Web Services Community Edition. +// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// This is an auto-generated class. Any manual changes to this file will be overwritten! +// +// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The CDP4 Web Services Community Edition 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace CDP4WebServices.API.Tests.Services +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + + using CDP4Common; + using CDP4Common.CommonData; + using CDP4Common.DTO; + using CDP4Common.MetaInfo; + using CDP4Common.Types; + + using CDP4Orm.Dao; + using CDP4Orm.Dao.Resolve; + + using CDP4WebServices.API.Helpers; + using CDP4WebServices.API.Services; + using CDP4WebServices.API.Services.Authorization; + using CDP4WebServices.API.Services.ChangeLog; + using CDP4WebServices.API.Services.Operations; + + using Moq; + + using Npgsql; + + using NUnit.Framework; + + using IServiceProvider = CDP4WebServices.API.Services.IServiceProvider; + using LogEntryChangelogItem = CDP4Common.DTO.LogEntryChangelogItem; + using Thing = CDP4Common.DTO.Thing; + + /// + /// Suite of tests for the + /// + [TestFixture] + public class ChangeLogTestFixture + { + private Mock serviceProvider; + private Mock operationProcessor; + private Mock requestUtils; + private Mock resolveService; + private Mock optionService; + private Mock transactionManager; + private Mock actualFiniteStateService; + private Mock possibleFiniteStateService; + private Mock parameterTypeService; + private Mock parameterService; + private Mock parameterOverrideService; + private Mock metaInfoProvider; + private Mock parameterSubscriptionService; + private Mock elementDefinitionService; + private Mock elementUsageService; + private Mock iterationService; + private Mock iterationSetupService; + private Mock parameterOrOverrideBaseService; + private Mock parameterValueSetService; + private Mock parameterValueSetBaseService; + private IDataModelUtils dataModelUtils; + private string partition; + private Guid actor; + private ModelLogEntry existingModelLogEntry; + private ElementDefinition elementDefinition_1; + private EngineeringModel engineeringModel; + private Iteration iteration; + private ChangeLogService changeLogService; + private RequirementsSpecification requirementsSpecification; + private ParameterType parameterType; + private Parameter parameter; + private DomainOfExpertise domain_ElementDefinition; + private DomainOfExpertise domain_Parameter; + private DomainOfExpertise domain_ElementUsage; + private DomainOfExpertise domain_ParameterOverride; + private DomainOfExpertise domain_ParameterSubscription; + private DomainOfExpertise domain_ParameterOverrideSubscription; + + private Category category_ElementDefinition; + private Category category_ParameterType; + private Category category_ElementUsage; + + private ParameterValueSet parameterValueSet_1; + private ActualFiniteState actualFiniteState; + private Option option; + private PossibleFiniteState possibleFiniteState; + private ElementUsage elementUsage_1; + private IterationSetup iterationSetup; + private ParameterOverrideValueSet parameterOverrideValueSet_1; + private ParameterOverride parameterOverride; + private ParameterSubscription parameterSubscription; + private ParameterSubscriptionValueSet parameterSubscriptionValueSet_1; + private ParameterSubscriptionValueSet parameterOverrideSubscriptionValueSet_1; + private ParameterSubscription parameterOverrideSubscription; + + [SetUp] + public void Setup() + { + this.serviceProvider = new Mock(); + this.operationProcessor = new Mock(); + this.requestUtils = new Mock(); + this.resolveService = new Mock(); + this.transactionManager = new Mock(); + this.optionService = new Mock(); + this.actualFiniteStateService = new Mock(); + this.possibleFiniteStateService = new Mock(); + this.parameterTypeService = new Mock(); + this.parameterService = new Mock(); + this.parameterOverrideService = new Mock(); + this.parameterSubscriptionService = new Mock(); + this.parameterValueSetService = new Mock(); + this.elementDefinitionService = new Mock(); + this.elementUsageService = new Mock(); + this.iterationService = new Mock(); + this.iterationSetupService = new Mock(); + this.parameterOrOverrideBaseService = new Mock(); + this.parameterValueSetBaseService = new Mock(); + + this.metaInfoProvider = new Mock(); + this.dataModelUtils = new DataModelUtils(); + + this.changeLogService = new ChangeLogService() + { + ServiceProvider = this.serviceProvider.Object, + OperationProcessor = this.operationProcessor.Object, + RequestUtils = this.requestUtils.Object, + OptionService = this.optionService.Object, + ActualFiniteStateService = this.actualFiniteStateService.Object, + PossibleFiniteStateService = this.possibleFiniteStateService.Object, + ParameterTypeService = this.parameterTypeService.Object, + ParameterService = this.parameterService.Object, + ParameterOverrideService = this.parameterOverrideService.Object, + ParameterSubscriptionService = this.parameterSubscriptionService.Object, + IterationSetupService = this.iterationSetupService.Object, + ParameterOrOverrideBaseService = this.parameterOrOverrideBaseService.Object, + ParameterValueSetService = this.parameterValueSetService.Object, + ParameterValueSetBaseService = this.parameterValueSetBaseService.Object, + ResolveService = this.resolveService.Object, + TransactionManager = this.transactionManager.Object, + DataModelUtils = this.dataModelUtils, + }; + + this.operationProcessor.Setup(x => x.OperationOriginalThingCache).Returns(new List()); + + this.requirementsSpecification = new RequirementsSpecification(Guid.NewGuid(), 0); + + this.domain_ElementDefinition = new DomainOfExpertise(Guid.NewGuid(), 0) + { + Name = "Domain 1", + ShortName = "D1" + }; + + this.domain_Parameter = new DomainOfExpertise(Guid.NewGuid(), 0) + { + Name = "Domain 2", + ShortName = "D2" + }; + + this.domain_ElementUsage = new DomainOfExpertise(Guid.NewGuid(), 0) + { + Name = "Domain 3", + ShortName = "D3" + }; + + this.domain_ParameterOverride = new DomainOfExpertise(Guid.NewGuid(), 0) + { + Name = "Domain 4", + ShortName = "D4" + }; + + this.domain_ParameterSubscription = new DomainOfExpertise(Guid.NewGuid(), 0) + { + Name = "Domain 5", + ShortName = "D5" + }; + + this.domain_ParameterOverrideSubscription = new DomainOfExpertise(Guid.NewGuid(), 0) + { + Name = "Domain 6", + ShortName = "D6" + }; + + this.category_ElementDefinition = new Category(Guid.NewGuid(), 0) + { + Name= "Category 1", + ShortName = "C1" + }; + + this.category_ParameterType = new Category(Guid.NewGuid(), 0) + { + Name= "Category 2", + ShortName = "C2" + }; + + this.category_ElementUsage = new Category(Guid.NewGuid(), 0) + { + Name= "Category 3", + ShortName = "C3" + }; + + this.option = new Option(Guid.NewGuid(), 0) + { + Name = "Option", + ShortName = "Option" + }; + + this.possibleFiniteState = new PossibleFiniteState(Guid.NewGuid(), 0) + { + Name = "State 1", + ShortName = "S1" + }; + + this.actualFiniteState = new ActualFiniteState(Guid.NewGuid(), 0); + this.actualFiniteState.PossibleState.Add(this.possibleFiniteState.Iid); + + this.partition = "EngineeringModel_partition"; + this.actor = Guid.NewGuid(); + + this.elementDefinition_1 = new ElementDefinition(Guid.NewGuid(), 0) + { + Name = "Element 1", + ShortName = "E1", + Owner = this.domain_ElementDefinition.Iid + }; + + this.elementDefinition_1.Category.Add(this.category_ElementDefinition.Iid); + + this.elementUsage_1 = new ElementUsage(Guid.NewGuid(), 0) + { + Name = "Usage 1", + ShortName = "U1", + Owner = this.domain_ElementUsage.Iid + }; + + this.elementUsage_1.Category.Add(this.category_ElementUsage.Iid); + + this.elementDefinition_1.ContainedElement.Add(this.elementUsage_1.Iid); + + this.engineeringModel = new EngineeringModel(Guid.NewGuid(), 0); + + this.iteration = new Iteration(Guid.NewGuid(), 0); + + this.iterationSetup = new IterationSetup(Guid.NewGuid(), 0) + { + IterationNumber = 1, + IterationIid = this.iteration.Iid + }; + + this.iteration.IterationSetup = this.iterationSetup.Iid; + this.iteration.Element.Add(this.elementDefinition_1.Iid); + + this.existingModelLogEntry = new ModelLogEntry(Guid.NewGuid(), 0) + { + Level = LogLevelKind.USER, + Author = this.actor, + LanguageCode = "en-GB", + Content = "User generated content" + }; + + this.parameterType = new SimpleQuantityKind(Guid.NewGuid(), 0) + { + Name = "Quantity", + ShortName = "Q" + }; + + this.parameterType.Category.Add(this.category_ParameterType.Iid); + + this.parameterValueSet_1 = new ParameterValueSet(Guid.NewGuid(), 0) + { + ActualOption = this.option.Iid, + ActualState = this.actualFiniteState.Iid, + }; + + this.parameter = new Parameter(Guid.NewGuid(), 0) + { + ParameterType = this.parameterType.Iid, + Owner = this.domain_Parameter.Iid, + }; + + this.parameter.ValueSet.Add(this.parameterValueSet_1.Iid); + + this.parameterSubscriptionValueSet_1 = new ParameterSubscriptionValueSet(Guid.NewGuid(), 0) + { + SubscribedValueSet = this.parameterValueSet_1.Iid + }; + + this.parameterSubscription = new ParameterSubscription(Guid.NewGuid(), 0) + { + Owner = this.domain_ParameterSubscription.Iid, + }; + + this.parameter.ParameterSubscription.Add(this.parameterSubscription.Iid); + + this.parameterSubscription.ValueSet.Add(this.parameterSubscriptionValueSet_1.Iid); + + this.elementDefinition_1.Parameter.Add(this.parameter.Iid); + + this.parameterOverrideValueSet_1 = new ParameterOverrideValueSet(Guid.NewGuid(), 0) + { + ParameterValueSet = this.parameterValueSet_1.Iid + }; + + this.parameterOverride = new ParameterOverride(Guid.NewGuid(), 0) + { + Parameter = this.parameter.Iid, + Owner = this.domain_ParameterOverride.Iid + }; + + this.parameterOverride.ValueSet.Add(this.parameterOverrideValueSet_1.Iid); + + this.parameterOverrideSubscriptionValueSet_1 = new ParameterSubscriptionValueSet(Guid.NewGuid(), 0) + { + SubscribedValueSet = this.parameterOverrideValueSet_1.Iid + }; + + this.parameterOverrideSubscription = new ParameterSubscription(Guid.NewGuid(), 0) + { + Owner = this.domain_ParameterOverrideSubscription.Iid, + }; + + this.parameterOverrideSubscription.ValueSet.Add(this.parameterOverrideSubscriptionValueSet_1.Iid); + + this.parameterOverride.ParameterSubscription.Add(this.parameterOverrideSubscription.Iid); + + this.elementUsage_1.ParameterOverride.Add(this.parameterOverride.Iid); + + this.metaInfoProvider.Setup(x => x.GetMetaInfo(ClassKind.EngineeringModel.ToString())).Returns(new EngineeringModelMetaInfo()); + this.metaInfoProvider.Setup(x => x.GetMetaInfo(ClassKind.ModelLogEntry.ToString())).Returns(new ModelLogEntryMetaInfo()); + + this.serviceProvider.Setup(x => x.MapToPersitableService(ClassKind.ElementDefinition.ToString())).Returns(this.elementDefinitionService.Object); + this.serviceProvider.Setup(x => x.MapToPersitableService(ClassKind.Iteration.ToString())).Returns(this.iterationService.Object); + this.serviceProvider.Setup(x => x.MapToPersitableService(ClassKind.ElementUsage.ToString())).Returns(this.elementUsageService.Object); + this.serviceProvider.Setup(x => x.MapToPersitableService(ClassKind.ParameterOverride.ToString())).Returns(this.parameterOverrideService.Object); + this.serviceProvider.Setup(x => x.MapToPersitableService(ClassKind.ParameterOrOverrideBase.ToString())).Returns(this.parameterOrOverrideBaseService.Object); + + this.transactionManager.Setup(x => x.IsFullAccessEnabled()).Returns(false); + + this.requestUtils.Setup(x => x.MetaInfoProvider).Returns(this.metaInfoProvider.Object); + + this.elementDefinitionService.Setup( + x => + x.GetShallow( + null, + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns(new[] { this.elementDefinition_1 }); + + this.elementUsageService.Setup( + x => + x.GetShallow(null, + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns(new[] { this.elementUsage_1 }); + + this.iterationService.Setup( + x => + x.GetShallow(null, + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns(new[] { this.iteration }); + + this.optionService.Setup( + x => x.GetShallow( + null, + It.IsAny(), + new[] { this.option.Iid }, + It.IsAny())) + .Returns(new[] { this.option }); + + this.actualFiniteStateService.Setup( + x => x.GetShallow( + null, + It.IsAny(), + new[] { this.actualFiniteState.Iid }, + It.IsAny())) + .Returns(new[] { this.actualFiniteState }); + + this.possibleFiniteStateService.Setup( + x => x.GetShallow( + null, + It.IsAny(), + this.actualFiniteState.PossibleState, + It.IsAny())) + .Returns(new[] { this.possibleFiniteState }); + + this.parameterService.Setup( + x => x.GetShallow( + null, + It.IsAny(), + null, + It.IsAny())) + .Returns(new[] { this.parameter }); + + this.parameterService.Setup( + x => x.GetShallow( + null, + It.IsAny(), + new[] { this.parameter.Iid }, + It.IsAny())) + .Returns(new[] { this.parameter }); + + this.parameterTypeService.Setup( + x => x.GetShallow( + null, + It.IsAny(), + new[] { this.parameterType.Iid }, + It.IsAny())) + .Returns(new[] { this.parameterType }); + + this.parameterOrOverrideBaseService.Setup( + x => x.GetShallow( + null, + It.IsAny(), + null, + It.IsAny())) + .Returns(new[] { (ParameterOrOverrideBase) this.parameter, this.parameterOverride }); + + this.parameterValueSetBaseService.Setup( + x => x.GetShallow( + null, + It.IsAny(), + new[] { this.parameterValueSet_1.Iid }, + It.IsAny())) + .Returns(new[] { (ParameterValueSetBase) this.parameterValueSet_1 }); + + this.parameterValueSetBaseService.Setup( + x => x.GetShallow( + null, + It.IsAny(), + new[] { this.parameterOverrideValueSet_1.Iid }, + It.IsAny())) + .Returns(new[] { (ParameterValueSetBase) this.parameterOverrideValueSet_1 }); + + this.elementDefinitionService.Setup( + x => x.GetShallow( + null, + It.IsAny(), + null, + It.IsAny())) + .Returns(new[] { this.elementDefinition_1 }); + + this.elementUsageService.Setup( + x => x.GetShallow( + null, + It.IsAny(), + null, + It.IsAny())) + .Returns(new[] { this.elementUsage_1 }); + + this.iterationSetupService.Setup( + x => x.GetShallow( + null, + It.IsAny(), + new[] { this.iteration.IterationSetup }, + It.IsAny())) + .Returns(new[] { this.iterationSetup }); + + this.parameterValueSetService.Setup( + x => x.GetShallow( + null, + It.IsAny(), + new[] { this.parameterOverrideValueSet_1.ParameterValueSet }, + It.IsAny())) + .Returns(new[] { this.parameterValueSet_1 }); + + this.parameterOverrideService.Setup( + x => x.GetShallow( + null, + It.IsAny(), + null, + It.IsAny())) + .Returns(new[] { this.parameterOverride }); + + this.parameterOverrideService.Setup( + x => x.GetShallow( + null, + It.IsAny(), + new[] { this.parameterOverride.Iid }, + It.IsAny())) + .Returns(new[] { this.parameterOverride }); + + this.parameterSubscriptionService.Setup( + x => x.GetShallow( + null, + It.IsAny(), + null, + It.IsAny())) + .Returns(new[] { this.parameterSubscription, this.parameterOverrideSubscription }); + } + + [Test] + public void VerifyAppendModelChangeLogDataOnlyWorksOnEngineeringModelChanges() + { + var postOperation = new CdpPostOperation(); + + var iterationClasslessDto = new ClasslessDTO() + { + { nameof(Thing.Iid), this.iteration.Iid }, + { nameof(Thing.ClassKind), ClassKind.Iteration.ToString() }, + { nameof(Iteration.Element), new[] { this.elementDefinition_1.Iid }.ToList() } + }; + + postOperation.Update.Add(iterationClasslessDto); + postOperation.Create.Add(this.elementDefinition_1); + + var things = new Thing[] { this.iteration, this.elementDefinition_1 }; + + var result = this.changeLogService.TryAppendModelChangeLogData(null, this.partition, this.actor, 0, postOperation, things); + + Assert.IsFalse(result); + + this.operationProcessor.Verify( + x => x.Process(It.IsAny(), null, It.IsAny(), null), Times.Never); + + var engineeringModelClasslessDto = new ClasslessDTO() + { + { nameof(Thing.Iid), this.engineeringModel.Iid }, + { nameof(Thing.ClassKind), ClassKind.EngineeringModel.ToString() }, + { nameof(EngineeringModel.LogEntry), new[] { this.existingModelLogEntry.Iid }.ToList() } + }; + + postOperation.Update.Add(engineeringModelClasslessDto); + postOperation.Create.Add(this.existingModelLogEntry); + + things = new Thing[] { this.iteration, this.elementDefinition_1, this.engineeringModel, this.existingModelLogEntry }; + + result = this.changeLogService.TryAppendModelChangeLogData(null, this.partition, this.actor, 0, postOperation, things); + + Assert.IsTrue(result); + + this.operationProcessor.Verify( + x => x.Process(It.IsAny(), null, It.IsAny(), null), Times.Once); + } + + [Test] + public void VerifyAppendModelChangeLogDataAddsModelLogEntryWhenNeeded() + { + var postOperation = new CdpPostOperation(); + + var iterationClasslessDto = new ClasslessDTO() + { + { nameof(Thing.Iid), this.iteration.Iid }, + { nameof(Thing.ClassKind), ClassKind.Iteration.ToString() }, + { nameof(Iteration.Element), new[] { this.elementDefinition_1.Iid }.ToList() } + }; + + postOperation.Update.Add(iterationClasslessDto); + postOperation.Create.Add(this.elementDefinition_1); + + var things = new Thing[] { this.iteration, this.elementDefinition_1, this.engineeringModel }; + + var result = this.changeLogService.TryAppendModelChangeLogData(null, this.partition, this.actor, 0, postOperation, things); + + Assert.IsTrue(result); + + this.operationProcessor.Verify( + x => x.Process(It.IsAny(), null, It.IsAny(), null), Times.Exactly(1)); + } + + [Test] + public void VerifyThatUnsupportedThingsAreNotAddedToTheModelLogEntry() + { + var postOperation = new CdpPostOperation(); + + var iterationClasslessDto = new ClasslessDTO() + { + { nameof(Thing.Iid), this.iteration.Iid }, + { nameof(Thing.ClassKind), ClassKind.Iteration.ToString() }, + { nameof(Iteration.RequirementsSpecification), new[] { this.requirementsSpecification.Iid }.ToList() }, + }; + + postOperation.Update.Add(iterationClasslessDto); + postOperation.Create.Add(this.requirementsSpecification); + + var things = new Thing[] { this.iteration, this.requirementsSpecification, this.engineeringModel }; + + var result = this.changeLogService.TryAppendModelChangeLogData(null, this.partition, this.actor, 0, postOperation, things); + + Assert.IsFalse(result); + + this.operationProcessor.Verify( + x => x.Process(It.IsAny(), null, It.IsAny(), null), Times.Exactly(0)); + + things = new Thing[] { this.iteration, this.requirementsSpecification, this.elementDefinition_1, this.engineeringModel }; + postOperation.Create.Add(this.elementDefinition_1); + postOperation.Create.Add(this.existingModelLogEntry); + + var engineeringModelClasslessDto = new ClasslessDTO() + { + { nameof(Thing.Iid), this.engineeringModel.Iid }, + { nameof(Thing.ClassKind), ClassKind.EngineeringModel.ToString() }, + { nameof(EngineeringModel.LogEntry), new[] { this.existingModelLogEntry.Iid }.ToList() } + }; + + postOperation.Update.Add(engineeringModelClasslessDto); + + result = this.changeLogService.TryAppendModelChangeLogData(null, this.partition, this.actor, 0, postOperation, things); + + Assert.IsTrue(result); + + this.operationProcessor.Verify( + x => x.Process(It.IsAny(), null, It.IsAny(), null), Times.Exactly(1)); + + Assert.AreEqual(1, this.existingModelLogEntry.LogEntryChangelogItem.Count); + } + + [Test] + public void VerifyThatResultsAreAsExpectedForNewParameter() + { + var postOperation = new CdpPostOperation(); + + var elementDefinitionClasslessDto = new ClasslessDTO() + { + { nameof(Thing.Iid), this.elementDefinition_1.Iid }, + { nameof(Thing.ClassKind), ClassKind.ElementDefinition.ToString() }, + { nameof(ElementDefinition.Parameter), new[] { this.parameter.Iid }.ToList() }, + }; + + var engineeringModelClasslessDto = new ClasslessDTO() + { + { nameof(Thing.Iid), this.engineeringModel.Iid }, + { nameof(Thing.ClassKind), ClassKind.EngineeringModel.ToString() }, + { nameof(EngineeringModel.LogEntry), new[] { this.existingModelLogEntry.Iid }.ToList() } + }; + + postOperation.Create.Add(this.parameter); + postOperation.Create.Add(this.existingModelLogEntry); + + postOperation.Update.Add(elementDefinitionClasslessDto); + postOperation.Update.Add(engineeringModelClasslessDto); + + var things = new Thing[] { this.parameter, this.elementDefinition_1, this.engineeringModel }; + + var createdLogEntries = new List(); + + this.operationProcessor.Setup( + x => + x.Process(It.IsAny(), null, It.IsAny(), null)) + .Callback>( + (operation, transaction, partition, files) + => + { + createdLogEntries.AddRange(operation.Create.Where(x => x.ClassKind == ClassKind.LogEntryChangelogItem).Cast().ToArray()); + }); + + var result = this.changeLogService.TryAppendModelChangeLogData(null, this.partition, this.actor, 0, postOperation, things); + + Assert.IsTrue(result); + + this.operationProcessor.Verify( + x => x.Process(It.IsAny(), null, It.IsAny(), null), Times.Exactly(1)); + + Assert.AreEqual(2, this.existingModelLogEntry.LogEntryChangelogItem.Count); + + var expectedParameterAffectedReferenceItems = new[] + { + this.elementDefinition_1.Iid, + this.parameterType.Iid, + this.category_ElementDefinition.Iid, + this.category_ParameterType.Iid, + this.domain_ElementDefinition.Iid, + this.domain_Parameter.Iid + }; + + var parameterCreatedLogEntries = createdLogEntries.Single(x => x.AffectedItemIid == this.parameter.Iid); + + var expectedElementDefinitionAffectedReferenceItems = new[] + { + this.iteration.Iid, + this.category_ElementDefinition.Iid, + this.domain_ElementDefinition.Iid + }; + + var elementDefinitionCreatedLogEntries = createdLogEntries.Single(x => x.AffectedItemIid == this.elementDefinition_1.Iid); + + var expectedAffectedDomains = new[] + { + this.domain_ElementDefinition.Iid, + this.domain_Parameter.Iid + }; + + var expectedAffectedItemIds = new[] + { + this.category_ElementDefinition.Iid, + this.category_ParameterType.Iid, + this.iteration.Iid, + this.elementDefinition_1.Iid, + this.parameterType.Iid, + this.parameter.Iid + }; + + CollectionAssert.AreEquivalent(expectedParameterAffectedReferenceItems, parameterCreatedLogEntries.AffectedReferenceIid); + CollectionAssert.AreEquivalent(expectedElementDefinitionAffectedReferenceItems, elementDefinitionCreatedLogEntries.AffectedReferenceIid); + + CollectionAssert.AreEquivalent(expectedAffectedDomains, this.existingModelLogEntry.AffectedDomainIid); + CollectionAssert.AreEquivalent(expectedAffectedItemIds, this.existingModelLogEntry.AffectedItemIid); + } + + [Test] + public void VerifyThatResultsAreAsExpectedForDeletedParameter() + { + var postOperation = new CdpPostOperation(); + + var parameterClasslessDto = new ClasslessDTO() + { + { nameof(Thing.Iid), this.parameter.Iid }, + { nameof(Thing.ClassKind), ClassKind.Parameter.ToString() } + }; + + var engineeringModelClasslessDto = new ClasslessDTO() + { + { nameof(Thing.Iid), this.engineeringModel.Iid }, + { nameof(Thing.ClassKind), ClassKind.EngineeringModel.ToString() }, + { nameof(EngineeringModel.LogEntry), new[] { this.existingModelLogEntry.Iid }.ToList() } + }; + + postOperation.Delete.Add(parameterClasslessDto); + postOperation.Create.Add(this.existingModelLogEntry); + postOperation.Update.Add(engineeringModelClasslessDto); + + this.operationProcessor.Setup(x => x.OperationOriginalThingCache).Returns(new List + { + this.parameter, + this.elementDefinition_1, + }); + + var things = new Thing[] { this.elementDefinition_1, this.engineeringModel }; + + var createdLogEntries = new List(); + + this.operationProcessor.Setup( + x => + x.Process(It.IsAny(), null, It.IsAny(), null)) + .Callback>( + (operation, transaction, partition, files) + => + { + createdLogEntries.AddRange(operation.Create.Where(x => x.ClassKind == ClassKind.LogEntryChangelogItem).Cast().ToArray()); + }); + + var result = this.changeLogService.TryAppendModelChangeLogData(null, this.partition, this.actor, 0, postOperation, things); + + Assert.IsTrue(result); + + this.operationProcessor.Verify( + x => x.Process(It.IsAny(), null, It.IsAny(), null), Times.Exactly(1)); + + Assert.AreEqual(1, this.existingModelLogEntry.LogEntryChangelogItem.Count); + + var expectedParameterAffectedReferenceItems = new[] + { + this.elementDefinition_1.Iid, + this.parameterType.Iid, + this.parameterSubscription.Iid, + this.category_ParameterType.Iid, + this.category_ElementDefinition.Iid, + this.domain_Parameter.Iid, + this.domain_ElementDefinition.Iid, + }; + + var parameterCreatedLogEntries = createdLogEntries.Single(x => x.AffectedItemIid == this.parameter.Iid); + + var expectedAffectedDomains = new[] + { + this.domain_ElementDefinition.Iid, + this.domain_Parameter.Iid + }; + + var expectedAffectedItemIds = new[] + { + this.category_ParameterType.Iid, + this.category_ElementDefinition.Iid, + this.parameterSubscription.Iid, + this.elementDefinition_1.Iid, + this.parameterType.Iid, + this.parameter.Iid + }; + + CollectionAssert.AreEquivalent(expectedParameterAffectedReferenceItems, parameterCreatedLogEntries.AffectedReferenceIid); + + CollectionAssert.AreEquivalent(expectedAffectedDomains, this.existingModelLogEntry.AffectedDomainIid); + CollectionAssert.AreEquivalent(expectedAffectedItemIds, this.existingModelLogEntry.AffectedItemIid); + } + + [Test] + public void VerifyThatResultsAreAsExpectedForUpdatedParameter() + { + var postOperation = new CdpPostOperation(); + + var parameterClasslessDto = new ClasslessDTO() + { + { nameof(Thing.Iid), this.parameter.Iid }, + { nameof(Thing.ClassKind), ClassKind.Parameter.ToString() }, + { nameof(Parameter.Owner), this.domain_ElementDefinition.Iid } + }; + + var engineeringModelClasslessDto = new ClasslessDTO + { + { nameof(Thing.Iid), this.engineeringModel.Iid }, + { nameof(Thing.ClassKind), ClassKind.EngineeringModel.ToString() }, + { nameof(EngineeringModel.LogEntry), new[] { this.existingModelLogEntry.Iid }.ToList() } + }; + + postOperation.Create.Add(this.existingModelLogEntry); + + postOperation.Update.Add(parameterClasslessDto); + postOperation.Update.Add(engineeringModelClasslessDto); + + var things = new Thing[] { this.iteration, this.parameter, this.engineeringModel }; + + LogEntryChangelogItem[] createdLogEntries = { }; + + this.operationProcessor.Setup( + x => + x.Process(It.IsAny(), null, It.IsAny(), null)) + .Callback>( + (operation, transaction, partition, files) + => + { + createdLogEntries = operation.Create.Where(x => x.ClassKind == ClassKind.LogEntryChangelogItem).Cast().ToArray(); + }); + + var result = this.changeLogService.TryAppendModelChangeLogData(null, this.partition, this.actor, 0, postOperation, things); + + Assert.IsTrue(result); + + this.operationProcessor.Verify( + x => x.Process(It.IsAny(), null, It.IsAny(), null), Times.Exactly(1)); + + Assert.AreEqual(1, this.existingModelLogEntry.LogEntryChangelogItem.Count); + + var expectedAffectedReferenceItems = new[] + { + this.elementDefinition_1.Iid, + this.parameterType.Iid, + this.parameterSubscription.Iid, + this.category_ElementDefinition.Iid, + this.category_ParameterType.Iid, + this.domain_ElementDefinition.Iid, + this.domain_Parameter.Iid, + }; + + var expectedAffectedDomains = new[] + { + this.domain_ElementDefinition.Iid, + this.domain_Parameter.Iid + }; + + var expectedAffectedItemIds = new[] + { + this.category_ElementDefinition.Iid, + this.category_ParameterType.Iid, + this.elementDefinition_1.Iid, + this.parameterType.Iid, + this.parameter.Iid, + this.parameterSubscription.Iid, + }; + + CollectionAssert.AreEquivalent(expectedAffectedReferenceItems, createdLogEntries.Single(x => x.Iid == this.existingModelLogEntry.LogEntryChangelogItem.First()).AffectedReferenceIid); + CollectionAssert.AreEquivalent(expectedAffectedDomains, this.existingModelLogEntry.AffectedDomainIid); + CollectionAssert.AreEquivalent(expectedAffectedItemIds, this.existingModelLogEntry.AffectedItemIid); + } + + [Test] + public void VerifyThatResultsAreAsExpectedForUpdatedParameterValueSet() + { + var postOperation = new CdpPostOperation(); + + var parameterValueSetClasslessDto = new ClasslessDTO() + { + { nameof(Thing.Iid), this.parameterValueSet_1.Iid }, + { nameof(Thing.ClassKind), ClassKind.ParameterValueSet.ToString() }, + { nameof(ParameterValueSet.Manual), new ValueArray(new[] { "100" }) } + }; + + var engineeringModelClasslessDto = new ClasslessDTO + { + { nameof(Thing.Iid), this.engineeringModel.Iid }, + { nameof(Thing.ClassKind), ClassKind.EngineeringModel.ToString() }, + { nameof(EngineeringModel.LogEntry), new[] { this.existingModelLogEntry.Iid }.ToList() } + }; + + postOperation.Update.Add(engineeringModelClasslessDto); + + postOperation.Create.Add(this.existingModelLogEntry); + postOperation.Update.Add(parameterValueSetClasslessDto); + + var things = new Thing[] { this.parameterValueSet_1, this.engineeringModel }; + + LogEntryChangelogItem[] createdLogEntries = { }; + + this.operationProcessor.Setup( + x => + x.Process(It.IsAny(), null, It.IsAny(), null)) + .Callback>( + (operation, transaction, partition, files) + => + { + createdLogEntries = operation.Create.Where(x => x.ClassKind == ClassKind.LogEntryChangelogItem).Cast().ToArray(); + }); + + var result = this.changeLogService.TryAppendModelChangeLogData(null, this.partition, this.actor, 0, postOperation, things); + + Assert.IsTrue(result); + + this.operationProcessor.Verify( + x => x.Process(It.IsAny(), null, It.IsAny(), null), Times.Exactly(1)); + + Assert.AreEqual(1, this.existingModelLogEntry.LogEntryChangelogItem.Count); + + var expectedAffectedReferenceItems = new[] + { + this.elementDefinition_1.Iid, + this.parameter.Iid, + this.parameterType.Iid, + this.parameterSubscription.Iid, + this.category_ElementDefinition.Iid, + this.category_ParameterType.Iid, + this.domain_ElementDefinition.Iid, + this.domain_Parameter.Iid + }; + + var expectedAffectedDomains = new[] + { + this.domain_ElementDefinition.Iid, + this.domain_Parameter.Iid + }; + + var expectedAffectedItemIds = new[] + { + this.category_ElementDefinition.Iid, + this.category_ParameterType.Iid, + this.elementDefinition_1.Iid, + this.parameterType.Iid, + this.parameter.Iid, + this.parameterValueSet_1.Iid, + this.parameterSubscription.Iid, + }; + + CollectionAssert.AreEquivalent(expectedAffectedReferenceItems, createdLogEntries.Single(x => x.Iid == this.existingModelLogEntry.LogEntryChangelogItem.First()).AffectedReferenceIid); + CollectionAssert.AreEquivalent(expectedAffectedDomains, this.existingModelLogEntry.AffectedDomainIid); + CollectionAssert.AreEquivalent(expectedAffectedItemIds, this.existingModelLogEntry.AffectedItemIid); + } + + [Test] + public void VerifyThatResultsAreAsExpectedForNewElementUsage() + { + var postOperation = new CdpPostOperation(); + + var elementDefinitionClasslessDto = new ClasslessDTO() + { + { nameof(Thing.Iid), this.elementDefinition_1.Iid }, + { nameof(Thing.ClassKind), ClassKind.ElementDefinition.ToString() }, + { nameof(ElementDefinition.ContainedElement), new[] { this.elementUsage_1.Iid }.ToList() } + }; + + var engineeringModelClasslessDto = new ClasslessDTO + { + { nameof(Thing.Iid), this.engineeringModel.Iid }, + { nameof(Thing.ClassKind), ClassKind.EngineeringModel.ToString() }, + { nameof(EngineeringModel.LogEntry), new[] { this.existingModelLogEntry.Iid }.ToList() } + }; + + postOperation.Update.Add(engineeringModelClasslessDto); + + postOperation.Update.Add(elementDefinitionClasslessDto); + postOperation.Create.Add(this.elementUsage_1); + postOperation.Create.Add(this.existingModelLogEntry); + + var things = new Thing[] { this.elementUsage_1, this.elementDefinition_1, this.engineeringModel }; + + LogEntryChangelogItem[] createdLogEntries = { }; + + this.operationProcessor.Setup( + x => + x.Process(It.IsAny(), null, It.IsAny(), null)) + .Callback>( + (operation, transaction, partition, files) + => + { + createdLogEntries = operation.Create.Where(x => x.ClassKind == ClassKind.LogEntryChangelogItem).Cast().ToArray(); + }); + + var result = this.changeLogService.TryAppendModelChangeLogData(null, this.partition, this.actor, 0, postOperation, things); + + Assert.IsTrue(result); + + this.operationProcessor.Verify( + x => x.Process(It.IsAny(), null, It.IsAny(), null), Times.Once); + + Assert.AreEqual(2, this.existingModelLogEntry.LogEntryChangelogItem.Count); + + var expectedUsageAffectedReferenceItems = new[] + { + this.elementDefinition_1.Iid, + this.category_ElementDefinition.Iid, + this.category_ElementUsage.Iid, + this.domain_ElementDefinition.Iid, + this.domain_ElementUsage.Iid + }; + + var usageCreatedLogEntries = createdLogEntries.Single(x => x.AffectedItemIid == this.elementUsage_1.Iid); + + var expectedElementDefinitionAffectedReferenceItems = new[] + { + this.iteration.Iid, + this.category_ElementDefinition.Iid, + this.domain_ElementDefinition.Iid + }; + + var elementDefinitionCreatedLogEntries = createdLogEntries.Single(x => x.AffectedItemIid == this.elementDefinition_1.Iid); + + var expectedAffectedDomains = new[] + { + this.domain_ElementDefinition.Iid, + this.domain_ElementUsage.Iid + }; + + var expectedAffectedItemIds = new[] + { + this.category_ElementDefinition.Iid, + this.category_ElementUsage.Iid, + this.iteration.Iid, + this.elementDefinition_1.Iid, + this.elementUsage_1.Iid, + }; + + CollectionAssert.AreEquivalent(expectedUsageAffectedReferenceItems, usageCreatedLogEntries.AffectedReferenceIid); + CollectionAssert.AreEquivalent(expectedElementDefinitionAffectedReferenceItems, elementDefinitionCreatedLogEntries.AffectedReferenceIid); + + CollectionAssert.AreEquivalent(expectedAffectedDomains, this.existingModelLogEntry.AffectedDomainIid); + CollectionAssert.AreEquivalent(expectedAffectedItemIds, this.existingModelLogEntry.AffectedItemIid); + } + + [Test] + public void VerifyThatResultsAreAsExpectedForDeletedElementUsage() + { + var postOperation = new CdpPostOperation(); + + var elementUsageClasslessDto = new ClasslessDTO() + { + { nameof(Thing.Iid), this.elementUsage_1.Iid }, + { nameof(Thing.ClassKind), ClassKind.ElementUsage.ToString() }, + }; + + var engineeringModelClasslessDto = new ClasslessDTO + { + { nameof(Thing.Iid), this.engineeringModel.Iid }, + { nameof(Thing.ClassKind), ClassKind.EngineeringModel.ToString() }, + { nameof(EngineeringModel.LogEntry), new[] { this.existingModelLogEntry.Iid }.ToList() } + }; + + postOperation.Update.Add(engineeringModelClasslessDto); + + postOperation.Delete.Add(elementUsageClasslessDto); + postOperation.Create.Add(this.existingModelLogEntry); + + this.operationProcessor.Setup(x => x.OperationOriginalThingCache).Returns(new List + { + this.elementUsage_1, + this.elementDefinition_1, + }); + + var things = new Thing[] { this.elementDefinition_1, this.engineeringModel }; + + LogEntryChangelogItem[] createdLogEntries = { }; + + this.operationProcessor.Setup( + x => + x.Process(It.IsAny(), null, It.IsAny(), null)) + .Callback>( + (operation, transaction, partition, files) + => + { + createdLogEntries = operation.Create.Where(x => x.ClassKind == ClassKind.LogEntryChangelogItem).Cast().ToArray(); + }); + + var result = this.changeLogService.TryAppendModelChangeLogData(null, this.partition, this.actor, 0, postOperation, things); + + Assert.IsTrue(result); + + this.operationProcessor.Verify( + x => x.Process(It.IsAny(), null, It.IsAny(), null), Times.Once); + + Assert.AreEqual(1, this.existingModelLogEntry.LogEntryChangelogItem.Count); + + var expectedUsageAffectedReferenceItems = new[] + { + this.elementDefinition_1.Iid, + this.domain_ElementUsage.Iid, + this.domain_ElementDefinition.Iid, + this.category_ElementDefinition.Iid, + this.category_ElementUsage.Iid, + }; + + var usageCreatedLogEntries = createdLogEntries.Single(x => x.AffectedItemIid == this.elementUsage_1.Iid); + + var expectedAffectedDomains = new[] + { + this.domain_ElementDefinition.Iid, + this.domain_ElementUsage.Iid + }; + + var expectedAffectedItemIds = new[] + { + this.category_ElementDefinition.Iid, + this.category_ElementUsage.Iid, + this.elementDefinition_1.Iid, + this.elementUsage_1.Iid, + }; + + CollectionAssert.AreEquivalent(expectedUsageAffectedReferenceItems, usageCreatedLogEntries.AffectedReferenceIid); + + CollectionAssert.AreEquivalent(expectedAffectedDomains, this.existingModelLogEntry.AffectedDomainIid); + CollectionAssert.AreEquivalent(expectedAffectedItemIds, this.existingModelLogEntry.AffectedItemIid); + } + + [Test] + public void VerifyThatResultsAreAsExpectedForUpdatedElementUsage() + { + var postOperation = new CdpPostOperation(); + + var elementUsageClasslessDto = new ClasslessDTO() + { + { nameof(Thing.Iid), this.elementUsage_1.Iid }, + { nameof(Thing.ClassKind), ClassKind.ElementUsage.ToString() }, + { nameof(ElementUsage.Name), $"{this.elementUsage_1.Name} was changed" } + }; + + var engineeringModelClasslessDto = new ClasslessDTO + { + { nameof(Thing.Iid), this.engineeringModel.Iid }, + { nameof(Thing.ClassKind), ClassKind.EngineeringModel.ToString() }, + { nameof(EngineeringModel.LogEntry), new[] { this.existingModelLogEntry.Iid }.ToList() } + }; + + postOperation.Create.Add(this.existingModelLogEntry); + + postOperation.Update.Add(elementUsageClasslessDto); + postOperation.Update.Add(engineeringModelClasslessDto); + + var things = new Thing[] { this.elementUsage_1, this.engineeringModel }; + + LogEntryChangelogItem[] createdLogEntries = { }; + + this.operationProcessor.Setup( + x => + x.Process(It.IsAny(), null, It.IsAny(), null)) + .Callback>( + (operation, transaction, partition, files) + => + { + createdLogEntries = operation.Create.Where(x => x.ClassKind == ClassKind.LogEntryChangelogItem).Cast().ToArray(); + }); + + var result = this.changeLogService.TryAppendModelChangeLogData(null, this.partition, this.actor, 0, postOperation, things); + + Assert.IsTrue(result); + + this.operationProcessor.Verify( + x => x.Process(It.IsAny(), null, It.IsAny(), null), Times.Exactly(1)); + + Assert.AreEqual(1, this.existingModelLogEntry.LogEntryChangelogItem.Count); + + var expectedAffectedReferenceItems = new[] + { + this.elementDefinition_1.Iid, + this.category_ElementDefinition.Iid, + this.category_ElementUsage.Iid, + this.domain_ElementDefinition.Iid, + this.domain_ElementUsage.Iid, + }; + + var expectedAffectedDomains = new[] + { + this.domain_ElementDefinition.Iid, + this.domain_ElementUsage.Iid, + }; + + var expectedAffectedItemIds = new[] + { + this.category_ElementDefinition.Iid, + this.category_ElementUsage.Iid, + this.elementDefinition_1.Iid, + this.elementUsage_1.Iid, + }; + + CollectionAssert.AreEquivalent(expectedAffectedReferenceItems, createdLogEntries.Single(x => x.Iid == this.existingModelLogEntry.LogEntryChangelogItem.First()).AffectedReferenceIid); + CollectionAssert.AreEquivalent(expectedAffectedDomains, this.existingModelLogEntry.AffectedDomainIid); + CollectionAssert.AreEquivalent(expectedAffectedItemIds, this.existingModelLogEntry.AffectedItemIid); + } + + [Test] + public void VerifyThatResultsAreAsExpectedForNewParameterOverride() + { + var postOperation = new CdpPostOperation(); + + var elementUsageClasslessDto = new ClasslessDTO() + { + { nameof(Thing.Iid), this.elementUsage_1.Iid }, + { nameof(Thing.ClassKind), ClassKind.ElementUsage.ToString() }, + { nameof(ElementUsage.ParameterOverride), new[] { this.parameterOverride.Iid }.ToList() }, + }; + + var engineeringModelClasslessDto = new ClasslessDTO() + { + { nameof(Thing.Iid), this.engineeringModel.Iid }, + { nameof(Thing.ClassKind), ClassKind.EngineeringModel.ToString() }, + { nameof(EngineeringModel.LogEntry), new[] { this.existingModelLogEntry.Iid }.ToList() } + }; + + postOperation.Create.Add(this.parameterOverride); + postOperation.Create.Add(this.existingModelLogEntry); + + postOperation.Update.Add(elementUsageClasslessDto); + postOperation.Update.Add(engineeringModelClasslessDto); + + var things = new Thing[] { this.parameterOverride, this.elementUsage_1, this.engineeringModel }; + + var createdLogEntries = new List(); + + this.operationProcessor.Setup( + x => + x.Process(It.IsAny(), null, It.IsAny(), null)) + .Callback>( + (operation, transaction, partition, files) + => + { + createdLogEntries.AddRange(operation.Create.Where(x => x.ClassKind == ClassKind.LogEntryChangelogItem).Cast().ToArray()); + }); + + var result = this.changeLogService.TryAppendModelChangeLogData(null, this.partition, this.actor, 0, postOperation, things); + + Assert.IsTrue(result); + + this.operationProcessor.Verify( + x => x.Process(It.IsAny(), null, It.IsAny(), null), Times.Exactly(1)); + + Assert.AreEqual(2, this.existingModelLogEntry.LogEntryChangelogItem.Count); + + var expectedParameterOverrideAffectedReferenceItems = new[] + { + this.elementDefinition_1.Iid, + this.elementUsage_1.Iid, + this.parameterType.Iid, + this.parameter.Iid, + this.category_ElementDefinition.Iid, + this.category_ElementUsage.Iid, + this.category_ParameterType.Iid, + this.domain_ElementDefinition.Iid, + this.domain_ElementUsage.Iid, + this.domain_ParameterOverride.Iid, + this.domain_Parameter.Iid, + }; + + var parameterOverrideCreatedLogEntries = createdLogEntries.Single(x => x.AffectedItemIid == this.parameterOverride.Iid); + + var expectedElementUsageAffectedReferenceItems = new[] + { + this.elementDefinition_1.Iid, + this.domain_ElementDefinition.Iid, + this.domain_ElementUsage.Iid, + this.category_ElementUsage.Iid, + this.category_ElementDefinition.Iid, + }; + + var elementDefinitionCreatedLogEntries = createdLogEntries.Single(x => x.AffectedItemIid == this.elementUsage_1.Iid); + + var expectedAffectedDomains = new[] + { + this.domain_ElementDefinition.Iid, + this.domain_ElementUsage.Iid, + this.domain_ParameterOverride.Iid, + this.domain_Parameter.Iid, + }; + + var expectedAffectedItemIds = new[] + { + this.elementDefinition_1.Iid, + this.elementUsage_1.Iid, + this.parameterType.Iid, + this.parameter.Iid, + this.category_ElementDefinition.Iid, + this.category_ElementUsage.Iid, + this.category_ParameterType.Iid, + this.parameterOverride.Iid, + }; + + CollectionAssert.AreEquivalent(expectedParameterOverrideAffectedReferenceItems, parameterOverrideCreatedLogEntries.AffectedReferenceIid); + CollectionAssert.AreEquivalent(expectedElementUsageAffectedReferenceItems, elementDefinitionCreatedLogEntries.AffectedReferenceIid); + + CollectionAssert.AreEquivalent(expectedAffectedDomains, this.existingModelLogEntry.AffectedDomainIid); + CollectionAssert.AreEquivalent(expectedAffectedItemIds, this.existingModelLogEntry.AffectedItemIid); + } + + [Test] + public void VerifyThatResultsAreAsExpectedForDeletedParameterOverride() + { + var postOperation = new CdpPostOperation(); + + var parameterOverrideClasslessDto = new ClasslessDTO() + { + { nameof(Thing.Iid), this.parameterOverride.Iid }, + { nameof(Thing.ClassKind), ClassKind.ElementUsage.ToString() } + }; + + var engineeringModelClasslessDto = new ClasslessDTO() + { + { nameof(Thing.Iid), this.engineeringModel.Iid }, + { nameof(Thing.ClassKind), ClassKind.EngineeringModel.ToString() }, + { nameof(EngineeringModel.LogEntry), new[] { this.existingModelLogEntry.Iid }.ToList() } + }; + + postOperation.Create.Add(this.parameterOverride); + postOperation.Create.Add(this.existingModelLogEntry); + + postOperation.Delete.Add(parameterOverrideClasslessDto); + postOperation.Update.Add(engineeringModelClasslessDto); + + this.operationProcessor.Setup(x => x.OperationOriginalThingCache).Returns(new List + { + this.parameterOverride, + this.elementUsage_1, + }); + + var things = new Thing[] { this.elementUsage_1, this.engineeringModel }; + + var createdLogEntries = new List(); + + this.operationProcessor.Setup( + x => + x.Process(It.IsAny(), null, It.IsAny(), null)) + .Callback>( + (operation, transaction, partition, files) + => + { + createdLogEntries.AddRange(operation.Create.Where(x => x.ClassKind == ClassKind.LogEntryChangelogItem).Cast().ToArray()); + }); + + var result = this.changeLogService.TryAppendModelChangeLogData(null, this.partition, this.actor, 0, postOperation, things); + + Assert.IsTrue(result); + + this.operationProcessor.Verify( + x => x.Process(It.IsAny(), null, It.IsAny(), null), Times.Exactly(1)); + + Assert.AreEqual(1, this.existingModelLogEntry.LogEntryChangelogItem.Count); + + var expectedParameterOverrideAffectedReferenceItems = new[] + { + this.elementDefinition_1.Iid, + this.elementUsage_1.Iid, + this.parameterType.Iid, + this.parameter.Iid, + this.parameterOverrideSubscription.Iid, + this.category_ElementDefinition.Iid, + this.category_ElementUsage.Iid, + this.category_ParameterType.Iid, + this.domain_ElementDefinition.Iid, + this.domain_ElementUsage.Iid, + this.domain_Parameter.Iid, + this.domain_ParameterOverride.Iid + }; + + var parameterOverrideCreatedLogEntries = createdLogEntries.Single(x => x.AffectedItemIid == this.parameterOverride.Iid); + + var expectedAffectedDomains = new[] + { + this.domain_ElementDefinition.Iid, + this.domain_ElementUsage.Iid, + this.domain_Parameter.Iid, + this.domain_ParameterOverride.Iid + }; + + var expectedAffectedItemIds = new[] + { + this.elementDefinition_1.Iid, + this.elementUsage_1.Iid, + this.parameterType.Iid, + this.parameter.Iid, + this.parameterOverride.Iid, + this.parameterOverrideSubscription.Iid, + this.category_ElementDefinition.Iid, + this.category_ElementUsage.Iid, + this.category_ParameterType.Iid, + }; + + CollectionAssert.AreEquivalent(expectedParameterOverrideAffectedReferenceItems, parameterOverrideCreatedLogEntries.AffectedReferenceIid); + + CollectionAssert.AreEquivalent(expectedAffectedDomains, this.existingModelLogEntry.AffectedDomainIid); + CollectionAssert.AreEquivalent(expectedAffectedItemIds, this.existingModelLogEntry.AffectedItemIid); + } + + [Test] + public void VerifyThatResultsAreAsExpectedForUpdatedParameterOverride() + { + var postOperation = new CdpPostOperation(); + + var parameterOverrideClasslessDto = new ClasslessDTO() + { + { nameof(Thing.Iid), this.parameterOverride.Iid }, + { nameof(Thing.ClassKind), ClassKind.ParameterOverride.ToString() }, + { nameof(Parameter.Owner), this.domain_ElementDefinition.Iid } + }; + + var engineeringModelClasslessDto = new ClasslessDTO + { + { nameof(Thing.Iid), this.engineeringModel.Iid }, + { nameof(Thing.ClassKind), ClassKind.EngineeringModel.ToString() }, + { nameof(EngineeringModel.LogEntry), new[] { this.existingModelLogEntry.Iid }.ToList() } + }; + + postOperation.Create.Add(this.existingModelLogEntry); + + postOperation.Update.Add(parameterOverrideClasslessDto); + postOperation.Update.Add(engineeringModelClasslessDto); + + var things = new Thing[] { this.parameterOverride, this.engineeringModel }; + + LogEntryChangelogItem[] createdLogEntries = { }; + + this.operationProcessor.Setup( + x => + x.Process(It.IsAny(), null, It.IsAny(), null)) + .Callback>( + (operation, transaction, partition, files) + => + { + createdLogEntries = operation.Create.Where(x => x.ClassKind == ClassKind.LogEntryChangelogItem).Cast().ToArray(); + }); + + var result = this.changeLogService.TryAppendModelChangeLogData(null, this.partition, this.actor, 0, postOperation, things); + + Assert.IsTrue(result); + + this.operationProcessor.Verify( + x => x.Process(It.IsAny(), null, It.IsAny(), null), Times.Exactly(1)); + + Assert.AreEqual(1, this.existingModelLogEntry.LogEntryChangelogItem.Count); + + var expectedAffectedReferenceItems = new[] + { + this.elementDefinition_1.Iid, + this.elementUsage_1.Iid, + this.parameter.Iid, + this.parameterType.Iid, + this.parameterOverrideSubscription.Iid, + this.category_ElementDefinition.Iid, + this.category_ElementUsage.Iid, + this.category_ParameterType.Iid, + this.domain_ElementDefinition.Iid, + this.domain_ElementUsage.Iid, + this.domain_Parameter.Iid, + this.domain_ParameterOverride.Iid, + }; + + var expectedAffectedDomains = new[] + { + this.domain_ElementDefinition.Iid, + this.domain_ElementUsage.Iid, + this.domain_Parameter.Iid, + this.domain_ParameterOverride.Iid, + }; + + var expectedAffectedItemIds = new[] + { + this.elementDefinition_1.Iid, + this.elementUsage_1.Iid, + this.parameter.Iid, + this.parameterType.Iid, + this.parameterOverride.Iid, + this.parameterOverrideSubscription.Iid, + this.category_ElementDefinition.Iid, + this.category_ElementUsage.Iid, + this.category_ParameterType.Iid, + }; + + CollectionAssert.AreEquivalent(expectedAffectedReferenceItems, createdLogEntries.Single(x => x.Iid == this.existingModelLogEntry.LogEntryChangelogItem.First()).AffectedReferenceIid); + CollectionAssert.AreEquivalent(expectedAffectedDomains, this.existingModelLogEntry.AffectedDomainIid); + CollectionAssert.AreEquivalent(expectedAffectedItemIds, this.existingModelLogEntry.AffectedItemIid); + } + + [Test] + public void VerifyThatResultsAreAsExpectedForUpdatedParameterOverrideValueSet() + { + var postOperation = new CdpPostOperation(); + + var parameterOverrideValueSetClasslessDto = new ClasslessDTO() + { + { nameof(Thing.Iid), this.parameterOverrideValueSet_1.Iid }, + { nameof(Thing.ClassKind), ClassKind.ParameterOverrideValueSet.ToString() }, + { nameof(ParameterValueSet.Manual), new ValueArray(new[] { "100" }) } + }; + + var engineeringModelClasslessDto = new ClasslessDTO + { + { nameof(Thing.Iid), this.engineeringModel.Iid }, + { nameof(Thing.ClassKind), ClassKind.EngineeringModel.ToString() }, + { nameof(EngineeringModel.LogEntry), new[] { this.existingModelLogEntry.Iid }.ToList() } + }; + + postOperation.Create.Add(this.existingModelLogEntry); + postOperation.Update.Add(engineeringModelClasslessDto); + postOperation.Update.Add(parameterOverrideValueSetClasslessDto); + + var things = new Thing[] { this.parameterOverrideValueSet_1, this.engineeringModel, this.existingModelLogEntry }; + + LogEntryChangelogItem[] createdLogEntries = { }; + + this.operationProcessor.Setup( + x => + x.Process(It.IsAny(), null, It.IsAny(), null)) + .Callback>( + (operation, transaction, partition, files) + => + { + createdLogEntries = operation.Create.Where(x => x.ClassKind == ClassKind.LogEntryChangelogItem).Cast().ToArray(); + }); + + var result = this.changeLogService.TryAppendModelChangeLogData(null, this.partition, this.actor, 0, postOperation, things); + + Assert.IsTrue(result); + + this.operationProcessor.Verify( + x => x.Process(It.IsAny(), null, It.IsAny(), null), Times.Exactly(1)); + + Assert.AreEqual(1, this.existingModelLogEntry.LogEntryChangelogItem.Count); + + var expectedAffectedReferenceItems = new[] + { + this.elementDefinition_1.Iid, + this.elementUsage_1.Iid, + this.parameter.Iid, + this.parameterType.Iid, + this.parameterOverride.Iid, + this.parameterOverrideSubscription.Iid, + this.category_ElementDefinition.Iid, + this.category_ElementUsage.Iid, + this.category_ParameterType.Iid, + this.domain_ElementDefinition.Iid, + this.domain_ElementUsage.Iid, + this.domain_Parameter.Iid, + this.domain_ParameterOverride.Iid, + }; + + var expectedAffectedDomains = new[] + { + this.domain_ElementDefinition.Iid, + this.domain_ElementUsage.Iid, + this.domain_Parameter.Iid, + this.domain_ParameterOverride.Iid, + }; + + var expectedAffectedItemIds = new[] + { + this.elementDefinition_1.Iid, + this.elementUsage_1.Iid, + this.parameter.Iid, + this.parameterType.Iid, + this.parameterOverride.Iid, + this.parameterOverrideSubscription.Iid, + this.category_ElementDefinition.Iid, + this.category_ElementUsage.Iid, + this.category_ParameterType.Iid, + this.parameterOverrideValueSet_1.Iid + }; + + CollectionAssert.AreEquivalent(expectedAffectedReferenceItems, createdLogEntries.Single(x => x.Iid == this.existingModelLogEntry.LogEntryChangelogItem.First()).AffectedReferenceIid); + CollectionAssert.AreEquivalent(expectedAffectedDomains, this.existingModelLogEntry.AffectedDomainIid); + CollectionAssert.AreEquivalent(expectedAffectedItemIds, this.existingModelLogEntry.AffectedItemIid); + } + + [Test] + public void VerifyThatResultsAreAsExpectedForNewParameterSubscription_SubscriptionIsOnParameter() + { + var postOperation = new CdpPostOperation(); + + this.parameter.ParameterSubscription.Add(this.parameterSubscription.Iid); + + var parameterClasslessDto = new ClasslessDTO() + { + { nameof(Thing.Iid), this.parameter.Iid }, + { nameof(Thing.ClassKind), ClassKind.Parameter.ToString() }, + { nameof(Parameter.ParameterSubscription), new[] { this.parameterSubscription.Iid }.ToList() }, + }; + + var engineeringModelClasslessDto = new ClasslessDTO() + { + { nameof(Thing.Iid), this.engineeringModel.Iid }, + { nameof(Thing.ClassKind), ClassKind.EngineeringModel.ToString() }, + { nameof(EngineeringModel.LogEntry), new[] { this.existingModelLogEntry.Iid }.ToList() } + }; + + postOperation.Create.Add(this.parameterSubscription); + postOperation.Create.Add(this.existingModelLogEntry); + + postOperation.Update.Add(parameterClasslessDto); + postOperation.Update.Add(engineeringModelClasslessDto); + + var things = new Thing[] { this.parameterSubscription, this.parameter, this.engineeringModel }; + + var createdLogEntries = new List(); + + this.operationProcessor.Setup( + x => + x.Process(It.IsAny(), null, It.IsAny(), null)) + .Callback>( + (operation, transaction, partition, files) + => + { + createdLogEntries.AddRange(operation.Create.Where(x => x.ClassKind == ClassKind.LogEntryChangelogItem).Cast().ToArray()); + }); + + var result = this.changeLogService.TryAppendModelChangeLogData(null, this.partition, this.actor, 0, postOperation, things); + + Assert.IsTrue(result); + + this.operationProcessor.Verify( + x => x.Process(It.IsAny(), null, It.IsAny(), null), Times.Exactly(1)); + + Assert.AreEqual(2, this.existingModelLogEntry.LogEntryChangelogItem.Count); + + var expectedParameterSubscriptionAffectedReferenceItems = new[] + { + this.parameter.Iid, + this.parameterType.Iid, + this.elementDefinition_1.Iid, + this.category_ParameterType.Iid, + this.category_ElementDefinition.Iid, + this.domain_ElementDefinition.Iid, + this.domain_Parameter.Iid, + this.domain_ParameterSubscription.Iid + }; + + var parameterSubscriptionCreatedLogEntries = createdLogEntries.Single(x => x.AffectedItemIid == this.parameterSubscription.Iid); + + var expectedParameterAffectedReferenceItems = new[] + { + this.elementDefinition_1.Iid, + this.parameterType.Iid, + this.category_ElementDefinition.Iid, + this.category_ParameterType.Iid, + this.domain_Parameter.Iid, + this.domain_ElementDefinition.Iid, + }; + + var parameterCreatedLogEntries = createdLogEntries.Single(x => x.AffectedItemIid == this.parameter.Iid); + + var expectedAffectedDomains = new[] + { + this.domain_ElementDefinition.Iid, + this.domain_Parameter.Iid, + this.domain_ParameterSubscription.Iid + }; + + var expectedAffectedItemIds = new[] + { + this.category_ParameterType.Iid, + this.category_ElementDefinition.Iid, + this.parameterSubscription.Iid, + this.parameter.Iid, + this.parameterType.Iid, + this.elementDefinition_1.Iid, + }; + + CollectionAssert.AreEquivalent(expectedParameterSubscriptionAffectedReferenceItems, parameterSubscriptionCreatedLogEntries.AffectedReferenceIid); + CollectionAssert.AreEquivalent(expectedParameterAffectedReferenceItems, parameterCreatedLogEntries.AffectedReferenceIid); + + CollectionAssert.AreEquivalent(expectedAffectedDomains, this.existingModelLogEntry.AffectedDomainIid); + CollectionAssert.AreEquivalent(expectedAffectedItemIds, this.existingModelLogEntry.AffectedItemIid); + } + + [Test] + public void VerifyThatResultsAreAsExpectedForDeletedParameterSubscription() + { + var postOperation = new CdpPostOperation(); + + this.parameter.ParameterSubscription.Add(this.parameterSubscription.Iid); + + var parameterSubscriptionClasslessDto = new ClasslessDTO() + { + { nameof(Thing.Iid), this.parameterSubscription.Iid }, + { nameof(Thing.ClassKind), ClassKind.ParameterSubscription.ToString() }, + }; + + var engineeringModelClasslessDto = new ClasslessDTO() + { + { nameof(Thing.Iid), this.engineeringModel.Iid }, + { nameof(Thing.ClassKind), ClassKind.EngineeringModel.ToString() }, + { nameof(EngineeringModel.LogEntry), new[] { this.existingModelLogEntry.Iid }.ToList() } + }; + + postOperation.Create.Add(this.existingModelLogEntry); + + postOperation.Delete.Add(parameterSubscriptionClasslessDto); + postOperation.Update.Add(engineeringModelClasslessDto); + + this.operationProcessor.Setup(x => x.OperationOriginalThingCache).Returns(new List + { + this.parameterSubscription, + this.parameter, + }); + + var things = new Thing[] { this.parameter, this.engineeringModel }; + + var createdLogEntries = new List(); + + this.operationProcessor.Setup( + x => + x.Process(It.IsAny(), null, It.IsAny(), null)) + .Callback>( + (operation, transaction, partition, files) + => + { + createdLogEntries.AddRange(operation.Create.Where(x => x.ClassKind == ClassKind.LogEntryChangelogItem).Cast().ToArray()); + }); + + var result = this.changeLogService.TryAppendModelChangeLogData(null, this.partition, this.actor, 0, postOperation, things); + + Assert.IsTrue(result); + + this.operationProcessor.Verify( + x => x.Process(It.IsAny(), null, It.IsAny(), null), Times.Exactly(1)); + + Assert.AreEqual(1, this.existingModelLogEntry.LogEntryChangelogItem.Count); + + var expectedParameterSubscriptionAffectedReferenceItems = new[] + { + this.elementDefinition_1.Iid, + this.parameter.Iid, + this.parameterType.Iid, + this.category_ParameterType.Iid, + this.category_ElementDefinition.Iid, + this.domain_Parameter.Iid, + this.domain_ParameterSubscription.Iid, + this.domain_ElementDefinition.Iid, + }; + + var parameterSubscriptionCreatedLogEntries = createdLogEntries.Single(x => x.AffectedItemIid == this.parameterSubscription.Iid); + + var expectedAffectedDomains = new[] + { + this.domain_ElementDefinition.Iid, + this.domain_Parameter.Iid, + this.domain_ParameterSubscription.Iid + }; + + var expectedAffectedItemIds = new[] + { + this.category_ParameterType.Iid, + this.category_ElementDefinition.Iid, + this.parameterSubscription.Iid, + this.parameter.Iid, + this.parameterType.Iid, + this.elementDefinition_1.Iid, + }; + + CollectionAssert.AreEquivalent(expectedParameterSubscriptionAffectedReferenceItems, parameterSubscriptionCreatedLogEntries.AffectedReferenceIid); + + CollectionAssert.AreEquivalent(expectedAffectedDomains, this.existingModelLogEntry.AffectedDomainIid); + CollectionAssert.AreEquivalent(expectedAffectedItemIds, this.existingModelLogEntry.AffectedItemIid); + } + + [Test] + public void VerifyThatResultsAreAsExpectedForUpdatedParameterSubscriptionValueSet_SubscriptionIsOnParameter() + { + var postOperation = new CdpPostOperation(); + + this.parameter.ParameterSubscription.Add(this.parameterSubscription.Iid); + + var parameterSubscriptionValueSetClasslessDto = new ClasslessDTO() + { + { nameof(Thing.Iid), this.parameterSubscriptionValueSet_1.Iid }, + { nameof(Thing.ClassKind), ClassKind.ParameterSubscriptionValueSet.ToString() }, + { nameof(ParameterSubscriptionValueSet.Manual), new ValueArray(new[] { "100" }) } + }; + + var engineeringModelClasslessDto = new ClasslessDTO + { + { nameof(Thing.Iid), this.engineeringModel.Iid }, + { nameof(Thing.ClassKind), ClassKind.EngineeringModel.ToString() }, + { nameof(EngineeringModel.LogEntry), new[] { this.existingModelLogEntry.Iid }.ToList() } + }; + + postOperation.Create.Add(this.existingModelLogEntry); + + postOperation.Update.Add(parameterSubscriptionValueSetClasslessDto); + postOperation.Update.Add(engineeringModelClasslessDto); + + var things = new Thing[] { this.parameterSubscriptionValueSet_1, this.engineeringModel }; + + LogEntryChangelogItem[] createdLogEntries = { }; + + this.operationProcessor.Setup( + x => + x.Process(It.IsAny(), null, It.IsAny(), null)) + .Callback>( + (operation, transaction, partition, files) + => + { + createdLogEntries = operation.Create.Where(x => x.ClassKind == ClassKind.LogEntryChangelogItem).Cast().ToArray(); + }); + + var result = this.changeLogService.TryAppendModelChangeLogData(null, this.partition, this.actor, 0, postOperation, things); + + Assert.IsTrue(result); + + this.operationProcessor.Verify( + x => x.Process(It.IsAny(), null, It.IsAny(), null), Times.Exactly(1)); + + Assert.AreEqual(1, this.existingModelLogEntry.LogEntryChangelogItem.Count); + + var expectedAffectedReferenceItems = new[] + { + this.elementDefinition_1.Iid, + this.parameter.Iid, + this.parameterType.Iid, + this.parameterSubscription.Iid, + this.category_ElementDefinition.Iid, + this.category_ParameterType.Iid, + this.domain_ElementDefinition.Iid, + this.domain_Parameter.Iid, + this.domain_ParameterSubscription.Iid + }; + + var expectedAffectedDomains = new[] + { + this.domain_ElementDefinition.Iid, + this.domain_Parameter.Iid, + this.domain_ParameterSubscription.Iid + }; + + var expectedAffectedItemIds = new[] + { + this.elementDefinition_1.Iid, + this.parameter.Iid, + this.parameterType.Iid, + this.parameterSubscription.Iid, + this.category_ElementDefinition.Iid, + this.category_ParameterType.Iid, + this.parameterSubscriptionValueSet_1.Iid + }; + + CollectionAssert.AreEquivalent(expectedAffectedDomains, this.existingModelLogEntry.AffectedDomainIid); + CollectionAssert.AreEquivalent(expectedAffectedItemIds, this.existingModelLogEntry.AffectedItemIid); + CollectionAssert.AreEquivalent(expectedAffectedReferenceItems, createdLogEntries.Single(x => x.Iid == this.existingModelLogEntry.LogEntryChangelogItem.First()).AffectedReferenceIid); + } + + [Test] + public void VerifyThatResultsAreAsExpectedForNewParameterSubscription_SubscriptionIsOnParameterOverride() + { + var postOperation = new CdpPostOperation(); + + this.parameterOverride.ParameterSubscription.Add(this.parameterOverrideSubscription.Iid); + + var parameterClasslessDto = new ClasslessDTO() + { + { nameof(Thing.Iid), this.parameterOverride.Iid }, + { nameof(Thing.ClassKind), ClassKind.ParameterOverride.ToString() }, + { nameof(Parameter.ParameterSubscription), new[] { this.parameterOverrideSubscription.Iid }.ToList() }, + }; + + var engineeringModelClasslessDto = new ClasslessDTO() + { + { nameof(Thing.Iid), this.engineeringModel.Iid }, + { nameof(Thing.ClassKind), ClassKind.EngineeringModel.ToString() }, + { nameof(EngineeringModel.LogEntry), new[] { this.existingModelLogEntry.Iid }.ToList() } + }; + + postOperation.Create.Add(this.parameterOverrideSubscription); + postOperation.Create.Add(this.existingModelLogEntry); + + postOperation.Update.Add(parameterClasslessDto); + postOperation.Update.Add(engineeringModelClasslessDto); + + var things = new Thing[] { this.parameterOverrideSubscription, this.parameterOverride, this.engineeringModel }; + + var createdLogEntries = new List(); + + this.operationProcessor.Setup( + x => + x.Process(It.IsAny(), null, It.IsAny(), null)) + .Callback>( + (operation, transaction, partition, files) + => + { + createdLogEntries.AddRange(operation.Create.Where(x => x.ClassKind == ClassKind.LogEntryChangelogItem).Cast().ToArray()); + }); + + var result = this.changeLogService.TryAppendModelChangeLogData(null, this.partition, this.actor, 0, postOperation, things); + + Assert.IsTrue(result); + + this.operationProcessor.Verify( + x => x.Process(It.IsAny(), null, It.IsAny(), null), Times.Exactly(1)); + + Assert.AreEqual(2, this.existingModelLogEntry.LogEntryChangelogItem.Count); + + var expectedParameterOverrideSubscriptionAffectedReferenceItems = new[] + { + this.elementDefinition_1.Iid, + this.elementUsage_1.Iid, + this.parameter.Iid, + this.parameterType.Iid, + this.parameterOverride.Iid, + this.category_ElementUsage.Iid, + this.category_ElementDefinition.Iid, + this.category_ParameterType.Iid, + this.domain_ElementDefinition.Iid, + this.domain_ElementUsage.Iid, + this.domain_Parameter.Iid, + this.domain_ParameterOverride.Iid, + this.domain_ParameterOverrideSubscription.Iid + }; + + var parameterOverrideSubscriptionCreatedLogEntries = createdLogEntries.Single(x => x.AffectedItemIid == this.parameterOverrideSubscription.Iid); + + var expectedParameterOverrideAffectedReferenceItems = new[] + { + this.elementDefinition_1.Iid, + this.elementUsage_1.Iid, + this.parameter.Iid, + this.parameterType.Iid, + this.category_ElementUsage.Iid, + this.category_ElementDefinition.Iid, + this.category_ParameterType.Iid, + this.domain_ElementDefinition.Iid, + this.domain_ElementUsage.Iid, + this.domain_Parameter.Iid, + this.domain_ParameterOverride.Iid + }; + + var parameterOverrideCreatedLogEntries = createdLogEntries.Single(x => x.AffectedItemIid == this.parameterOverride.Iid); + + var expectedAffectedDomains = new[] + { + this.domain_ElementDefinition.Iid, + this.domain_ElementUsage.Iid, + this.domain_Parameter.Iid, + this.domain_ParameterOverride.Iid, + this.domain_ParameterOverrideSubscription.Iid + }; + + var expectedAffectedItemIds = new[] + { + this.elementDefinition_1.Iid, + this.elementUsage_1.Iid, + this.parameter.Iid, + this.parameterType.Iid, + this.parameterOverride.Iid, + this.category_ElementUsage.Iid, + this.category_ElementDefinition.Iid, + this.category_ParameterType.Iid, + this.parameterOverrideSubscription.Iid, + }; + + CollectionAssert.AreEquivalent(expectedParameterOverrideSubscriptionAffectedReferenceItems, parameterOverrideSubscriptionCreatedLogEntries.AffectedReferenceIid); + CollectionAssert.AreEquivalent(expectedParameterOverrideAffectedReferenceItems, parameterOverrideCreatedLogEntries.AffectedReferenceIid); + + CollectionAssert.AreEquivalent(expectedAffectedDomains, this.existingModelLogEntry.AffectedDomainIid); + CollectionAssert.AreEquivalent(expectedAffectedItemIds, this.existingModelLogEntry.AffectedItemIid); + } + + [Test] + public void VerifyThatResultsAreAsExpectedForUpdatedParameterSubscriptionValueSet_SubscriptionIsOnParameterOverride() + { + var postOperation = new CdpPostOperation(); + + this.parameterOverride.ParameterSubscription.Add(this.parameterOverrideSubscription.Iid); + + var parameterOverrideSubscriptionValueSetClasslessDto = new ClasslessDTO() + { + { nameof(Thing.Iid), this.parameterOverrideSubscriptionValueSet_1.Iid }, + { nameof(Thing.ClassKind), ClassKind.ParameterSubscriptionValueSet.ToString() }, + { nameof(ParameterSubscriptionValueSet.Manual), new ValueArray(new[] { "100" }) } + }; + + var engineeringModelClasslessDto = new ClasslessDTO + { + { nameof(Thing.Iid), this.engineeringModel.Iid }, + { nameof(Thing.ClassKind), ClassKind.EngineeringModel.ToString() }, + { nameof(EngineeringModel.LogEntry), new[] { this.existingModelLogEntry.Iid }.ToList() } + }; + + postOperation.Create.Add(this.existingModelLogEntry); + + postOperation.Update.Add(parameterOverrideSubscriptionValueSetClasslessDto); + postOperation.Update.Add(engineeringModelClasslessDto); + + var things = new Thing[] { this.parameterOverrideSubscriptionValueSet_1, this.engineeringModel }; + + LogEntryChangelogItem[] createdLogEntries = { }; + + this.operationProcessor.Setup( + x => + x.Process(It.IsAny(), null, It.IsAny(), null)) + .Callback>( + (operation, transaction, partition, files) + => + { + createdLogEntries = operation.Create.Where(x => x.ClassKind == ClassKind.LogEntryChangelogItem).Cast().ToArray(); + }); + + var result = this.changeLogService.TryAppendModelChangeLogData(null, this.partition, this.actor, 0, postOperation, things); + + Assert.IsTrue(result); + + this.operationProcessor.Verify( + x => x.Process(It.IsAny(), null, It.IsAny(), null), Times.Exactly(1)); + + Assert.AreEqual(1, this.existingModelLogEntry.LogEntryChangelogItem.Count); + + var expectedAffectedReferenceItems = new[] + { + this.elementDefinition_1.Iid, + this.elementUsage_1.Iid, + this.parameter.Iid, + this.parameterType.Iid, + this.parameterOverride.Iid, + this.parameterOverrideSubscription.Iid, + this.category_ElementDefinition.Iid, + this.category_ElementUsage.Iid, + this.category_ParameterType.Iid, + this.domain_ElementDefinition.Iid, + this.domain_ElementUsage.Iid, + this.domain_Parameter.Iid, + this.domain_ParameterOverride.Iid, + this.domain_ParameterOverrideSubscription.Iid + }; + + var expectedAffectedDomains = new[] + { + this.domain_ElementDefinition.Iid, + this.domain_ElementUsage.Iid, + this.domain_Parameter.Iid, + this.domain_ParameterOverride.Iid, + this.domain_ParameterOverrideSubscription.Iid + }; + + var expectedAffectedItemIds = new[] + { + this.elementDefinition_1.Iid, + this.elementUsage_1.Iid, + this.parameter.Iid, + this.parameterType.Iid, + this.parameterOverride.Iid, + this.parameterOverrideSubscription.Iid, + this.category_ElementDefinition.Iid, + this.category_ElementUsage.Iid, + this.category_ParameterType.Iid, + this.parameterOverrideSubscriptionValueSet_1.Iid + }; + + CollectionAssert.AreEquivalent(expectedAffectedReferenceItems, createdLogEntries.Single(x => x.Iid == this.existingModelLogEntry.LogEntryChangelogItem.First()).AffectedReferenceIid); + CollectionAssert.AreEquivalent(expectedAffectedDomains, this.existingModelLogEntry.AffectedDomainIid); + CollectionAssert.AreEquivalent(expectedAffectedItemIds, this.existingModelLogEntry.AffectedItemIid); + } + } +} diff --git a/CDP4WebServices.API.Tests/SideEffects/MeasurementScaleSideEffectTestFixture.cs b/CDP4WebServices.API.Tests/SideEffects/MeasurementScaleSideEffectTestFixture.cs index f9cebaa3..7bee6fcc 100644 --- a/CDP4WebServices.API.Tests/SideEffects/MeasurementScaleSideEffectTestFixture.cs +++ b/CDP4WebServices.API.Tests/SideEffects/MeasurementScaleSideEffectTestFixture.cs @@ -65,7 +65,7 @@ public class MeasurementScaleSideEffectTestFixture private Mock scaleValueDefinitionService; private Mock measurementScaleService; - private MeasurementScaleSideEffect sideEffect; + private MeasurementScaleSideEffect sideEffect; [SetUp] public void Setup() @@ -172,7 +172,7 @@ public void Setup() this.containerMeasurementScale }); - this.sideEffect = new MeasurementScaleSideEffect + this.sideEffect = new MeasurementScaleSideEffect { SiteReferenceDataLibraryService = this.siteReferenceDataLibraryService.Object, MappingToReferenceScaleService = this.mappingToReferenceScaleService.Object, diff --git a/CDP4WebServices.API/CDP4WebServices.API.csproj b/CDP4WebServices.API/CDP4WebServices.API.csproj index bfd1861c..9815a48c 100644 --- a/CDP4WebServices.API/CDP4WebServices.API.csproj +++ b/CDP4WebServices.API/CDP4WebServices.API.csproj @@ -13,6 +13,12 @@ true + + + + + + @@ -20,6 +26,8 @@ + + @@ -38,10 +46,6 @@ - - - - Never diff --git a/CDP4WebServices.API/Cdp4Bootstrapper.cs b/CDP4WebServices.API/Cdp4Bootstrapper.cs index e55a72b5..57f66b6f 100644 --- a/CDP4WebServices.API/Cdp4Bootstrapper.cs +++ b/CDP4WebServices.API/Cdp4Bootstrapper.cs @@ -1,6 +1,25 @@ // -------------------------------------------------------------------------------------------------------------------- // -// Copyright (c) 2016-2020 RHEA System S.A. +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Alex Vorobiev, Alexander van Delft +// +// This file is part of CDP4 Web Services Community Edition. +// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// This is an auto-generated class. Any manual changes to this file will be overwritten! +// +// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The CDP4 Web Services Community Edition 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 +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . // // -------------------------------------------------------------------------------------------------------------------- @@ -28,11 +47,13 @@ namespace CDP4WebServices.API using CDP4WebService.Authentication; + using CDP4WebServices.API.ChangeNotification; using CDP4WebServices.API.Configuration; using CDP4WebServices.API.Helpers; using CDP4WebServices.API.Services; using CDP4WebServices.API.Services.Authentication; using CDP4WebServices.API.Services.Authorization; + using CDP4WebServices.API.Services.ChangeLog; using CDP4WebServices.API.Services.ContributorsLocation; using CDP4WebServices.API.Services.DataStore; using CDP4WebServices.API.Services.Email; @@ -41,6 +62,8 @@ namespace CDP4WebServices.API using CDP4WebServices.API.Services.Operations.SideEffects; using CDP4WebServices.API.Services.Supplemental; + using Hangfire; + using Nancy; using Nancy.Bootstrapper; using Nancy.Bootstrappers.Autofac; @@ -109,12 +132,32 @@ protected override void ConfigureApplicationContainer(ILifetimeScope existingCon builder.RegisterTypeAsPropertyInjectedSingleton(); builder.RegisterTypeAsPropertyInjectedSingleton(); builder.RegisterTypeAsPropertyInjectedSingleton(); - builder.RegisterTypeAsPropertyInjectedSingleton(); builder.RegisterTypeAsPropertyInjectedSingleton(); builder.RegisterTypeAsPropertyInjectedSingleton(); builder.RegisterTypeAsPropertyInjectedSingleton(); - builder.RegisterTypeAsPropertyInjectedSingleton(); + builder.RegisterTypeAsPropertyInjectedSingleton(); }); + + this.ConfigureRecurringJobs(); + } + + /// + /// Configures recurring jobs + /// + public void ConfigureRecurringJobs() + { + Logger.Info("Configuring cron jobs"); + + var builder = new ContainerBuilder(); + builder.RegisterType().InstancePerBackgroundJob(); + GlobalConfiguration.Configuration.UseAutofacActivator(builder.Build()); + + if (AppConfig.Current.Changelog.AllowEmailNotification) + { + RecurringJob.AddOrUpdate("ChangeNotificationService.Execute", notificationService => notificationService.Execute(), Cron.Weekly(DayOfWeek.Monday, 0, 15)); + } + + Logger.Info("Cron jobs configured"); } /// @@ -140,6 +183,9 @@ protected override void ConfigureRequestContainer(ILifetimeScope container, Nanc container.Update( builder => { + // obfuscation service + builder.RegisterTypeAsPropertyInjectedSingleton(); + // local storage controller to stream binary to disk builder.RegisterTypeAsPropertyInjectedSingleton(); @@ -155,6 +201,9 @@ protected override void ConfigureRequestContainer(ILifetimeScope container, Nanc // wireup service provider builder.RegisterTypeAsPropertyInjectedSingleton(); + // wireup change log service + builder.RegisterTypeAsPropertyInjectedSingleton(); + // wireup class meta info provider builder.RegisterTypeAsPropertyInjectedSingleton(); diff --git a/CDP4WebServices.API/ChangeNotification/ChangeNoticationService.cs b/CDP4WebServices.API/ChangeNotification/ChangeNoticationService.cs new file mode 100644 index 00000000..46968226 --- /dev/null +++ b/CDP4WebServices.API/ChangeNotification/ChangeNoticationService.cs @@ -0,0 +1,299 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Alex Vorobiev, Alexander van Delft +// +// This file is part of CDP4 Web Services Community Edition. +// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// This is an auto-generated class. Any manual changes to this file will be overwritten! +// +// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The CDP4 Web Services Community Edition 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 +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace CDP4WebServices.API.ChangeNotification +{ + using System; + using System.Collections.Generic; + using System.Data; + using System.Diagnostics; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + + using Autofac; + + using CDP4Common.DTO; + + using CDP4JsonSerializer; + + using CDP4Orm.Dao; + using CDP4Orm.Dao.Resolve; + + using CDP4WebServices.API.ChangeNotification.Data; + using CDP4WebServices.API.ChangeNotification.UserPreference; + using CDP4WebServices.API.Configuration; + using CDP4WebServices.API.Services; + using CDP4WebServices.API.Services.Email; + + using Newtonsoft.Json; + + using NLog; + + using Npgsql; + + /// + /// The purpose of the is to send email notifications to users + /// + public class ChangeNoticationService + { + /// + /// A instance + /// + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + /// + /// The DI container used to resolve the services required to interact with the database + /// + private readonly IContainer container; + + /// + /// Initializes a new instance of the + /// + public ChangeNoticationService() + { + this.container = this.RegisterServices(); + } + + /// + /// Register the required services to interact with the database + /// + public IContainer RegisterServices() + { + var builder = new ContainerBuilder(); + + //wireup data model utils + builder.RegisterTypeAsPropertyInjectedSingleton(); + + // wireup command logger for this request + builder.RegisterTypeAsPropertyInjectedSingleton(); + + // wireup class meta info provider + builder.RegisterTypeAsPropertyInjectedSingleton(); + + // wireup class cdp4JsonSerializer + builder.RegisterTypeAsPropertyInjectedSingleton(); + + // the ResolveDao is used to get type info on any Thing instance based on it's unique identifier + builder.RegisterTypeAsPropertyInjectedSingleton(); + + // The ChanglogRetriever retrieves changelog data from the database + builder.RegisterTypeAsPropertyInjectedSingleton(); + + // The ChanglogRetriever retrieves changelog data from the database + builder.RegisterTypeAsPropertyInjectedSingleton(); + + // wireup DAO classes + builder.RegisterDerivedTypesAsPropertyInjectedSingleton(); + + // Used to send emails + builder.RegisterTypeAsPropertyInjectedSingleton(); + + return builder.Build(); + } + + /// + /// Executes the , processes all + /// + /// + /// An awaitable + /// + public async Task Execute() + { + var sw = Stopwatch.StartNew(); + + var connection = new NpgsqlConnection(Services.Utils.GetConnectionString(AppConfig.Current.Backtier.Database)); + + // ensure an open connection + if (connection.State != ConnectionState.Open) + { + try + { + connection.Open(); + var transaction = connection.BeginTransaction(); + + var personDao = this.container.Resolve(); + + var persons = personDao.Read(transaction, "SiteDirectory", null, true).ToList(); + + foreach (var person in persons) + { + var changelogBodyComposer = this.container.Resolve(); + + if (!person.IsActive) + { + continue; + } + + var emailAddresses = this.GetEmailAdressess(transaction, person).ToList(); + + if (!emailAddresses.Any()) + { + continue; + } + + var changeNotificationSubscriptionUserPreferences = this.GetChangeLogSubscriptionUserPreferences(transaction, person); + + var endDateTime = this.GetEndDateTime(DayOfWeek.Monday); + var startDateTime = endDateTime.AddDays(-7); + var htmlStringBuilder = new StringBuilder(); + var textStringBuilder = new StringBuilder(); + var subject = $"Weekly Changelog from server '{AppConfig.Current.Midtier.HostName}'"; + htmlStringBuilder.AppendLine($"

{subject}
{startDateTime:R} - {endDateTime:R}

"); + textStringBuilder.AppendLine($"{subject}\n{startDateTime:R} - {endDateTime:R}"); + + foreach (var changeNotificationSubscriptionUserPreference in changeNotificationSubscriptionUserPreferences) + { + if (changeNotificationSubscriptionUserPreference.Value.ChangeNotificationSubscriptions.Any() + && changeNotificationSubscriptionUserPreference.Value.ChangeNotificationReportType != ChangeNotificationReportType.None) + { + var changelogSections = changelogBodyComposer.CreateChangelogSections( + transaction, + this.container, + Guid.Parse(changeNotificationSubscriptionUserPreference.Key), + person, + changeNotificationSubscriptionUserPreference.Value, + startDateTime, + endDateTime + ).ToList(); + + htmlStringBuilder.Append(changelogBodyComposer.CreateHtmlBody(changelogSections)); + textStringBuilder.Append(changelogBodyComposer.CreateTextBody(changelogSections)); + } + } + + var emailService = this.container.Resolve(); + await emailService.Send(emailAddresses, subject, textStringBuilder.ToString(), htmlStringBuilder.ToString()); + } + } + catch (PostgresException postgresException) + { + Logger.Error("Could not connect to the database to process Change Notifications. Error message: {0}", postgresException.Message); + } + catch (Exception ex) + { + Logger.Error(ex); + } + finally + { + if (connection?.State == ConnectionState.Open) + { + await connection.CloseAsync(); + } + + Logger.Info($"ChangeNotifications processed in {sw.ElapsedMilliseconds} [ms]"); + } + } + } + + /// + /// Retrieves a 's es used to send the change log email to + /// + /// + /// The current to the database. + /// + /// + /// The + /// + /// + /// An of type + /// + private IEnumerable GetEmailAdressess(NpgsqlTransaction transaction, Person person) + { + if (!person.EmailAddress.Any()) + { + yield break; + } + + var emailAddressDao = this.container.Resolve(); + var emailAddresses = emailAddressDao.Read(transaction, "SiteDirectory", person.EmailAddress).ToList(); + + if (!emailAddresses.Any()) + { + yield break; + } + + if (person.DefaultEmailAddress != null && emailAddresses.Any(x => x.Iid == person.DefaultEmailAddress.Value)) + { + yield return emailAddresses.Single(x => x.Iid == person.DefaultEmailAddress.Value); + } + else + { + yield return emailAddresses.First(); + } + } + + /// + /// Gets the end of the period in which we want to find change log data + /// + /// + /// The for which to return the end + /// + /// + /// The end + /// + private DateTime GetEndDateTime(DayOfWeek dayOfWeek) + { + var dt = DateTime.UtcNow; + var diff = (7 + (dt.DayOfWeek - dayOfWeek)) % 7; + return dt.AddDays(-1 * diff).Date; + } + + /// + /// Gets a of type and + /// + /// + /// The to the database + /// + /// + /// The + /// + /// + /// A of type and + /// + private Dictionary GetChangeLogSubscriptionUserPreferences(NpgsqlTransaction transaction, Person person) + { + var userPreferenceDao = this.container.Resolve(); + + var changeLogSubscriptions = new Dictionary(); + + var userPreferences = + userPreferenceDao + .Read(transaction, "SiteDirectory", person.UserPreference, true) + .Where(x => x.ShortName.StartsWith("ChangeLogSubscriptions_")) + .ToList(); + + foreach (var userPreference in userPreferences) + { + var engineeringModelSuffix = userPreference.ShortName.Replace("ChangeLogSubscriptions_", ""); + var changeNotificationSubscriptionUserPreference = JsonConvert.DeserializeObject(userPreference.Value); + + changeLogSubscriptions.Add(engineeringModelSuffix, changeNotificationSubscriptionUserPreference); + } + + return changeLogSubscriptions; + } + } +} diff --git a/CDP4WebServices.API/ChangeNotification/ChangelogBodyComposer.cs b/CDP4WebServices.API/ChangeNotification/ChangelogBodyComposer.cs new file mode 100644 index 00000000..da872498 --- /dev/null +++ b/CDP4WebServices.API/ChangeNotification/ChangelogBodyComposer.cs @@ -0,0 +1,374 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Merlin Bieze, Alex Vorobiev, Naron Phou, Alexander van Delft, Nathanael Smiechowski, +// Ahmed Abulwafa Ahmed +// +// This file is part of CDP4 Web Services Community Edition. +// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// This is an auto-generated class. Any manual changes to this file will be overwritten! +// +// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The CDP4 Web Services Community Edition 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace CDP4WebServices.API.ChangeNotification +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + + using Autofac; + + using CDP4Common.DTO; + + using CDP4Orm.Dao; + + using CDP4WebServices.API.ChangeNotification.Data; + using CDP4WebServices.API.ChangeNotification.UserPreference; + + using Npgsql; + + /// + /// of a change log body composer that composes the body of an email that contais change log information + /// + public class ChangelogBodyComposer : IChangelogBodyComposer + { + /// + /// Creates the body of the email in text form + /// + /// + /// The of type that contains all data to show in the HTML body + /// + /// + /// The email body text as plain text. + /// + public string CreateTextBody(IEnumerable changeLogSections) + { + var stringBuilder = new StringBuilder(); + + foreach (var section in changeLogSections) + { + stringBuilder.AppendLine(); + stringBuilder.AppendLine(section.Title); + stringBuilder.AppendLine(section.SubTitle); + stringBuilder.AppendLine(section.Description); + } + + return stringBuilder.ToString(); + } + + /// + /// Creates the body of the email in html form + /// + /// + /// The of type that contains all data to show in the HTML body + /// + /// + /// The email body text as HTML. + /// + public string CreateHtmlBody(IEnumerable changeLogSections) + { + var stringBuilder = new StringBuilder(); + + foreach (var section in changeLogSections) + { + stringBuilder.AppendLine("
"); + stringBuilder.AppendLine($"

{section.Title}

"); + stringBuilder.AppendLine($"

{section.SubTitle}

"); + stringBuilder.AppendLine($"

{section.Description.Replace(Environment.NewLine, "
")}

"); + } + + return stringBuilder.ToString(); + } + + /// + /// Create an of type s to be used to compose the email body + /// + /// + /// The current to the database. + /// + /// + /// The used to resolve injectable objects + /// + /// + /// The property of the related + /// + /// + /// The for whom to compose the email + /// + /// + /// The that contains the change notification subscriptions + /// + /// + /// The start of the period we want to collect change log rows for. + /// + /// + /// The end of the period we want to collect change log rows for. + /// + /// + /// An of type s + /// + public IEnumerable CreateChangelogSections( + NpgsqlTransaction transaction , + IContainer container, + Guid engineeringModelIid, + Person person, + ChangeNotificationSubscriptionUserPreference changeNotificationSubscriptionUserPreference, + DateTime startDateTime, + DateTime endDateTime) + { + var partition = $"EngineeringModel_{engineeringModelIid.ToString().Replace("-", "_")}"; + + // if a model does not exist anymore, do not send report + var engineeringModelSetupDao = container.Resolve(); + + var engineeringModelSetup = + engineeringModelSetupDao.Read(transaction, "SiteDirectory") + .FirstOrDefault(x => x.EngineeringModelIid == engineeringModelIid); + + if (engineeringModelSetup == null) + { + yield return this.CreateEngineeringModelNotFoundSection(changeNotificationSubscriptionUserPreference); + + yield break; + } + + // if a user is no longer a participant in a model, or if the participant is not active, then do not send report + var participantDao = container.Resolve(); + + var participants = + participantDao + .Read(transaction, "SiteDirectory") + .Where(x => x.Person == person.Iid && x.IsActive) + .ToList(); + + if (!participants.Any()) + { + yield return this.CreateParticipantNotActiveSection(engineeringModelSetup); + + yield break; + } + + var engineeringModelParticipants = + participants + .Where(x => engineeringModelSetup.Participant.Contains(x.Iid)) + .ToList(); + + if (!engineeringModelParticipants.Any()) + { + yield return this.CreateNoEngineeringModelParticipantSection(engineeringModelSetup); + + yield break; + } + + var domains = + participants + .SelectMany(x => x.Domain) + .Distinct() + .ToList(); + + if (!domains.Any()) + { + yield return this.CreateNoDomainOfExpertiseSection(engineeringModelSetup); + + yield break; + } + + var modelLogEntryDao = container.Resolve(); + + var modelLogEntries = + modelLogEntryDao + .Read(transaction, partition) + .Where(x => + x.ModifiedOn >= startDateTime + && x.ModifiedOn < endDateTime) + .ToList(); + + if (!modelLogEntries.Any() || !modelLogEntries.SelectMany(x => x.LogEntryChangelogItem).Any()) + { + yield return this.CreateNoModelLogEntriesSection(engineeringModelSetup); + + yield break; + } + + if (!modelLogEntries.Any(x => x.AffectedDomainIid.Intersect(domains).Any())) + { + yield return this.CreateNoRelevantChangesFoundSection(engineeringModelSetup); + + yield break; + } + + var filteredModelLogEntries = this.FilterDomains(modelLogEntries, domains); + + var modelLogEntryDataCreator = container.Resolve(); + + var modelLogEntryData = modelLogEntryDataCreator.Create(transaction, partition, container, filteredModelLogEntries, domains, changeNotificationSubscriptionUserPreference); + + var changeNotificationSubscriptionDataGroups = + modelLogEntryData + .SelectMany(x => + x.LogEntryChangelogItemData + .SelectMany(y => + y.ChangeNotificationSubscriptionData)) + .GroupBy(x => x.ChangeNotificationSubscription, x => x.LogEntryChangelogItemData) + .OrderBy(x => x.Key.ChangeNotificationSubscriptionType) + .ThenBy(x => x.Key.ClassKind) + .ThenBy(x => x.Key.Name); + + foreach (var changeNotificationSubscriptionGroup in changeNotificationSubscriptionDataGroups) + { + var changeNotificationSubscriptionData = changeNotificationSubscriptionGroup.Key; + var subTitle = $"{changeNotificationSubscriptionData.Name} / {changeNotificationSubscriptionData.ClassKind} / {changeNotificationSubscriptionData.ChangeNotificationSubscriptionType}"; + var descriptionBuilder = new StringBuilder(); + + foreach (var logEntryChangelogItemData in changeNotificationSubscriptionGroup.OrderBy(x => x.ModelLogEntryData.ModifiedOn)) + { + descriptionBuilder.AppendLine($"{logEntryChangelogItemData.ModelLogEntryData.ModifiedOn}"); + descriptionBuilder.AppendLine($"{logEntryChangelogItemData.ModelLogEntryData.JustificationText}"); + descriptionBuilder.AppendLine($"{logEntryChangelogItemData.ChangelogKind}"); + descriptionBuilder.AppendLine($"{logEntryChangelogItemData.ChangeDescription}"); + descriptionBuilder.AppendLine(""); + } + + yield return new ChangelogSection($"{engineeringModelSetup.Name}", subTitle, descriptionBuilder.ToString()); + } + } + + /// + /// Filter a of type on the presence of any + /// in its + /// + /// + /// The s + /// + /// + /// s + /// + /// + /// The filtered of type . + /// + private List FilterDomains(List modelLogEntries, List domains) + { + return modelLogEntries.Where(x => x.AffectedDomainIid.Intersect(domains).Any()).ToList(); + } + + /// + /// Create a that states that no relevant changes were found + /// + /// + /// The for which to create the + /// + /// + /// The created + /// + private ChangelogSection CreateNoRelevantChangesFoundSection(EngineeringModelSetup engineeringModelSetup) + { + return new ChangelogSection( + $"{engineeringModelSetup.Name}", + "Not Found", + "No relevant change log items found."); + } + + /// + /// Create a that states that no change log entries were found + /// + /// + /// The for which to create the + /// + /// + /// The created + /// + private ChangelogSection CreateNoModelLogEntriesSection(EngineeringModelSetup engineeringModelSetup) + { + return new ChangelogSection( + $"{engineeringModelSetup.Name}", + "Not Found", + "No change log entries found."); + } + + /// + /// Create a that states that the doesn't contain a + /// that matches the s active s. + /// + /// + /// The for which to create the + /// + /// + /// The created + /// + private ChangelogSection CreateNoDomainOfExpertiseSection(EngineeringModelSetup engineeringModelSetup) + { + return new ChangelogSection( + $"{engineeringModelSetup.Name}", + "Failed", + "No active DomainOfExpertise."); + } + + /// + /// Create a that states that the wasn't found in + /// + /// + /// The for which to create the + /// + /// + /// The created + /// + private ChangelogSection CreateNoEngineeringModelParticipantSection(EngineeringModelSetup engineeringModelSetup) + { + return new ChangelogSection( + $"{engineeringModelSetup.Name}", + "Failed", + "Participant not found."); + } + + /// + /// Create a that states that the found in isn't active + /// + /// + /// The for which to create the + /// + /// + /// The created + /// + private ChangelogSection CreateParticipantNotActiveSection(EngineeringModelSetup engineeringModelSetup) + { + return + new ChangelogSection( + $"{engineeringModelSetup.Name}", + "Failed", + "Participant not active."); + } + + /// + /// Create a that states that the wasn't found in the database (anymore) + /// + /// + /// The + /// + /// + /// The created + /// + private ChangelogSection CreateEngineeringModelNotFoundSection(ChangeNotificationSubscriptionUserPreference changeNotificationSubscriptionUserPreference) + { + return + new ChangelogSection( + $"{changeNotificationSubscriptionUserPreference.Name}", + "Failed", + "EngineeringModel not found"); + } + } +} diff --git a/CDP4WebServices.API/ChangeNotification/ChangelogSection.cs b/CDP4WebServices.API/ChangeNotification/ChangelogSection.cs new file mode 100644 index 00000000..bf3b70f3 --- /dev/null +++ b/CDP4WebServices.API/ChangeNotification/ChangelogSection.cs @@ -0,0 +1,62 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Merlin Bieze, Alex Vorobiev, Naron Phou, Alexander van Delft, Nathanael Smiechowski, +// Ahmed Abulwafa Ahmed +// +// This file is part of CDP4 Web Services Community Edition. +// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// This is an auto-generated class. Any manual changes to this file will be overwritten! +// +// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The CDP4 Web Services Community Edition 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace CDP4WebServices.API.ChangeNotification +{ + /// + /// Defines data to be used to compose html/text in a structured form for layout purposes + /// + public class ChangelogSection + { + /// + /// The title + /// + public string Title { get; private set; } + + /// + /// The subtitle + /// + public string SubTitle { get; private set; } + + /// + /// The description + /// + public string Description { get; private set; } + + /// + /// Creates a new instance of the class + /// + /// The title + /// The subtitle + /// The description + public ChangelogSection(string title, string subTitle, string description) + { + this.Title = title; + this.SubTitle = subTitle; + this.Description = description; + } + } +} diff --git a/CDP4WebServices.API/ChangeNotification/Data/ChangeNotificationSubscriptionData.cs b/CDP4WebServices.API/ChangeNotification/Data/ChangeNotificationSubscriptionData.cs new file mode 100644 index 00000000..45e598d2 --- /dev/null +++ b/CDP4WebServices.API/ChangeNotification/Data/ChangeNotificationSubscriptionData.cs @@ -0,0 +1,66 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Merlin Bieze, Alex Vorobiev, Naron Phou, Alexander van Delft, Nathanael Smiechowski, +// Ahmed Abulwafa Ahmed +// +// This file is part of CDP4 Web Services Community Edition. +// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// This is an auto-generated class. Any manual changes to this file will be overwritten! +// +// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The CDP4 Web Services Community Edition 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace CDP4WebServices.API.ChangeNotification.Data +{ + using CDP4WebServices.API.ChangeNotification.Subscription; + + /// + /// Used a part of a structure + /// + public class ChangeNotificationSubscriptionData + { + /// + /// Gets the + /// + public ChangeNotificationSubscription ChangeNotificationSubscription { get; } + + /// + /// Gets the + /// + public LogEntryChangelogItemData LogEntryChangelogItemData { get; } + + /// + /// Gets the name of the + /// + public string Name => this.ChangeNotificationSubscription.Name; + + /// + /// Creates a new instance of + /// + /// + /// The + /// + /// + /// The + /// + public ChangeNotificationSubscriptionData(ChangeNotificationSubscription changeNotificationSubscription, LogEntryChangelogItemData logEntryChangelogItemData) + { + this.ChangeNotificationSubscription = changeNotificationSubscription; + this.LogEntryChangelogItemData = logEntryChangelogItemData; + } + } +} diff --git a/CDP4WebServices.API/ChangeNotification/Data/IModelLogEntryDataCreator.cs b/CDP4WebServices.API/ChangeNotification/Data/IModelLogEntryDataCreator.cs new file mode 100644 index 00000000..31ee93f6 --- /dev/null +++ b/CDP4WebServices.API/ChangeNotification/Data/IModelLogEntryDataCreator.cs @@ -0,0 +1,78 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Merlin Bieze, Alex Vorobiev, Naron Phou, Alexander van Delft, Nathanael Smiechowski, +// Ahmed Abulwafa Ahmed +// +// This file is part of CDP4 Web Services Community Edition. +// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// This is an auto-generated class. Any manual changes to this file will be overwritten! +// +// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The CDP4 Web Services Community Edition 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace CDP4WebServices.API.ChangeNotification.Data +{ + using System; + using System.Collections.Generic; + + using Autofac; + + using CDP4Common.DTO; + + using CDP4WebServices.API.ChangeNotification.UserPreference; + + using Npgsql; + + /// + /// Defines the implementation of a creator + /// + public interface IModelLogEntryDataCreator + { + /// + /// Create an of type for a specific + /// base on a list of filtered s + /// + /// + /// The current to the database. + /// + /// + /// The partition in the database + /// + /// + /// The used to resolve injectable objects + /// + /// + /// The s + /// + /// + /// The s used to filter s to be used. + /// + /// + /// The that contains the change notification subscriptions + /// + /// + /// The created of type + /// + IEnumerable Create( + NpgsqlTransaction transaction, + string engineeringModelPartition, + IContainer container, + IEnumerable modelLogEntries, + IEnumerable domains, + ChangeNotificationSubscriptionUserPreference changeNotificationSubscriptionUserPreference); + } +} diff --git a/CDP4WebServices.API/ChangeNotification/Data/LogEntryChangelogItemData.cs b/CDP4WebServices.API/ChangeNotification/Data/LogEntryChangelogItemData.cs new file mode 100644 index 00000000..1254f937 --- /dev/null +++ b/CDP4WebServices.API/ChangeNotification/Data/LogEntryChangelogItemData.cs @@ -0,0 +1,95 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Merlin Bieze, Alex Vorobiev, Naron Phou, Alexander van Delft, Nathanael Smiechowski, +// Ahmed Abulwafa Ahmed +// +// This file is part of CDP4 Web Services Community Edition. +// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// This is an auto-generated class. Any manual changes to this file will be overwritten! +// +// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The CDP4 Web Services Community Edition 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace CDP4WebServices.API.ChangeNotification.Data +{ + using System.Collections.Generic; + using System.Linq; + + using CDP4Common.DTO; + + using CDP4WebServices.API.ChangeNotification.Subscription; + + public class LogEntryChangelogItemData + { + /// + /// Gets the + /// + public ModelLogEntryData ModelLogEntryData { get; } + + /// + /// Gets the + /// + public LogEntryChangelogItem LogEntryChangelogItem { get; } + + /// + /// Gets the s property + /// + public string ChangeDescription => this.LogEntryChangelogItem.ChangeDescription; + + /// + /// Gets the s property + /// + public string ChangelogKind => this.LogEntryChangelogItem.ChangelogKind.ToString(); + + /// + /// Gets the related of type + /// + public ICollection ChangeNotificationSubscriptionData { get; } = new List(); + + /// + /// Creates a new instance of + /// + /// + /// The + /// + /// + /// The + /// + public LogEntryChangelogItemData(LogEntryChangelogItem logEntryChangelogItem, ModelLogEntryData modelLogEntryData) + { + this.ModelLogEntryData = modelLogEntryData; + this.LogEntryChangelogItem = logEntryChangelogItem; + } + + /// + /// Tries to add a based on a instance + /// to the property. + /// If a for the was already create in the + /// property, then no new one will be created. + /// + /// + /// The + /// + public void TryAddChangeNotificationSubscriptionData(ChangeNotificationSubscription changeNotificationSubscription) + { + if (this.ChangeNotificationSubscriptionData.All(x => x.ChangeNotificationSubscription != changeNotificationSubscription)) + { + this.ChangeNotificationSubscriptionData.Add(new ChangeNotificationSubscriptionData(changeNotificationSubscription, this)); + } + } + } +} diff --git a/CDP4WebServices.API/ChangeNotification/Data/ModelLogEntryData.cs b/CDP4WebServices.API/ChangeNotification/Data/ModelLogEntryData.cs new file mode 100644 index 00000000..86026459 --- /dev/null +++ b/CDP4WebServices.API/ChangeNotification/Data/ModelLogEntryData.cs @@ -0,0 +1,97 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Merlin Bieze, Alex Vorobiev, Naron Phou, Alexander van Delft, Nathanael Smiechowski, +// Ahmed Abulwafa Ahmed +// +// This file is part of CDP4 Web Services Community Edition. +// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// This is an auto-generated class. Any manual changes to this file will be overwritten! +// +// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The CDP4 Web Services Community Edition 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace CDP4WebServices.API.ChangeNotification.Data +{ + using System; + using System.Collections.Generic; + using System.Linq; + + using CDP4Common.DTO; + + using CDP4WebServices.API.ChangeNotification.Subscription; + + /// + /// Parent class of an object structure used to create a selection of data from s, + /// s and s. + /// + public class ModelLogEntryData + { + /// + /// Gets the + /// + public ModelLogEntry ModelLogEntry { get; } + + /// + /// Get sthe 's + /// + public string JustificationText => this.ModelLogEntry.Content; + + /// + /// Get sthe 's + /// + public DateTime ModifiedOn => this.ModelLogEntry.ModifiedOn; + + /// + /// Gets the related of type + /// + public ICollection LogEntryChangelogItemData { get; } = new List(); + + /// + /// Creates a new instance of + /// + /// + /// The + /// + public ModelLogEntryData(ModelLogEntry modelLogEntry) + { + this.ModelLogEntry = modelLogEntry; + } + + /// + /// Tries to add objects base on a combination of and instances to the object structure + /// where this is the root object of. + /// If a for the was already create in the + /// property, then no new one will be created. + /// + /// + /// The + /// + /// + /// The + /// + public void TryAddLogEntryChangelogData(LogEntryChangelogItem logEntryChangelogItem, ChangeNotificationSubscription changeNotificationSubscription) + { + if (this.LogEntryChangelogItemData.All(x => x.LogEntryChangelogItem != logEntryChangelogItem)) + { + this.LogEntryChangelogItemData.Add(new LogEntryChangelogItemData(logEntryChangelogItem, this)); + } + + var logEntryChangelogItemData = this.LogEntryChangelogItemData.Single(x => x.LogEntryChangelogItem == logEntryChangelogItem); + logEntryChangelogItemData.TryAddChangeNotificationSubscriptionData(changeNotificationSubscription); + } + } +} diff --git a/CDP4WebServices.API/ChangeNotification/Data/ModelLogEntryDataCreator.cs b/CDP4WebServices.API/ChangeNotification/Data/ModelLogEntryDataCreator.cs new file mode 100644 index 00000000..90e6cd09 --- /dev/null +++ b/CDP4WebServices.API/ChangeNotification/Data/ModelLogEntryDataCreator.cs @@ -0,0 +1,161 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Merlin Bieze, Alex Vorobiev, Naron Phou, Alexander van Delft, Nathanael Smiechowski, +// Ahmed Abulwafa Ahmed +// +// This file is part of CDP4 Web Services Community Edition. +// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// This is an auto-generated class. Any manual changes to this file will be overwritten! +// +// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The CDP4 Web Services Community Edition 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace CDP4WebServices.API.ChangeNotification.Data +{ + using System; + using System.Collections.Generic; + using System.Linq; + + using Autofac; + + using CDP4Common.DTO; + + using CDP4Orm.Dao; + + using CDP4WebServices.API.ChangeNotification.Notification; + using CDP4WebServices.API.ChangeNotification.Subscription; + using CDP4WebServices.API.ChangeNotification.UserPreference; + + using Npgsql; + + /// + /// Contains logic used to create an of type for a specific + /// base on a list of filtered s. + /// + public class ModelLogEntryDataCreator : IModelLogEntryDataCreator + { + /// + /// Create an of type for a specific + /// base on a list of filtered s + /// + /// + /// The current to the database. + /// + /// + /// The partition in the database + /// + /// + /// The used to resolve injectable objects + /// + /// + /// The s + /// + /// + /// The s used to filter s to be used. + /// + /// + /// The that contains the change notification subscriptions + /// + /// + /// The created of type + /// + public IEnumerable Create( + NpgsqlTransaction transaction, + string engineeringModelPartition, + IContainer container, + IEnumerable modelLogEntries, + IEnumerable domains, + ChangeNotificationSubscriptionUserPreference changeNotificationSubscriptionUserPreference) + { + var modelLogEntryDataList = new List(); + + var domainOfExpertiseDao = container.Resolve(); + var logEntryChangeLogItemDao = container.Resolve(); + + var domainOfExpertises = domainOfExpertiseDao.Read(transaction, "SiteDirectory", domains).ToList(); + + foreach (var changeNotificationSubscription in changeNotificationSubscriptionUserPreference.ChangeNotificationSubscriptions) + { + var changeNotificationFilter = this.CreateChangeLogNotificationFilter(changeNotificationSubscription, domainOfExpertises); + + foreach (var modelLogEntry in modelLogEntries.OrderBy(x => x.CreatedOn)) + { + if (changeNotificationFilter.CheckFilter(modelLogEntry)) + { + var modelLogEntryData = new ModelLogEntryData(modelLogEntry); + var addModelLogEntryData = false; + + var logEntryChangeLogItems = + logEntryChangeLogItemDao.Read(transaction, engineeringModelPartition, modelLogEntry.LogEntryChangelogItem).ToList(); + + foreach (var logEntryChangelogItem in logEntryChangeLogItems) + { + if (changeNotificationFilter.CheckFilter(logEntryChangelogItem)) + { + addModelLogEntryData = true; + modelLogEntryData.TryAddLogEntryChangelogData(logEntryChangelogItem, changeNotificationSubscription); + } + } + + if (addModelLogEntryData) + { + modelLogEntryDataList.Add(modelLogEntryData); + } + } + } + } + + return modelLogEntryDataList; + } + + /// + /// Create a for a . + /// + /// + /// The + /// + /// + /// The current user's s + /// + /// + /// The + /// + private IChangeNotificationFilter CreateChangeLogNotificationFilter(ChangeNotificationSubscription changeNotificationSubscription, IEnumerable domainOfExpertises) + { + switch (changeNotificationSubscription.ChangeNotificationSubscriptionType) + { + case ChangeNotificationSubscriptionType.AppliedCategory: + return new CategoryChangeNotificationFilter(changeNotificationSubscription, domainOfExpertises); + + case ChangeNotificationSubscriptionType.ParameterSubscription: + return new ParameterSubscriptionChangeNotificationFilter(changeNotificationSubscription, domainOfExpertises); + + case ChangeNotificationSubscriptionType.ParameterType: + return new ParameterTypeChangeNotificationFilter(changeNotificationSubscription, domainOfExpertises); + + case ChangeNotificationSubscriptionType.NamedThing: + return new DefaultThingChangeNotificationFilter(changeNotificationSubscription, domainOfExpertises); + + case ChangeNotificationSubscriptionType.ParameterOrOverride: + return new ParameterOrOverrideChangeNotificationFilter(changeNotificationSubscription, domainOfExpertises); + + default: + throw new NotImplementedException($"{changeNotificationSubscription.ChangeNotificationSubscriptionType} is not implemented."); + } + } + } +} diff --git a/CDP4WebServices.API/ChangeNotification/IChangelogBodyComposer.cs b/CDP4WebServices.API/ChangeNotification/IChangelogBodyComposer.cs new file mode 100644 index 00000000..ba81f3b7 --- /dev/null +++ b/CDP4WebServices.API/ChangeNotification/IChangelogBodyComposer.cs @@ -0,0 +1,103 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Merlin Bieze, Alex Vorobiev, Naron Phou, Alexander van Delft, Nathanael Smiechowski, +// Ahmed Abulwafa Ahmed +// +// This file is part of CDP4 Web Services Community Edition. +// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// This is an auto-generated class. Any manual changes to this file will be overwritten! +// +// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The CDP4 Web Services Community Edition 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace CDP4WebServices.API.ChangeNotification +{ + using System; + using System.Collections.Generic; + + using Autofac; + + using CDP4Common.DTO; + + using CDP4WebServices.API.ChangeNotification.UserPreference; + + using Npgsql; + + /// + /// Defines the implementation of a change log body composer that composes the body of an email that contais change log information + /// + public interface IChangelogBodyComposer + { + /// + /// Creates the body of the email in text form + /// + /// + /// The of type that contains all data to show in the HTML body + /// + /// + /// The email body text as plain text. + /// + string CreateTextBody(IEnumerable changeLogSections); + + /// + /// Creates the body of the email in html form + /// + /// + /// The of type that contains all data to show in the HTML body + /// + /// + /// The email body text as HTML. + /// + string CreateHtmlBody(IEnumerable changeLogSections); + + /// + /// Create an of type s to be used to compose the email body + /// + /// + /// The current to the database. + /// + /// + /// The used to resolve injectable objects + /// + /// + /// The property of the related + /// + /// + /// The for whom to compose the email + /// + /// + /// The that contains the change notification subscriptions + /// + /// + /// The start of the period we want to collect change log rows for. + /// + /// + /// The end of the period we want to collect change log rows for. + /// + /// + /// An of type s + /// + IEnumerable CreateChangelogSections( + NpgsqlTransaction transaction , + IContainer container, + Guid engineeringModelIid, + Person person, + ChangeNotificationSubscriptionUserPreference changeNotificationSubscriptionUserPreference, + DateTime startDateTime, + DateTime endDateTime); + } +} diff --git a/CDP4WebServices.API/ChangeNotification/Notification/CategoryChangeNotificationFilter.cs b/CDP4WebServices.API/ChangeNotification/Notification/CategoryChangeNotificationFilter.cs new file mode 100644 index 00000000..7f917f79 --- /dev/null +++ b/CDP4WebServices.API/ChangeNotification/Notification/CategoryChangeNotificationFilter.cs @@ -0,0 +1,71 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Merlin Bieze, Alex Vorobiev, Naron Phou, Alexander van Delft, Nathanael Smiechowski, +// Ahmed Abulwafa Ahmed +// +// This file is part of CDP4 Web Services Community Edition. +// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// This is an auto-generated class. Any manual changes to this file will be overwritten! +// +// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The CDP4 Web Services Community Edition 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace CDP4WebServices.API.ChangeNotification.Notification +{ + using System.Collections.Generic; + using System.Linq; + + using CDP4Common.DTO; + + using CDP4WebServices.API.ChangeNotification; + using CDP4WebServices.API.ChangeNotification.Subscription; + + /// + /// Implements logic for Change notification filtering for a . + /// + public class CategoryChangeNotificationFilter : ChangeNotificationFilter + { + /// + /// Creates a new instance of the class. + /// + /// + /// The that resulted into this . + /// + /// + /// The s where to filter on. + /// + public CategoryChangeNotificationFilter(ChangeNotificationSubscription changeNotificationSubscription, IEnumerable domainOfExpertises) + : base(changeNotificationSubscription, domainOfExpertises) + { + } + + /// + /// Checks if a has certain specifics related to the . + /// + /// + /// The + /// + /// + /// True is the specifics of the match certain criteria, otherwise false. + /// + public override bool CheckFilter(LogEntryChangelogItem logEntryChangelogItem) + { + return logEntryChangelogItem.AffectedReferenceIid.Intersect(this.DomainOfExpertises.Select(x => x.Iid)).Any() + && logEntryChangelogItem.AffectedReferenceIid.Contains(this.Iid); + } + } +} diff --git a/CDP4WebServices.API/ChangeNotification/Notification/ChangeNotificationFilter.cs b/CDP4WebServices.API/ChangeNotification/Notification/ChangeNotificationFilter.cs new file mode 100644 index 00000000..4846fa24 --- /dev/null +++ b/CDP4WebServices.API/ChangeNotification/Notification/ChangeNotificationFilter.cs @@ -0,0 +1,97 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Merlin Bieze, Alex Vorobiev, Naron Phou, Alexander van Delft, Nathanael Smiechowski, +// Ahmed Abulwafa Ahmed +// +// This file is part of CDP4 Web Services Community Edition. +// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// This is an auto-generated class. Any manual changes to this file will be overwritten! +// +// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The CDP4 Web Services Community Edition 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace CDP4WebServices.API.ChangeNotification.Notification +{ + using System; + using System.Collections.Generic; + using System.Linq; + + using CDP4Common.DTO; + + using CDP4WebServices.API.ChangeNotification.Subscription; + + /// + /// Abstract class that implements the basic logic of an . + /// + public abstract class ChangeNotificationFilter : IChangeNotificationFilter + { + /// + /// The s where to filter on + /// + public IEnumerable DomainOfExpertises { get; } + + /// + /// Gets the of the to be checked for. + /// + public Guid Iid => this.ChangeNotificationSubscription.Iid; + + /// + /// The that resulted into this . + /// + public ChangeNotificationSubscription ChangeNotificationSubscription { get; } + + /// + /// Creates a new instance of the class. + /// + /// + /// The that resulted into this . + /// + /// + /// The s where to filter on. + /// + protected ChangeNotificationFilter(ChangeNotificationSubscription changeNotificationSubscription, IEnumerable domainOfExpertises) + { + this.DomainOfExpertises = domainOfExpertises; + this.ChangeNotificationSubscription = changeNotificationSubscription; + } + + /// + /// Checks if a has certain specifics related to the . + /// + /// + /// The + /// + /// + /// True is the specifics of the match certain criteria, otherwise false. + /// + public bool CheckFilter(ModelLogEntry modelLogEntry) + { + return modelLogEntry.AffectedDomainIid.Intersect(this.DomainOfExpertises.Select(x => x.Iid)).Any(); + } + + /// + /// Checks if a has certain specifics related to the . + /// + /// + /// The + /// + /// + /// True is the specifics of the match certain criteria, otherwise false. + /// + public abstract bool CheckFilter(LogEntryChangelogItem logEntryChangelogItem); + } +} diff --git a/CDP4WebServices.API/ChangeNotification/Notification/DefaultThingChangeNotificationFilter.cs b/CDP4WebServices.API/ChangeNotification/Notification/DefaultThingChangeNotificationFilter.cs new file mode 100644 index 00000000..36667a97 --- /dev/null +++ b/CDP4WebServices.API/ChangeNotification/Notification/DefaultThingChangeNotificationFilter.cs @@ -0,0 +1,71 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Merlin Bieze, Alex Vorobiev, Naron Phou, Alexander van Delft, Nathanael Smiechowski, +// Ahmed Abulwafa Ahmed +// +// This file is part of CDP4 Web Services Community Edition. +// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// This is an auto-generated class. Any manual changes to this file will be overwritten! +// +// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The CDP4 Web Services Community Edition 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace CDP4WebServices.API.ChangeNotification.Notification +{ + using System.Collections.Generic; + using System.Linq; + + using CDP4Common.DTO; + + using CDP4WebServices.API.ChangeNotification.Subscription; + + /// + /// Implements logic for Change notification filtering for an individual . + /// + public class DefaultThingChangeNotificationFilter : ChangeNotificationFilter + { + /// + /// Creates a new instance of the class. + /// + /// + /// The that resulted into this . + /// + /// + /// The s where to filter on. + /// + public DefaultThingChangeNotificationFilter(ChangeNotificationSubscription changeNotificationSubscription, IEnumerable domainOfExpertises) + : base(changeNotificationSubscription, domainOfExpertises) + { + } + + /// + /// Checks if a has certain specifics related to the . + /// + /// + /// The + /// + /// + /// True is the specifics of the match certain criteria, otherwise false. + /// + public override bool CheckFilter(LogEntryChangelogItem logEntryChangelogItem) + { + return logEntryChangelogItem.AffectedReferenceIid.Intersect(this.DomainOfExpertises.Select(x => x.Iid)).Any() + && + (logEntryChangelogItem.AffectedItemIid == this.Iid || logEntryChangelogItem.AffectedReferenceIid.Contains(this.Iid)); + } + } +} diff --git a/CDP4WebServices.API/ChangeNotification/Notification/IChangeNotificationFilter.cs b/CDP4WebServices.API/ChangeNotification/Notification/IChangeNotificationFilter.cs new file mode 100644 index 00000000..3e9e7a99 --- /dev/null +++ b/CDP4WebServices.API/ChangeNotification/Notification/IChangeNotificationFilter.cs @@ -0,0 +1,65 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Merlin Bieze, Alex Vorobiev, Naron Phou, Alexander van Delft, Nathanael Smiechowski, +// Ahmed Abulwafa Ahmed +// +// This file is part of CDP4 Web Services Community Edition. +// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// This is an auto-generated class. Any manual changes to this file will be overwritten! +// +// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The CDP4 Web Services Community Edition 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace CDP4WebServices.API.ChangeNotification.Notification +{ + using System; + + using CDP4Common.DTO; + + /// + /// The interface that defines the implementation of a changelog notification filter + /// + public interface IChangeNotificationFilter + { + /// + /// The that represents a 's . + /// + Guid Iid { get; } + + /// + /// Checks if a has certain specifics related to the . + /// + /// + /// The + /// + /// + /// True is the specifics of the match certain criteria, otherwise false. + /// + bool CheckFilter(ModelLogEntry modelLogEntry); + + /// + /// Checks if a has certain specifics related to the . + /// + /// + /// The + /// + /// + /// True is the specifics of the match certain criteria, otherwise false. + /// + bool CheckFilter(LogEntryChangelogItem logEntryChangelogItem); + } +} diff --git a/CDP4WebServices.API/ChangeNotification/Notification/ParameterOrOverrideChangeNotificationFilter.cs b/CDP4WebServices.API/ChangeNotification/Notification/ParameterOrOverrideChangeNotificationFilter.cs new file mode 100644 index 00000000..4f96588b --- /dev/null +++ b/CDP4WebServices.API/ChangeNotification/Notification/ParameterOrOverrideChangeNotificationFilter.cs @@ -0,0 +1,54 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Merlin Bieze, Alex Vorobiev, Naron Phou, Alexander van Delft, Nathanael Smiechowski, +// Ahmed Abulwafa Ahmed +// +// This file is part of CDP4 Web Services Community Edition. +// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// This is an auto-generated class. Any manual changes to this file will be overwritten! +// +// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The CDP4 Web Services Community Edition 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace CDP4WebServices.API.ChangeNotification.Notification +{ + using System.Collections.Generic; + + using CDP4Common.DTO; + + using CDP4WebServices.API.ChangeNotification.Subscription; + + /// + /// Implements logic for Change notification filtering for an . + /// + public class ParameterOrOverrideChangeNotificationFilter : DefaultThingChangeNotificationFilter + { + /// + /// Creates a new instance of the class. + /// + /// + /// The that resulted into this . + /// + /// + /// The s where to filter on. + /// + public ParameterOrOverrideChangeNotificationFilter(ChangeNotificationSubscription changeNotificationSubscription, IEnumerable domainOfExpertises) + : base(changeNotificationSubscription, domainOfExpertises) + { + } + } +} diff --git a/CDP4WebServices.API/ChangeNotification/Notification/ParameterSubscriptionChangeNotificationFilter.cs b/CDP4WebServices.API/ChangeNotification/Notification/ParameterSubscriptionChangeNotificationFilter.cs new file mode 100644 index 00000000..9f515da1 --- /dev/null +++ b/CDP4WebServices.API/ChangeNotification/Notification/ParameterSubscriptionChangeNotificationFilter.cs @@ -0,0 +1,54 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Merlin Bieze, Alex Vorobiev, Naron Phou, Alexander van Delft, Nathanael Smiechowski, +// Ahmed Abulwafa Ahmed +// +// This file is part of CDP4 Web Services Community Edition. +// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// This is an auto-generated class. Any manual changes to this file will be overwritten! +// +// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The CDP4 Web Services Community Edition 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace CDP4WebServices.API.ChangeNotification.Notification +{ + using System.Collections.Generic; + + using CDP4Common.DTO; + + using CDP4WebServices.API.ChangeNotification.Subscription; + + /// + /// Implements logic for Change notification filtering for an . + /// + public class ParameterSubscriptionChangeNotificationFilter : DefaultThingChangeNotificationFilter + { + /// + /// Creates a new instance of the class. + /// + /// + /// The that resulted into this . + /// + /// + /// The s where to filter on. + /// + public ParameterSubscriptionChangeNotificationFilter(ChangeNotificationSubscription changeNotificationSubscription, IEnumerable domainOfExpertises) + : base(changeNotificationSubscription, domainOfExpertises) + { + } + } +} diff --git a/CDP4WebServices.API/ChangeNotification/Notification/ParameterTypeChangeNotificationFilter.cs b/CDP4WebServices.API/ChangeNotification/Notification/ParameterTypeChangeNotificationFilter.cs new file mode 100644 index 00000000..a6ff1c31 --- /dev/null +++ b/CDP4WebServices.API/ChangeNotification/Notification/ParameterTypeChangeNotificationFilter.cs @@ -0,0 +1,54 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Merlin Bieze, Alex Vorobiev, Naron Phou, Alexander van Delft, Nathanael Smiechowski, +// Ahmed Abulwafa Ahmed +// +// This file is part of CDP4 Web Services Community Edition. +// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// This is an auto-generated class. Any manual changes to this file will be overwritten! +// +// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The CDP4 Web Services Community Edition 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace CDP4WebServices.API.ChangeNotification.Notification +{ + using System.Collections.Generic; + + using CDP4Common.DTO; + + using CDP4WebServices.API.ChangeNotification.Subscription; + + /// + /// Implements logic for Change notification filtering for an . + /// + public class ParameterTypeChangeNotificationFilter : DefaultThingChangeNotificationFilter + { + /// + /// Creates a new instance of the class. + /// + /// + /// The that resulted into this . + /// + /// + /// The s where to filter on. + /// + public ParameterTypeChangeNotificationFilter(ChangeNotificationSubscription changeNotificationSubscription, IEnumerable domainOfExpertises) + : base(changeNotificationSubscription, domainOfExpertises) + { + } + } +} diff --git a/CDP4WebServices.API/ChangeNotification/Subscription/ChangeNotificationSubscription.cs b/CDP4WebServices.API/ChangeNotification/Subscription/ChangeNotificationSubscription.cs new file mode 100644 index 00000000..62a5184f --- /dev/null +++ b/CDP4WebServices.API/ChangeNotification/Subscription/ChangeNotificationSubscription.cs @@ -0,0 +1,89 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Merlin Bieze, Alex Vorobiev, Naron Phou, Alexander van Delft, Nathanael Smiechowski, +// Ahmed Abulwafa Ahmed +// +// This file is part of CDP4 Web Services Community Edition. +// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// This is an auto-generated class. Any manual changes to this file will be overwritten! +// +// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The CDP4 Web Services Community Edition 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- +namespace CDP4WebServices.API.ChangeNotification.Subscription +{ + using System; + + using CDP4Common.CommonData; + + using CDP4WebServices.API.ChangeNotification.Notification; + + using Newtonsoft.Json; + using Newtonsoft.Json.Converters; + + /// + /// Holds data about an individual change notification subscription + /// + public class ChangeNotificationSubscription + { + /// + /// Gets the of the subscription. + /// This will be used to create the correct accordingly. + /// + [JsonConverter(typeof(StringEnumConverter))] + public ChangeNotificationSubscriptionType ChangeNotificationSubscriptionType { get; } + + /// + /// Gets the of the the subscription references. + /// + public Guid Iid { get; } + + /// + /// Gets the name of the the subscription references. + /// + public string Name { get; } + + /// + /// Gets the of the the subscription references. + /// + [JsonConverter(typeof(StringEnumConverter))] + public ClassKind ClassKind { get; } + + /// + /// Create a new instance of the class. + /// + /// + /// The of the the subscription references. + /// + /// + /// The name of the the subscription references + /// + /// + /// The + /// + /// + /// The . + /// + [JsonConstructor] + public ChangeNotificationSubscription(Guid iid, string name, ClassKind classKind, ChangeNotificationSubscriptionType changeNotificationSubscriptionType) + { + this.Iid = iid; + this.Name = name; + this.ClassKind = classKind; + this.ChangeNotificationSubscriptionType = changeNotificationSubscriptionType; + } + } +} diff --git a/CDP4WebServices.API/ChangeNotification/Subscription/ChangeNotificationSubscriptionType.cs b/CDP4WebServices.API/ChangeNotification/Subscription/ChangeNotificationSubscriptionType.cs new file mode 100644 index 00000000..e573a018 --- /dev/null +++ b/CDP4WebServices.API/ChangeNotification/Subscription/ChangeNotificationSubscriptionType.cs @@ -0,0 +1,59 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Merlin Bieze, Alex Vorobiev, Naron Phou, Alexander van Delft, Nathanael Smiechowski, +// Ahmed Abulwafa Ahmed +// +// This file is part of CDP4 Web Services Community Edition. +// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// This is an auto-generated class. Any manual changes to this file will be overwritten! +// +// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The CDP4 Web Services Community Edition 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace CDP4WebServices.API.ChangeNotification.Subscription +{ + /// + /// Assertions that determine the kind of . + /// + public enum ChangeNotificationSubscriptionType + { + /// + /// For s + /// + NamedThing, + + /// + /// For applied s + /// + AppliedCategory, + + /// + /// For s + /// + ParameterType, + + /// + /// For s + /// + ParameterSubscription, + + /// + /// For s and s + /// + ParameterOrOverride + } +} diff --git a/CDP4WebServices.API/Services/Operations/SideEffects/Implementation/IntervalScaleSideEffect.cs b/CDP4WebServices.API/ChangeNotification/UserPreference/ChangeNotificationReportType.cs similarity index 50% rename from CDP4WebServices.API/Services/Operations/SideEffects/Implementation/IntervalScaleSideEffect.cs rename to CDP4WebServices.API/ChangeNotification/UserPreference/ChangeNotificationReportType.cs index e9e5016c..d09c9391 100644 --- a/CDP4WebServices.API/Services/Operations/SideEffects/Implementation/IntervalScaleSideEffect.cs +++ b/CDP4WebServices.API/ChangeNotification/UserPreference/ChangeNotificationReportType.cs @@ -1,37 +1,43 @@ -// -------------------------------------------------------------------------------------------------------------------- -// -// Copyright (c) 2016-2020 RHEA System S.A. -// -// Author: Cozmin Velciu. -// -// This file is part of CDP4 Web Services Community Edition. +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Merlin Bieze, Alex Vorobiev, Naron Phou, Alexander van Delft, Nathanael Smiechowski, +// Ahmed Abulwafa Ahmed +// +// This file is part of CDP4 Web Services Community Edition. // The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. // This is an auto-generated class. Any manual changes to this file will be overwritten! -// +// // The CDP4 Web Services Community Edition is free software; you can redistribute it and/or // modify it under the terms of the GNU Affero General Public // License as published by the Free Software Foundation; either // version 3 of the License, or (at your option) any later version. -// +// // The CDP4 Web Services Community Edition 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 +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. -// +// // You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . +// along with this program. If not, see . // // -------------------------------------------------------------------------------------------------------------------- - -namespace CDP4WebServices.API.Services.Operations.SideEffects +namespace CDP4WebServices.API.ChangeNotification.UserPreference { - using CDP4Common.DTO; - /// - /// The purpose of the class is to execute additional logic before and - /// after a specific operation is performed. + /// Contains all types of change notification reporting. /// - public class IntervalScaleSideEffect : MeasurementScaleSideEffect + public enum ChangeNotificationReportType { + /// + /// No reporting + /// + None, + + /// + /// Report as a weekly email + /// + WeeklyEmail } } diff --git a/CDP4WebServices.API/ChangeNotification/UserPreference/ChangeNotificationSubscriptionUserPreference.cs b/CDP4WebServices.API/ChangeNotification/UserPreference/ChangeNotificationSubscriptionUserPreference.cs new file mode 100644 index 00000000..90a22810 --- /dev/null +++ b/CDP4WebServices.API/ChangeNotification/UserPreference/ChangeNotificationSubscriptionUserPreference.cs @@ -0,0 +1,113 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Merlin Bieze, Alex Vorobiev, Naron Phou, Alexander van Delft, Nathanael Smiechowski, +// Ahmed Abulwafa Ahmed +// +// This file is part of CDP4 Web Services Community Edition. +// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// This is an auto-generated class. Any manual changes to this file will be overwritten! +// +// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The CDP4 Web Services Community Edition 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace CDP4WebServices.API.ChangeNotification.UserPreference +{ + using System.Collections.Generic; + + using CDP4Common.EngineeringModelData; + using CDP4Common.SiteDirectoryData; + + using CDP4WebServices.API.ChangeNotification.Subscription; + + using Newtonsoft.Json; + using Newtonsoft.Json.Converters; + + /// + /// Class that holds information about the user's (='s) ChangeLog s. + /// + public class ChangeNotificationSubscriptionUserPreference : SavedUserPreference + { + /// + /// Gets a that holds all individual s + /// + public ICollection ChangeNotificationSubscriptions { get; } + + /// + /// Gets the for email/reporting purposes. + /// + [JsonConverter(typeof(StringEnumConverter))] + public ChangeNotificationReportType ChangeNotificationReportType { get; set; } + + /// + /// Retrieves the key for which the settings will be saved (). + /// + /// + /// The for which to calculate the key. + /// + /// + /// The key as a . + /// + public static string GetUserPreferenceKey(EngineeringModel engineeringModel) + { + return $"ChangeLogSubscriptions_{engineeringModel.Iid}"; + } + + /// + /// Creates a new instance of the class, specifically used during Json Deserialization + /// + /// + /// The name of the + /// + /// + /// The value of the + /// + /// + /// The Description of the + /// + /// + /// The of type + /// + /// + /// The + /// + [JsonConstructor] + public ChangeNotificationSubscriptionUserPreference( + string name, + string value, + string description, + ICollection changeNotificationSubscriptions, + ChangeNotificationReportType changeNotificationReportType) + : base(name, value, description) + { + this.ChangeNotificationSubscriptions = changeNotificationSubscriptions; + this.ChangeNotificationReportType = changeNotificationReportType; + } + + /// + /// Creates a new instance of the class. + /// + /// + /// The for whic to create a + /// + public ChangeNotificationSubscriptionUserPreference(EngineeringModel engineeringModel) + : base(GetUserPreferenceKey(engineeringModel), "", "") + { + this.ChangeNotificationSubscriptions = new List(); + this.ChangeNotificationReportType = ChangeNotificationReportType.None; + } + } +} diff --git a/CDP4WebServices.API/Services/Operations/SideEffects/Implementation/CyclicRatioScaleSideEffect.cs b/CDP4WebServices.API/ChangeNotification/UserPreference/ISavedUserPreference.cs similarity index 50% rename from CDP4WebServices.API/Services/Operations/SideEffects/Implementation/CyclicRatioScaleSideEffect.cs rename to CDP4WebServices.API/ChangeNotification/UserPreference/ISavedUserPreference.cs index 1e3eecc8..0bdff883 100644 --- a/CDP4WebServices.API/Services/Operations/SideEffects/Implementation/CyclicRatioScaleSideEffect.cs +++ b/CDP4WebServices.API/ChangeNotification/UserPreference/ISavedUserPreference.cs @@ -1,37 +1,49 @@ // -------------------------------------------------------------------------------------------------------------------- -// -// Copyright (c) 2016-2020 RHEA System S.A. -// -// Author: Cozmin Velciu. -// -// This file is part of CDP4 Web Services Community Edition. +// +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Merlin Bieze, Alex Vorobiev, Naron Phou, Alexander van Delft, Nathanael Smiechowski, +// Ahmed Abulwafa Ahmed +// +// This file is part of CDP4 Web Services Community Edition. // The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. // This is an auto-generated class. Any manual changes to this file will be overwritten! -// +// // The CDP4 Web Services Community Edition is free software; you can redistribute it and/or // modify it under the terms of the GNU Affero General Public // License as published by the Free Software Foundation; either // version 3 of the License, or (at your option) any later version. -// +// // The CDP4 Web Services Community Edition 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 +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. -// +// // You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . +// along with this program. If not, see . // // -------------------------------------------------------------------------------------------------------------------- -namespace CDP4WebServices.API.Services.Operations.SideEffects +namespace CDP4WebServices.API.ChangeNotification.UserPreference { - using CDP4Common.DTO; - /// - /// The purpose of the class is to execute additional logic before and - /// after a specific operation is performed. + /// Definition of the used to load user filter preferences from the active user. /// - public class CyclicRatioScaleSideEffect : MeasurementScaleSideEffect + public interface ISavedUserPreference { + /// + /// The name to be stored + /// + public string Name { get; set; } + + /// + /// The description to be stored + /// + public string Description { get; set; } + + /// + /// The value to be stored + /// + public string Value { get; set; } } } diff --git a/CDP4WebServices.API/ChangeNotification/UserPreference/SavedUserPreference.cs b/CDP4WebServices.API/ChangeNotification/UserPreference/SavedUserPreference.cs new file mode 100644 index 00000000..eb4784a5 --- /dev/null +++ b/CDP4WebServices.API/ChangeNotification/UserPreference/SavedUserPreference.cs @@ -0,0 +1,68 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Merlin Bieze, Alex Vorobiev, Naron Phou, Alexander van Delft, Nathanael Smiechowski, +// Ahmed Abulwafa Ahmed +// +// This file is part of CDP4 Web Services Community Edition. +// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// This is an auto-generated class. Any manual changes to this file will be overwritten! +// +// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The CDP4 Web Services Community Edition 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace CDP4WebServices.API.ChangeNotification.UserPreference +{ + using CDP4Common.SiteDirectoryData; + + using Newtonsoft.Json; + + /// + /// Holds data that can be used to store the property in a . + /// + public class SavedUserPreference : ISavedUserPreference + { + /// + /// The name to be stored + /// + public string Name { get; set; } + + /// + /// The description to be stored + /// + public string Description { get; set; } + + /// + /// The value to be stored + /// + public string Value { get; set; } + + /// + /// Instanciates a new + /// + /// The name + /// The value + /// The description + [JsonConstructor] + public SavedUserPreference(string name, string value, string description = "") + { + this.Name = name; + this.Value = value; + this.Description = description; + } + } +} + \ No newline at end of file diff --git a/CDP4WebServices.API/Configuration/AppConfig.cs b/CDP4WebServices.API/Configuration/AppConfig.cs index 50948293..f1e2fe51 100644 --- a/CDP4WebServices.API/Configuration/AppConfig.cs +++ b/CDP4WebServices.API/Configuration/AppConfig.cs @@ -1,6 +1,25 @@ // -------------------------------------------------------------------------------------------------------------------- // -// Copyright (c) 2016 RHEA System S.A. +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Alex Vorobiev, Alexander van Delft, Nathanael Smiechowski, Ahmed Abulwafa Ahmed +// +// This file is part of CDP4 Web Services Community Edition. +// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// This is an auto-generated class. Any manual changes to this file will be overwritten! +// +// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The CDP4 Web Services Community Edition 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . // // -------------------------------------------------------------------------------------------------------------------- @@ -31,6 +50,7 @@ protected AppConfig() this.Backtier = new BacktierConfig(); this.Defaults = new DefaultsConfig(); this.Midtier = new MidtierConfig(); + this.Changelog = new ChangelogConfig(); this.EmailService = new EmailConfig(); } @@ -59,6 +79,11 @@ protected AppConfig() ///
public DefaultsConfig Defaults { get; set; } + /// + /// Gets the current changelog configuration. + /// + public ChangelogConfig Changelog { get; set; } + /// /// Read configuration from file. /// diff --git a/CDP4WebServices.API/Configuration/ChanglogConfig.cs b/CDP4WebServices.API/Configuration/ChanglogConfig.cs new file mode 100644 index 00000000..edbaf46d --- /dev/null +++ b/CDP4WebServices.API/Configuration/ChanglogConfig.cs @@ -0,0 +1,65 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Alex Vorobiev, Alexander van Delft, Nathanael Smiechowski, Ahmed Abulwafa Ahmed +// +// This file is part of CDP4 Web Services Community Edition. +// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// This is an auto-generated class. Any manual changes to this file will be overwritten! +// +// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The CDP4 Web Services Community Edition 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace CDP4WebServices.API.Configuration +{ + using CDP4Common.DTO; + + /// + /// The change log configuration. + /// + public class ChangelogConfig + { + /// + /// Initializes a new instance of the class. + /// + public ChangelogConfig() + { + // set defaults + this.CollectChanges = false; + this.AllowEmailNotification = false; + } + + /// + /// Gets or sets the Changelog collection setting. + /// If set to true, s will automatically be created for specific changes to s. + /// + /// + /// The default value is false + /// + public bool CollectChanges { get; set; } + + /// + /// Gets or sets the AllowEmailNotification setting. + /// If set to true, periodical emails are automatically sent to person's that have subscriptions on them + /// and have periodical emails set in their UserPreferences. + /// + /// + /// The default value is false + /// + public bool AllowEmailNotification { get; set; } + + } +} diff --git a/CDP4WebServices.API/Modules/10-25/EngineeringModelApi.cs b/CDP4WebServices.API/Modules/10-25/EngineeringModelApi.cs index c7bd114b..4c08be53 100644 --- a/CDP4WebServices.API/Modules/10-25/EngineeringModelApi.cs +++ b/CDP4WebServices.API/Modules/10-25/EngineeringModelApi.cs @@ -1,6 +1,6 @@ // -------------------------------------------------------------------------------------------------------------------- // -// Copyright (c) 2015-2020 RHEA System S.A. +// Copyright (c) 2015-2021 RHEA System S.A. // // Author: Sam Gerené, Merlin Bieze, Alex Vorobiev, Naron Phou, Alexander van Delft. // @@ -35,9 +35,11 @@ namespace CDP4WebServices.API.Modules using CDP4Common.DTO; using CDP4Orm.Dao; - + + using CDP4WebServices.API.Configuration; using CDP4WebServices.API.Services.Authentication; - + using CDP4WebServices.API.Services.ChangeLog; + using Helpers; using Nancy; @@ -115,6 +117,11 @@ public EngineeringModelApi() /// public IPersonResolver PersonResolver { get; set; } + /// + /// Gets or sets the change log service + /// + public IChangeLogService ChangeLogService { get; set; } + /// /// Gets or sets the obfuscation service. /// @@ -390,16 +397,23 @@ protected override Response PostResponseData(dynamic routeParams) this.OperationProcessor.Process(operationData, transaction, partition, fileDictionary); - // save revision-history var actor = this.PermissionService.Credentials.Person.Iid; - var changedThings = this.RevisionService.SaveRevisions(transaction, partition, actor, transactionRevision); + + if (AppConfig.Current.Changelog.CollectChanges) + { + var initiallyChangedThings = this.RevisionService.GetCurrentChanges(transaction, partition, transactionRevision, true).ToList(); + this.ChangeLogService?.TryAppendModelChangeLogData(transaction, partition, actor, transactionRevision, operationData, initiallyChangedThings); + } + + // save revision-history + var changedThings = this.RevisionService.SaveRevisions(transaction, partition, actor, transactionRevision).ToList(); transaction.Commit(); if (this.RequestUtils.QueryParameters.RevisionNumber == -1) { Logger.Info("{0} completed in {1} [ms]", requestToken, sw.ElapsedMilliseconds); - return this.GetJsonResponse(changedThings.ToList(), this.RequestUtils.GetRequestDataModelVersion); + return this.GetJsonResponse(changedThings, this.RequestUtils.GetRequestDataModelVersion); } Logger.Info(this.ConstructLog()); @@ -408,6 +422,7 @@ protected override Response PostResponseData(dynamic routeParams) // use new transaction to include latest database state transaction = this.TransactionManager.SetupTransaction(ref connection, credentials); var revisionResponse = this.RevisionService.Get(transaction, partition, fromRevision, true).ToArray(); + transaction.Commit(); Logger.Info("{0} completed in {1} [ms]", requestToken, sw.ElapsedMilliseconds); diff --git a/CDP4WebServices.API/Services/Authorization/ObfuscationService.cs b/CDP4WebServices.API/Services/Authorization/ObfuscationService.cs index 10d72637..9ab55a68 100644 --- a/CDP4WebServices.API/Services/Authorization/ObfuscationService.cs +++ b/CDP4WebServices.API/Services/Authorization/ObfuscationService.cs @@ -38,6 +38,7 @@ namespace CDP4WebServices.API.Services.Authorization using ElementDefinition = CDP4Common.DTO.ElementDefinition; using ElementUsage = CDP4Common.DTO.ElementUsage; + using ModelLogEntry = CDP4Common.DTO.ModelLogEntry; using Parameter = CDP4Common.DTO.Parameter; using ParameterGroup = CDP4Common.DTO.ParameterGroup; using ParameterOverride = CDP4Common.DTO.ParameterOverride; @@ -53,6 +54,11 @@ namespace CDP4WebServices.API.Services.Authorization /// public class ObfuscationService : IObfuscationService { + /// + /// A cache of obfuscated things to be used to ensure s are cleaned up. + /// + private HashSet obfuscatedCache; + /// /// Obfuscates the entire response /// @@ -60,6 +66,8 @@ public class ObfuscationService : IObfuscationService /// The public void ObfuscateResponse(List resourceResponse, Credentials credentials) { + this.obfuscatedCache = new HashSet(); + if (credentials.IsDefaultOrganizationalParticipant) { // if member of the defaultorganization for engineering model, do nothing @@ -80,6 +88,8 @@ public void ObfuscateResponse(List resourceResponse, Credentials credenti var citations = resourceResponse.OfType().ToList(); var aliases = resourceResponse.OfType().ToList(); var hyperlinks = resourceResponse.OfType().ToList(); + var modelLogEntries = resourceResponse.OfType().ToList(); + var logEntryChangelogItems = resourceResponse.OfType().ToList(); if (credentials.OrganizationalParticipant == null) { @@ -99,6 +109,29 @@ public void ObfuscateResponse(List resourceResponse, Credentials credenti { this.ObfuscateElementDefinition(forbiddenElementDefinition, parameters, parameterSubscriptions, parameterValueSets, parameterSubscriptionValueSets, elementUsages, parameterOverrides, parameterOverrideValueSets, parameterGroups, allowedElementDefinitions, definitions, citations, aliases, hyperlinks); } + + foreach (var modelLogEntry in modelLogEntries) + { + var affectedItemHashSet = new HashSet(modelLogEntry.AffectedItemIid); + affectedItemHashSet.IntersectWith(this.obfuscatedCache); + + if (!affectedItemHashSet.Any()) + { + continue; + } + + modelLogEntry.Content = "Hidden Model Log Entry"; + + foreach (var logitem in modelLogEntry.LogEntryChangelogItem) + { + var item = logEntryChangelogItems.FirstOrDefault(l => l.Iid.Equals(logitem)); + + if (item != null) + { + item.ChangeDescription = "Hidden Changelog Entry"; + } + } + } } /// @@ -141,9 +174,13 @@ private void ObfuscateElementDefinition( thing.Name = "Hidden Element Definition"; thing.ShortName = "hiddenElementDefinition"; + this.obfuscatedCache.Add(thing.Iid); + // obfuscate all parameter values foreach (var paramIid in thing.Parameter) { + this.obfuscatedCache.Add(paramIid); + var paramDto = parameters.FirstOrDefault(p => p.Iid.Equals(paramIid)); if (paramDto == null) @@ -261,6 +298,7 @@ private void ObfuscateElementDefinition( private void ObfuscateParameterGroup(ParameterGroup thing) { thing.Name = "Hidden Group"; + this.obfuscatedCache.Add(thing.Iid); } /// @@ -270,6 +308,7 @@ private void ObfuscateParameterGroup(ParameterGroup thing) private void ObfuscateAlias(Alias thing) { thing.Content = "Hidden Alias"; + this.obfuscatedCache.Add(thing.Iid); } /// @@ -280,6 +319,7 @@ private void ObfuscateHyperlink(HyperLink thing) { thing.Content = "Hidden Alias"; thing.Uri = "Hidden Uri"; + this.obfuscatedCache.Add(thing.Iid); } /// @@ -303,6 +343,8 @@ private void ObfuscateElementUsage(ElementUsage thing, thing.Name = "Hidden Element Usage"; thing.ShortName = "hiddenElementUsage"; + this.obfuscatedCache.Add(thing.Iid); + // obfuscate all parameter values foreach (var paramIid in thing.ParameterOverride) { @@ -374,6 +416,7 @@ private void ObfuscateElementUsage(ElementUsage thing, private void ObfuscateDefinition(Definition thing, List citations) { thing.Content = "Hidden Definition"; + this.obfuscatedCache.Add(thing.Iid); // obfuscate citations foreach (var citationIid in thing.Citation) @@ -398,6 +441,7 @@ private void ObfuscateCitation(Citation thing) thing.ShortName = "Hidden Citation"; thing.Location = "Hidden Location"; thing.Remark = "Hidden Remark"; + this.obfuscatedCache.Add(thing.Iid); } /// @@ -423,6 +467,8 @@ private void ObfuscateValueSet(ParameterValueSetBase thing) thing.Published = newArray; thing.Formula = newArray; thing.ValueSwitch = ParameterSwitchKind.MANUAL; + + this.obfuscatedCache.Add(thing.Iid); } /// @@ -444,6 +490,7 @@ private void ObfuscateSubscriptionValueSet(ParameterSubscriptionValueSet thing) thing.Manual = newArray; thing.ValueSwitch = ParameterSwitchKind.MANUAL; + this.obfuscatedCache.Add(thing.Iid); } } } diff --git a/CDP4WebServices.API/Services/ChangeLog/AffectedThingsData.cs b/CDP4WebServices.API/Services/ChangeLog/AffectedThingsData.cs new file mode 100644 index 00000000..4724324c --- /dev/null +++ b/CDP4WebServices.API/Services/ChangeLog/AffectedThingsData.cs @@ -0,0 +1,76 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Merlin Bieze, Alex Vorobiev, Naron Phou, Alexander van Delft. +// +// This file is part of CDP4 Web Services Community Edition. +// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// This is an auto-generated class. Any manual changes to this file will be overwritten! +// +// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The CDP4 Web Services Community Edition 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace CDP4WebServices.API.Services.ChangeLog +{ + using System; + using System.Collections.Generic; + + using CDP4Common.DTO; + + /// + /// This class holds data that should be added to a , and/or . + /// + public class AffectedThingsData + { + /// + /// A of type that contains s from items that need to be added + /// to a the and properties. + /// + public HashSet AffectedItemIds { get; } = new (); + + /// + /// A of type that contains s from items that need to be added + /// to a the and properties. + /// + public HashSet AffectedDomainIds { get; } = new (); + + /// + /// A of type that holds extra data. + /// + public List ExtraChangeDescriptions { get; } = new (); + + /// + /// Add data from another to this + /// + /// + /// The other + /// + public void AddFrom(AffectedThingsData affectedThingsData) + { + foreach (var affectedItemId in affectedThingsData.AffectedItemIds) + { + this.AffectedItemIds.Add(affectedItemId); + } + + foreach (var affectedDomainId in affectedThingsData.AffectedDomainIds) + { + this.AffectedDomainIds.Add(affectedDomainId); + } + + this.ExtraChangeDescriptions.AddRange(affectedThingsData.ExtraChangeDescriptions); + } + } +} diff --git a/CDP4WebServices.API/Services/ChangeLog/ChangeLogService.cs b/CDP4WebServices.API/Services/ChangeLog/ChangeLogService.cs new file mode 100644 index 00000000..fdf55ccb --- /dev/null +++ b/CDP4WebServices.API/Services/ChangeLog/ChangeLogService.cs @@ -0,0 +1,1482 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Merlin Bieze, Alex Vorobiev, Naron Phou, Alexander van Delft. +// +// This file is part of CDP4 Web Services Community Edition. +// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// This is an auto-generated class. Any manual changes to this file will be overwritten! +// +// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The CDP4 Web Services Community Edition 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace CDP4WebServices.API.Services.ChangeLog +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Linq; + using System.Text; + + using CDP4Common; + using CDP4Common.CommonData; + using CDP4Common.DTO; + using CDP4Common.Helpers; + using CDP4Common.Polyfills; + + using CDP4Orm.Dao; + using CDP4Orm.Dao.Resolve; + + using CDP4WebServices.API.Helpers; + using CDP4WebServices.API.Services.Authorization; + using CDP4WebServices.API.Services.Operations; + + using NLog; + + using Npgsql; + + using INamedThing = CDP4Common.DTO.INamedThing; + using IServiceProvider = CDP4WebServices.API.Services.IServiceProvider; + using IShortNamedThing = CDP4Common.DTO.IShortNamedThing; + using LogEntryChangelogItem = CDP4Common.DTO.LogEntryChangelogItem; + using Thing = CDP4Common.DTO.Thing; + + /// + /// Implements the injectable interface used to append change log data to the database. + /// + public class ChangeLogService : IChangeLogService + { + /// + /// A instance + /// + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + /// + /// Property names that are excluded from text. + /// + private readonly string[] excludedPropertyNames = { nameof(Thing.Iid), nameof(Thing.ClassKind), nameof(Thing.ModifiedOn) }; + + /// + /// The injected + /// + public IServiceProvider ServiceProvider { get; set; } + + /// + /// The injected + /// + public IOperationProcessor OperationProcessor { get; set; } + + /// + /// The injected + /// + public IRequestUtils RequestUtils { get; set; } + + /// + /// The injected + /// + public IOptionService OptionService { get; set; } + + /// + /// The injected + /// + public IActualFiniteStateService ActualFiniteStateService { get; set; } + + /// + /// The injected + /// + public IPossibleFiniteStateService PossibleFiniteStateService { get; set; } + + /// + /// The injected + /// + public IParameterTypeService ParameterTypeService { get; set; } + + /// + /// The injected + /// + public IParameterService ParameterService { get; set; } + + /// + /// The injected + /// + public IParameterOverrideService ParameterOverrideService { get; set; } + + /// + /// The injected + /// + public IParameterSubscriptionService ParameterSubscriptionService { get; set; } + + /// + /// The injected + /// + public IIterationSetupService IterationSetupService { get; set; } + + /// + /// The injected + /// + public IParameterOrOverrideBaseService ParameterOrOverrideBaseService { get; set; } + + /// + /// The injected + /// + public IParameterValueSetService ParameterValueSetService { get; set; } + + /// + /// The injected + /// + public IParameterValueSetBaseService ParameterValueSetBaseService { get; set; } + + /// + /// The injected + /// + public IDataModelUtils DataModelUtils { get; set; } + + /// + /// The injected + /// + public IResolveService ResolveService { get; set; } + + /// + /// The injected + /// + public ICdp4TransactionManager TransactionManager { get; set; } + + /// + /// Tries to append changelog data based on the changes made to certain s. + /// + /// + /// The current to the database. + /// + /// + /// The database partition (schema) where the requested resource is stored. + /// + /// + /// The of the person that made the changes. + /// + /// + /// The revisionNumber of the + /// + /// + /// that resulted to all the changes. + /// + /// + /// The of type that contains changed + /// + /// + /// True if change log data was added, otherwise false + /// + public bool TryAppendModelChangeLogData(NpgsqlTransaction transaction, string partition, Guid actor, int transactionRevision, CdpPostOperation operation, IReadOnlyList things) + { + var isFullAccessEnabled = this.TransactionManager.IsFullAccessEnabled(); + var result = false; + + if (!isFullAccessEnabled) + { + this.TransactionManager.SetFullAccessState(true); + } + + try + { + var changedEngineeringModels = + things + .Where(x => x.ClassKind == ClassKind.EngineeringModel) + .Cast() + .ToList(); + + if (changedEngineeringModels.Count == 1) + { + var addModelLogEntryToOperation = false; + + var engineeringModel = changedEngineeringModels.First(); + var changedThings = things.Where(x => x.RevisionNumber == transactionRevision).ToList(); + + var modelLogEntries = + operation + .Create + .Where(x => x.ClassKind == ClassKind.ModelLogEntry) + .Cast() + .ToList(); + + ModelLogEntry modelLogEntry; + + if (modelLogEntries.Count != 1) + { + var newModelLogEntry = new ModelLogEntry(Guid.NewGuid(), 0) + { + Level = LogLevelKind.INFO, + Author = actor, + LanguageCode = "en-GB", + Content = "-" + }; + + addModelLogEntryToOperation = true; + + engineeringModel.LogEntry.Add(newModelLogEntry.Iid); + + modelLogEntry = newModelLogEntry; + } + else + { + modelLogEntry = modelLogEntries.First(); + } + + var newLogEntryChangelogItems = new List(); + + foreach (var changedThing in changedThings.Where(x => x.ClassKind != ClassKind.ModelLogEntry)) + { + var newLogEntryChangelogItem = this.CreateAddOrUpdateLogEntryChangelogItem(transaction, partition, changedThing, modelLogEntry, operation, changedThings); + + if (newLogEntryChangelogItem == null) + { + continue; + } + + newLogEntryChangelogItems.Add(newLogEntryChangelogItem); + + if (changedThing is IOwnedThing ownedThing && !this.DataModelUtils.IsDerived(changedThing.ClassKind.ToString(), nameof(IOwnedThing.Owner))) + { + this.AddIfNotExists(modelLogEntry.AffectedDomainIid, ownedThing.Owner); + this.AddIfNotExists(newLogEntryChangelogItem.AffectedReferenceIid, ownedThing.Owner); + } + + if (changedThing is ICategorizableThing categorizableThing) + { + foreach (var categoryIid in categorizableThing.Category) + { + this.AddIfNotExists(modelLogEntry.AffectedItemIid, categoryIid); + this.AddIfNotExists(newLogEntryChangelogItem.AffectedReferenceIid, categoryIid); + } + } + } + + foreach (var deleteInfo in operation.Delete) + { + var newLogEntryChangelogItem = + this.CreateDeleteLogEntryChangelogItems(transaction, partition, operation, deleteInfo, modelLogEntry, changedThings); + + if (newLogEntryChangelogItem != null) + { + newLogEntryChangelogItems.Add(newLogEntryChangelogItem); + } + } + + if (newLogEntryChangelogItems.Any()) + { + modelLogEntry.LogEntryChangelogItem.AddRange(newLogEntryChangelogItems.Select(x => x.Iid)); + + var operationData = new CdpPostOperation(); + + operationData.Create.AddRange(newLogEntryChangelogItems); + + if (addModelLogEntryToOperation) + { + var engineeringModelClasslessDTO = ClasslessDtoFactory + .FromThing(this.RequestUtils.MetaInfoProvider, + engineeringModel); + + engineeringModelClasslessDTO.Add(nameof(EngineeringModel.LogEntry), new[] { modelLogEntry.Iid }); + + operationData.Update.Add(engineeringModelClasslessDTO); + operationData.Create.Add(modelLogEntry); + } + else + { + var modelLogEntryClasslessDTO = + ClasslessDtoFactory + .FromThing(this.RequestUtils.MetaInfoProvider, + modelLogEntry, + new[] { nameof(ModelLogEntry.LogEntryChangelogItem), nameof(ModelLogEntry.AffectedItemIid), nameof(ModelLogEntry.AffectedDomainIid) }); + + operationData.Update.Add(modelLogEntryClasslessDTO); + } + + this.OperationProcessor.Process(operationData, transaction, partition); + + result = true; + } + } + } + finally + { + if (!isFullAccessEnabled) + { + this.TransactionManager.SetFullAccessState(false); + } + } + + return result; + } + + /// + /// Adds a to a for update and create operations + /// + /// + /// The current to the database. + /// + /// + /// The database partition (schema) where the requested resource is stored. + /// + /// + /// The that was changed. + /// + /// The + /// + /// + /// The + /// + /// + /// An of type that contains all changed things. + /// + /// + /// The created if one was created, otherwise null. + /// + private LogEntryChangelogItem CreateAddOrUpdateLogEntryChangelogItem(NpgsqlTransaction transaction, string partition, Thing changedThing, ModelLogEntry modelLogEntry, CdpPostOperation operation, IReadOnlyList changedThings) + { + if (!this.IsAddLogEntryChangeLogItemAllowed(changedThing.ClassKind)) + { + return null; + } + + var logEntryChangeLogItem = new LogEntryChangelogItem(Guid.NewGuid(), 0); + this.AddIfNotExists(modelLogEntry.AffectedItemIid, changedThing.Iid); + + if (operation.Create.SingleOrDefault(x => x.Iid == changedThing.Iid) is { }) + { + this.SetCreateLogEntryChangeLogItem(transaction, partition, changedThing, modelLogEntry, logEntryChangeLogItem, changedThings); + return logEntryChangeLogItem; + } + else if (operation.Update.SingleOrDefault(x => x[nameof(Thing.Iid)] as Guid? == changedThing.Iid) is { } updateOperation) + { + this.SetUpdateLogEntryChangeLogItem(transaction, partition, changedThing, modelLogEntry, logEntryChangeLogItem, updateOperation); + return logEntryChangeLogItem; + } + + return null; + } + + /// + /// Adds one or more s to a for delete operations + /// + /// + /// The current to the database. + /// + /// + /// The database partition (schema) where the requested resource is stored. + /// + /// + /// The + /// + /// + /// The . + /// + /// + /// The + /// + /// + /// An of type that contains all changed things. + /// + /// + /// The created s. + /// + private LogEntryChangelogItem CreateDeleteLogEntryChangelogItems(NpgsqlTransaction transaction, string partition, CdpPostOperation operation, ClasslessDTO deleteInfo, ModelLogEntry modelLogEntry, IReadOnlyList changedThings) + { + if (Enum.TryParse(deleteInfo[nameof(Thing.ClassKind)].ToString(), out var classKind) && this.IsAddLogEntryChangeLogItemAllowed(classKind)) + { + var newLogEntryChangelogItem = new LogEntryChangelogItem(Guid.NewGuid(), 0); + this.AddIfNotExists(modelLogEntry.AffectedItemIid, (Guid) deleteInfo[nameof(Thing.Iid)]); + + var deletedThing = this.OperationProcessor.OperationOriginalThingCache.FirstOrDefault(x => x.Iid == (Guid) deleteInfo[nameof(Thing.Iid)]); + + if (deletedThing != null) + { + this.SetDeleteLogEntryChangeLogItem(transaction, partition, deletedThing, modelLogEntry, newLogEntryChangelogItem, deleteInfo, changedThings); + } + + return newLogEntryChangelogItem; + } + + return null; + } + + /// + /// Fills a based on a newly created with appropriate data + /// + /// + /// The current to the database. + /// + /// + /// The database partition (schema) where the requested resource is stored. + /// + /// + /// The newly created + /// + /// + /// The + /// + /// + /// The + /// + /// + /// An of type that contains all changed things. + /// + private void SetCreateLogEntryChangeLogItem(NpgsqlTransaction transaction, string partition, Thing createdThing, ModelLogEntry modelLogEntry, LogEntryChangelogItem logEntryChangeLogItem, IReadOnlyList changedThings) + { + logEntryChangeLogItem.ChangelogKind = LogEntryChangelogItemKind.ADD; + logEntryChangeLogItem.AffectedItemIid = createdThing.Iid; + + if (createdThing is IOwnedThing ownedThing && !this.DataModelUtils.IsDerived(createdThing.ClassKind.ToString(), nameof(IOwnedThing.Owner))) + { + this.AddIfNotExists(modelLogEntry.AffectedDomainIid, ownedThing.Owner); + this.AddIfNotExists(logEntryChangeLogItem.AffectedReferenceIid, ownedThing.Owner); + } + + if (createdThing is ICategorizableThing categorizableThing) + { + foreach (var categoryIid in categorizableThing.Category) + { + this.AddIfNotExists(modelLogEntry.AffectedItemIid, categoryIid); + this.AddIfNotExists(logEntryChangeLogItem.AffectedReferenceIid, categoryIid); + } + } + + var stringBuilder = new StringBuilder(); + + var containerThing = this.GetContainerFromChangedThings(createdThing, changedThings); + + if (containerThing != null) + { + this.AddIfNotExists(modelLogEntry.AffectedItemIid, containerThing.Iid); + this.AddIfNotExists(logEntryChangeLogItem.AffectedReferenceIid, containerThing.Iid); + + stringBuilder.AppendLine($"* {this.GetThingDescription(transaction, partition, containerThing)}"); + } + + var affectedThingsData = this.GetAffectedThingsData(transaction, partition, createdThing, null); + + foreach (var affectedItemId in affectedThingsData.AffectedItemIds) + { + this.AddIfNotExists(logEntryChangeLogItem.AffectedReferenceIid, affectedItemId); + this.AddIfNotExists(modelLogEntry.AffectedItemIid, affectedItemId); + } + + foreach (var affectedDomainId in affectedThingsData.AffectedDomainIds) + { + this.AddIfNotExists(logEntryChangeLogItem.AffectedReferenceIid, affectedDomainId); + this.AddIfNotExists(modelLogEntry.AffectedDomainIid, affectedDomainId); + } + + foreach (var extraChangeDescription in affectedThingsData.ExtraChangeDescriptions) + { + stringBuilder.AppendLine(extraChangeDescription); + } + + stringBuilder.AppendLine($"* {this.GetThingDescription(transaction, partition, createdThing)}"); + + var customDescriptions = this.GetCustomDescriptions(transaction, partition, createdThing); + + if (!string.IsNullOrWhiteSpace(customDescriptions)) + { + stringBuilder.AppendLine($"* {customDescriptions}"); + } + + logEntryChangeLogItem.ChangeDescription = stringBuilder.ToString(); + } + + /// + /// Fills a based on a modified with appropriate data + /// + /// + /// The current to the database. + /// + /// + /// The database partition (schema) where the requested resource is stored. + /// + /// + /// The modified + /// + /// + /// The + /// + /// + /// The + /// + /// + /// The original update operation as a + /// + private void SetUpdateLogEntryChangeLogItem(NpgsqlTransaction transaction, string partition, Thing updatedThing, ModelLogEntry modelLogEntry, LogEntryChangelogItem logEntryChangeLogItem, ClasslessDTO updateOperation) + { + logEntryChangeLogItem.ChangelogKind = LogEntryChangelogItemKind.UPDATE; + logEntryChangeLogItem.AffectedItemIid = updatedThing.Iid; + + if (updatedThing is IOwnedThing ownedThing && !this.DataModelUtils.IsDerived(updatedThing.ClassKind.ToString(), nameof(IOwnedThing.Owner))) + { + this.AddIfNotExists(logEntryChangeLogItem.AffectedReferenceIid, ownedThing.Owner); + this.AddIfNotExists(modelLogEntry.AffectedDomainIid, ownedThing.Owner); + } + + if (updatedThing is ICategorizableThing categorizableThing) + { + foreach (var categoryIid in categorizableThing.Category) + { + this.AddIfNotExists(logEntryChangeLogItem.AffectedReferenceIid, categoryIid); + this.AddIfNotExists(modelLogEntry.AffectedItemIid, categoryIid); + } + } + + var affectedThingsData = this.GetAffectedThingsData(transaction, partition, updatedThing, updateOperation); + + foreach (var affectedItemId in affectedThingsData.AffectedItemIds) + { + this.AddIfNotExists(logEntryChangeLogItem.AffectedReferenceIid, affectedItemId); + this.AddIfNotExists(modelLogEntry.AffectedItemIid, affectedItemId); + } + + foreach (var affectedDomainId in affectedThingsData.AffectedDomainIds) + { + this.AddIfNotExists(logEntryChangeLogItem.AffectedReferenceIid, affectedDomainId); + this.AddIfNotExists(modelLogEntry.AffectedDomainIid, affectedDomainId); + } + + var stringBuilder = new StringBuilder(); + + foreach (var extraChangeDescription in affectedThingsData.ExtraChangeDescriptions) + { + stringBuilder.AppendLine(extraChangeDescription); + } + + var containerThing = this.GetContainer(transaction, partition, updatedThing); + + if (containerThing != null) + { + this.AddIfNotExists(logEntryChangeLogItem.AffectedReferenceIid, containerThing.Iid); + this.AddIfNotExists(modelLogEntry.AffectedItemIid, containerThing.Iid); + stringBuilder.AppendLine($"* {this.GetThingDescription(transaction, partition, containerThing)}"); + } + + stringBuilder.AppendLine($"* {this.GetThingDescription(transaction, partition, updatedThing)}"); + + var extraDescriptions = this.GetCustomDescriptions(transaction, partition, updatedThing); + + if (!string.IsNullOrEmpty(extraDescriptions)) + { + stringBuilder.AppendLine(extraDescriptions); + } + + var changedValues = this.BuildUpdateOperationText(transaction, partition, updateOperation); + + stringBuilder.AppendLine(changedValues); + + logEntryChangeLogItem.ChangeDescription = stringBuilder.ToString(); + } + + /// + /// Builds a textual representation of a changed property + /// + /// + /// The current to the database. + /// + /// + /// The database partition (schema) where the requested resource is stored. + /// + /// + /// The + /// + /// T + private string BuildUpdateOperationText(NpgsqlTransaction transaction, string partition, ClasslessDTO updateOperation) + { + var relevantOperations = updateOperation.Where(x => !this.excludedPropertyNames.Contains(x.Key)).ToList(); + var stringBuilder = new StringBuilder(); + + foreach (var operation in relevantOperations) + { + var originalThingIid = (Guid) updateOperation[nameof(Thing.Iid)]; + + var originalThing = this.FindOriginalThing(originalThingIid); + + if (originalThing == null) + { + stringBuilder.AppendLine($" - {operation.Key} was changed"); + continue; + } + + if (operation.Value is Guid changedValue) + { + this.AddChangedThingIidLine(transaction, partition, originalThing, operation.Key, changedValue, stringBuilder); + } + else if (operation.Value is IEnumerable guids) + { + foreach (var guid in guids) + { + this.AddChangedThingIidLine(transaction, partition, originalThing, operation.Key, guid, stringBuilder); + } + } + else + { + var metaInfoProvider = this.RequestUtils.MetaInfoProvider.GetMetaInfo(originalThing); + var orgValue = metaInfoProvider.GetValue(operation.Key, originalThing); + stringBuilder.AppendLine($" - {operation.Key}: {orgValue} => {operation.Value}"); + } + } + + return stringBuilder.ToString(); + } + + /// + /// Adds a line to a that holds the textual representation of a changed property that holds a , + /// or an of type that holds s + /// + /// + /// The current to the database. + /// + /// + /// The database partition (schema) where the requested resource is stored. + /// + /// + /// The original changed + /// + /// + /// The name of the changed property + /// + /// + /// The of the that was changed in the 's property. + /// + /// + /// The + /// + private void AddChangedThingIidLine(NpgsqlTransaction transaction, string partition, Thing originalThing, string propertyName, Guid changedValue, StringBuilder stringBuilder) + { + var metaInfoProvider = this.RequestUtils.MetaInfoProvider.GetMetaInfo(originalThing); + var metaInfo = metaInfoProvider.GetPropertyMetaInfo(propertyName); + + var securityContext = new RequestSecurityContext { ContainerReadAllowed = true }; + var service = this.ServiceProvider.MapToPersitableService(metaInfo.TypeName); + + var dtoInfo = new DtoInfo(metaInfo.TypeName, changedValue); + var dtoResolverHelper = new DtoResolveHelper(dtoInfo); + var resolverDictionary = new Dictionary { { dtoInfo, dtoResolverHelper } }; + this.ResolveService.ResolveItems(transaction, partition, resolverDictionary); + + var changedValueThing = service.GetShallow(transaction, dtoResolverHelper.Partition, new[] { changedValue }, securityContext).FirstOrDefault(); + var changedNamedThing = changedValueThing as INamedThing; + + var orgValue = metaInfoProvider.GetValue(propertyName, originalThing); + + if (orgValue is IEnumerable) + { + if (changedNamedThing != null) + { + stringBuilder.AppendLine($" - {propertyName}: Added => {changedNamedThing.Name}"); + return; + } + + if (changedValueThing != null) + { + stringBuilder.AppendLine($" - {propertyName}: Added => {changedValueThing.ClassKind}"); + return; + } + } + else + { + if (orgValue != null) + { + var orgValueThing = service.GetShallow(transaction, dtoResolverHelper.Partition, new[] { (Guid) orgValue }, securityContext).FirstOrDefault(); + var orgNamedThing = orgValueThing as INamedThing; + + if ((changedNamedThing ?? orgNamedThing) != null) + { + stringBuilder.AppendLine($" - {propertyName}: {orgNamedThing?.Name} => {changedNamedThing?.Name}"); + return; + } + } + } + + stringBuilder.AppendLine($" - {propertyName} was changed"); + } + + /// + /// Finds a in the 's cache by its + /// + /// + /// The + /// + /// + /// A is found, otherwise null. + /// + private Thing FindOriginalThing(Guid guid) + { + foreach (var thing in this.OperationProcessor.OperationOriginalThingCache) + { + if (thing.Iid.Equals(guid)) + { + return thing; + } + } + + return null; + } + + /// + /// Fills a based on a deleted with appropriate data + /// + /// + /// The current to the database. + /// + /// + /// The database partition (schema) where the requested resource is stored. + /// + /// + /// The modified + /// + /// + /// The + /// + /// + /// The + /// + /// + /// The original update operation as a + /// + /// + /// An of type that contains all changed things. + /// + private void SetDeleteLogEntryChangeLogItem(NpgsqlTransaction transaction, string partition, Thing deletedThing, ModelLogEntry modelLogEntry, LogEntryChangelogItem logEntryChangeLogItem, ClasslessDTO deleteOperation, IReadOnlyList changedThings) + { + logEntryChangeLogItem.ChangelogKind = LogEntryChangelogItemKind.DELETE; + logEntryChangeLogItem.AffectedItemIid = deletedThing.Iid; + + if (deletedThing is IOwnedThing ownedThing && !this.DataModelUtils.IsDerived(deletedThing.ClassKind.ToString(), nameof(IOwnedThing.Owner))) + { + this.AddIfNotExists(logEntryChangeLogItem.AffectedReferenceIid, ownedThing.Owner); + this.AddIfNotExists(modelLogEntry.AffectedDomainIid, ownedThing.Owner); + } + + if (deletedThing is ICategorizableThing categorizableThing) + { + foreach (var categoryIid in categorizableThing.Category) + { + this.AddIfNotExists(logEntryChangeLogItem.AffectedReferenceIid, categoryIid); + this.AddIfNotExists(modelLogEntry.AffectedItemIid, categoryIid); + } + } + + var affectedThingsData = this.GetAffectedThingsData(transaction, partition, deletedThing, deleteOperation, deletedThing); + + foreach (var affectedItemId in affectedThingsData.AffectedItemIds) + { + this.AddIfNotExists(logEntryChangeLogItem.AffectedReferenceIid, affectedItemId); + this.AddIfNotExists(modelLogEntry.AffectedItemIid, affectedItemId); + } + + foreach (var affectedDomainId in affectedThingsData.AffectedDomainIds) + { + this.AddIfNotExists(logEntryChangeLogItem.AffectedReferenceIid, affectedDomainId); + this.AddIfNotExists(modelLogEntry.AffectedDomainIid, affectedDomainId); + } + + var stringBuilder = new StringBuilder(); + + foreach (var extraChangeDescription in affectedThingsData.ExtraChangeDescriptions) + { + stringBuilder.AppendLine(extraChangeDescription); + } + + var containerThing = this.GetDeletedThingContainer(deletedThing, changedThings); + + if (containerThing != null) + { + this.AddIfNotExists(logEntryChangeLogItem.AffectedReferenceIid, containerThing.Iid); + this.AddIfNotExists(modelLogEntry.AffectedItemIid, containerThing.Iid); + stringBuilder.AppendLine($"* {this.GetThingDescription(transaction, partition, containerThing)}"); + } + + stringBuilder.AppendLine($"* {this.GetThingDescription(transaction, partition, deletedThing)}"); + + var extraDescriptions = this.GetCustomDescriptions(transaction, partition, deletedThing); + + if (!string.IsNullOrEmpty(extraDescriptions)) + { + stringBuilder.AppendLine(extraDescriptions); + } + + logEntryChangeLogItem.ChangeDescription = stringBuilder.ToString(); + } + + /// + /// Gets custom affected s data as a object. + /// contains AffectedItems, AffectedDomains and extra ChangeDescription text + /// + /// + /// The current to the database. + /// + /// + /// The database partition (schema) where the requested resource is stored. + /// + /// + /// The original that started a nested tree of calls to this method + /// + /// + /// + /// The we want to get custom data for. + /// + /// + /// The + /// + private AffectedThingsData GetAffectedThingsData(NpgsqlTransaction transaction, string partition, Thing rootThing, ClasslessDTO updateOperation, Thing thing = null) + { + thing ??= rootThing; + var affectedThingsData = new AffectedThingsData(); + var getContainerReferences = thing != rootThing; + + var basePartition = partition.Replace("EngineeringModel", "").Replace("Iteration", ""); + var engineeringModelPartition = $"EngineeringModel{basePartition}"; + var iterationPartition = $"Iteration{basePartition}"; + var siteDirectoryPartition = "SiteDirectory"; + + var securityContext = new RequestSecurityContext { ContainerReadAllowed = true }; + + switch (thing) + { + case Parameter parameter: + { + this.AddIfNotExists(affectedThingsData.AffectedItemIds, parameter.ParameterType); + + if (this.ParameterTypeService.GetShallow(transaction, siteDirectoryPartition, new[] { parameter.ParameterType }, securityContext).Single() is ParameterType parameterType) + { + foreach (var category in parameterType.Category) + { + this.AddIfNotExists(affectedThingsData.AffectedItemIds, category); + } + } + + if (!(rootThing is ParameterSubscription) && !(rootThing is ParameterSubscriptionValueSet) && !(rootThing is ParameterOverride) && !(rootThing is ParameterOverrideValueSet)) + { + if (updateOperation != null && + (updateOperation.Keys.Count > 3 || !updateOperation.Keys.Contains(nameof(Parameter.ParameterSubscription)))) + { + foreach (var parameterSubscription in parameter.ParameterSubscription) + { + this.AddIfNotExists(affectedThingsData.AffectedItemIds, parameterSubscription); + } + } + } + + if (this.GetContainer(transaction, engineeringModelPartition, parameter) is ElementDefinition elementDefinition) + { + var newAffectedItemsData = this.GetAffectedThingsData(transaction, partition, rootThing, updateOperation, elementDefinition); + affectedThingsData.AddFrom(newAffectedItemsData); + } + + if (getContainerReferences) + { + this.AddIfNotExists(affectedThingsData.AffectedDomainIds, parameter.Owner); + this.AddIfNotExists(affectedThingsData.AffectedItemIds, parameter.Iid); + } + + break; + } + case ElementUsage elementUsage: + { + if (this.GetContainer(transaction, engineeringModelPartition, elementUsage) is ElementDefinition elementDefinition) + { + var newAffectedItemsData = this.GetAffectedThingsData(transaction, partition, rootThing, updateOperation, elementDefinition); + affectedThingsData.AddFrom(newAffectedItemsData); + } + + if (getContainerReferences) + { + affectedThingsData.ExtraChangeDescriptions.Add($"* ElementUsage: {elementUsage.Name} ({elementUsage.ShortName})"); + + foreach (var category in elementUsage.Category) + { + this.AddIfNotExists(affectedThingsData.AffectedItemIds, category); + } + + this.AddIfNotExists(affectedThingsData.AffectedDomainIds, elementUsage.Owner); + this.AddIfNotExists(affectedThingsData.AffectedItemIds, elementUsage.Iid); + } + + break; + } + case ElementDefinition elementDefinition: + { + if (getContainerReferences) + { + affectedThingsData.ExtraChangeDescriptions.Add($"* ElementDefinition: {elementDefinition.Name} ({elementDefinition.ShortName})"); + + foreach (var category in elementDefinition.Category) + { + this.AddIfNotExists(affectedThingsData.AffectedItemIds, category); + } + + this.AddIfNotExists(affectedThingsData.AffectedDomainIds, elementDefinition.Owner); + this.AddIfNotExists(affectedThingsData.AffectedItemIds, elementDefinition.Iid); + } + + break; + } + + case ParameterOverride parameterOverride: + { + if (!(rootThing is ParameterSubscription) && !(rootThing is ParameterSubscriptionValueSet)) + { + if (updateOperation != null && + (updateOperation.Keys.Count > 3 || !updateOperation.Keys.Contains(nameof(Parameter.ParameterSubscription)))) + { + foreach (var parameterSubscription in parameterOverride.ParameterSubscription) + { + this.AddIfNotExists(affectedThingsData.AffectedItemIds, parameterSubscription); + } + } + } + + var parameterOverrideParameter = this.ParameterService.GetShallow(transaction, iterationPartition, new[] { parameterOverride.Parameter }, securityContext) + .Cast() + .SingleOrDefault(); + + if (parameterOverrideParameter != null) + { + var newAffectedItemsData = this.GetAffectedThingsData(transaction, partition, rootThing, updateOperation, parameterOverrideParameter); + affectedThingsData.AddFrom(newAffectedItemsData); + } + + if (this.GetContainer(transaction, engineeringModelPartition, parameterOverride) is ElementUsage elementUsage) + { + var newAffectedItemsData = this.GetAffectedThingsData(transaction, partition, rootThing, updateOperation, elementUsage); + affectedThingsData.AddFrom(newAffectedItemsData); + } + + if (getContainerReferences) + { + this.AddIfNotExists(affectedThingsData.AffectedDomainIds, parameterOverride.Owner); + this.AddIfNotExists(affectedThingsData.AffectedItemIds, parameterOverride.Iid); + } + + break; + } + case ParameterValueSet parameterValueSet: + { + var parameterValueSetParameters = this.ParameterService.GetShallow(transaction, iterationPartition, null, securityContext) + .Cast() + .ToList(); + + var parameterValueSetParameter = parameterValueSetParameters.SingleOrDefault(x => x.ValueSet.Contains(parameterValueSet.Iid)); + + if (parameterValueSetParameter != null) + { + var newAffectedItemsData = this.GetAffectedThingsData(transaction, partition, rootThing, updateOperation, parameterValueSetParameter); + affectedThingsData.AddFrom(newAffectedItemsData); + } + + break; + } + case ParameterOverrideValueSet parameterOverrideValueSet: + { + var parameterOverrideValueSetParameterOverrides = this.ParameterOverrideService.GetShallow(transaction, iterationPartition, null, securityContext) + .Cast() + .ToList(); + + var parameterOverrideValueSetParameterOverride = parameterOverrideValueSetParameterOverrides.SingleOrDefault(x => x.ValueSet.Contains(parameterOverrideValueSet.Iid)); + + if (parameterOverrideValueSetParameterOverride != null) + { + var newAffectedItemsData = this.GetAffectedThingsData(transaction, partition, rootThing, updateOperation, parameterOverrideValueSetParameterOverride); + affectedThingsData.AddFrom(newAffectedItemsData); + } + + break; + } + case ParameterSubscription parameterSubscription: + { + var parameterOrOverrideBases = this.ParameterOrOverrideBaseService.GetShallow(transaction, iterationPartition, null, securityContext) + .Cast() + .ToList(); + + var parameterOrOverrideBase = parameterOrOverrideBases.SingleOrDefault(x => x.ParameterSubscription.Contains(parameterSubscription.Iid)); + + if (parameterOrOverrideBase != null) + { + var newAffectedItemsData = this.GetAffectedThingsData(transaction, partition, rootThing, updateOperation, parameterOrOverrideBase); + affectedThingsData.AddFrom(newAffectedItemsData); + } + + if (getContainerReferences) + { + this.AddIfNotExists(affectedThingsData.AffectedDomainIds, parameterSubscription.Owner); + this.AddIfNotExists(affectedThingsData.AffectedItemIds, parameterSubscription.Iid); + } + + break; + } + case ParameterSubscriptionValueSet parameterSubscriptionValueSet: + { + var parameterSubscriptionValueSetParameterSubscriptions = this.ParameterSubscriptionService.GetShallow(transaction, iterationPartition, null, securityContext) + .Cast() + .ToList(); + + var parameterSubscriptionValueSetParameterSubscription = parameterSubscriptionValueSetParameterSubscriptions.SingleOrDefault(x => x.ValueSet.Contains(parameterSubscriptionValueSet.Iid)); + + if (parameterSubscriptionValueSetParameterSubscription != null) + { + var newAffectedItemsData = this.GetAffectedThingsData(transaction, partition, rootThing, updateOperation, parameterSubscriptionValueSetParameterSubscription); + affectedThingsData.AddFrom(newAffectedItemsData); + } + + break; + } + } + + return affectedThingsData; + } + + /// + /// Tries to get the 's container. + /// + /// + /// The current to the database. + /// + /// + /// The database partition (schema) where the requested resource is stored. + /// + /// + /// The updated + /// + /// + /// 's container if found, otherwise null + /// + private Thing GetContainer(NpgsqlTransaction transaction, string partition, Thing updatedThing) + { + try + { + var containerClassType = ContainerPropertyHelper.ContainerClassName(updatedThing.ClassKind); + + if (containerClassType == updatedThing.ClassKind.ToString()) + { + return null; + } + + var containerPropertyName = ContainerPropertyHelper.ContainerPropertyName(updatedThing.ClassKind); + var securityContext = new RequestSecurityContext { ContainerReadAllowed = true }; + var service = this.ServiceProvider.MapToPersitableService(containerClassType); + + if (service == null) + { + return null; + } + + var containerPartition = partition; + + if (!EngineeringModelContainmentClassType.ClassKindArray.Contains((ClassKind) Enum.Parse(typeof(ClassKind), containerClassType))) + { + containerPartition = partition.Replace("EngineeringModel", "Iteration"); + } + + var possibleContainers = service.GetShallow(transaction, containerPartition, null, securityContext).ToList(); + + if (!possibleContainers.Any()) + { + return null; + } + + var addContainer = this.GetContainerFromPossibleContainers(updatedThing, possibleContainers, containerPropertyName); + + return addContainer; + } + catch (Exception ex) + { + // Ignore + Logger.Error(ex); + } + + return null; + } + + /// + /// Tries to get the 's container. + /// + /// + /// The updated + /// + /// + /// 's container if found, otherwise null + /// + private Thing GetDeletedThingContainer(Thing deletedThing, IReadOnlyList changedThings) + { + try + { + var containerClassType = ContainerPropertyHelper.ContainerClassName(deletedThing.ClassKind); + + if (containerClassType == deletedThing.ClassKind.ToString()) + { + return null; + } + + var containerPropertyName = ContainerPropertyHelper.ContainerPropertyName(deletedThing.ClassKind); + + var possibleContainers = + this.OperationProcessor.OperationOriginalThingCache.Where(x => x.ClassKind.ToString() == containerClassType).ToList(); + + if (!possibleContainers.Any()) + { + foreach (var possibleContainerThing in changedThings) + { + if (possibleContainerThing.GetType().Name == containerClassType) + { + possibleContainers.Add(possibleContainerThing); + } + else if (possibleContainerThing.GetType().QueryBaseType()?.Name == containerClassType) + { + possibleContainers.Add(possibleContainerThing); + } + } + } + + var addContainer = this.GetContainerFromPossibleContainers(deletedThing, possibleContainers, containerPropertyName); + + return addContainer; + } + catch (Exception ex) + { + // Ignore + Logger.Error(ex); + } + + return null; + } + + /// + /// Tries to get the 's container from the parameter. + /// + /// + /// The updated + /// + /// + /// The of type that holds all possible containers. + /// + /// + /// The name of the containers property where to search for a reference to + /// + /// + /// 's container if found, otherwise null + /// + private Thing GetContainerFromPossibleContainers(Thing updatedThing, List possibleContainers, string containerPropertyName) + { + var propInfo = + possibleContainers + .FirstOrDefault()? + .GetType() + .GetProperties() + .FirstOrDefault(x => string.Equals(x.Name, containerPropertyName, StringComparison.CurrentCultureIgnoreCase)); + + if (propInfo == null) + { + { + return null; + } + } + + foreach (var container in possibleContainers) + { + if (propInfo.PropertyType == typeof(List)) + { + if (!((propInfo.GetValue(container) as List)?.Contains(updatedThing.Iid) ?? false)) + { + continue; + } + + return container; + } + + if (propInfo.PropertyType == typeof(Guid)) + { + if (!((propInfo.GetValue(container) as Guid?)?.Equals(updatedThing.Iid) ?? false)) + { + continue; + } + + return container; + } + } + + return null; + } + + /// + /// Searches a of type for containment of a + /// + /// + /// The to search for + /// + /// + /// The of type to search in + /// + /// + /// The 's container if found, otherwise null. + /// + private Thing GetContainerFromChangedThings(Thing thing, IReadOnlyList containerThings) + { + var containerClassType = ContainerPropertyHelper.ContainerClassName(thing.ClassKind); + var containerPropertyName = ContainerPropertyHelper.ContainerPropertyName(thing.ClassKind); + + if (containerClassType == thing.ClassKind.ToString()) + { + return null; + } + + var possibleContainers = containerThings.Where(x => x.ClassKind.ToString() == containerClassType).ToList(); + + if (!possibleContainers.Any()) + { + foreach (var possibleContainerThing in containerThings) + { + if (possibleContainerThing.GetType().QueryBaseType()?.Name == containerClassType) + { + possibleContainers.Add(possibleContainerThing); + } + } + } + + return this.GetContainerFromPossibleContainers(thing, possibleContainers, containerPropertyName); + } + + /// + /// Gets the description for a specific to be added to + /// + /// + /// The current to the database. + /// + /// + /// The database partition (schema) where the requested resource is stored. + /// + /// + /// The + /// + /// + /// The s description. + /// + private string GetThingDescription(NpgsqlTransaction transaction, string partition, Thing thing) + { + var basePartition = partition.Replace("EngineeringModel", "").Replace("Iteration", ""); + var iterationPartition = $"Iteration{basePartition}"; + var siteDirectoryPartition = "SiteDirectory"; + + var description = thing.ClassKind.ToString(); + + if (thing is INamedThing namedThing) + { + description = $"{description} => {namedThing.Name}"; + } + + if (thing is IShortNamedThing shortNamedThing) + { + description = $"{description} ({shortNamedThing.ShortName})"; + } + + var securityContext = new RequestSecurityContext { ContainerReadAllowed = true }; + + if (thing is Iteration iteration + && this.IterationSetupService.GetShallow(transaction, siteDirectoryPartition, new[] { iteration.IterationSetup }, securityContext).Single() is IterationSetup iterationSetup) + { + description = $"{description} {iterationSetup.IterationNumber}"; + } + + if (thing is Parameter parameter + && this.ParameterTypeService.GetShallow(transaction, siteDirectoryPartition, new[] { parameter.ParameterType }, securityContext).Single() is ParameterType parameterType) + { + description = $"{description} {this.GetThingDescription(transaction, partition, parameterType)}"; + } + + if (thing is ParameterOverride parameterOverride + && this.ParameterService.GetShallow(transaction, iterationPartition, new[] { parameterOverride.Parameter }, securityContext).Single() is Parameter parameterOverrideParameter) + { + description = $"ParameterOverride: {this.GetThingDescription(transaction, partition, parameterOverrideParameter)}"; + } + + if (thing is ParameterSubscription parameterSubscription + && this.ParameterOrOverrideBaseService + .GetShallow(transaction, iterationPartition, null, securityContext) + .Cast() + .SingleOrDefault(x => x.ParameterSubscription.Contains(parameterSubscription.Iid)) is { } parameterOrOverride) + { + description = $"ParameterSubscription => {this.GetThingDescription(transaction, partition, parameterOrOverride)}"; + } + + return description; + } + + /// + /// Gets custom descriptions for specific s to be added to + /// + /// + /// The current to the database. + /// + /// + /// The database partition (schema) where the requested resource is stored. + /// + /// The specific + /// A string containing the custom descriptions + private string GetCustomDescriptions(NpgsqlTransaction transaction, string partition, Thing thing) + { + var basePartition = partition.Replace("EngineeringModel", "").Replace("Iteration", ""); + var iterationPartition = $"Iteration{basePartition}"; + + var securityContext = new RequestSecurityContext { ContainerReadAllowed = true }; + + var stringBuilder = new StringBuilder(); + + switch (thing) + { + case ParameterValueSet parameterValueSet: + { + if (parameterValueSet.ActualOption.HasValue) + { + this.TryAddOptionLine(transaction, partition, parameterValueSet.ActualOption.Value, securityContext, stringBuilder); + } + + if (parameterValueSet.ActualState.HasValue) + { + this.TryAddStateLine(transaction, partition, parameterValueSet.ActualState.Value, securityContext, stringBuilder); + } + + break; + } + case ParameterOverrideValueSet parameterOverrideValueSet: + { + if (this.ParameterValueSetService.GetShallow(transaction, iterationPartition, new[] { parameterOverrideValueSet.ParameterValueSet }, securityContext).Single() is ParameterValueSet parameterOverrideValueSetParameterValueSet) + { + var description = this.GetCustomDescriptions(transaction, partition, parameterOverrideValueSetParameterValueSet); + + if (!string.IsNullOrWhiteSpace(description)) + { + stringBuilder.Append(description); + } + } + + break; + } + case ParameterSubscriptionValueSet parameterSubscriptionValueSet: + { + if (this.ParameterValueSetBaseService.GetShallow(transaction, iterationPartition, new[] { parameterSubscriptionValueSet.SubscribedValueSet }, securityContext).Single() is ParameterValueSetBase parameterSubscriptionValueSetParameterValueSetBase) + { + var description = this.GetCustomDescriptions(transaction, partition, parameterSubscriptionValueSetParameterValueSetBase); + + if (!string.IsNullOrWhiteSpace(description)) + { + stringBuilder.Append(description); + } + } + + break; + } + } + + return stringBuilder.ToString(); + } + + /// + /// Adds a to an of type if it doesn't already exist. + /// + /// The of type + /// The + private void AddIfNotExists(ICollection guids, Guid newGuid) + { + if (!guids.Contains(newGuid)) + { + guids.Add(newGuid); + } + } + + /// + /// Tries to add a descriptive line to a for a specific . + /// + /// + /// The current to the database. + /// + /// + /// The database partition (schema) where the requested resource is stored. + /// + /// The of the + /// The + /// The + private void TryAddStateLine(NpgsqlTransaction transaction, string partition, Guid stateIid, ISecurityContext securityContext, StringBuilder stringBuilder) + { + var basePartition = partition.Replace("EngineeringModel", "").Replace("Iteration", ""); + var iterationPartition = $"Iteration{basePartition}"; + + if (this.ActualFiniteStateService.GetShallow(transaction, iterationPartition, new[] { stateIid }, securityContext).Single() is not ActualFiniteState actualState) + { + return; + } + + if (this.PossibleFiniteStateService.GetShallow(transaction, iterationPartition, actualState.PossibleState, securityContext).Single() is not PossibleFiniteState possibleFiniteState) + { + return; + } + + var possibleFiniteStateString = string.Join(" ", possibleFiniteState.ShortName); + + stringBuilder.AppendLine($"* ActualFiniteState: {possibleFiniteStateString}"); + } + + /// + /// Tries to add a descriptive line to a for a specific . + /// + /// + /// The current to the database. + /// + /// + /// The database partition (schema) where the requested resource is stored. + /// + /// The of the + /// The + /// The + private void TryAddOptionLine(NpgsqlTransaction transaction, string partition, Guid optionIid, ISecurityContext securityContext, StringBuilder stringBuilder) + { + var basePartition = partition.Replace("EngineeringModel", "").Replace("Iteration", ""); + var iterationPartition = $"Iteration{basePartition}"; + + if (this.OptionService.GetShallow(transaction, iterationPartition, new[] { optionIid }, securityContext).Single() is Option actualOption) + { + stringBuilder.AppendLine($"* Option: {actualOption.Name} ({actualOption.ShortName})"); + } + } + + /// + /// Checks if creating a is allowed + /// + /// + /// The of the hanged + /// + /// + /// True if creation is allowed, otherwise false. + /// + private bool IsAddLogEntryChangeLogItemAllowed(ClassKind classKind) + { + var relevantClassKinds = new List + { + ClassKind.ElementDefinition, + ClassKind.ElementUsage, + ClassKind.Parameter, + ClassKind.ParameterOverride, + ClassKind.ParameterSubscription, + ClassKind.ParameterValueSet, + ClassKind.ParameterOverrideValueSet, + ClassKind.ParameterSubscriptionValueSet + }; + + return relevantClassKinds.Contains(classKind); + } + } +} diff --git a/CDP4WebServices.API/Services/ChangeLog/IChangeLogService.cs b/CDP4WebServices.API/Services/ChangeLog/IChangeLogService.cs new file mode 100644 index 00000000..31c87d9a --- /dev/null +++ b/CDP4WebServices.API/Services/ChangeLog/IChangeLogService.cs @@ -0,0 +1,68 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Merlin Bieze, Alex Vorobiev, Naron Phou, Alexander van Delft. +// +// This file is part of CDP4 Web Services Community Edition. +// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// This is an auto-generated class. Any manual changes to this file will be overwritten! +// +// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The CDP4 Web Services Community Edition 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace CDP4WebServices.API.Services.ChangeLog +{ + using System; + using System.Collections.Generic; + + using CDP4Common.DTO; + + using CDP4WebServices.API.Services.Operations; + + using Npgsql; + + /// + /// Defines the implementation of the interface + /// + public interface IChangeLogService + { + /// + /// Tries to append changelog data based on the changes made to certain s. + /// + /// + /// The current to the database. + /// + /// + /// The database partition (schema) where the requested resource is stored. + /// + /// + /// The of the person that made the changes. + /// + /// + /// The revisionNumber of the + /// + /// + /// that resulted to all the changes. + /// + /// + /// The of type that contains changed + /// + /// + /// True if change log data was added, otherwise false + /// + bool TryAppendModelChangeLogData(NpgsqlTransaction transaction, string partition, Guid actor, int transactionRevision, CdpPostOperation operation, IReadOnlyList things); + } +} diff --git a/CDP4WebServices.API/Services/Email/EmailService.cs b/CDP4WebServices.API/Services/Email/EmailService.cs index 12d18bca..32071c55 100644 --- a/CDP4WebServices.API/Services/Email/EmailService.cs +++ b/CDP4WebServices.API/Services/Email/EmailService.cs @@ -25,6 +25,7 @@ namespace CDP4WebServices.API.Services.Email { + using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -35,7 +36,6 @@ namespace CDP4WebServices.API.Services.Email using CDP4WebServices.API.Configuration; using MimeKit; - using MimeKit.Text; using NLog; @@ -60,11 +60,11 @@ public class EmailService : IEmailService /// /// The subject of the email /// - /// - /// The body of the email + /// + /// The text part for the body of the email /// - /// - /// The of the body + /// + /// The html part for the body of the email /// /// /// An of file paths of files that can be attached to the email @@ -72,38 +72,36 @@ public class EmailService : IEmailService /// /// an awaitable /// - public async Task Send(IEnumerable emailAddresses, string subject, string body, TextFormat textFormat, IEnumerable filePaths) + public async Task Send(IEnumerable emailAddresses, string subject, string textBody, string htmlBody, IEnumerable filePaths = null) { var message = new MimeMessage(); message.From.Add(new MailboxAddress(AppConfig.Current.EmailService.Sender, AppConfig.Current.EmailService.SMTP)); + foreach (var emailAddress in emailAddresses) { message.To.Add(new MailboxAddress(emailAddress.Value, emailAddress.Value)); } message.Subject = subject; - var textPart = new TextPart(TextFormat.Plain) + + var bodyBuilder = new BodyBuilder { - Text = body + HtmlBody = htmlBody, + TextBody = textBody }; - if (filePaths != null && filePaths.Any()) + if ((filePaths ?? Array.Empty()).Any()) { - var multipart = new Multipart("mixed") { textPart }; - var attachments = this.CreateAttachments(filePaths); + foreach (var attachment in attachments) { - multipart.Add(attachment); + bodyBuilder.Attachments.Add(attachment); } - - message.Body = multipart; - } - else - { - message.Body = textPart; } + message.Body = bodyBuilder.ToMessageBody(); + using (var smtpClient = new SmtpClient()) { await smtpClient.ConnectAsync(AppConfig.Current.EmailService.SMTP, AppConfig.Current.EmailService.Port); diff --git a/CDP4WebServices.API/Services/Email/IEmailService.cs b/CDP4WebServices.API/Services/Email/IEmailService.cs index a8b827a8..315905a5 100644 --- a/CDP4WebServices.API/Services/Email/IEmailService.cs +++ b/CDP4WebServices.API/Services/Email/IEmailService.cs @@ -30,8 +30,6 @@ namespace CDP4WebServices.API.Services.Email using CDP4Common.DTO; - using MimeKit.Text; - /// /// Definition of the Email Service responsible for sending automated emails to s /// @@ -46,11 +44,11 @@ public interface IEmailService /// /// The subject of the email /// - /// - /// The body of the email + /// + /// The text part for the body of the email /// - /// - /// The of the body + /// + /// The html part for the body of the email /// /// /// /// An of file paths of files that can be attached to the email @@ -58,6 +56,6 @@ public interface IEmailService /// /// an awaitable /// - Task Send(IEnumerable emailAddresses, string subject, string body, TextFormat textFormat, IEnumerable filePaths); + Task Send(IEnumerable emailAddresses, string subject, string textBody, string htmlBody, IEnumerable filePaths = null); } } diff --git a/CDP4WebServices.API/Services/Operations/IOperationProcessor.cs b/CDP4WebServices.API/Services/Operations/IOperationProcessor.cs index 776d432e..38728c83 100644 --- a/CDP4WebServices.API/Services/Operations/IOperationProcessor.cs +++ b/CDP4WebServices.API/Services/Operations/IOperationProcessor.cs @@ -1,6 +1,24 @@ // -------------------------------------------------------------------------------------------------------------------- // -// Copyright (c) 2016 RHEA System S.A. +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Merlin Bieze, Alex Vorobiev, Naron Phou, Alexander van Delft. +// +// This file is part of CDP4 Web Services Community Edition. +// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// +// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The CDP4 Web Services Community Edition 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . // // -------------------------------------------------------------------------------------------------------------------- @@ -9,6 +27,10 @@ namespace CDP4WebServices.API.Services.Operations using System.Collections.Generic; using System.IO; + using CDP4Common.DTO; + + using CDP4Orm.Dao.Resolve; + using Npgsql; /// @@ -32,5 +54,14 @@ public interface IOperationProcessor /// The optional file binaries that were included in the request. /// void Process(CdpPostOperation operation, NpgsqlTransaction transaction, string partition, Dictionary fileStore = null); + + /// + /// Gets the operation original instance cache. + /// + /// + /// Do NOT use this cache for things that influence database concurrency, + /// because that could lead to unexpected results. + /// + IReadOnlyList OperationOriginalThingCache { get; } } -} \ No newline at end of file +} diff --git a/CDP4WebServices.API/Services/Operations/OperationProcessor.cs b/CDP4WebServices.API/Services/Operations/OperationProcessor.cs index 88d4adf7..1488f594 100644 --- a/CDP4WebServices.API/Services/Operations/OperationProcessor.cs +++ b/CDP4WebServices.API/Services/Operations/OperationProcessor.cs @@ -89,9 +89,25 @@ public class OperationProcessor : IOperationProcessor private readonly string[] topContainerTypes = { "SiteDirectory", "EngineeringModel" }; /// - /// The operation instance cache. + /// Gets the operation instance cache. + /// In this cache you can find s, or s and their s + /// from s that were resolved during the execution of the method. /// - private readonly Dictionary operationThingCache = new Dictionary(); + private readonly Dictionary operationThingCache = new (); + + /// + /// Backing field for + /// + private readonly List operationOriginalThingCache = new (); + + /// + /// Gets the operation original instance cache. + /// + /// + /// Do NOT use this cache for things that influence database concurrency, + /// because that could lead to unexpected results. + /// + public IReadOnlyList OperationOriginalThingCache => this.operationOriginalThingCache; /// /// Gets or sets the service registry. @@ -970,6 +986,11 @@ private void ApplyCreateOperations(CdpPostOperation operation, NpgsqlTransaction // keep a copy of the orginal thing to pass to the after create hook var originalThing = resolvedInfo.Thing.DeepClone(); + if (this.operationOriginalThingCache.All(x => x.Iid != originalThing.Iid)) + { + this.operationOriginalThingCache.Add(originalThing); + } + // call before create hook if (!this.OperationSideEffectProcessor.BeforeCreate(resolvedInfo.Thing, resolvedContainerInfo.Thing, transaction, resolvedInfo.Partition, securityContext)) { @@ -1133,6 +1154,11 @@ private void ApplyUpdateOperations(CdpPostOperation operation, NpgsqlTransaction // keep a copy of the orginal thing to pass to the after update hook var originalThing = updatableThing.DeepClone(); + if (this.operationOriginalThingCache.All(x => x.Iid != originalThing.Iid)) + { + this.operationOriginalThingCache.Add(originalThing); + } + // call before update hook this.OperationSideEffectProcessor.BeforeUpdate(updatableThing, containerInfo, transaction, resolvedInfo.Partition, securityContext, updateInfo); @@ -1342,24 +1368,36 @@ private void ExecuteDeleteOperation(DtoInfo dtoInfo, NpgsqlTransaction transacti var persistedThing = resolvedInfo.Thing; - var containerInfo = + var container = metaInfo.IsTopContainer ? null : this.GetContainerInfo(persistedThing).Thing; + if (container != null) + { + var originalContainer = container.DeepClone(); + this.operationOriginalThingCache.Add(originalContainer); + } + // keep a copy of the orginal thing to pass to the after delete hook var originalThing = persistedThing.DeepClone(); + + if (this.operationOriginalThingCache.All(x => x.Iid != originalThing.Iid)) + { + this.operationOriginalThingCache.Add(originalThing); + } + var securityContext = new RequestSecurityContext { ContainerReadAllowed = true }; // call before delete hook - this.OperationSideEffectProcessor.BeforeDelete(persistedThing, containerInfo, transaction, resolvedInfo.Partition, securityContext); + this.OperationSideEffectProcessor.BeforeDelete(persistedThing, container, transaction, resolvedInfo.Partition, securityContext); // delete the item var propertyService = this.ServiceProvider.MapToPersitableService(dtoInfo.TypeName); this.DeletePersistedItem(transaction, resolvedInfo.Partition, propertyService, persistedThing); // call after delete hook - this.OperationSideEffectProcessor.AfterDelete(persistedThing, containerInfo, originalThing, transaction, resolvedInfo.Partition, securityContext); + this.OperationSideEffectProcessor.AfterDelete(persistedThing, container, originalThing, transaction, resolvedInfo.Partition, securityContext); } /// diff --git a/CDP4WebServices.API/Services/Operations/SideEffects/Implementation/LogarithmicScaleSideEffect.cs b/CDP4WebServices.API/Services/Operations/SideEffects/Implementation/LogarithmicScaleSideEffect.cs deleted file mode 100644 index e3addc69..00000000 --- a/CDP4WebServices.API/Services/Operations/SideEffects/Implementation/LogarithmicScaleSideEffect.cs +++ /dev/null @@ -1,37 +0,0 @@ -// -------------------------------------------------------------------------------------------------------------------- -// -// Copyright (c) 2016-2020 RHEA System S.A. -// -// Author: Cozmin Velciu. -// -// This file is part of CDP4 Web Services Community Edition. -// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. -// This is an auto-generated class. Any manual changes to this file will be overwritten! -// -// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or -// modify it under the terms of the GNU Affero General Public -// License as published by the Free Software Foundation; either -// version 3 of the License, or (at your option) any later version. -// -// The CDP4 Web Services Community Edition 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 -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -// -// -------------------------------------------------------------------------------------------------------------------- - -namespace CDP4WebServices.API.Services.Operations.SideEffects -{ - using CDP4Common.DTO; - - /// - /// The purpose of the class is to execute additional logic before and - /// after a specific operation is performed. - /// - public class LogarithmicScaleSideEffect : MeasurementScaleSideEffect - { - } -} diff --git a/CDP4WebServices.API/Services/Operations/SideEffects/Implementation/MeasurementScaleSideEffect.cs b/CDP4WebServices.API/Services/Operations/SideEffects/Implementation/MeasurementScaleSideEffect.cs index 8e9d951b..2ae04598 100644 --- a/CDP4WebServices.API/Services/Operations/SideEffects/Implementation/MeasurementScaleSideEffect.cs +++ b/CDP4WebServices.API/Services/Operations/SideEffects/Implementation/MeasurementScaleSideEffect.cs @@ -38,11 +38,10 @@ namespace CDP4WebServices.API.Services.Operations.SideEffects using Npgsql; /// - /// The purpose of the class is to execute additional logic before and + /// The purpose of the class is to execute additional logic before and /// after a specific operation is performed. /// - public class MeasurementScaleSideEffect : OperationSideEffect - where T : MeasurementScale + public class MeasurementScaleSideEffect : OperationSideEffect { /// /// Gets or sets the @@ -88,7 +87,7 @@ public class MeasurementScaleSideEffect : OperationSideEffect /// It is important to note that this variable is not to be changed likely as it can/will change the operation processor outcome. /// public override void BeforeUpdate( - T thing, + MeasurementScale thing, Thing container, NpgsqlTransaction transaction, string partition, diff --git a/CDP4WebServices.API/Services/Operations/SideEffects/Implementation/OrdinalScaleSideEffect.cs b/CDP4WebServices.API/Services/Operations/SideEffects/Implementation/OrdinalScaleSideEffect.cs deleted file mode 100644 index cab5a671..00000000 --- a/CDP4WebServices.API/Services/Operations/SideEffects/Implementation/OrdinalScaleSideEffect.cs +++ /dev/null @@ -1,37 +0,0 @@ -// -------------------------------------------------------------------------------------------------------------------- -// -// Copyright (c) 2016-2020 RHEA System S.A. -// -// Author: Cozmin Velciu. -// -// This file is part of CDP4 Web Services Community Edition. -// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. -// This is an auto-generated class. Any manual changes to this file will be overwritten! -// -// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or -// modify it under the terms of the GNU Affero General Public -// License as published by the Free Software Foundation; either -// version 3 of the License, or (at your option) any later version. -// -// The CDP4 Web Services Community Edition 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 -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -// -// -------------------------------------------------------------------------------------------------------------------- - -namespace CDP4WebServices.API.Services.Operations.SideEffects -{ - using CDP4Common.DTO; - - /// - /// The purpose of the class is to execute additional logic before and - /// after a specific operation is performed. - /// - public class OrdinalScaleSideEffect : MeasurementScaleSideEffect - { - } -} diff --git a/CDP4WebServices.API/Services/Operations/SideEffects/Implementation/RatioScaleSideEffect.cs b/CDP4WebServices.API/Services/Operations/SideEffects/Implementation/RatioScaleSideEffect.cs deleted file mode 100644 index b00a5ebd..00000000 --- a/CDP4WebServices.API/Services/Operations/SideEffects/Implementation/RatioScaleSideEffect.cs +++ /dev/null @@ -1,37 +0,0 @@ -// -------------------------------------------------------------------------------------------------------------------- -// -// Copyright (c) 2016-2020 RHEA System S.A. -// -// Author: Cozmin Velciu. -// -// This file is part of CDP4 Web Services Community Edition. -// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. -// This is an auto-generated class. Any manual changes to this file will be overwritten! -// -// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or -// modify it under the terms of the GNU Affero General Public -// License as published by the Free Software Foundation; either -// version 3 of the License, or (at your option) any later version. -// -// The CDP4 Web Services Community Edition 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 -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -// -// -------------------------------------------------------------------------------------------------------------------- - -namespace CDP4WebServices.API.Services.Operations.SideEffects -{ - using CDP4Common.DTO; - - /// - /// The purpose of the class is to execute additional logic before and - /// after a specific operation is performed. - /// - public class RatioScaleSideEffect : MeasurementScaleSideEffect - { - } -} diff --git a/CDP4WebServices.API/Services/Resolve/ResolveService.cs b/CDP4WebServices.API/Services/Resolve/ResolveService.cs index 51f0c34d..0967f7ea 100644 --- a/CDP4WebServices.API/Services/Resolve/ResolveService.cs +++ b/CDP4WebServices.API/Services/Resolve/ResolveService.cs @@ -319,6 +319,11 @@ private string ResolvePartition(string partition, string typeName) var staticTypeInfo = this.DataModelUtils.GetSourcePartition(typeName); + if (staticTypeInfo == "SiteDirectory") + { + return staticTypeInfo; + } + if (staticTypeInfo != null) { // source partition info found diff --git a/CDP4WebServices.API/Services/Revision/IRevisionService.cs b/CDP4WebServices.API/Services/Revision/IRevisionService.cs index 6663a1db..1739889d 100644 --- a/CDP4WebServices.API/Services/Revision/IRevisionService.cs +++ b/CDP4WebServices.API/Services/Revision/IRevisionService.cs @@ -1,6 +1,24 @@ // -------------------------------------------------------------------------------------------------------------------- // -// Copyright (c) 2016 RHEA System S.A. +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Merlin Bieze, Alex Vorobiev, Naron Phou, Alexander van Delft. +// +// This file is part of CDP4 Web Services Community Edition. +// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// +// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The CDP4 Web Services Community Edition 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . // // -------------------------------------------------------------------------------------------------------------------- @@ -118,5 +136,26 @@ public interface IRevisionService /// The current or next available revision number /// int GetRevisionForTransaction(NpgsqlTransaction transaction, string partition); + + /// + /// Get the requested revision data from the ORM layer. + /// + /// + /// The current transaction to the database. + /// + /// + /// The database partition (schema) where the requested resource is stored. + /// + /// + /// The revision number used to retrieve data from the database + /// + /// + /// Indicates whether the default context shall be used. Else use the request context (set at module-level). + /// should only be false for engineering-model data + /// + /// + /// List of instances of . + /// + IEnumerable GetCurrentChanges(NpgsqlTransaction transaction, string partition, int revision, bool useDefaultContext); } -} \ No newline at end of file +} diff --git a/CDP4WebServices.API/Services/Revision/RevisionService.cs b/CDP4WebServices.API/Services/Revision/RevisionService.cs index a35f8f60..6e38a663 100644 --- a/CDP4WebServices.API/Services/Revision/RevisionService.cs +++ b/CDP4WebServices.API/Services/Revision/RevisionService.cs @@ -1,6 +1,24 @@ // -------------------------------------------------------------------------------------------------------------------- // -// Copyright (c) 2016 RHEA System S.A. +// Copyright (c) 2015-2021 RHEA System S.A. +// +// Author: Sam Gerené, Merlin Bieze, Alex Vorobiev, Naron Phou, Alexander van Delft. +// +// This file is part of CDP4 Web Services Community Edition. +// The CDP4 Web Services Community Edition is the RHEA implementation of ECSS-E-TM-10-25 Annex A and Annex C. +// +// The CDP4 Web Services Community Edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public +// License as published by the Free Software Foundation; either +// version 3 of the License, or (at your option) any later version. +// +// The CDP4 Web Services Community Edition 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . // // -------------------------------------------------------------------------------------------------------------------- @@ -74,6 +92,35 @@ public IEnumerable Get(NpgsqlTransaction transaction, string partition, i return this.InternalGet(transaction, partition, revision, true, useDefaultContext).Select(x => x.Thing); } + /// + /// Get the requested revision data from the ORM layer. + /// + /// + /// The current transaction to the database. + /// + /// + /// The database partition (schema) where the requested resource is stored. + /// + /// + /// The revision number used to retrieve data from the database + /// + /// + /// Indicates whether the default context shall be used. Else use the request context (set at module-level). + /// should only be false for engineering-model data + /// + /// + /// List of instances of . + /// + public IEnumerable GetCurrentChanges(NpgsqlTransaction transaction, string partition, int revision, bool useDefaultContext) + { + if (partition == Cdp4TransactionManager.SITE_DIRECTORY_PARTITION && !useDefaultContext) + { + throw new ArgumentException("the parameter shall be true for Sitedirectory data", nameof(useDefaultContext)); + } + + return this.InternalGet(transaction, partition, revision, false, useDefaultContext).Select(x => x.Thing); + } + /// /// Gets the revisions of the with the given /// diff --git a/CDP4WebServices.API/config_debug.json b/CDP4WebServices.API/config_debug.json index ebd19244..4f6ec9f2 100644 --- a/CDP4WebServices.API/config_debug.json +++ b/CDP4WebServices.API/config_debug.json @@ -20,7 +20,7 @@ "IsDbRestoreEnabled": false }, "EmailService": { - "Sender": "CDP4" , + "Sender": "CDP4", "SMTP": "smpt.cdp4.org", "Port": 587, "UserName": "cdp4postmaster-username", @@ -31,5 +31,9 @@ "DevServerPath": "http://localhost:4200/app", "LocationServicePath": "http://freegeoip.io/json/", "ContributorsCacheTimeout": 60 + }, + "Changelog": { + "CollectChanges": true, + "AllowEmailNotification": false } } \ No newline at end of file diff --git a/CDP4WebServices.API/config_release.json b/CDP4WebServices.API/config_release.json index 3f350101..ba5eda99 100644 --- a/CDP4WebServices.API/config_release.json +++ b/CDP4WebServices.API/config_release.json @@ -19,12 +19,16 @@ "EmailService": { "SMTP": "smpt.cdp4.org", "Port": 587, - "UserName": "cdp4postmaseter-username", - "Password": "cdp4postmaseter-password", + "UserName": "cdp4postmaster-username", + "Password": "cdp4postmaster-password", }, "Defaults": { - "PersonPassword": "pass", - "LocationServicePath": "http://freegeoip.io/json/", - "ContributorsCacheTimeout": 60 - } - } \ No newline at end of file + "PersonPassword": "pass", + "LocationServicePath": "http://freegeoip.io/json/", + "ContributorsCacheTimeout": 60 + }, + "Changelog": { + "CollectChanges": false, + "AllowEmailNotification": false + } +} \ No newline at end of file diff --git a/CDP4WebServices.API/config_test.json b/CDP4WebServices.API/config_test.json index 5bcd3351..e79b9bdf 100644 --- a/CDP4WebServices.API/config_test.json +++ b/CDP4WebServices.API/config_test.json @@ -23,5 +23,9 @@ "PersonPassword": "pass", "LocationServicePath": "http://freegeoip.io/json/", "ContributorsCacheTimeout": 60 + }, + "Changelog": { + "CollectChanges": false, + "AllowEmailNotification": false } } \ No newline at end of file