From eb6ba2ef3b6d4bb8b7fee59f4bfea82d45e58eb2 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Thu, 5 Mar 2026 14:45:09 +0530 Subject: [PATCH 01/16] refactor: Change public methods to private in SemanticIdHandler for encapsulation --- .../Services/SubmodelRepository/SemanticIdHandler.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandler.cs index aa6df935..72e77e8e 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandler.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandler.cs @@ -513,7 +513,7 @@ private ISubmodelElement FillOutTemplate(ISubmodelElement submodelElementTemplat return submodelElementTemplate; } - public void FillOutSubmodelElementList(SubmodelElementList list, SemanticTreeNode values) + private void FillOutSubmodelElementList(SubmodelElementList list, SemanticTreeNode values) { if (list?.Value == null || list.Value.Count == 0) { @@ -523,7 +523,7 @@ public void FillOutSubmodelElementList(SubmodelElementList list, SemanticTreeNod FillOutSubmodelElementValue(list.Value, values, false); } - public void FillOutSubmodelElementCollection(SubmodelElementCollection collection, SemanticTreeNode values) + private void FillOutSubmodelElementCollection(SubmodelElementCollection collection, SemanticTreeNode values) { if (collection?.Value == null || collection.Value.Count == 0) { @@ -647,7 +647,7 @@ private void FillOutMultiLanguageProperty(MultiLanguageProperty mlp, SemanticTre } } - public void FillOutEntity(Entity entity, SemanticTreeNode values) + private void FillOutEntity(Entity entity, SemanticTreeNode values) { if (entity.EntityType == EntityType.SelfManagedEntity) { From 21cd26869585e145ee406423861fd067a62a6a4d Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Fri, 6 Mar 2026 19:17:01 +0530 Subject: [PATCH 02/16] added implementation for refactoring semanticidhandler --- example/docker-compose.yml | 2 +- .../SemanticIdHandlerTests.cs | 69 +- .../Extraction/ISemanticTreeExtractor.cs | 12 + .../Extraction/SemanticTreeExtractor.cs | 244 +++++ .../SemanticId/FillOut/ISubmodelFiller.cs | 10 + .../SemanticId/FillOut/SubmodelFiller.cs | 360 +++++++ .../Helpers/Interfaces/IReferenceHelper.cs | 14 + .../Helpers/Interfaces/ISemanticIdResolver.cs | 24 + .../Interfaces/ISubmodelElementHelper.cs | 16 + .../SemanticId/Helpers/ReferenceHelper.cs | 99 ++ .../SemanticId/Helpers/SemanticIdResolver.cs | 139 +++ .../Helpers/SemanticTreeNavigator.cs | 55 + .../Helpers/SubmodelElementHelper.cs | 119 +++ .../SubmodelRepository/SemanticIdHandler.cs | 946 +----------------- ...pplicationDependencyInjectionExtensions.cs | 9 + 15 files changed, 1150 insertions(+), 968 deletions(-) create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/ISemanticTreeExtractor.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractor.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/ISubmodelFiller.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFiller.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/Interfaces/IReferenceHelper.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/Interfaces/ISemanticIdResolver.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/Interfaces/ISubmodelElementHelper.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/ReferenceHelper.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolver.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigator.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelper.cs diff --git a/example/docker-compose.yml b/example/docker-compose.yml index f7c96541..94208f84 100644 --- a/example/docker-compose.yml +++ b/example/docker-compose.yml @@ -23,7 +23,7 @@ services: - twinengine-network twinengine-dataengine: - image: ghcr.io/aas-twinengine/dataengine:v1.0.0 + image: dataengine:1.0.0 container_name: twinengine-dataengine depends_on: dpp-plugin: diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs index a7367903..0beea9f6 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs @@ -1,8 +1,12 @@ -using System.Reflection; +/*using System.Reflection; using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.Config; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Extraction; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.FillOut; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; using AasCore.Aas3_0; @@ -23,18 +27,23 @@ namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.Submodel public class SemanticIdHandlerTests { private readonly SemanticIdHandler _sut; - private readonly ILogger _logger; + private readonly ILogger _extractorLogger; + private readonly ILogger _fillerLogger; private readonly IOptions _mlpSettings; private readonly IOptions _semantics; + private readonly ISemanticIdResolver _resolver; public SemanticIdHandlerTests() { - _logger = Substitute.For>(); + _extractorLogger = Substitute.For>(); + _fillerLogger = Substitute.For>(); _mlpSettings = Substitute.For>(); _ = _mlpSettings.Value.Returns(new MultiLanguagePropertySettings { DefaultLanguages = null }); _semantics = Substitute.For>(); _ = _semantics.Value.Returns(new Semantics { MultiLanguageSemanticPostfixSeparator = "_", SubmodelElementIndexContextPrefix = "_aastwinengineindex_" }); - _sut = new SemanticIdHandler(_logger, _semantics, _mlpSettings); + + _resolver = new SemanticIdResolver(_semantics); + _sut = CreateSut(_semantics, _mlpSettings); } [Fact] @@ -57,9 +66,8 @@ public void Extract_TemplateNull_ThrowsException() public void SemanticIdHandler_NullSemantics_ThrowsException() { var options = Options.Create(options: null!); - var logger = Substitute.For>(); - _ = Throws(() => new SemanticIdHandler(logger, options, _mlpSettings)); + _ = Throws(() => new SemanticIdResolver(options)); } [Fact] @@ -235,7 +243,7 @@ public void Extract_EmptyMultiLanguageProperty_WithDefaultLanguagesAsNull() public void Extract_EmptyMultiLanguageProperty_WithDefaultLanguagesAs_En_De_Fr() { var mlpSettings = CreateMlpSettings(["de", "en", "fr"]); - var sut = new SemanticIdHandler(_logger, _semantics, mlpSettings); + var sut = CreateSut(_semantics, mlpSettings); var mlp = TestData.CreateSubmodelWithManufacturerNameWithOutElements(); var node = sut.Extract(mlp) as SemanticBranchNode; @@ -253,7 +261,7 @@ public void Extract_EmptyMultiLanguageProperty_WithDefaultLanguagesAs_En_De_Fr() public void Extract_MultiLanguageProperty_WithDefaultLanguagesAs_En_De_Fr() { var mlpSettings = CreateMlpSettings(["de", "en", "fr"]); - var sut = new SemanticIdHandler(_logger, _semantics, mlpSettings); + var sut = CreateSut(_semantics, mlpSettings); var mlp = TestData.CreateSubmodelWithManufacturerNameWithTwoLanguagesInTemplate(); var node = sut.Extract(mlp) as SemanticBranchNode; @@ -279,7 +287,7 @@ public void Extract_EmptySubmodelElementCollection_LogsWarningAndReturnsNode() var contactInformationNode = node.Children[0] as SemanticBranchNode; Equal("http://example.com/idta/digital-nameplate/contact-information", contactInformationNode?.SemanticId); Empty(contactInformationNode!.Children); - _logger.Received(1).Log(LogLevel.Warning, Arg.Any(), + _extractorLogger.Received(1).Log(LogLevel.Warning, Arg.Any(), Arg.Is(state => state.ToString()! .Contains("No elements defined in SubmodelElementCollection ContactInformation")), null, @@ -298,7 +306,7 @@ public void Extract_EmptySubmodelElementList_LogsWarningAndReturnsNode() var contactInformationNode = node.Children[0] as SemanticBranchNode; Equal("http://example.com/idta/digital-nameplate/contact-list", contactInformationNode?.SemanticId); Empty(contactInformationNode!.Children); - _logger.Received(1).Log(LogLevel.Warning, Arg.Any(), + _extractorLogger.Received(1).Log(LogLevel.Warning, Arg.Any(), Arg.Is(state => state.ToString()! .Contains("No elements defined in SubmodelElementList ContactList")), null, @@ -406,9 +414,7 @@ public void GetCardinality_VariousQualifierValues_ReturnsExpected(string? qualif var element = Substitute.For(); element.Qualifiers.Returns([qualifier]); - var actual = (Cardinality)typeof(SemanticIdHandler) - .GetMethod("GetCardinality", BindingFlags.NonPublic | BindingFlags.Static)! - .Invoke(null, [element])!; + var actual = _resolver.GetCardinality(element); Equal(expected, actual); } @@ -419,9 +425,7 @@ public void GetCardinality_QualifiersNull_ReturnsUnknown() var element = Substitute.For(); element.Qualifiers.Returns((List?)null); - var actual = (Cardinality)typeof(SemanticIdHandler) - .GetMethod("GetCardinality", BindingFlags.NonPublic | BindingFlags.Static)! - .Invoke(null, [element])!; + var actual = _resolver.GetCardinality(element); Equal(Cardinality.Unknown, actual); } @@ -432,9 +436,7 @@ public void GetCardinality_EmptyQualifiers_ReturnsUnknown() var element = Substitute.For(); element.Qualifiers.Returns([]); - var actual = (Cardinality)typeof(SemanticIdHandler) - .GetMethod("GetCardinality", BindingFlags.NonPublic | BindingFlags.Static)! - .Invoke(null, [element])!; + var actual = _resolver.GetCardinality(element); Equal(Cardinality.Unknown, actual); } @@ -463,9 +465,7 @@ public void GetValueType_PropertyValueType_ReturnsExpected(DataTypeDefXsd valueT qualifiers: [] ); - var actual = (DataType)typeof(SemanticIdHandler) - .GetMethod("GetValueType", BindingFlags.NonPublic | BindingFlags.Static)! - .Invoke(null, [prop])!; + var actual = _resolver.GetValueType(prop); Equal(expected, actual); } @@ -475,9 +475,7 @@ public void GetValueType_ElementWithoutValueProperty_ReturnsUnknown() { var element = Substitute.For(); - var actual = (DataType)typeof(SemanticIdHandler) - .GetMethod("GetValueType", BindingFlags.NonPublic | BindingFlags.Static)! - .Invoke(null, [element])!; + var actual = _resolver.GetValueType(element); Equal(DataType.Unknown, actual); } @@ -833,7 +831,7 @@ public void FillOutTemplate_MultiLanguageProperty_WithDefaultLanguagesAsNull_Pop public void FillOutTemplate_EmptyMultiLanguageProperty_WithDefaultLanguagesAs_En_De_Fr_AddsAllLanguages() { var mlpSettings = CreateMlpSettings(["de", "en", "fr"]); - var sut = new SemanticIdHandler(_logger, _semantics, mlpSettings); + var sut = CreateSut(_semantics, mlpSettings); var submodel = TestData.CreateSubmodelWithManufacturerNameWithOutElements(); var semanticTree = TestData.CreateSubmodelWithManufacturerName(); @@ -846,7 +844,7 @@ public void FillOutTemplate_EmptyMultiLanguageProperty_WithDefaultLanguagesAs_En Equal(3, mlp.Value!.Count); var languages = mlp.Value.Select(v => v.Language).OrderBy(l => l).ToList(); Equal(["de", "en", "fr"], languages); - _logger.Received(3).Log( + _fillerLogger.Received(3).Log( LogLevel.Information, Arg.Any(), Arg.Is(state => state.ToString()!.Contains("Added language")), @@ -859,7 +857,7 @@ public void FillOutTemplate_EmptyMultiLanguageProperty_WithDefaultLanguagesAs_En public void FillOutTemplate_MultiLanguageProperty_WithDefaultLanguagesAs_En_De_Fr_MergesWithTemplateLanguages() { var mlpSettings = CreateMlpSettings(["de", "en", "fr"]); - var sut = new SemanticIdHandler(_logger, _semantics, mlpSettings); + var sut = CreateSut(_semantics, mlpSettings); var submodel = TestData.CreateSubmodelWithManufacturerNameWithTwoLanguagesInTemplate(); var semanticTree = TestData.CreateSubmodelWithManufacturerName(); @@ -879,7 +877,7 @@ public void FillOutTemplate_MultiLanguageProperty_WithDefaultLanguagesAs_En_De_F var frValue = mlp.Value.FirstOrDefault(v => v.Language == "fr"); NotNull(frValue); Equal("Exemple de test Fabricant", frValue.Text); - _logger.Received(1).Log( + _fillerLogger.Received(1).Log( LogLevel.Information, Arg.Any(), Arg.Is(state => state.ToString()!.Contains("Added language 'fr'")), @@ -969,6 +967,18 @@ private static Submodel CreateSubmodelWithSubmodelElement(ISubmodelElement submo ); } + private SemanticIdHandler CreateSut(IOptions semantics, IOptions mlpSettings) + { + var resolver = new SemanticIdResolver(semantics); + var navigator = new SemanticTreeNavigator(); + var helper = new SubmodelElementHelper(Substitute.For>()); + var multiLanguageHelper = new MultiLanguageHelper(mlpSettings); + var referenceHelper = new ReferenceHelper(resolver, navigator, Substitute.For>()); + var extractor = new SemanticTreeExtractor(resolver, helper, multiLanguageHelper, referenceHelper, _extractorLogger); + var filler = new SubmodelFiller(resolver, navigator, helper, multiLanguageHelper, referenceHelper, _fillerLogger); + return new SemanticIdHandler(extractor, filler); + } + private static string GetSemanticId(IHasSemantics hasSemantics) => hasSemantics.SemanticId?.Keys?.FirstOrDefault()?.Value ?? string.Empty; private static IOptions CreateMlpSettings(List? defaultLanguages) @@ -980,3 +990,4 @@ private static IOptions CreateMlpSettings(List logger) : ISemanticTreeExtractor +{ + public SemanticTreeNode Extract(ISubmodel submodelTemplate) + { + ArgumentNullException.ThrowIfNull(submodelTemplate); + + var rootNode = new SemanticBranchNode(semanticIdResolver.ResolveSemanticId(submodelTemplate, submodelTemplate.IdShort!), Cardinality.Unknown); + var childNodes = submodelTemplate.SubmodelElements! + .Select(ExtractElement) + .Where(childNode => childNode != null) + .ToList(); + + foreach (var childNode in childNodes) + { + rootNode.AddChild(childNode!); + } + + return rootNode; + } + + public ISubmodelElement Extract(ISubmodel submodelTemplate, string idShortPath) + { + ArgumentNullException.ThrowIfNull(submodelTemplate); + ArgumentNullException.ThrowIfNull(idShortPath); + + var currentSubmodelElements = submodelTemplate.SubmodelElements; + var idShortPathSegments = idShortPath.Split('.'); + for (var index = 0; index < idShortPathSegments.Length; index++) + { + var currentIdShort = idShortPathSegments[index]; + var isLastSegment = index == idShortPathSegments.Length - 1; + + var matchedElement = elementHelper.GetElementByIdShort(currentSubmodelElements, currentIdShort) + ?? throw new InternalDataProcessingException(); + if (isLastSegment) + { + return matchedElement; + } + + currentSubmodelElements = elementHelper.GetChildElements(matchedElement) as List + ?? throw new InternalDataProcessingException(); + } + + throw new InternalDataProcessingException(); + } + + private SemanticTreeNode? ExtractElement(ISubmodelElement submodelElementTemplate) + { + ArgumentNullException.ThrowIfNull(submodelElementTemplate); + + return submodelElementTemplate switch + { + SubmodelElementCollection collection => ExtractCollection(collection), + SubmodelElementList list => ExtractList(list), + MultiLanguageProperty mlp => ExtractMultiLanguageProperty(mlp), + Range range => ExtractRange(range), + ReferenceElement re => ExtractReferenceElement(re), + RelationshipElement relationshipElement => ExtractRelationshipElement(relationshipElement), + Entity entity => ExtractEntity(entity), + _ => CreateLeafNode(submodelElementTemplate) + }; + } + + private SemanticBranchNode ExtractList(SubmodelElementList list) + { + var node = new SemanticBranchNode(semanticIdResolver.ResolveElementSemanticId(list, list.IdShort!), semanticIdResolver.GetCardinality(list)); + if (list.Value?.Count > 0) + { + foreach (var element in list.Value) + { + var child = ExtractElement(element); + if (child != null) + { + node.AddChild(child); + } + } + } + else + { + logger.LogWarning("No elements defined in SubmodelElementList {ListIdShort}", list.IdShort); + } + + return node; + } + + private SemanticBranchNode ExtractCollection(SubmodelElementCollection collection) + { + var node = new SemanticBranchNode(semanticIdResolver.ResolveElementSemanticId(collection, collection.IdShort!), semanticIdResolver.GetCardinality(collection)); + if (collection.Value?.Count > 0) + { + foreach (var element in collection.Value.Where(_ => true)) + { + var child = ExtractElement(element); + if (child != null) + { + node.AddChild(child); + } + } + } + else + { + logger.LogWarning("No elements defined in SubmodelElementCollection {CollectionIdShort}", collection.IdShort); + } + + return node; + } + + private SemanticBranchNode? ExtractReferenceElement(ReferenceElement referenceElement) + { + if (referenceElement.Value == null || referenceElement.Value.Type == ReferenceTypes.ExternalReference) + { + return null; + } + + return referenceHelper.ExtractReferenceKeys(referenceElement.Value, semanticIdResolver.ResolveElementSemanticId(referenceElement, referenceElement.IdShort!), semanticIdResolver.GetCardinality(referenceElement)); + } + + private SemanticBranchNode? ExtractRelationshipElement(RelationshipElement relationshipElement) + { + if (relationshipElement.First.Type == ReferenceTypes.ExternalReference && relationshipElement.Second.Type == ReferenceTypes.ExternalReference) + { + return null; + } + + var semanticId = semanticIdResolver.GetSemanticId(relationshipElement); + var cardinality = semanticIdResolver.GetCardinality(relationshipElement); + var relationshipElementNode = new SemanticBranchNode(semanticId, cardinality); + + if (relationshipElement.First.Type == ReferenceTypes.ModelReference) + { + var referenceNode = referenceHelper.ExtractReferenceKeys(relationshipElement.First, $"{semanticId}{SemanticIdResolver.RelationshipElementFirstPostFixSeparator}", cardinality); + if (referenceNode != null) + { + relationshipElementNode.AddChild(referenceNode); + } + } + + if (relationshipElement.Second.Type == ReferenceTypes.ModelReference) + { + var referenceNode = referenceHelper.ExtractReferenceKeys(relationshipElement.Second, $"{semanticId}{SemanticIdResolver.RelationshipElementSecondPostFixSeparator}", cardinality); + if (referenceNode != null) + { + relationshipElementNode.AddChild(referenceNode); + } + } + + return relationshipElementNode; + } + + private SemanticBranchNode ExtractEntity(Entity entity) + { + var semanticId = semanticIdResolver.ResolveElementSemanticId(entity, entity.IdShort!); + var node = new SemanticBranchNode(semanticId, semanticIdResolver.GetCardinality(entity)); + if (entity.EntityType == EntityType.SelfManagedEntity) + { + var globalAssetIdNode = new SemanticLeafNode(semanticId + SemanticIdResolver.EntityGlobalAssetIdPostFix, string.Empty, DataType.String, Cardinality.One); + node.AddChild(globalAssetIdNode); + if (entity.SpecificAssetIds != null) + { + foreach (var specificAssetId in entity.SpecificAssetIds) + { + IHasSemantics specificAsset = specificAssetId; + if (specificAsset.SemanticId == null) + { + continue; + } + + var specificAssetIdNode = new SemanticLeafNode(semanticIdResolver.GetSemanticId(specificAssetId), string.Empty, DataType.String, Cardinality.One); + node.AddChild(specificAssetIdNode); + } + } + } + + if (entity.Statements?.Count > 0) + { + foreach (var child in entity.Statements.Select(ExtractElement).OfType()) + { + node.AddChild(child); + } + } + else + { + logger.LogWarning("No elements defined in Entity {EntityIdShort}", entity.IdShort); + } + + return node; + } + + private SemanticBranchNode? ExtractMultiLanguageProperty(MultiLanguageProperty mlp) + { + var semanticId = semanticIdResolver.ExtractSemanticId(mlp); + var node = new SemanticBranchNode(semanticId, semanticIdResolver.GetCardinality(mlp)); + + var languages = elementHelper.ResolveLanguages(mlp); + + if (mlp.Value is not { Count: > 0 }) + { + logger.LogInformation("No languages defined in template for MultiLanguageProperty {MlpIdShort}", mlp.IdShort); + } + + var mlpSeparator = semanticIdResolver.MlpPostFixSeparator; + foreach (var langSemanticId in languages.Select(language => string.Concat(semanticId, mlpSeparator, language))) + { + node.AddChild(new SemanticLeafNode(langSemanticId, string.Empty, DataType.String, Cardinality.ZeroToOne)); + } + + return node; + } + + private SemanticBranchNode ExtractRange(Range range) + { + var semanticId = semanticIdResolver.ExtractSemanticId(range); + var valueType = semanticIdResolver.GetValueType(range); + var node = new SemanticBranchNode(semanticId, semanticIdResolver.GetCardinality(range)); + + node.AddChild(new SemanticLeafNode(semanticId + SemanticIdResolver.RangeMinimumPostFixSeparator, string.Empty, valueType, Cardinality.ZeroToOne)); + node.AddChild(new SemanticLeafNode(semanticId + SemanticIdResolver.RangeMaximumPostFixSeparator, string.Empty, valueType, Cardinality.ZeroToOne)); + + return node; + } + + private SemanticLeafNode CreateLeafNode(ISubmodelElement element) + { + var semanticId = semanticIdResolver.ResolveElementSemanticId(element, element.IdShort!); + var valueType = semanticIdResolver.GetValueType(element); + var cardinality = semanticIdResolver.GetCardinality(element); + return new SemanticLeafNode(semanticId, string.Empty, valueType, cardinality); + } +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/ISubmodelFiller.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/ISubmodelFiller.cs new file mode 100644 index 00000000..3a9ddd3c --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/ISubmodelFiller.cs @@ -0,0 +1,10 @@ +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.FillOut; + +public interface ISubmodelFiller +{ + ISubmodel FillOutTemplate(ISubmodel submodelTemplate, SemanticTreeNode values); +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFiller.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFiller.cs new file mode 100644 index 00000000..129a1f12 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFiller.cs @@ -0,0 +1,360 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using File = AasCore.Aas3_0.File; +using Range = AasCore.Aas3_0.Range; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.FillOut; + +public class SubmodelFiller( + ISemanticIdResolver semanticIdResolver, + ISubmodelElementHelper elementHelper, + IReferenceHelper referenceHelper, + ILogger logger) : ISubmodelFiller +{ + + public ISubmodel FillOutTemplate(ISubmodel submodelTemplate, SemanticTreeNode values) + { + ArgumentNullException.ThrowIfNull(submodelTemplate); + ArgumentNullException.ThrowIfNull(submodelTemplate.SubmodelElements); + ArgumentNullException.ThrowIfNull(values); + + var submodelElements = submodelTemplate.SubmodelElements.ToList(); + foreach (var submodelElement in submodelElements) + { + var semanticId = semanticIdResolver.ExtractSemanticId(submodelElement); + + var matchingNodes = SemanticTreeNavigator.FindBranchNodesBySemanticId(values, semanticId)?.ToList(); + + if (matchingNodes == null || matchingNodes.Count == 0) + { + continue; + } + + _ = submodelTemplate.SubmodelElements.Remove(submodelElement); + + if (matchingNodes.Count > 1) + { + HandleMultipleMatchingNodes(matchingNodes, submodelElement, submodelTemplate); + } + else + { + HandleSingleMatchingNode(matchingNodes[0], submodelElement, submodelTemplate); + } + } + + return submodelTemplate; + } + + private void HandleMultipleMatchingNodes( + List matchingNodes, + ISubmodelElement baseElement, + ISubmodel submodelTemplate) + { + for (var i = 0; i < matchingNodes.Count; i++) + { + var node = matchingNodes[i]; + var clonedElement = elementHelper.CloneElement(baseElement); + + if (baseElement is SubmodelElementCollection) + { + clonedElement.IdShort = $"{clonedElement.IdShort}{i}"; + } + + _ = FillOutElement(clonedElement, node); + submodelTemplate.SubmodelElements?.Add(clonedElement); + } + } + + private void HandleSingleMatchingNode( + SemanticTreeNode node, + ISubmodelElement element, + ISubmodel submodelTemplate) + { + _ = FillOutElement(element, node); + submodelTemplate.SubmodelElements?.Add(element); + } + + private ISubmodelElement FillOutElement(ISubmodelElement submodelElementTemplate, SemanticTreeNode values) + { + ArgumentNullException.ThrowIfNull(submodelElementTemplate); + ArgumentNullException.ThrowIfNull(values); + + switch (submodelElementTemplate) + { + case SubmodelElementCollection collection: + FillOutSubmodelElementCollection(collection, values); + break; + + case SubmodelElementList list: + FillOutSubmodelElementList(list, values); + break; + + case MultiLanguageProperty mlp: + FillOutMultiLanguageProperty(mlp, values); + break; + + case Property property: + FillOutProperty(property, values); + break; + + case File file: + FillOutFile(file, values); + break; + + case Blob blob: + FillOutBlob(blob, values); + break; + + case RelationshipElement relationship: + FillOutRelationshipElement(relationship, values); + break; + + case ReferenceElement reference: + FillOutReferenceElement(reference, values); + break; + + case Range range: + FillOutRange(range, values); + break; + + case Entity entity: + FillOutEntity(entity, values); + break; + + default: + logger.LogError("InValid submodelElementTemplate Type. IdShort : {IdShort}", submodelElementTemplate.IdShort); + throw new InternalDataProcessingException(); + } + + return submodelElementTemplate; + } + + private void FillOutSubmodelElementList(SubmodelElementList list, SemanticTreeNode values) + { + if (list?.Value == null || list.Value.Count == 0) + { + return; + } + + FillOutSubmodelElementValue(list.Value, values, false); + } + + private void FillOutSubmodelElementCollection(SubmodelElementCollection collection, SemanticTreeNode values) + { + if (collection?.Value == null || collection.Value.Count == 0) + { + return; + } + + FillOutSubmodelElementValue(collection.Value, values); + } + + private void FillOutSubmodelElementValue(List elements, SemanticTreeNode values, bool updateIdShort = true) + { + var originalElements = elements.ToList(); + foreach (var element in originalElements) + { + var valueNode = SemanticTreeNavigator.FindNodeBySemanticId(values, semanticIdResolver.ExtractSemanticId(element)); + var semanticTreeNodes = valueNode?.ToList(); + + if (semanticTreeNodes == null || semanticTreeNodes.Count == 0) + { + continue; + } + + if (!SemanticTreeNavigator.AreAllNodesOfSameType(semanticTreeNodes, out _)) + { + logger.LogWarning("Mixed node types found for element '{IdShort}' with SemanticId '{SemanticId}'. Expected all nodes to be either SemanticBranchNode or SemanticLeafNode. Removing element.", + element.IdShort, + semanticIdResolver.ExtractSemanticId(element)); + _ = elements.Remove(element); + continue; + } + + if (semanticTreeNodes.Count > 1 && element is not Property && element is not ReferenceElement) + { + _ = elements.Remove(element); + for (var i = 0; i < semanticTreeNodes.Count; i++) + { + var cloned = elementHelper.CloneElement(element); + if (updateIdShort) + { + cloned.IdShort = $"{cloned.IdShort}{i}"; + } + + _ = FillOutElement(cloned, semanticTreeNodes[i]); + elements.Add(cloned); + } + } + else + { + FillOutElement(element, semanticTreeNodes[0]); + } + } + } + + private void FillOutMultiLanguageProperty(MultiLanguageProperty mlp, SemanticTreeNode values) + { + var semanticId = semanticIdResolver.ExtractSemanticId(mlp); + + if (SemanticTreeNavigator.FindNodeBySemanticId(values, semanticId).FirstOrDefault() is not SemanticBranchNode valueNode) + { + logger.LogInformation("No value node found for MultiLanguageProperty {MlpIdShort}", mlp.IdShort); + return; + } + + mlp.Value ??= []; + + var languageValueMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var langValue in mlp.Value) + { + languageValueMap[langValue.Language] = (LangStringTextType)langValue; + } + + var languages = elementHelper.ResolveLanguages(mlp); + + var mlpSeparator = semanticIdResolver.MlpPostFixSeparator; + foreach (var language in languages) + { + if (!languageValueMap.TryGetValue(language, out var languageValue)) + { + languageValue = new LangStringTextType(language, string.Empty); + mlp.Value.Add(languageValue); + languageValueMap[language] = languageValue; + + logger.LogInformation("Added language '{Language}' to MultiLanguageProperty {MlpIdShort}", language, mlp.IdShort); + } + + var languageSemanticId = semanticId + mlpSeparator + language; + + var leafNode = valueNode.Children + .OfType() + .FirstOrDefault(child => child.SemanticId.Equals(languageSemanticId, StringComparison.Ordinal)); + + if (leafNode != null) + { + languageValue.Text = leafNode.Value; + } + } + } + + private void FillOutEntity(Entity entity, SemanticTreeNode values) + { + if (entity.EntityType == EntityType.SelfManagedEntity) + { + FillOutSelfManagedEntity(entity, values); + } + + if (entity?.Statements == null || entity.Statements.Count == 0) + { + return; + } + + FillOutSubmodelElementValue(entity.Statements, values); + } + + private void FillOutSelfManagedEntity(Entity entity, SemanticTreeNode values) + { + var semanticId = semanticIdResolver.ResolveElementSemanticId(entity, entity.IdShort!); + + if (SemanticTreeNavigator.FindNodeBySemanticId(values, semanticId).FirstOrDefault() is not SemanticBranchNode valueNode) + { + return; + } + + var globalAssetSemanticId = semanticId + SemanticIdResolver.EntityGlobalAssetIdPostFix; + + var globalAssetNode = valueNode.Children + .OfType() + .FirstOrDefault(c => c.SemanticId == globalAssetSemanticId); + + if (globalAssetNode != null) + { + entity.GlobalAssetId = globalAssetNode.Value; + } + + if (entity.SpecificAssetIds != null) + { + foreach (var specificAssetId in entity.SpecificAssetIds) + { + var specSemanticId = semanticIdResolver.GetSemanticId(specificAssetId); + + var specNode = valueNode.Children + .OfType() + .FirstOrDefault(c => c.SemanticId == specSemanticId); + + if (specNode != null) + { + specificAssetId.Value = specNode.Value; + } + } + } + } + + private static void FillOutProperty(Property valueElement, SemanticTreeNode values) + { + if (values is SemanticLeafNode leafValueNode) + { + valueElement.Value = leafValueNode.Value; + } + } + + private static void FillOutFile(File valueElement, SemanticTreeNode values) + { + if (values is SemanticLeafNode leafValueNode) + { + valueElement.Value = leafValueNode.Value; + } + } + + private static void FillOutBlob(Blob valueElement, SemanticTreeNode values) + { + if (values is SemanticLeafNode leafValueNode) + { + valueElement.Value = Convert.FromBase64String(leafValueNode.Value); + } + } + + private static void FillOutRange(Range valueElement, SemanticTreeNode values) + { + if (values is not SemanticBranchNode branchNode) + { + return; + } + + var leafNodes = branchNode.Children.OfType().ToList(); + + valueElement.Min = leafNodes.FirstOrDefault(n => n.SemanticId + .EndsWith(SemanticIdResolver.RangeMinimumPostFixSeparator, StringComparison.Ordinal))? + .Value; + + valueElement.Max = leafNodes.FirstOrDefault(n => n.SemanticId + .EndsWith(SemanticIdResolver.RangeMaximumPostFixSeparator, StringComparison.Ordinal))? + .Value; + } + + private void FillOutReferenceElement(ReferenceElement referenceElement, SemanticTreeNode semanticNode) + { + if (referenceElement?.Value?.Type != ReferenceTypes.ModelReference) + { + logger.LogInformation("ReferenceElement does not contain a ModelReference for SemanticId '{SemanticId}'. Skipping population.", semanticIdResolver.GetSemanticId(referenceElement!)); + return; + } + + referenceHelper.PopulateReferenceKeys(referenceElement.Value, semanticNode, semanticIdResolver.GetSemanticId(referenceElement)); + } + + private void FillOutRelationshipElement(RelationshipElement relationshipElement, SemanticTreeNode semanticTreeNode) + { + var semanticId = semanticTreeNode.SemanticId; + + referenceHelper.PopulateRelationshipReference(relationshipElement.First, semanticTreeNode, semanticId, SemanticIdResolver.RelationshipElementFirstPostFixSeparator); + + referenceHelper.PopulateRelationshipReference(relationshipElement.Second, semanticTreeNode, semanticId, SemanticIdResolver.RelationshipElementSecondPostFixSeparator); + } +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/Interfaces/IReferenceHelper.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/Interfaces/IReferenceHelper.cs new file mode 100644 index 00000000..e7c72726 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/Interfaces/IReferenceHelper.cs @@ -0,0 +1,14 @@ +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; + +public interface IReferenceHelper +{ + SemanticBranchNode? ExtractReferenceKeys(IReference reference, string semanticId, Cardinality cardinality); + + void PopulateReferenceKeys(IReference reference, SemanticTreeNode semanticNode, string semanticId); + + void PopulateRelationshipReference(IReference reference, SemanticTreeNode semanticTreeNode, string semanticId, string postfixSeparator); +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/Interfaces/ISemanticIdResolver.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/Interfaces/ISemanticIdResolver.cs new file mode 100644 index 00000000..379365ac --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/Interfaces/ISemanticIdResolver.cs @@ -0,0 +1,24 @@ +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; + +public interface ISemanticIdResolver +{ + string MlpPostFixSeparator { get; } + + string GetSemanticId(IHasSemantics hasSemantics); + + string ExtractSemanticId(ISubmodelElement element); + + string ResolveSemanticId(IHasSemantics hasSemantics, string idShort); + + string ResolveElementSemanticId(ISubmodelElement element, string idShort); + + Cardinality GetCardinality(ISubmodelElement element); + + DataType GetValueType(ISubmodelElement element); + + string BuildReferenceKeySemanticId(string baseSemanticId, KeyTypes keyType, int index, int totalCount); +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/Interfaces/ISubmodelElementHelper.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/Interfaces/ISubmodelElementHelper.cs new file mode 100644 index 00000000..5b78c41f --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/Interfaces/ISubmodelElementHelper.cs @@ -0,0 +1,16 @@ +using AasCore.Aas3_0; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; + +public interface ISubmodelElementHelper +{ + ISubmodelElement CloneElement(ISubmodelElement element); + + ISubmodelElement? GetElementByIdShort(IEnumerable? submodelElements, string idShort); + + ISubmodelElement GetElementFromListByIndex(IEnumerable? elements, string idShortWithoutIndex, int index); + + IList? GetChildElements(ISubmodelElement submodelElement); + + HashSet ResolveLanguages(MultiLanguageProperty mlp); +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/ReferenceHelper.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/ReferenceHelper.cs new file mode 100644 index 00000000..0359fecb --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/ReferenceHelper.cs @@ -0,0 +1,99 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; + +public class ReferenceHelper( + ISemanticIdResolver semanticIdResolver, + ILogger logger) : IReferenceHelper +{ + public SemanticBranchNode? ExtractReferenceKeys(IReference reference, string semanticId, Cardinality cardinality) + { + var keys = reference.Keys; + if (keys.Count <= 0) + { + return null; + } + + var branchNode = new SemanticBranchNode(semanticId, cardinality); + + foreach (var group in keys.GroupBy(k => k.Type)) + { + group.Select((_, index) => new SemanticLeafNode( + semanticIdResolver.BuildReferenceKeySemanticId(semanticId, group.Key, index, group.Count()), + string.Empty, + DataType.String, + Cardinality.ZeroToOne)) + .ToList() + .ForEach(branchNode.AddChild); + } + + return branchNode; + } + + public void PopulateReferenceKeys(IReference reference, SemanticTreeNode semanticNode, string semanticId) + { + if (semanticNode is not SemanticBranchNode branchNode) + { + logger.LogWarning("Expected SemanticBranchNode for SemanticId '{SemanticId}', but got {NodeType}. Skipping population.", semanticId, semanticNode.GetType().Name); + return; + } + + var keys = reference.Keys; + + if (keys.Count <= 0) + { + logger.LogInformation("ReferenceElement has no keys for SemanticId '{SemanticId}'. Nothing to populate.", semanticId); + return; + } + + foreach (var group in keys.GroupBy(k => k.Type)) + { + PopulateReferenceKeyGroup(group, branchNode, semanticId); + } + } + + public void PopulateRelationshipReference(IReference reference, SemanticTreeNode semanticTreeNode, string semanticId, string postfixSeparator) + { + if (reference.Type != ReferenceTypes.ModelReference) + { + return; + } + + var searchPattern = semanticId + postfixSeparator; + var valueNode = SemanticTreeNavigator.FindNodeBySemanticId(semanticTreeNode, searchPattern).FirstOrDefault(); + + if (valueNode != null) + { + PopulateReferenceKeys(reference, valueNode, searchPattern); + } + else + { + logger.LogWarning("No matching node found for reference with pattern: {Pattern}", searchPattern); + } + } + + private void PopulateReferenceKeyGroup(IGrouping group, SemanticBranchNode branchNode, string semanticId) + { + var keyList = group.ToList(); + for (var i = 0; i < keyList.Count; i++) + { + var indexedSemanticId = semanticIdResolver.BuildReferenceKeySemanticId(semanticId, group.Key, i, keyList.Count); + + var leafNode = branchNode.Children + .OfType() + .FirstOrDefault(child => child.SemanticId == indexedSemanticId); + + if (leafNode != null) + { + keyList[i].Value = !string.IsNullOrEmpty(leafNode.Value) ? leafNode.Value : keyList[i].Value; + } + else + { + logger.LogWarning("No matching leaf node found for SemanticId '{IndexedSemanticId}'.", indexedSemanticId); + } + } + } +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolver.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolver.cs new file mode 100644 index 00000000..eb9990a6 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolver.cs @@ -0,0 +1,139 @@ +using System.Text.RegularExpressions; + +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.Config; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using Microsoft.Extensions.Options; + +using File = AasCore.Aas3_0.File; +using Range = AasCore.Aas3_0.Range; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; + +public partial class SemanticIdResolver(IOptions semantics) : ISemanticIdResolver +{ + public const string RangeMinimumPostFixSeparator = "_min"; + public const string RangeMaximumPostFixSeparator = "_max"; + public const string EntityGlobalAssetIdPostFix = "_globalAssetId"; + public const string RelationshipElementFirstPostFixSeparator = "_first"; + public const string RelationshipElementSecondPostFixSeparator = "_second"; + + private readonly string _internalSemanticId = semantics.Value.InternalSemanticId; + private readonly string _submodelElementIndexContextPrefix = semantics.Value.SubmodelElementIndexContextPrefix; + + public string MlpPostFixSeparator { get; } = semantics.Value.MultiLanguageSemanticPostfixSeparator; + + private static readonly HashSet StringTypes = + [ + DataTypeDefXsd.String, DataTypeDefXsd.AnyUri, DataTypeDefXsd.Byte, DataTypeDefXsd.Date, + DataTypeDefXsd.DateTime, DataTypeDefXsd.Duration, DataTypeDefXsd.GDay, DataTypeDefXsd.GYear, + DataTypeDefXsd.GYearMonth, DataTypeDefXsd.HexBinary, DataTypeDefXsd.Time, DataTypeDefXsd.Base64Binary, + DataTypeDefXsd.GMonth, DataTypeDefXsd.GMonthDay + ]; + + private static readonly HashSet IntegerTypes = + [ + DataTypeDefXsd.Int, DataTypeDefXsd.Integer, DataTypeDefXsd.Long, DataTypeDefXsd.NegativeInteger, + DataTypeDefXsd.NonNegativeInteger, DataTypeDefXsd.NonPositiveInteger, DataTypeDefXsd.PositiveInteger, + DataTypeDefXsd.Short, DataTypeDefXsd.UnsignedShort, DataTypeDefXsd.UnsignedLong, + DataTypeDefXsd.UnsignedInt, DataTypeDefXsd.UnsignedByte + ]; + + private static readonly HashSet NumberTypes = + [ + DataTypeDefXsd.Float, DataTypeDefXsd.Double, DataTypeDefXsd.Decimal + ]; + + public string GetSemanticId(IHasSemantics hasSemantics) => hasSemantics.SemanticId?.Keys?.FirstOrDefault()?.Value ?? string.Empty; + + public string ExtractSemanticId(ISubmodelElement element) + { + if (element.Qualifiers == null) + { + return GetSemanticId(element); + } + + var qualifier = element.Qualifiers.FirstOrDefault(q => q.Type == _internalSemanticId); + return qualifier != null ? qualifier.Value! : GetSemanticId(element); + } + + public string ResolveSemanticId(IHasSemantics hasSemantics, string idShort) + { + var baseSemanticId = GetSemanticId(hasSemantics); + return AppendIndex(baseSemanticId, idShort); + } + + public string ResolveElementSemanticId(ISubmodelElement element, string idShort) + { + var baseSemanticId = ExtractSemanticId(element); + return AppendIndex(baseSemanticId, idShort); + } + + public Cardinality GetCardinality(ISubmodelElement element) + { + var qualifierValue = element.Qualifiers?.FirstOrDefault()?.Value; + if (qualifierValue is null) + { + return Cardinality.Unknown; + } + + return Enum.TryParse(qualifierValue, ignoreCase: true, out var result) + ? result + : Cardinality.Unknown; + } + + public DataType GetValueType(ISubmodelElement element) + { + return element switch + { + Property p => GetDataTypeFromValueType(p.ValueType), + Range r => GetDataTypeFromValueType(r.ValueType), + File => DataType.String, + Blob => DataType.String, + _ => DataType.Unknown + }; + } + + private static DataType GetDataTypeFromValueType(DataTypeDefXsd valueType) + { + return valueType switch + { + _ when StringTypes.Contains(valueType) => DataType.String, + _ when IntegerTypes.Contains(valueType) => DataType.Integer, + _ when NumberTypes.Contains(valueType) => DataType.Number, + DataTypeDefXsd.Boolean => DataType.Boolean, + _ => DataType.Unknown + }; + } + + public string BuildReferenceKeySemanticId(string baseSemanticId, KeyTypes keyType, int index, int totalCount) + { + return totalCount > 1 + ? $"{baseSemanticId}{MlpPostFixSeparator}{keyType}{MlpPostFixSeparator}{index}" + : $"{baseSemanticId}{MlpPostFixSeparator}{keyType}"; + } + + private string AppendIndex(string semanticId, string? idShort) + { + var index = string.Empty; + if (idShort != null) + { + index = SubmodelElementCollectionIndex().Match(idShort).Value; + } + + return string.IsNullOrWhiteSpace(index) + ? semanticId + : $"{semanticId}{_submodelElementIndexContextPrefix}{index}"; + } + + /// + /// Matches one or more digits at the end of a string, + /// e.g., "element42" → matches "42" + /// Pattern: \d+$ + /// + [GeneratedRegex(@"\d+$")] + private static partial Regex SubmodelElementCollectionIndex(); +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigator.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigator.cs new file mode 100644 index 00000000..a2a4ab33 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigator.cs @@ -0,0 +1,55 @@ +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; + +public class SemanticTreeNavigator +{ + public static IEnumerable FindBranchNodesBySemanticId(SemanticTreeNode tree, string semanticId) + { + var node = tree as SemanticBranchNode; + + return node?.Children! + .Where(child => child.SemanticId.Equals(semanticId, StringComparison.Ordinal)) + ?? []; + } + + public static IEnumerable FindNodeBySemanticId(SemanticTreeNode tree, string semanticId) + { + if (tree.SemanticId == semanticId) + { + yield return tree; + } + + if (tree is not SemanticBranchNode branchNode) + { + yield break; + } + + foreach (var child in branchNode.Children) + { + foreach (var matchingNode in FindNodeBySemanticId(child, semanticId)) + { + yield return matchingNode; + } + } + } + + public static bool AreAllNodesOfSameType(List nodes, out Type? nodeType) + { + if (nodes.Count == 0) + { + nodeType = null; + return true; + } + + var firstNodeType = nodes[0].GetType(); + nodeType = firstNodeType; + + if (firstNodeType != typeof(SemanticBranchNode) && firstNodeType != typeof(SemanticLeafNode)) + { + return false; + } + + return nodes.All(node => node.GetType() == firstNodeType); + } +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelper.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelper.cs new file mode 100644 index 00000000..04d4db4c --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelper.cs @@ -0,0 +1,119 @@ +using System.Globalization; +using System.Text.RegularExpressions; + +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.Config; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; + +using AasCore.Aas3_0; + +using Microsoft.Extensions.Options; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; + +public partial class SubmodelElementHelper(ILogger logger, IOptions mlpSettings) : ISubmodelElementHelper +{ + private readonly HashSet? _defaultLanguagesSet = mlpSettings.Value.DefaultLanguages != null && mlpSettings.Value.DefaultLanguages.Count > 0 + ? new HashSet(mlpSettings.Value.DefaultLanguages, StringComparer.OrdinalIgnoreCase) + : null; + + public ISubmodelElement CloneElement(ISubmodelElement element) + { + var jsonElement = Jsonization.Serialize.ToJsonObject(element); + + return Jsonization.Deserialize.ISubmodelElementFrom(jsonElement); + } + + public ISubmodelElement? GetElementByIdShort(IEnumerable? submodelElements, string idShort) + { + if (TryParseIdShortWithBracketIndex(idShort, out var idShortWithoutIndex, out var index)) + { + return GetElementFromListByIndex(submodelElements, idShortWithoutIndex, index); + } + + return submodelElements?.FirstOrDefault(e => e.IdShort == idShort); + } + + public ISubmodelElement GetElementFromListByIndex(IEnumerable? elements, string idShortWithoutIndex, int index) + { + var baseElement = elements?.FirstOrDefault(e => e.IdShort == idShortWithoutIndex); + + if (baseElement is not ISubmodelElementList list) + { + logger.LogError("Expected list element with IdShort '{IdShortWithoutIndex}' not found or is not a list.", idShortWithoutIndex); + throw new InternalDataProcessingException(); + } + + if (index >= 0 && index < list.Value!.Count) + { + return list.Value[index]; + } + + logger.LogError("Index {Index} is out of bounds for list '{IdShortWithoutIndex}' with count {Count}.", index, idShortWithoutIndex, list.Value!.Count); + throw new InternalDataProcessingException(); + } + + public IList? GetChildElements(ISubmodelElement submodelElement) + { + return submodelElement switch + { + ISubmodelElementCollection c => c.Value, + ISubmodelElementList l => l.Value, + IEntity entity => entity.Statements, + _ => null + }; + } + + public HashSet ResolveLanguages(MultiLanguageProperty mlp) + { + var languages = new HashSet(StringComparer.OrdinalIgnoreCase); + + if (mlp.Value is { Count: > 0 }) + { + foreach (var langValue in mlp.Value) + { + _ = languages.Add(langValue.Language); + } + } + + if (_defaultLanguagesSet != null) + { + languages.UnionWith(_defaultLanguagesSet); + } + + return languages; + } + + private static bool TryParseIdShortWithBracketIndex(string idShort, out string idShortWithoutIndex, out int index) + { + var match = SubmodelElementListIndex().Match(idShort); + if (!match.Success) + { + idShortWithoutIndex = string.Empty; + index = -1; + return false; + } + + idShortWithoutIndex = match.Groups[1].Value; + var indexGroup = match.Groups[2].Success ? match.Groups[2] : match.Groups[3]; + if (!indexGroup.Success) + { + idShortWithoutIndex = string.Empty; + index = -1; + return false; + } + + index = int.Parse(indexGroup.Value, CultureInfo.InvariantCulture); + return true; + } + + /// + /// Matches strings like "element[3]" and captures: + /// Group 1 → element name (any characters, lazy match) + /// Group 2 → index (digits inside square brackets) + /// e.g. "element[3]" -> matches Group1= "element", Group2 = "3" + /// Pattern: ^(.+?)\[(\d+)\]$ + /// + [GeneratedRegex(@"^(.+?)(?:\[(\d+)\]|%5B(\d+)%5D)$")] + private static partial Regex SubmodelElementListIndex(); +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandler.cs index 72e77e8e..8711f051 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandler.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandler.cs @@ -1,948 +1,18 @@ -using System.Globalization; -using System.Text.RegularExpressions; - -using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.Config; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Extraction; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.FillOut; using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; using AasCore.Aas3_0; -using Microsoft.Extensions.Options; - -using File = AasCore.Aas3_0.File; -using Range = AasCore.Aas3_0.Range; - namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository; -public partial class SemanticIdHandler( - ILogger logger, - IOptions semantics, - IOptions mlpSettings) : ISemanticIdHandler +public class SemanticIdHandler( + ISemanticTreeExtractor extractor, + ISubmodelFiller filler) : ISemanticIdHandler { - private readonly string _mlpPostFixSeparator = semantics.Value.MultiLanguageSemanticPostfixSeparator; - private readonly string _submodelElementIndexContextPrefix = semantics.Value.SubmodelElementIndexContextPrefix; - private readonly string _internalSemanticId = semantics.Value.InternalSemanticId; - private const string RangeMinimumPostFixSeparator = "_min"; - private const string RangeMaximumPostFixSeparator = "_max"; - private const string EntityGlobalAssetIdPostFix = "_globalAssetId"; - private const string RelationshipElementFirstPostFixSeparator = "_first"; - private const string RelationshipElementSecondPostFixSeparator = "_second"; - - private readonly HashSet? _defaultLanguagesSet = mlpSettings.Value.DefaultLanguages != null && mlpSettings.Value.DefaultLanguages.Count > 0 - ? new HashSet(mlpSettings.Value.DefaultLanguages, StringComparer.OrdinalIgnoreCase) - : null; - - private static readonly HashSet StringTypes = - [ - DataTypeDefXsd.String, DataTypeDefXsd.AnyUri, DataTypeDefXsd.Byte, DataTypeDefXsd.Date, - DataTypeDefXsd.DateTime, DataTypeDefXsd.Duration, DataTypeDefXsd.GDay, DataTypeDefXsd.GYear, - DataTypeDefXsd.GYearMonth, DataTypeDefXsd.HexBinary, DataTypeDefXsd.Time, DataTypeDefXsd.Base64Binary, - DataTypeDefXsd.GMonth, DataTypeDefXsd.GMonthDay - ]; - - private static readonly HashSet IntegerTypes = - [ - DataTypeDefXsd.Int, DataTypeDefXsd.Integer, DataTypeDefXsd.Long, DataTypeDefXsd.NegativeInteger, - DataTypeDefXsd.NonNegativeInteger, DataTypeDefXsd.NonPositiveInteger, DataTypeDefXsd.PositiveInteger, - DataTypeDefXsd.Short, DataTypeDefXsd.UnsignedShort, DataTypeDefXsd.UnsignedLong, - DataTypeDefXsd.UnsignedInt, DataTypeDefXsd.UnsignedByte - ]; - - private static readonly HashSet NumberTypes = - [ - DataTypeDefXsd.Float, DataTypeDefXsd.Double, DataTypeDefXsd.Decimal - ]; - - public SemanticTreeNode Extract(ISubmodel submodelTemplate) - { - ArgumentNullException.ThrowIfNull(submodelTemplate); - - var rootNode = new SemanticBranchNode(GetSemanticId(submodelTemplate, submodelTemplate.IdShort!), Cardinality.Unknown); - var childNodes = submodelTemplate.SubmodelElements! - .Select(Extract) - .Where(childNode => childNode != null) - .ToList(); - - foreach (var childNode in childNodes) - { - rootNode.AddChild(childNode!); - } - - return rootNode; - } - - public ISubmodelElement Extract(ISubmodel submodelTemplate, string idShortPath) - { - ArgumentNullException.ThrowIfNull(submodelTemplate); - ArgumentNullException.ThrowIfNull(idShortPath); - - var currentSubmodelElements = submodelTemplate.SubmodelElements; - var idShortPathSegments = idShortPath.Split('.'); - for (var index = 0; index < idShortPathSegments.Length; index++) - { - var currentIdShort = idShortPathSegments[index]; - var isLastSegment = index == idShortPathSegments.Length - 1; - - var matchedElement = GetElementByIdShort(currentSubmodelElements, currentIdShort) - ?? throw new InternalDataProcessingException(); - if (isLastSegment) - { - return matchedElement; - } - - currentSubmodelElements = GetChildElements(matchedElement) as List - ?? throw new InternalDataProcessingException(); - } - - throw new InternalDataProcessingException(); - } - - private SemanticTreeNode? Extract(ISubmodelElement submodelElementTemplate) - { - ArgumentNullException.ThrowIfNull(submodelElementTemplate); - - return submodelElementTemplate switch - { - SubmodelElementCollection collection => ExtractCollection(collection), - SubmodelElementList list => ExtractList(list), - MultiLanguageProperty mlp => ExtractMultiLanguageProperty(mlp), - Range range => ExtractRange(range), - ReferenceElement re => ExtractReferenceElement(re), - RelationshipElement relationshipElement => ExtractRelationshipElement(relationshipElement), - Entity entity => ExtractEntity(entity), - _ => CreateLeafNode(submodelElementTemplate) - }; - } - - private SemanticBranchNode ExtractList(SubmodelElementList list) - { - var node = new SemanticBranchNode(GetSemanticId(list, list.IdShort!), GetCardinality(list)); - if (list.Value?.Count > 0) - { - foreach (var element in list.Value) - { - var child = Extract(element); - if (child != null) - { - node.AddChild(child); - } - } - } - else - { - logger.LogWarning("No elements defined in SubmodelElementList {ListIdShort}", list.IdShort); - } - - return node; - } - - private SemanticBranchNode ExtractCollection(SubmodelElementCollection collection) - { - var node = new SemanticBranchNode(GetSemanticId(collection, collection.IdShort!), GetCardinality(collection)); - if (collection.Value?.Count > 0) - { - foreach (var element in collection.Value.Where(_ => true)) - { - var child = Extract(element); - if (child != null) - { - node.AddChild(child); - } - } - } - else - { - logger.LogWarning("No elements defined in SubmodelElementCollection {CollectionIdShort}", collection.IdShort); - } - - return node; - } - - private SemanticBranchNode? ExtractReferenceElement(ReferenceElement referenceElement) - { - if (referenceElement.Value == null || referenceElement.Value.Type == ReferenceTypes.ExternalReference) - { - return null; - } - - return ExtractFormReference(referenceElement.Value, GetSemanticId(referenceElement, referenceElement.IdShort!), GetCardinality(referenceElement)); - } - - private SemanticBranchNode? ExtractRelationshipElement(RelationshipElement relationshipElement) - { - if (relationshipElement.First.Type == ReferenceTypes.ExternalReference && relationshipElement.Second.Type == ReferenceTypes.ExternalReference) - { - return null; - } - - var semanticId = GetSemanticId(relationshipElement); - var cardinality = GetCardinality(relationshipElement); - var relationshipElementNode = new SemanticBranchNode(semanticId, cardinality); - - if (relationshipElement.First.Type == ReferenceTypes.ModelReference) - { - var referenceNode = ExtractFormReference(relationshipElement.First, $"{semanticId}{RelationshipElementFirstPostFixSeparator}", cardinality); - if (referenceNode != null) - { - relationshipElementNode.AddChild(referenceNode); - } - } - - if (relationshipElement.Second.Type == ReferenceTypes.ModelReference) - { - var referenceNode = ExtractFormReference(relationshipElement.Second, $"{semanticId}{RelationshipElementSecondPostFixSeparator}", cardinality); - if (referenceNode != null) - { - relationshipElementNode.AddChild(referenceNode); - } - } - - return relationshipElementNode; - } - - private SemanticBranchNode? ExtractFormReference(IReference reference, string semanticId, Cardinality cardinality) - { - var keys = reference.Keys; - if (keys.Count <= 0) - { - return null; - } - - var branchNode = new SemanticBranchNode(semanticId, cardinality); - - foreach (var group in keys.GroupBy(k => k.Type)) - { - group.Select((_, index) => new SemanticLeafNode(group.Count() > 1 - ? $"{semanticId}{_mlpPostFixSeparator}{group.Key}{_mlpPostFixSeparator}{index}" - : $"{semanticId}{_mlpPostFixSeparator}{group.Key}", - string.Empty, - DataType.String, - Cardinality.ZeroToOne)) - .ToList() - .ForEach(branchNode.AddChild); - } - - return branchNode; - } - - private SemanticBranchNode ExtractEntity(Entity entity) - { - var semanticId = GetSemanticId(entity, entity.IdShort!); - var node = new SemanticBranchNode(semanticId, GetCardinality(entity)); - if (entity.EntityType == EntityType.SelfManagedEntity) - { - var globalAssetIdNode = new SemanticLeafNode(semanticId + EntityGlobalAssetIdPostFix, string.Empty, DataType.String, Cardinality.One); - node.AddChild(globalAssetIdNode); - if (entity.SpecificAssetIds != null) - { - foreach (var specificAssetId in entity.SpecificAssetIds) - { - IHasSemantics specificAsset = specificAssetId; - if (specificAsset.SemanticId == null) - { - continue; - } - - var specificAssetIdNode = new SemanticLeafNode(GetSemanticId(specificAssetId), string.Empty, DataType.String, Cardinality.One); - node.AddChild(specificAssetIdNode); - } - } - } - - if (entity.Statements?.Count > 0) - { - foreach (var child in entity.Statements.Select(Extract).OfType()) - { - node.AddChild(child); - } - } - else - { - logger.LogWarning("No elements defined in Entity {EntityIdShort}", entity.IdShort); - } - - return node; - } - - private string ExtractSemanticId(ISubmodelElement element) - { - if (element.Qualifiers == null) - { - return GetSemanticId(element); - } - - var qualifier = element.Qualifiers.FirstOrDefault(q => q.Type == _internalSemanticId); - if (qualifier != null) - { - return qualifier.Value!; - } - - return GetSemanticId(element); - } - - private SemanticBranchNode? ExtractMultiLanguageProperty(MultiLanguageProperty mlp) - { - var semanticId = ExtractSemanticId(mlp); - var node = new SemanticBranchNode(semanticId, GetCardinality(mlp)); - - var languages = new HashSet(StringComparer.OrdinalIgnoreCase); - - if (mlp.Value is { Count: > 0 }) - { - foreach (var langValue in mlp.Value) - { - languages.Add(langValue.Language); - } - } - else - { - logger.LogInformation("No languages defined in template for MultiLanguageProperty {MlpIdShort}", mlp.IdShort); - } - - if (_defaultLanguagesSet != null) - { - languages.UnionWith(_defaultLanguagesSet); - } - - foreach (var langSemanticId in languages.Select(language => string.Concat(semanticId, _mlpPostFixSeparator, language))) - { - node.AddChild(new SemanticLeafNode(langSemanticId, string.Empty, DataType.String, Cardinality.ZeroToOne)); - } - - return node; - } - - private SemanticBranchNode ExtractRange(Range range) - { - var semanticId = ExtractSemanticId(range); - var valueType = GetValueType(range); - var node = new SemanticBranchNode(semanticId, GetCardinality(range)); - - node.AddChild(new SemanticLeafNode(semanticId + RangeMinimumPostFixSeparator, string.Empty, valueType, Cardinality.ZeroToOne)); - node.AddChild(new SemanticLeafNode(semanticId + RangeMaximumPostFixSeparator, string.Empty, valueType, Cardinality.ZeroToOne)); - - return node; - } - - private SemanticLeafNode CreateLeafNode(ISubmodelElement element) - { - var semanticId = GetSemanticId(element, element.IdShort!); - var valueType = GetValueType(element); - var cardinality = GetCardinality(element); - return new SemanticLeafNode(semanticId, string.Empty, valueType, cardinality); - } - - private static Cardinality GetCardinality(ISubmodelElement element) - { - var qualifierValue = element.Qualifiers?.FirstOrDefault()?.Value; - if (qualifierValue is null) - { - return Cardinality.Unknown; - } - - return Enum.TryParse(qualifierValue, ignoreCase: true, out var result) - ? result - : Cardinality.Unknown; - } - - private static DataType GetValueType(ISubmodelElement element) - { - return element switch - { - Property p => GetDataTypeFromValueType(p.ValueType), - Range r => GetDataTypeFromValueType(r.ValueType), - File => DataType.String, - Blob => DataType.String, - _ => DataType.Unknown - }; - } - - private static DataType GetDataTypeFromValueType(DataTypeDefXsd valueType) - { - return valueType switch - { - _ when StringTypes.Contains(valueType) => DataType.String, - _ when IntegerTypes.Contains(valueType) => DataType.Integer, - _ when NumberTypes.Contains(valueType) => DataType.Number, - DataTypeDefXsd.Boolean => DataType.Boolean, - _ => DataType.Unknown - }; - } - - private string GetSemanticId(ISubmodelElement element, string idShort) - { - var baseSemanticId = ExtractSemanticId(element); - return AppendIndex(baseSemanticId, idShort); - } - - private string GetSemanticId(IHasSemantics hasSemantics, string idShort) - { - var baseSemanticId = GetSemanticId(hasSemantics); - return AppendIndex(baseSemanticId, idShort); - } - - private static string GetSemanticId(IHasSemantics hasSemantics) => hasSemantics.SemanticId?.Keys?.FirstOrDefault()?.Value ?? string.Empty; - - private string AppendIndex(string semanticId, string? idShort) - { - var index = string.Empty; - if (idShort != null) - { - index = SubmodelElementCollectionIndex().Match(idShort).Value; - } - - return string.IsNullOrWhiteSpace(index) - ? semanticId - : $"{semanticId}{_submodelElementIndexContextPrefix}{index}"; - } - - public ISubmodel FillOutTemplate(ISubmodel submodelTemplate, SemanticTreeNode values) - { - ArgumentNullException.ThrowIfNull(submodelTemplate); - ArgumentNullException.ThrowIfNull(submodelTemplate.SubmodelElements); - ArgumentNullException.ThrowIfNull(values); - - var submodelElements = submodelTemplate.SubmodelElements.ToList(); - foreach (var submodelElement in submodelElements) - { - var semanticId = ExtractSemanticId(submodelElement); - - var matchingNodes = FindBranchNodesBySemanticId(values, semanticId)?.ToList(); - - if (matchingNodes == null || matchingNodes.Count == 0) - { - continue; - } - - _ = submodelTemplate.SubmodelElements.Remove(submodelElement); - - if (matchingNodes.Count > 1) - { - HandleMultipleMatchingNodes(matchingNodes, submodelElement, submodelTemplate); - } - else - { - HandleSingleMatchingNode(matchingNodes[0], submodelElement, submodelTemplate); - } - } - - return submodelTemplate; - } - - private void HandleMultipleMatchingNodes( - List matchingNodes, - ISubmodelElement baseElement, - ISubmodel submodelTemplate) - { - for (var i = 0; i < matchingNodes.Count; i++) - { - var node = matchingNodes[i]; - var clonedElement = CloneElementJson(baseElement); - - if (baseElement is SubmodelElementCollection) - { - clonedElement.IdShort = $"{clonedElement.IdShort}{i}"; - } - - _ = FillOutTemplate(clonedElement, node); - submodelTemplate.SubmodelElements?.Add(clonedElement); - } - } - - private void HandleSingleMatchingNode( - SemanticTreeNode node, - ISubmodelElement element, - ISubmodel submodelTemplate) - { - _ = FillOutTemplate(element, node); - submodelTemplate.SubmodelElements?.Add(element); - } - - private ISubmodelElement FillOutTemplate(ISubmodelElement submodelElementTemplate, SemanticTreeNode values) - { - ArgumentNullException.ThrowIfNull(submodelElementTemplate); - ArgumentNullException.ThrowIfNull(values); - - switch (submodelElementTemplate) - { - case SubmodelElementCollection collection: - FillOutSubmodelElementCollection(collection, values); - break; - - case SubmodelElementList list: - FillOutSubmodelElementList(list, values); - break; - - case MultiLanguageProperty mlp: - FillOutMultiLanguageProperty(mlp, values); - break; - - case Property property: - FillOutProperty(property, values); - break; - - case File file: - FillOutFile(file, values); - break; - - case Blob blob: - FillOutBlob(blob, values); - break; - - case RelationshipElement relationship: - FillOutRelationshipElement(relationship, values); - break; - - case ReferenceElement reference: - FillOutReferenceElement(reference, values); - break; - - case Range range: - FillOutRange(range, values); - break; - - case Entity entity: - FillOutEntity(entity, values); - break; - - default: - logger.LogError("InValid submodelElementTemplate Type. IdShort : {IdShort}", submodelElementTemplate.IdShort); - throw new InternalDataProcessingException(); - } - - return submodelElementTemplate; - } - - private void FillOutSubmodelElementList(SubmodelElementList list, SemanticTreeNode values) - { - if (list?.Value == null || list.Value.Count == 0) - { - return; - } - - FillOutSubmodelElementValue(list.Value, values, false); - } - - private void FillOutSubmodelElementCollection(SubmodelElementCollection collection, SemanticTreeNode values) - { - if (collection?.Value == null || collection.Value.Count == 0) - { - return; - } - - FillOutSubmodelElementValue(collection.Value, values); - } - - private void FillOutSubmodelElementValue(List elements, SemanticTreeNode values, bool updateIdShort = true) - { - var originalElements = elements.ToList(); - foreach (var element in originalElements) - { - var valueNode = FindNodeBySemanticId(values, ExtractSemanticId(element)); - var semanticTreeNodes = valueNode?.ToList(); - - if (semanticTreeNodes == null || semanticTreeNodes.Count == 0) - { - continue; - } - - if (!AreAllNodesOfSameType(semanticTreeNodes, out _)) - { - logger.LogWarning("Mixed node types found for element '{IdShort}' with SemanticId '{SemanticId}'. Expected all nodes to be either SemanticBranchNode or SemanticLeafNode. Removing element.", - element.IdShort, - ExtractSemanticId(element)); - _ = elements.Remove(element); - continue; - } - - if (semanticTreeNodes.Count > 1 && element is not Property && element is not ReferenceElement) - { - _ = elements.Remove(element); - for (var i = 0; i < semanticTreeNodes.Count; i++) - { - var cloned = CloneElementJson(element); - if (updateIdShort) - { - cloned.IdShort = $"{cloned.IdShort}{i}"; - } - - _ = FillOutTemplate(cloned, semanticTreeNodes[i]); - elements.Add(cloned); - } - } - else - { - HandleSingleSemanticTreeNode(element, semanticTreeNodes[0]); - } - } - } - - private static bool AreAllNodesOfSameType(List nodes, out Type? nodeType) - { - if (nodes.Count == 0) - { - nodeType = null; - return true; - } - - var firstNodeType = nodes[0].GetType(); - nodeType = firstNodeType; - - if (firstNodeType != typeof(SemanticBranchNode) && firstNodeType != typeof(SemanticLeafNode)) - { - return false; - } - - return nodes.All(node => node.GetType() == firstNodeType); - } - - private void HandleSingleSemanticTreeNode(ISubmodelElement element, SemanticTreeNode node) => FillOutTemplate(element, node); - - private void FillOutMultiLanguageProperty(MultiLanguageProperty mlp, SemanticTreeNode values) - { - var semanticId = ExtractSemanticId(mlp); - - if (FindNodeBySemanticId(values, semanticId).FirstOrDefault() is not SemanticBranchNode valueNode) - { - logger.LogInformation("No value node found for MultiLanguageProperty {MlpIdShort}", mlp.IdShort); - return; - } - - mlp.Value ??= []; - - var languageValueMap = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var langValue in mlp.Value) - { - languageValueMap[langValue.Language] = (LangStringTextType)langValue; - } - - var languages = new HashSet(languageValueMap.Keys, StringComparer.OrdinalIgnoreCase); - - if (_defaultLanguagesSet != null) - { - languages.UnionWith(_defaultLanguagesSet); - } - - foreach (var language in languages) - { - if (!languageValueMap.TryGetValue(language, out var languageValue)) - { - languageValue = new LangStringTextType(language, string.Empty); - mlp.Value.Add(languageValue); - languageValueMap[language] = languageValue; - - logger.LogInformation("Added language '{Language}' to MultiLanguageProperty {MlpIdShort}", language, mlp.IdShort); - } - - var languageSemanticId = semanticId + _mlpPostFixSeparator + language; - - var leafNode = valueNode.Children - .OfType() - .FirstOrDefault(child => child.SemanticId.Equals(languageSemanticId, StringComparison.Ordinal)); - - if (leafNode != null) - { - languageValue.Text = leafNode.Value; - } - } - } - - private void FillOutEntity(Entity entity, SemanticTreeNode values) - { - if (entity.EntityType == EntityType.SelfManagedEntity) - { - FillOutSelfManagedEntity(entity, values); - } - - if (entity?.Statements == null || entity.Statements.Count == 0) - { - return; - } - - FillOutSubmodelElementValue(entity.Statements, values); - } - - private void FillOutSelfManagedEntity(Entity entity, SemanticTreeNode values) - { - var semanticId = GetSemanticId(entity, entity.IdShort!); - - if (FindNodeBySemanticId(values, semanticId).FirstOrDefault() is not SemanticBranchNode valueNode) - { - return; - } - - var globalAssetSemanticId = semanticId + EntityGlobalAssetIdPostFix; - - var globalAssetNode = valueNode.Children - .OfType() - .FirstOrDefault(c => c.SemanticId == globalAssetSemanticId); - - if (globalAssetNode != null) - { - entity.GlobalAssetId = globalAssetNode.Value; - } - - if (entity.SpecificAssetIds != null) - { - foreach (var specificAssetId in entity.SpecificAssetIds) - { - var specSemanticId = GetSemanticId(specificAssetId); - - var specNode = valueNode.Children - .OfType() - .FirstOrDefault(c => c.SemanticId == specSemanticId); - - if (specNode != null) - { - specificAssetId.Value = specNode.Value; - } - } - } - } - - private static void FillOutProperty(Property valueElement, SemanticTreeNode values) - { - if (values is SemanticLeafNode leafValueNode) - { - valueElement.Value = leafValueNode.Value; - } - } - - private static void FillOutFile(File valueElement, SemanticTreeNode values) - { - if (values is SemanticLeafNode leafValueNode) - { - valueElement.Value = leafValueNode.Value; - } - } - - private static void FillOutBlob(Blob valueElement, SemanticTreeNode values) - { - if (values is SemanticLeafNode leafValueNode) - { - valueElement.Value = Convert.FromBase64String(leafValueNode.Value); - } - } - - private static void FillOutRange(Range valueElement, SemanticTreeNode values) - { - if (values is not SemanticBranchNode branchNode) - { - return; - } - - var leafNodes = branchNode.Children.OfType().ToList(); - - valueElement.Min = leafNodes.FirstOrDefault(n => n.SemanticId - .EndsWith(RangeMinimumPostFixSeparator, StringComparison.Ordinal))? - .Value; - - valueElement.Max = leafNodes.FirstOrDefault(n => n.SemanticId - .EndsWith(RangeMaximumPostFixSeparator, StringComparison.Ordinal))? - .Value; - } - - private void FillOutReferenceElement(ReferenceElement referenceElement, SemanticTreeNode semanticNode) - { - if (referenceElement?.Value?.Type != ReferenceTypes.ModelReference) - { - logger.LogInformation("ReferenceElement does not contain a ModelReference for SemanticId '{SemanticId}'. Skipping population.", GetSemanticId(referenceElement!)); - return; - } - - ProcessReferenceNode(referenceElement.Value, semanticNode, GetSemanticId(referenceElement)); - } - - private void FillOutRelationshipElement(RelationshipElement relationshipElement, SemanticTreeNode semanticTreeNode) - { - var semanticId = semanticTreeNode.SemanticId; - - ProcessRelationshipReference(relationshipElement.First, semanticTreeNode, semanticId, RelationshipElementFirstPostFixSeparator); - - ProcessRelationshipReference(relationshipElement.Second, semanticTreeNode, semanticId, RelationshipElementSecondPostFixSeparator); - } - - private void ProcessReferenceNode(IReference reference, SemanticTreeNode semanticNode, string semanticId) - { - if (semanticNode is not SemanticBranchNode branchNode) - { - logger.LogWarning("Expected SemanticBranchNode for SemanticId '{SemanticId}', but got {NodeType}. Skipping population.", semanticId, semanticNode.GetType().Name); - return; - } - - var keys = reference.Keys; - - if (keys.Count <= 0) - { - logger.LogInformation("ReferenceElement has no keys for SemanticId '{SemanticId}'. Nothing to populate.", semanticId); - return; - } - - foreach (var group in keys.GroupBy(k => k.Type)) - { - ProcessReferenceKeyGroup(group, branchNode, semanticId); - } - } - - private void ProcessReferenceKeyGroup(IGrouping group, SemanticBranchNode branchNode, string semanticId) - { - var keyList = group.ToList(); - for (var i = 0; i < keyList.Count; i++) - { - var indexedSemanticId = keyList.Count > 1 - ? $"{semanticId}{_mlpPostFixSeparator}{group.Key}{_mlpPostFixSeparator}{i}" - : $"{semanticId}{_mlpPostFixSeparator}{group.Key}"; - - var leafNode = branchNode.Children - .OfType() - .FirstOrDefault(child => child.SemanticId == indexedSemanticId); - - if (leafNode != null) - { - keyList[i].Value = !string.IsNullOrEmpty(leafNode.Value) ? leafNode.Value : keyList[i].Value; - } - else - { - logger.LogWarning("No matching leaf node found for SemanticId '{IndexedSemanticId}'.", indexedSemanticId); - } - } - } - - private void ProcessRelationshipReference(IReference reference, SemanticTreeNode semanticTreeNode, string semanticId, string postfixSeparator) - { - if (reference.Type != ReferenceTypes.ModelReference) - { - return; - } - - var searchPattern = semanticId + postfixSeparator; - var valueNode = FindNodeBySemanticId(semanticTreeNode, searchPattern).FirstOrDefault(); - - if (valueNode != null) - { - ProcessReferenceNode(reference, valueNode, searchPattern); - } - else - { - logger.LogWarning("No matching node found for reference with pattern: {Pattern}", searchPattern); - } - } - - private static ISubmodelElement CloneElementJson(ISubmodelElement element) - { - var jsonElement = Jsonization.Serialize.ToJsonObject(element); - - return Jsonization.Deserialize.ISubmodelElementFrom(jsonElement); - } - - private static IEnumerable FindBranchNodesBySemanticId(SemanticTreeNode tree, string semanticId) - { - var node = tree as SemanticBranchNode; - - return node?.Children! - .Where(child => child.SemanticId.Equals(semanticId, StringComparison.Ordinal)) - ?? []; - } - - private static IEnumerable FindNodeBySemanticId(SemanticTreeNode tree, string semanticId) - { - if (tree.SemanticId == semanticId) - { - yield return tree; - } - - if (tree is not SemanticBranchNode branchNode) - { - yield break; - } - - foreach (var child in branchNode.Children) - { - foreach (var matchingNode in FindNodeBySemanticId(child, semanticId)) - { - yield return matchingNode; - } - } - } - - /// - /// Matches strings like "element[3]" and captures: - /// Group 1 → element name (any characters, lazy match) - /// Group 2 → index (digits inside square brackets) - /// e.g. "element[3]" -> matches Group1= "element", Group2 = "3" - /// Pattern: ^(.+?)\[(\d+)\]$ - /// - [GeneratedRegex(@"^(.+?)(?:\[(\d+)\]|%5B(\d+)%5D)$")] - private static partial Regex SubmodelElementListIndex(); - - /// - /// Matches one or more digits at the end of a string, - /// e.g., "element42" → matches "42" - /// Pattern: \d+$ - /// - [GeneratedRegex(@"\d+$")] - private static partial Regex SubmodelElementCollectionIndex(); - - private ISubmodelElement? GetElementByIdShort(IEnumerable? submodelElements, string idShort) - { - if (TryParseIdShortWithBracketIndex(idShort, out var idShortWithoutIndex, out var index)) - { - return GetElementFromListByIndex(submodelElements, idShortWithoutIndex, index); - } - - return submodelElements?.FirstOrDefault(e => e.IdShort == idShort); - } - - private static bool TryParseIdShortWithBracketIndex(string idShort, out string idShortWithoutIndex, out int index) - { - var match = SubmodelElementListIndex().Match(idShort); - if (!match.Success) - { - idShortWithoutIndex = string.Empty; - index = -1; - return false; - } - - idShortWithoutIndex = match.Groups[1].Value; - var indexGroup = match.Groups[2].Success ? match.Groups[2] : match.Groups[3]; - if (!indexGroup.Success) - { - idShortWithoutIndex = string.Empty; - index = -1; - return false; - } - - index = int.Parse(indexGroup.Value, CultureInfo.InvariantCulture); - return true; - } - - private ISubmodelElement GetElementFromListByIndex(IEnumerable? elements, string idShortWithoutIndex, int index) - { - var baseElement = elements?.FirstOrDefault(e => e.IdShort == idShortWithoutIndex); - - if (baseElement is not ISubmodelElementList list) - { - logger.LogError("Expected list element with IdShort '{IdShortWithoutIndex}' not found or is not a list.", idShortWithoutIndex); - throw new InternalDataProcessingException(); - } - - if (index >= 0 && index < list.Value!.Count) - { - return list.Value[index]; - } + public SemanticTreeNode Extract(ISubmodel submodelTemplate) => extractor.Extract(submodelTemplate); - logger.LogError("Index {Index} is out of bounds for list '{IdShortWithoutIndex}' with count {Count}.", index, idShortWithoutIndex, list.Value!.Count); - throw new InternalDataProcessingException(); - } + public ISubmodelElement Extract(ISubmodel submodelTemplate, string idShortPath) => extractor.Extract(submodelTemplate, idShortPath); - private static List? GetChildElements(ISubmodelElement submodelElement) - { - return submodelElement switch - { - ISubmodelElementCollection c => c.Value, - ISubmodelElementList l => l.Value, - IEntity entity => entity.Statements, - _ => null - }; - } + public ISubmodel FillOutTemplate(ISubmodel submodelTemplate, SemanticTreeNode values) => filler.FillOutTemplate(submodelTemplate, values); } diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/ApplicationDependencyInjectionExtensions.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/ApplicationDependencyInjectionExtensions.cs index 419656e0..40fe3bda 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/ApplicationDependencyInjectionExtensions.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/ApplicationDependencyInjectionExtensions.cs @@ -9,6 +9,10 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRegistry; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Extraction; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.FillOut; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.AasRegistryProvider.Services; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Services; @@ -31,6 +35,11 @@ public static void ConfigureApplication(this IServiceCollection services, IConfi _ = services.AddScoped(); _ = services.AddScoped(); _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); _ = services.AddScoped(); _ = services.AddScoped(); _ = services.AddScoped(); From 332a6ddc6b196cd5ad56891baa0398b88ce526bc Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Thu, 12 Mar 2026 11:54:48 +0530 Subject: [PATCH 03/16] Added Refactored SemanticIdHandler --- .../SubmodelRepositoryControllerTests.cs | 4 +- .../ElementHandlers/BlobHandlerTests.cs | 79 +++++ .../ElementHandlers/CollectionHandlerTests.cs | 138 ++++++++ .../ElementHandlers/EntityHandlerTests.cs | 185 +++++++++++ .../ElementHandlers/FileHandlerTests.cs | 78 +++++ .../ElementHandlers/ListHandlerTests.cs | 122 +++++++ .../MultiLanguagePropertyHandlerTests.cs | 156 +++++++++ .../ElementHandlers/PropertyHandlerTests.cs | 77 +++++ .../ElementHandlers/RangeHandlerTests.cs | 102 ++++++ .../ReferenceElementHandlerTests.cs | 138 ++++++++ .../RelationshipElementHandlerTests.cs | 131 ++++++++ .../Extraction/SemanticTreeExtractorTests.cs | 186 +++++++++++ .../SemanticId/FillOut/SubmodelFillerTests.cs | 131 ++++++++ .../Helpers/ReferenceHelperTests.cs | 221 +++++++++++++ .../Helpers/SemanticIdResolverTests.cs | 303 ++++++++++++++++++ .../Helpers/SemanticTreeNavigatorTests.cs | 154 +++++++++ .../Helpers/SubmodelElementHelperTests.cs | 290 +++++++++++++++++ .../SemanticIdHandlerTests.cs | 154 ++------- .../Services/SubmodelRepository/TestData.cs | 8 +- .../SemanticId/ElementHandlers/BlobHandler.cs | 25 ++ .../ElementHandlers/CollectionHandler.cs | 49 +++ .../ElementHandlers/EntityHandler.cs | 111 +++++++ .../SemanticId/ElementHandlers/FileHandler.cs | 27 ++ .../ISubmodelElementTypeHandler.cs | 14 + .../SemanticId/ElementHandlers/ListHandler.cs | 45 +++ .../MultiLanguagePropertyHandler.cs | 83 +++++ .../ElementHandlers/PropertyHandler.cs | 25 ++ .../ElementHandlers/RangeHandler.cs | 47 +++ .../ReferenceElementHandler.cs | 42 +++ .../RelationshipElementHandler.cs | 58 ++++ .../Extraction/SemanticTreeExtractor.cs | 184 +---------- .../SemanticId/FillOut/SubmodelFiller.cs | 246 +------------- .../Helpers/SemanticTreeNavigator.cs | 4 +- .../Helpers/SubmodelElementHelper.cs | 2 +- .../SubmodelRepositoryService.cs | 23 +- ...pplicationDependencyInjectionExtensions.cs | 11 + ...astructureDependencyInjectionExtensions.cs | 3 - 37 files changed, 3104 insertions(+), 552 deletions(-) create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/BlobHandlerTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/CollectionHandlerTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/EntityHandlerTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/FileHandlerTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ListHandlerTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/MultiLanguagePropertyHandlerTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/PropertyHandlerTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RangeHandlerTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ReferenceElementHandlerTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RelationshipElementHandlerTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFillerTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/ReferenceHelperTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolverTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigatorTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelperTests.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/BlobHandler.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/CollectionHandler.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/EntityHandler.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/FileHandler.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ISubmodelElementTypeHandler.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ListHandler.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/MultiLanguagePropertyHandler.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/PropertyHandler.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RangeHandler.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ReferenceElementHandler.cs create mode 100644 source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RelationshipElementHandler.cs diff --git a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRepository/SubmodelRepositoryControllerTests.cs b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRepository/SubmodelRepositoryControllerTests.cs index 3c5ee1cc..474fd6cb 100644 --- a/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRepository/SubmodelRepositoryControllerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.ModuleTests/Api/Services/SubmodelRepository/SubmodelRepositoryControllerTests.cs @@ -71,7 +71,7 @@ public async Task GetSubmodelAsync_WithValidIdentifier_ReturnsOkAsync() _ = _httpClientFactory.CreateClient(HttpClientNamePlugin1).Returns(httpClientPlugin1); const string HttpClientNamePlugin2 = $"{PluginConfig.HttpClientNamePrefix}TestPlugin2"; - _httpClientFactory.CreateClient(HttpClientNamePlugin2).Returns(httpClientPlugin2); + _ = _httpClientFactory.CreateClient(HttpClientNamePlugin2).Returns(httpClientPlugin2); var submodelId = "Q29udGFjdEluZm9ybWF0aW9u"; var mockSubmodel = TestData.CreateSubmodel(); @@ -131,7 +131,7 @@ public async Task GetSubmodelElementAsync_ReturnsOkAsync() const string SubmodelId = "Q29udGFjdEluZm9ybWF0aW9u"; const string IdShortPath = "ContactName"; var mockSubmodel = TestData.CreateSubmodel(); - TestData.CreatePluginResponseForSubmodelElement(); + _ = TestData.CreatePluginResponseForSubmodelElement(); using var messageHandler = new FakeHttpMessageHandler((_, _) => Task.FromResult(new HttpResponseMessage { diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/BlobHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/BlobHandlerTests.cs new file mode 100644 index 00000000..1471f0a9 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/BlobHandlerTests.cs @@ -0,0 +1,79 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using NSubstitute; + +using static Xunit.Assert; + +namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class BlobHandlerTests +{ + private readonly BlobHandler _sut; + private readonly ISemanticIdResolver _resolver; + + public BlobHandlerTests() + { + _resolver = Substitute.For(); + _sut = new BlobHandler(_resolver); + } + + [Fact] + public void CanHandle_Blob_ReturnsTrue() + { + var blob = new Blob(contentType: "application/octet-stream", idShort: "Test"); + + True(_sut.CanHandle(blob)); + } + + [Fact] + public void CanHandle_NonBlob_ReturnsFalse() + { + var property = new Property(idShort: "Test", valueType: DataTypeDefXsd.String); + + False(_sut.CanHandle(property)); + } + + [Fact] + public void Extract_ReturnsLeafNode() + { + var blob = new Blob(contentType: "image/png", idShort: "MyBlob"); + _resolver.ResolveElementSemanticId(blob, "MyBlob").Returns("http://test/blob"); + _resolver.GetValueType(blob).Returns(DataType.String); + _resolver.GetCardinality(blob).Returns(Cardinality.One); + + var result = _sut.Extract(blob, _ => null); + + var leaf = IsType(result); + Equal("http://test/blob", leaf.SemanticId); + Equal(DataType.String, leaf.DataType); + } + + [Fact] + public void FillOut_WithLeafNode_SetsBase64Value() + { + var blob = new Blob(contentType: "image/png", idShort: "MyBlob"); + var base64 = Convert.ToBase64String(new byte[] { 1, 2, 3 }); + var values = new SemanticLeafNode("http://test/blob", base64, DataType.String, Cardinality.One); + + _sut.FillOut(blob, values, (_, _, _) => { }); + + NotNull(blob.Value); + Equal([1, 2, 3], blob.Value); + } + + [Fact] + public void FillOut_WithBranchNode_DoesNotModifyValue() + { + var originalBytes = new byte[] { 10, 20, 30 }; + var blob = new Blob(contentType: "image/png", idShort: "MyBlob", value: originalBytes); + var values = new SemanticBranchNode("http://test/blob", Cardinality.One); + + _sut.FillOut(blob, values, (_, _, _) => { }); + + Equal(originalBytes, blob.Value); + } +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/CollectionHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/CollectionHandlerTests.cs new file mode 100644 index 00000000..fca9abc1 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/CollectionHandlerTests.cs @@ -0,0 +1,138 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using Microsoft.Extensions.Logging; + +using NSubstitute; + +using static Xunit.Assert; + +namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class CollectionHandlerTests +{ + private readonly CollectionHandler _sut; + private readonly ISemanticIdResolver _resolver; + private readonly ILogger _logger; + + public CollectionHandlerTests() + { + _resolver = Substitute.For(); + _logger = Substitute.For>(); + _sut = new CollectionHandler(_resolver, _logger); + } + + [Fact] + public void CanHandle_Collection_ReturnsTrue() + { + var collection = new SubmodelElementCollection(idShort: "Test"); + + True(_sut.CanHandle(collection)); + } + + [Fact] + public void CanHandle_NonCollection_ReturnsFalse() + { + var property = new Property(idShort: "Test", valueType: DataTypeDefXsd.String); + + False(_sut.CanHandle(property)); + } + + [Fact] + public void Extract_WithChildren_ReturnsBranchNodeWithChildren() + { + var child = new Property(idShort: "Child", valueType: DataTypeDefXsd.String); + var collection = new SubmodelElementCollection(idShort: "MyCollection", value: [child]); + _resolver.ResolveElementSemanticId(collection, "MyCollection").Returns("http://test/collection"); + _resolver.GetCardinality(collection).Returns(Cardinality.ZeroToMany); + + var childNode = new SemanticLeafNode("http://test/child", "", DataType.String, Cardinality.One); + SemanticTreeNode? extractChild(ISubmodelElement _) => childNode; + + var result = _sut.Extract(collection, extractChild); + + var branch = IsType(result); + Equal("http://test/collection", branch.SemanticId); + Equal(Cardinality.ZeroToMany, branch.Cardinality); + Single(branch.Children); + } + + [Fact] + public void Extract_WithNullValue_ReturnsBranchNodeAndLogsWarning() + { + var collection = new SubmodelElementCollection(idShort: "EmptyCollection", value: null); + _resolver.ResolveElementSemanticId(collection, "EmptyCollection").Returns("http://test/empty"); + _resolver.GetCardinality(collection).Returns(Cardinality.Unknown); + + var result = _sut.Extract(collection, _ => null); + + var branch = IsType(result); + Empty(branch.Children); + _logger.Received(1).Log( + LogLevel.Warning, + Arg.Any(), + Arg.Is(state => state.ToString()!.Contains("No elements defined in SubmodelElementCollection EmptyCollection")), + null, + Arg.Any>()! + ); + } + + [Fact] + public void Extract_WithEmptyValue_ReturnsBranchNodeAndLogsWarning() + { + var collection = new SubmodelElementCollection(idShort: "EmptyCollection", value: []); + _resolver.ResolveElementSemanticId(collection, "EmptyCollection").Returns("http://test/empty"); + _resolver.GetCardinality(collection).Returns(Cardinality.Unknown); + + var result = _sut.Extract(collection, _ => null); + + var branch = IsType(result); + Empty(branch.Children); + _logger.Received(1).Log( + LogLevel.Warning, + Arg.Any(), + Arg.Is(state => state.ToString()!.Contains("No elements defined in SubmodelElementCollection EmptyCollection")), + null, + Arg.Any>()! + ); + } + + [Fact] + public void FillOut_WithChildren_DelegatesToFillOutChildren() + { + var child = new Property(idShort: "Child", valueType: DataTypeDefXsd.String); + var collection = new SubmodelElementCollection(idShort: "Col", value: [child]); + var values = new SemanticBranchNode("http://test/col", Cardinality.One); + var fillOutCalled = false; + + _sut.FillOut(collection, values, (elements, node, updateIdShort) => + { + fillOutCalled = true; + True(updateIdShort); + Same(collection.Value, elements); + }); + + True(fillOutCalled); + } + + [Fact] + public void FillOut_WithNullValue_DoesNotCallFillOutChildren() + { + var collection = new SubmodelElementCollection(idShort: "Col", value: null); + var values = new SemanticBranchNode("http://test/col", Cardinality.One); + + _sut.FillOut(collection, values, (_, _, _) => Fail("Should not be called")); + } + + [Fact] + public void FillOut_WithEmptyValue_DoesNotCallFillOutChildren() + { + var collection = new SubmodelElementCollection(idShort: "Col", value: []); + var values = new SemanticBranchNode("http://test/col", Cardinality.One); + + _sut.FillOut(collection, values, (_, _, _) => Fail("Should not be called")); + } +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/EntityHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/EntityHandlerTests.cs new file mode 100644 index 00000000..c444370a --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/EntityHandlerTests.cs @@ -0,0 +1,185 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using Microsoft.Extensions.Logging; + +using NSubstitute; + +using static Xunit.Assert; + +namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class EntityHandlerTests +{ + private readonly EntityHandler _sut; + private readonly ISemanticIdResolver _resolver; + private readonly ILogger _logger; + + public EntityHandlerTests() + { + _resolver = Substitute.For(); + _logger = Substitute.For>(); + _sut = new EntityHandler(_resolver, _logger); + } + + [Fact] + public void CanHandle_Entity_ReturnsTrue() + { + var entity = new Entity(idShort: "Test", entityType: EntityType.SelfManagedEntity); + + True(_sut.CanHandle(entity)); + } + + [Fact] + public void CanHandle_NonEntity_ReturnsFalse() + { + var property = new Property(idShort: "Test", valueType: DataTypeDefXsd.String); + + False(_sut.CanHandle(property)); + } + + [Fact] + public void Extract_SelfManagedEntity_ReturnsBranchWithGlobalAssetIdAndSpecificAssetIds() + { + var specificAssetId = new SpecificAssetId(name: "Manufacturer", value: "Corp") + { + SemanticId = new Reference(ReferenceTypes.ModelReference, + [new Key(KeyTypes.ConceptDescription, "https://example.com/cd/manufacturer")]) + }; + + var entity = new Entity( + idShort: "MyEntity", + entityType: EntityType.SelfManagedEntity, + globalAssetId: "", + specificAssetIds: [specificAssetId], + statements: [new Property(idShort: "Stmt", valueType: DataTypeDefXsd.String)] + ); + + _resolver.ResolveElementSemanticId(entity, "MyEntity").Returns("http://test/entity"); + _resolver.GetCardinality(entity).Returns(Cardinality.ZeroToMany); + _resolver.GetSemanticId(specificAssetId).Returns("https://example.com/cd/manufacturer"); + + var stmtNode = new SemanticLeafNode("http://test/stmt", "", DataType.String, Cardinality.One); + + var result = _sut.Extract(entity, _ => stmtNode); + + var branch = IsType(result); + Equal("http://test/entity", branch.SemanticId); + Equal(3, branch.Children.Count); + var globalAssetLeaf = IsType(branch.Children[0]); + Equal("http://test/entity" + SemanticIdResolver.EntityGlobalAssetIdPostFix, globalAssetLeaf.SemanticId); + var specificLeaf = IsType(branch.Children[1]); + Equal("https://example.com/cd/manufacturer", specificLeaf.SemanticId); + } + + [Fact] + public void Extract_CoManagedEntity_DoesNotAddGlobalAssetId() + { + var entity = new Entity( + idShort: "MyEntity", + entityType: EntityType.CoManagedEntity, + statements: [new Property(idShort: "Stmt", valueType: DataTypeDefXsd.String)] + ); + + _resolver.ResolveElementSemanticId(entity, "MyEntity").Returns("http://test/entity"); + _resolver.GetCardinality(entity).Returns(Cardinality.One); + + var stmtNode = new SemanticLeafNode("http://test/stmt", "", DataType.String, Cardinality.One); + + var result = _sut.Extract(entity, _ => stmtNode); + + var branch = IsType(result); + Single(branch.Children); + } + + [Fact] + public void Extract_EntityWithNoStatements_LogsWarning() + { + var entity = new Entity( + idShort: "MyEntity", + entityType: EntityType.CoManagedEntity, + statements: null + ); + + _resolver.ResolveElementSemanticId(entity, "MyEntity").Returns("http://test/entity"); + _resolver.GetCardinality(entity).Returns(Cardinality.One); + + _sut.Extract(entity, _ => null); + + _logger.Received(1).Log( + LogLevel.Warning, + Arg.Any(), + Arg.Is(state => state.ToString()!.Contains("No elements defined in Entity MyEntity")), + null, + Arg.Any>()! + ); + } + + [Fact] + public void FillOut_SelfManagedEntity_SetsGlobalAssetIdAndSpecificAssetIds() + { + var specificAssetId = new SpecificAssetId(name: "Manufacturer", value: "") + { + SemanticId = new Reference(ReferenceTypes.ModelReference, + [new Key(KeyTypes.ConceptDescription, "https://example.com/cd/manufacturer")]) + }; + + var entity = new Entity( + idShort: "MyEntity", + entityType: EntityType.SelfManagedEntity, + globalAssetId: "", + specificAssetIds: [specificAssetId], + statements: [new Property(idShort: "Stmt", valueType: DataTypeDefXsd.String)] + ); + + _resolver.ResolveElementSemanticId(entity, "MyEntity").Returns("http://test/entity"); + _resolver.GetSemanticId(specificAssetId).Returns("https://example.com/cd/manufacturer"); + + var valueNode = new SemanticBranchNode("http://test/entity", Cardinality.One); + valueNode.AddChild(new SemanticLeafNode("http://test/entity_globalAssetId", "urn:uuid:12345", DataType.String, Cardinality.One)); + valueNode.AddChild(new SemanticLeafNode("https://example.com/cd/manufacturer", "NewCorp", DataType.String, Cardinality.One)); + + _sut.FillOut(entity, valueNode, (_, _, _) => { }); + + Equal("urn:uuid:12345", entity.GlobalAssetId); + Equal("NewCorp", specificAssetId.Value); + } + + [Fact] + public void FillOut_WithStatements_DelegatesToFillOutChildren() + { + var stmt = new Property(idShort: "Stmt", valueType: DataTypeDefXsd.String); + var entity = new Entity( + idShort: "MyEntity", + entityType: EntityType.CoManagedEntity, + statements: [stmt] + ); + var values = new SemanticBranchNode("http://test/entity", Cardinality.One); + var fillOutCalled = false; + + _sut.FillOut(entity, values, (elements, node, updateIdShort) => + { + fillOutCalled = true; + True(updateIdShort); + }); + + True(fillOutCalled); + } + + [Fact] + public void FillOut_EntityWithNullStatements_DoesNotCallFillOutChildren() + { + var entity = new Entity( + idShort: "MyEntity", + entityType: EntityType.CoManagedEntity, + statements: null + ); + var values = new SemanticBranchNode("http://test/entity", Cardinality.One); + + _sut.FillOut(entity, values, (_, _, _) => Fail("Should not be called")); + } +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/FileHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/FileHandlerTests.cs new file mode 100644 index 00000000..b9a38447 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/FileHandlerTests.cs @@ -0,0 +1,78 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using NSubstitute; + +using static Xunit.Assert; + +using File = AasCore.Aas3_0.File; + +namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class FileHandlerTests +{ + private readonly FileHandler _sut; + private readonly ISemanticIdResolver _resolver; + + public FileHandlerTests() + { + _resolver = Substitute.For(); + _sut = new FileHandler(_resolver); + } + + [Fact] + public void CanHandle_File_ReturnsTrue() + { + var file = new File(contentType: "image/png", idShort: "Test"); + + True(_sut.CanHandle(file)); + } + + [Fact] + public void CanHandle_NonFile_ReturnsFalse() + { + var property = new Property(idShort: "Test", valueType: DataTypeDefXsd.String); + + False(_sut.CanHandle(property)); + } + + [Fact] + public void Extract_ReturnsLeafNode() + { + var file = new File(contentType: "image/png", idShort: "Thumbnail"); + _resolver.ResolveElementSemanticId(file, "Thumbnail").Returns("http://test/thumbnail"); + _resolver.GetValueType(file).Returns(DataType.String); + _resolver.GetCardinality(file).Returns(Cardinality.ZeroToOne); + + var result = _sut.Extract(file, _ => null); + + var leaf = IsType(result); + Equal("http://test/thumbnail", leaf.SemanticId); + Equal(DataType.String, leaf.DataType); + } + + [Fact] + public void FillOut_WithLeafNode_SetsFileValue() + { + var file = new File(contentType: "image/png", idShort: "Thumbnail", value: ""); + var values = new SemanticLeafNode("http://test/thumbnail", "https://localhost/image.png", DataType.String, Cardinality.One); + + _sut.FillOut(file, values, (_, _, _) => { }); + + Equal("https://localhost/image.png", file.Value); + } + + [Fact] + public void FillOut_WithBranchNode_DoesNotModifyValue() + { + var file = new File(contentType: "image/png", idShort: "Thumbnail", value: "original"); + var values = new SemanticBranchNode("http://test/thumbnail", Cardinality.One); + + _sut.FillOut(file, values, (_, _, _) => { }); + + Equal("original", file.Value); + } +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ListHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ListHandlerTests.cs new file mode 100644 index 00000000..4fca5e9a --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ListHandlerTests.cs @@ -0,0 +1,122 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using Microsoft.Extensions.Logging; + +using NSubstitute; + +using static Xunit.Assert; + +namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class ListHandlerTests +{ + private readonly ListHandler _sut; + private readonly ISemanticIdResolver _resolver; + private readonly ILogger _logger; + + public ListHandlerTests() + { + _resolver = Substitute.For(); + _logger = Substitute.For>(); + _sut = new ListHandler(_resolver, _logger); + } + + [Fact] + public void CanHandle_SubmodelElementList_ReturnsTrue() + { + var list = new SubmodelElementList(idShort: "Test", typeValueListElement: AasSubmodelElements.Property); + + True(_sut.CanHandle(list)); + } + + [Fact] + public void CanHandle_NonList_ReturnsFalse() + { + var property = new Property(idShort: "Test", valueType: DataTypeDefXsd.String); + + False(_sut.CanHandle(property)); + } + + [Fact] + public void Extract_WithChildren_ReturnsBranchNodeWithChildren() + { + var child = new Property(idShort: "Item", valueType: DataTypeDefXsd.String); + var list = new SubmodelElementList( + idShort: "MyList", + typeValueListElement: AasSubmodelElements.Property, + value: [child] + ); + _resolver.ResolveElementSemanticId(list, "MyList").Returns("http://test/list"); + _resolver.GetCardinality(list).Returns(Cardinality.ZeroToMany); + + var childNode = new SemanticLeafNode("http://test/item", "", DataType.String, Cardinality.One); + + var result = _sut.Extract(list, _ => childNode); + + var branch = IsType(result); + Equal("http://test/list", branch.SemanticId); + Single(branch.Children); + } + + [Fact] + public void Extract_WithNullValue_LogsWarningAndReturnsEmptyBranch() + { + var list = new SubmodelElementList( + idShort: "EmptyList", + typeValueListElement: AasSubmodelElements.Property, + value: null + ); + _resolver.ResolveElementSemanticId(list, "EmptyList").Returns("http://test/empty"); + _resolver.GetCardinality(list).Returns(Cardinality.Unknown); + + var result = _sut.Extract(list, _ => null); + + var branch = IsType(result); + Empty(branch.Children); + _logger.Received(1).Log( + LogLevel.Warning, + Arg.Any(), + Arg.Is(state => state.ToString()!.Contains("No elements defined in SubmodelElementList EmptyList")), + null, + Arg.Any>()! + ); + } + + [Fact] + public void FillOut_WithChildren_DelegatesToFillOutChildren() + { + var child = new Property(idShort: "Item", valueType: DataTypeDefXsd.String); + var list = new SubmodelElementList( + idShort: "List", + typeValueListElement: AasSubmodelElements.Property, + value: [child] + ); + var values = new SemanticBranchNode("http://test/list", Cardinality.One); + var fillOutCalled = false; + + _sut.FillOut(list, values, (elements, node, updateIdShort) => + { + fillOutCalled = true; + False(updateIdShort); + }); + + True(fillOutCalled); + } + + [Fact] + public void FillOut_WithNullValue_DoesNotCallFillOutChildren() + { + var list = new SubmodelElementList( + idShort: "List", + typeValueListElement: AasSubmodelElements.Property, + value: null + ); + var values = new SemanticBranchNode("http://test/list", Cardinality.One); + + _sut.FillOut(list, values, (_, _, _) => Fail("Should not be called")); + } +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/MultiLanguagePropertyHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/MultiLanguagePropertyHandlerTests.cs new file mode 100644 index 00000000..70fddbc4 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/MultiLanguagePropertyHandlerTests.cs @@ -0,0 +1,156 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using Microsoft.Extensions.Logging; + +using NSubstitute; + +using static Xunit.Assert; + +namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class MultiLanguagePropertyHandlerTests +{ + private readonly MultiLanguagePropertyHandler _sut; + private readonly ISemanticIdResolver _resolver; + private readonly ISubmodelElementHelper _elementHelper; + private readonly ILogger _logger; + + public MultiLanguagePropertyHandlerTests() + { + _resolver = Substitute.For(); + _elementHelper = Substitute.For(); + _logger = Substitute.For>(); + _sut = new MultiLanguagePropertyHandler(_resolver, _elementHelper, _logger); + } + + [Fact] + public void CanHandle_MultiLanguageProperty_ReturnsTrue() + { + var mlp = new MultiLanguageProperty(idShort: "Test"); + + True(_sut.CanHandle(mlp)); + } + + [Fact] + public void CanHandle_NonMlp_ReturnsFalse() + { + var property = new Property(idShort: "Test", valueType: DataTypeDefXsd.String); + + False(_sut.CanHandle(property)); + } + + [Fact] + public void Extract_WithLanguages_ReturnsBranchWithLanguageLeaves() + { + var mlp = new MultiLanguageProperty( + idShort: "ManufacturerName", + value: [new LangStringTextType("en", ""), new LangStringTextType("de", "")] + ); + _resolver.ExtractSemanticId(mlp).Returns("http://test/manufacturer-name"); + _resolver.GetCardinality(mlp).Returns(Cardinality.One); + _resolver.MlpPostFixSeparator.Returns("_"); + _elementHelper.ResolveLanguages(mlp).Returns(["en", "de"]); + + var result = _sut.Extract(mlp, _ => null); + + var branch = IsType(result); + Equal("http://test/manufacturer-name", branch.SemanticId); + Equal(2, branch.Children.Count); + var semanticIds = branch.Children.Select(c => c.SemanticId).OrderBy(s => s).ToList(); + Contains("http://test/manufacturer-name_de", semanticIds); + Contains("http://test/manufacturer-name_en", semanticIds); + } + + [Fact] + public void Extract_WithNoLanguages_ReturnsEmptyBranchAndLogsInfo() + { + var mlp = new MultiLanguageProperty(idShort: "EmptyMlp", value: null); + _resolver.ExtractSemanticId(mlp).Returns("http://test/empty"); + _resolver.GetCardinality(mlp).Returns(Cardinality.Unknown); + _resolver.MlpPostFixSeparator.Returns("_"); + _elementHelper.ResolveLanguages(mlp).Returns([]); + + var result = _sut.Extract(mlp, _ => null); + + var branch = IsType(result); + Empty(branch.Children); + _logger.Received(1).Log( + LogLevel.Information, + Arg.Any(), + Arg.Is(state => state.ToString()!.Contains("No languages defined")), + null, + Arg.Any>()! + ); + } + + [Fact] + public void FillOut_WithMatchingLeafNodes_SetsLanguageValues() + { + var mlp = new MultiLanguageProperty( + idShort: "MfName", + value: [new LangStringTextType("en", ""), new LangStringTextType("de", "")] + ); + _resolver.ExtractSemanticId(mlp).Returns("http://test/mfname"); + _resolver.MlpPostFixSeparator.Returns("_"); + _elementHelper.ResolveLanguages(mlp).Returns(["en", "de"]); + + var valueNode = new SemanticBranchNode("http://test/mfname", Cardinality.One); + valueNode.AddChild(new SemanticLeafNode("http://test/mfname_en", "English Value", DataType.String, Cardinality.One)); + valueNode.AddChild(new SemanticLeafNode("http://test/mfname_de", "German Value", DataType.String, Cardinality.One)); + + _sut.FillOut(mlp, valueNode, (_, _, _) => { }); + + Equal("English Value", mlp.Value!.First(v => v.Language == "en").Text); + Equal("German Value", mlp.Value!.First(v => v.Language == "de").Text); + } + + [Fact] + public void FillOut_WithNoMatchingValueNode_LogsInfo() + { + var mlp = new MultiLanguageProperty(idShort: "MfName"); + _resolver.ExtractSemanticId(mlp).Returns("http://test/mfname"); + var nonMatchingNode = new SemanticBranchNode("http://test/other", Cardinality.One); + + _sut.FillOut(mlp, nonMatchingNode, (_, _, _) => { }); + + _logger.Received(1).Log( + LogLevel.Information, + Arg.Any(), + Arg.Is(state => state.ToString()!.Contains("No value node found")), + null, + Arg.Any>()! + ); + } + + [Fact] + public void FillOut_WithNewDefaultLanguage_AddsLanguageAndLogsInfo() + { + var mlp = new MultiLanguageProperty( + idShort: "MfName", + value: [new LangStringTextType("en", "")] + ); + _resolver.ExtractSemanticId(mlp).Returns("http://test/mfname"); + _resolver.MlpPostFixSeparator.Returns("_"); + _elementHelper.ResolveLanguages(mlp).Returns(["en", "fr"]); + + var valueNode = new SemanticBranchNode("http://test/mfname", Cardinality.One); + valueNode.AddChild(new SemanticLeafNode("http://test/mfname_en", "English", DataType.String, Cardinality.One)); + valueNode.AddChild(new SemanticLeafNode("http://test/mfname_fr", "French", DataType.String, Cardinality.One)); + + _sut.FillOut(mlp, valueNode, (_, _, _) => { }); + + Equal(2, mlp.Value!.Count); + Equal("French", mlp.Value.First(v => v.Language == "fr").Text); + _logger.Received(1).Log( + LogLevel.Information, + Arg.Any(), + Arg.Is(state => state.ToString()!.Contains("Added language 'fr'")), + null, + Arg.Any>()! + ); + } +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/PropertyHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/PropertyHandlerTests.cs new file mode 100644 index 00000000..f19467ba --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/PropertyHandlerTests.cs @@ -0,0 +1,77 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using NSubstitute; + +using static Xunit.Assert; + +namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class PropertyHandlerTests +{ + private readonly PropertyHandler _sut; + private readonly ISemanticIdResolver _resolver; + + public PropertyHandlerTests() + { + _resolver = Substitute.For(); + _sut = new PropertyHandler(_resolver); + } + + [Fact] + public void CanHandle_Property_ReturnsTrue() + { + var property = new Property(idShort: "Test", valueType: DataTypeDefXsd.String); + + True(_sut.CanHandle(property)); + } + + [Fact] + public void CanHandle_NonProperty_ReturnsFalse() + { + var collection = new SubmodelElementCollection(idShort: "Test"); + + False(_sut.CanHandle(collection)); + } + + [Fact] + public void Extract_ReturnsLeafNodeWithSemanticIdAndType() + { + var property = new Property(idShort: "MyProp", valueType: DataTypeDefXsd.String, value: "test"); + _resolver.ResolveElementSemanticId(property, "MyProp").Returns("http://test/my-prop"); + _resolver.GetValueType(property).Returns(DataType.String); + _resolver.GetCardinality(property).Returns(Cardinality.One); + + var result = _sut.Extract(property, _ => null); + + var leaf = IsType(result); + Equal("http://test/my-prop", leaf.SemanticId); + Equal(DataType.String, leaf.DataType); + Equal(Cardinality.One, leaf.Cardinality); + } + + [Fact] + public void FillOut_WithLeafNode_SetsPropertyValue() + { + var property = new Property(idShort: "MyProp", valueType: DataTypeDefXsd.String, value: ""); + var values = new SemanticLeafNode("http://test/my-prop", "NewValue", DataType.String, Cardinality.One); + + _sut.FillOut(property, values, (_, _, _) => { }); + + Equal("NewValue", property.Value); + } + + [Fact] + public void FillOut_WithBranchNode_DoesNotModifyPropertyValue() + { + var property = new Property(idShort: "MyProp", valueType: DataTypeDefXsd.String, value: "original"); + var values = new SemanticBranchNode("http://test/my-prop", Cardinality.One); + + _sut.FillOut(property, values, (_, _, _) => { }); + + Equal("original", property.Value); + } +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RangeHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RangeHandlerTests.cs new file mode 100644 index 00000000..bf9ebb88 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RangeHandlerTests.cs @@ -0,0 +1,102 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using NSubstitute; + +using static Xunit.Assert; + +using Range = AasCore.Aas3_0.Range; + +namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class RangeHandlerTests +{ + private readonly RangeHandler _sut; + private readonly ISemanticIdResolver _resolver; + + public RangeHandlerTests() + { + _resolver = Substitute.For(); + _sut = new RangeHandler(_resolver); + } + + [Fact] + public void CanHandle_Range_ReturnsTrue() + { + var range = new Range(valueType: DataTypeDefXsd.Double, idShort: "Test"); + + True(_sut.CanHandle(range)); + } + + [Fact] + public void CanHandle_NonRange_ReturnsFalse() + { + var property = new Property(idShort: "Test", valueType: DataTypeDefXsd.String); + + False(_sut.CanHandle(property)); + } + + [Fact] + public void Extract_ReturnsBranchWithMinAndMaxLeaves() + { + var range = new Range(valueType: DataTypeDefXsd.Double, idShort: "TestRange"); + _resolver.ExtractSemanticId(range).Returns("http://test/range"); + _resolver.GetValueType(range).Returns(DataType.Number); + _resolver.GetCardinality(range).Returns(Cardinality.One); + + var result = _sut.Extract(range, _ => null); + + var branch = IsType(result); + Equal("http://test/range", branch.SemanticId); + Equal(2, branch.Children.Count); + var minLeaf = IsType(branch.Children[0]); + Equal("http://test/range" + SemanticIdResolver.RangeMinimumPostFixSeparator, minLeaf.SemanticId); + Equal(DataType.Number, minLeaf.DataType); + var maxLeaf = IsType(branch.Children[1]); + Equal("http://test/range" + SemanticIdResolver.RangeMaximumPostFixSeparator, maxLeaf.SemanticId); + Equal(DataType.Number, maxLeaf.DataType); + } + + [Fact] + public void FillOut_WithBranchNode_SetsMinAndMax() + { + var range = new Range(valueType: DataTypeDefXsd.Double, idShort: "TestRange"); + var branchNode = new SemanticBranchNode("http://test/range", Cardinality.One); + branchNode.AddChild(new SemanticLeafNode("http://test/range_min", "10.5", DataType.Number, Cardinality.One)); + branchNode.AddChild(new SemanticLeafNode("http://test/range_max", "99.9", DataType.Number, Cardinality.One)); + + _sut.FillOut(range, branchNode, (_, _, _) => { }); + + Equal("10.5", range.Min); + Equal("99.9", range.Max); + } + + [Fact] + public void FillOut_WithLeafNode_DoesNotSetMinMax() + { + var range = new Range(valueType: DataTypeDefXsd.Double, idShort: "TestRange", min: "0", max: "100"); + var leafNode = new SemanticLeafNode("http://test/range", "val", DataType.Number, Cardinality.One); + + _sut.FillOut(range, leafNode, (_, _, _) => { }); + + Equal("0", range.Min); + Equal("100", range.Max); + } + + [Fact] + public void FillOut_WithMissingMinLeaf_SetsMinToNull() + { + var range = new Range(valueType: DataTypeDefXsd.Double, idShort: "TestRange"); + var branchNode = new SemanticBranchNode("http://test/range", Cardinality.One); + branchNode.AddChild(new SemanticLeafNode("http://test/range_max", "99.9", DataType.Number, Cardinality.One)); + + _sut.FillOut(range, branchNode, (_, _, _) => { }); + + Null(range.Min); + Equal("99.9", range.Max); + } +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ReferenceElementHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ReferenceElementHandlerTests.cs new file mode 100644 index 00000000..e56a58d0 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ReferenceElementHandlerTests.cs @@ -0,0 +1,138 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using Microsoft.Extensions.Logging; + +using NSubstitute; + +using static Xunit.Assert; + +namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class ReferenceElementHandlerTests +{ + private readonly ReferenceElementHandler _sut; + private readonly ISemanticIdResolver _resolver; + private readonly IReferenceHelper _referenceHelper; + private readonly ILogger _logger; + + public ReferenceElementHandlerTests() + { + _resolver = Substitute.For(); + _referenceHelper = Substitute.For(); + _logger = Substitute.For>(); + _sut = new ReferenceElementHandler(_resolver, _referenceHelper, _logger); + } + + [Fact] + public void CanHandle_ReferenceElement_ReturnsTrue() + { + var refElement = new ReferenceElement(idShort: "Test"); + + True(_sut.CanHandle(refElement)); + } + + [Fact] + public void CanHandle_NonReferenceElement_ReturnsFalse() + { + var property = new Property(idShort: "Test", valueType: DataTypeDefXsd.String); + + False(_sut.CanHandle(property)); + } + + [Fact] + public void Extract_WithNullValue_ReturnsNull() + { + var refElement = new ReferenceElement(idShort: "Test", value: null); + + var result = _sut.Extract(refElement, _ => null); + + Null(result); + } + + [Fact] + public void Extract_WithExternalReference_ReturnsNull() + { + var refElement = new ReferenceElement( + idShort: "Test", + value: new Reference(ReferenceTypes.ExternalReference, + [new Key(KeyTypes.GlobalReference, "http://external")]) + ); + + var result = _sut.Extract(refElement, _ => null); + + Null(result); + } + + [Fact] + public void Extract_WithModelReference_DelegatesToReferenceHelper() + { + var modelRef = new Reference(ReferenceTypes.ModelReference, + [new Key(KeyTypes.Submodel, "http://submodel")]); + var refElement = new ReferenceElement(idShort: "Test", value: modelRef); + _resolver.ResolveElementSemanticId(refElement, "Test").Returns("http://test/ref"); + _resolver.GetCardinality(refElement).Returns(Cardinality.One); + + var expectedNode = new SemanticBranchNode("http://test/ref", Cardinality.One); + _referenceHelper.ExtractReferenceKeys(modelRef, "http://test/ref", Cardinality.One).Returns(expectedNode); + + var result = _sut.Extract(refElement, _ => null); + + Same(expectedNode, result); + } + + [Fact] + public void FillOut_WithModelReference_DelegatesToReferenceHelper() + { + var modelRef = new Reference(ReferenceTypes.ModelReference, + [new Key(KeyTypes.Submodel, "")]); + var refElement = new ReferenceElement(idShort: "Test", value: modelRef); + _resolver.GetSemanticId(refElement).Returns("http://test/ref"); + + var values = new SemanticBranchNode("http://test/ref", Cardinality.One); + + _sut.FillOut(refElement, values, (_, _, _) => { }); + + _referenceHelper.Received(1).PopulateReferenceKeys(modelRef, values, "http://test/ref"); + } + + [Fact] + public void FillOut_WithNullValue_LogsInfoAndSkips() + { + var refElement = new ReferenceElement(idShort: "Test", value: null); + _resolver.GetSemanticId(refElement).Returns("http://test/ref"); + + var values = new SemanticBranchNode("http://test/ref", Cardinality.One); + + _sut.FillOut(refElement, values, (_, _, _) => { }); + + _referenceHelper.DidNotReceive().PopulateReferenceKeys( + Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public void FillOut_WithExternalReference_LogsInfoAndSkips() + { + var externalRef = new Reference(ReferenceTypes.ExternalReference, + [new Key(KeyTypes.GlobalReference, "http://external")]); + var refElement = new ReferenceElement(idShort: "Test", value: externalRef); + _resolver.GetSemanticId(refElement).Returns("http://test/ref"); + + var values = new SemanticBranchNode("http://test/ref", Cardinality.One); + + _sut.FillOut(refElement, values, (_, _, _) => { }); + + _referenceHelper.DidNotReceive().PopulateReferenceKeys( + Arg.Any(), Arg.Any(), Arg.Any()); + _logger.Received(1).Log( + LogLevel.Information, + Arg.Any(), + Arg.Is(state => state.ToString()!.Contains("does not contain a ModelReference")), + null, + Arg.Any>()! + ); + } +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RelationshipElementHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RelationshipElementHandlerTests.cs new file mode 100644 index 00000000..27f40c61 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RelationshipElementHandlerTests.cs @@ -0,0 +1,131 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using NSubstitute; + +using static Xunit.Assert; + +namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class RelationshipElementHandlerTests +{ + private readonly RelationshipElementHandler _sut; + private readonly ISemanticIdResolver _resolver; + private readonly IReferenceHelper _referenceHelper; + + public RelationshipElementHandlerTests() + { + _resolver = Substitute.For(); + _referenceHelper = Substitute.For(); + _sut = new RelationshipElementHandler(_resolver, _referenceHelper); + } + + [Fact] + public void CanHandle_RelationshipElement_ReturnsTrue() + { + var rel = new RelationshipElement( + first: new Reference(ReferenceTypes.ExternalReference, [new Key(KeyTypes.GlobalReference, "a")]), + second: new Reference(ReferenceTypes.ExternalReference, [new Key(KeyTypes.GlobalReference, "b")]), + idShort: "Test" + ); + + True(_sut.CanHandle(rel)); + } + + [Fact] + public void CanHandle_NonRelationshipElement_ReturnsFalse() + { + var property = new Property(idShort: "Test", valueType: DataTypeDefXsd.String); + + False(_sut.CanHandle(property)); + } + + [Fact] + public void Extract_BothExternalReferences_ReturnsNull() + { + var rel = new RelationshipElement( + first: new Reference(ReferenceTypes.ExternalReference, [new Key(KeyTypes.GlobalReference, "a")]), + second: new Reference(ReferenceTypes.ExternalReference, [new Key(KeyTypes.GlobalReference, "b")]), + idShort: "Test" + ); + + var result = _sut.Extract(rel, _ => null); + + Null(result); + } + + [Fact] + public void Extract_FirstModelReference_ExtractsFirstAndDelegatesToReferenceHelper() + { + var firstRef = new Reference(ReferenceTypes.ModelReference, [new Key(KeyTypes.Submodel, "sub")]); + var secondRef = new Reference(ReferenceTypes.ExternalReference, [new Key(KeyTypes.GlobalReference, "ext")]); + var rel = new RelationshipElement(first: firstRef, second: secondRef, idShort: "Test"); + _resolver.GetSemanticId(rel).Returns("http://test/rel"); + _resolver.GetCardinality(rel).Returns(Cardinality.One); + + var firstNode = new SemanticBranchNode("http://test/rel_first", Cardinality.One); + _referenceHelper.ExtractReferenceKeys( + firstRef, + "http://test/rel" + SemanticIdResolver.RelationshipElementFirstPostFixSeparator, + Cardinality.One + ).Returns(firstNode); + + var result = _sut.Extract(rel, _ => null); + + var branch = IsType(result); + Equal("http://test/rel", branch.SemanticId); + Single(branch.Children); + Same(firstNode, branch.Children[0]); + } + + [Fact] + public void Extract_BothModelReferences_ExtractsBoth() + { + var firstRef = new Reference(ReferenceTypes.ModelReference, [new Key(KeyTypes.Submodel, "sub1")]); + var secondRef = new Reference(ReferenceTypes.ModelReference, [new Key(KeyTypes.Submodel, "sub2")]); + var rel = new RelationshipElement(first: firstRef, second: secondRef, idShort: "Test"); + _resolver.GetSemanticId(rel).Returns("http://test/rel"); + _resolver.GetCardinality(rel).Returns(Cardinality.One); + + var firstNode = new SemanticBranchNode("http://test/rel_first", Cardinality.One); + var secondNode = new SemanticBranchNode("http://test/rel_second", Cardinality.One); + _referenceHelper.ExtractReferenceKeys( + firstRef, + "http://test/rel" + SemanticIdResolver.RelationshipElementFirstPostFixSeparator, + Cardinality.One + ).Returns(firstNode); + _referenceHelper.ExtractReferenceKeys( + secondRef, + "http://test/rel" + SemanticIdResolver.RelationshipElementSecondPostFixSeparator, + Cardinality.One + ).Returns(secondNode); + + var result = _sut.Extract(rel, _ => null); + + var branch = IsType(result); + Equal(2, branch.Children.Count); + } + + [Fact] + public void FillOut_DelegatesToReferenceHelperForBothReferences() + { + var firstRef = new Reference(ReferenceTypes.ModelReference, [new Key(KeyTypes.Submodel, "")]); + var secondRef = new Reference(ReferenceTypes.ModelReference, [new Key(KeyTypes.Submodel, "")]); + var rel = new RelationshipElement(first: firstRef, second: secondRef, idShort: "Test"); + + var values = new SemanticBranchNode("http://test/rel", Cardinality.One); + + _sut.FillOut(rel, values, (_, _, _) => { }); + + _referenceHelper.Received(1).PopulateRelationshipReference( + firstRef, values, "http://test/rel", + SemanticIdResolver.RelationshipElementFirstPostFixSeparator); + _referenceHelper.Received(1).PopulateRelationshipReference( + secondRef, values, "http://test/rel", + SemanticIdResolver.RelationshipElementSecondPostFixSeparator); + } +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs new file mode 100644 index 00000000..b81680ef --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs @@ -0,0 +1,186 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Extraction; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using Microsoft.Extensions.Logging; + +using NSubstitute; + +using static Xunit.Assert; + +namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.Extraction; + +public class SemanticTreeExtractorTests +{ + private readonly SemanticTreeExtractor _sut; + private readonly ISemanticIdResolver _resolver; + private readonly ISubmodelElementHelper _elementHelper; + private readonly ILogger _logger; + private readonly List _handlers; + + public SemanticTreeExtractorTests() + { + _resolver = Substitute.For(); + _elementHelper = Substitute.For(); + _logger = Substitute.For>(); + _handlers = []; + _sut = new SemanticTreeExtractor(_resolver, _elementHelper, _handlers, _logger); + } + + [Fact] + public void Extract_NullSubmodel_ThrowsArgumentNullException() + { + Throws(() => _sut.Extract(null!)); + } + + [Fact] + public void Extract_SubmodelWithNoElements_ReturnsRootNodeWithNoChildren() + { + var submodel = Substitute.For(); + submodel.IdShort.Returns("TestSubmodel"); + submodel.SubmodelElements.Returns(new List()); + _resolver.ResolveSemanticId(submodel, "TestSubmodel").Returns("http://test/root"); + + var result = _sut.Extract(submodel) as SemanticBranchNode; + + NotNull(result); + Equal("http://test/root", result!.SemanticId); + Empty(result.Children); + } + + [Fact] + public void Extract_SubmodelWithElements_DelegatesToHandlers() + { + var property = new Property(idShort: "Prop", valueType: DataTypeDefXsd.String); + var submodel = Substitute.For(); + submodel.IdShort.Returns("Test"); + submodel.SubmodelElements.Returns(new List { property }); + _resolver.ResolveSemanticId(submodel, "Test").Returns("http://test/root"); + + var handler = Substitute.For(); + handler.CanHandle(property).Returns(true); + var expectedNode = new SemanticLeafNode("http://test/prop", "", DataType.String, Cardinality.One); + handler.Extract(property, Arg.Any>()).Returns(expectedNode); + _handlers.Add(handler); + + var result = _sut.Extract(submodel) as SemanticBranchNode; + + NotNull(result); + Single(result!.Children); + Same(expectedNode, result.Children[0]); + } + + [Fact] + public void Extract_ElementWithNoHandler_CreatesLeafNodeFallback() + { + var element = Substitute.For(); + element.IdShort.Returns("UnknownElement"); + var submodel = Substitute.For(); + submodel.IdShort.Returns("Test"); + submodel.SubmodelElements.Returns(new List { element }); + _resolver.ResolveSemanticId(submodel, "Test").Returns("http://test/root"); + _resolver.ResolveElementSemanticId(element, "UnknownElement").Returns("http://test/unknown"); + _resolver.GetValueType(element).Returns(DataType.Unknown); + _resolver.GetCardinality(element).Returns(Cardinality.Unknown); + + var result = _sut.Extract(submodel) as SemanticBranchNode; + + NotNull(result); + Single(result!.Children); + var leaf = IsType(result.Children[0]); + Equal("http://test/unknown", leaf.SemanticId); + Equal(DataType.Unknown, leaf.DataType); + } + + [Fact] + public void Extract_ByIdShortPath_NullSubmodel_ThrowsArgumentNullException() + { + Throws(() => _sut.Extract(null!, "path")); + } + + [Fact] + public void Extract_ByIdShortPath_NullPath_ThrowsArgumentNullException() + { + var submodel = Substitute.For(); + Throws(() => _sut.Extract(submodel, null!)); + } + + [Fact] + public void Extract_ByIdShortPath_SingleSegment_ReturnsMatchingElement() + { + var property = new Property(idShort: "MyProp", valueType: DataTypeDefXsd.String, value: "test"); + var submodel = Substitute.For(); + submodel.SubmodelElements.Returns(new List { property }); + _elementHelper.GetElementByIdShort(Arg.Any>(), "MyProp").Returns(property); + + var result = _sut.Extract(submodel, "MyProp"); + + Same(property, result); + } + + [Fact] + public void Extract_ByIdShortPath_NestedPath_ReturnsNestedElement() + { + var childProp = new Property(idShort: "ChildProp", valueType: DataTypeDefXsd.String); + var collection = new SubmodelElementCollection(idShort: "Parent", value: [childProp]); + var submodel = Substitute.For(); + submodel.SubmodelElements.Returns(new List { collection }); + _elementHelper.GetElementByIdShort(Arg.Any>(), "Parent").Returns(collection); + _elementHelper.GetChildElements(collection).Returns(collection.Value); + _elementHelper.GetElementByIdShort(collection.Value, "ChildProp").Returns(childProp); + + var result = _sut.Extract(submodel, "Parent.ChildProp"); + + Same(childProp, result); + } + + [Fact] + public void Extract_ByIdShortPath_ElementNotFound_ThrowsException() + { + var submodel = Substitute.For(); + submodel.SubmodelElements.Returns(new List()); + _elementHelper.GetElementByIdShort(Arg.Any>(), "NonExistent").Returns((ISubmodelElement?)null); + + Throws(() => _sut.Extract(submodel, "NonExistent")); + } + + [Fact] + public void Extract_ByIdShortPath_ChildElementsNull_ThrowsException() + { + var property = new Property(idShort: "Prop", valueType: DataTypeDefXsd.String); + var submodel = Substitute.For(); + submodel.SubmodelElements.Returns(new List { property }); + _elementHelper.GetElementByIdShort(Arg.Any>(), "Prop").Returns(property); + _elementHelper.GetChildElements(property).Returns((IList?)null); + + Throws(() => _sut.Extract(submodel, "Prop.Child")); + } + + [Fact] + public void ExtractElement_NullElement_ThrowsArgumentNullException() + { + Throws(() => _sut.ExtractElement(null!)); + } + + [Fact] + public void ExtractElement_HandlerReturnsNull_CreatesFallbackLeaf() + { + var element = Substitute.For(); + element.IdShort.Returns("Test"); + _resolver.ResolveElementSemanticId(element, "Test").Returns("http://test/element"); + _resolver.GetValueType(element).Returns(DataType.String); + _resolver.GetCardinality(element).Returns(Cardinality.ZeroToOne); + + var result = _sut.ExtractElement(element); + + NotNull(result); + var leaf = IsType(result); + Equal("http://test/element", leaf.SemanticId); + Equal(DataType.String, leaf.DataType); + Equal(Cardinality.ZeroToOne, leaf.Cardinality); + } +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFillerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFillerTests.cs new file mode 100644 index 00000000..b42dd023 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFillerTests.cs @@ -0,0 +1,131 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.FillOut; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using Microsoft.Extensions.Logging; + +using NSubstitute; + +using static Xunit.Assert; + +namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.FillOut; + +public class SubmodelFillerTests +{ + private readonly SubmodelFiller _sut; + private readonly ISemanticIdResolver _resolver; + private readonly ISubmodelElementHelper _elementHelper; + private readonly ILogger _logger; + private readonly List _handlers; + + public SubmodelFillerTests() + { + _resolver = Substitute.For(); + _elementHelper = Substitute.For(); + _logger = Substitute.For>(); + _handlers = []; + _sut = new SubmodelFiller(_resolver, _elementHelper, _handlers, _logger); + } + + [Fact] + public void FillOutTemplate_NullSubmodel_ThrowsArgumentNullException() + { + var values = new SemanticBranchNode("root", Cardinality.Unknown); + + Throws(() => _sut.FillOutTemplate(null!, values)); + } + + [Fact] + public void FillOutTemplate_NullValues_ThrowsArgumentNullException() + { + var submodel = Substitute.For(); + submodel.SubmodelElements.Returns(new List()); + + Throws(() => _sut.FillOutTemplate(submodel, null!)); + } + + [Fact] + public void FillOutTemplate_NullSubmodelElements_ThrowsArgumentNullException() + { + var submodel = Substitute.For(); + submodel.SubmodelElements.Returns((List?)null); + var values = new SemanticBranchNode("root", Cardinality.Unknown); + + Throws(() => _sut.FillOutTemplate(submodel, values)); + } + + [Fact] + public void FillOutTemplate_NoMatchingNodes_PreservesElements() + { + var property = new Property(idShort: "Prop", valueType: DataTypeDefXsd.String); + var submodel = Substitute.For(); + var elements = new List { property }; + submodel.SubmodelElements.Returns(elements); + _resolver.ExtractSemanticId(property).Returns("http://test/prop"); + + var values = new SemanticBranchNode("root", Cardinality.Unknown); + + _sut.FillOutTemplate(submodel, values); + + Single(elements); + } + + [Fact] + public void FillOutElement_NullElement_ThrowsArgumentNullException() + { + var values = new SemanticLeafNode("test", "val", DataType.String, Cardinality.One); + + Throws(() => _sut.FillOutElement(null!, values)); + } + + [Fact] + public void FillOutElement_NullValues_ThrowsArgumentNullException() + { + var element = new Property(idShort: "Prop", valueType: DataTypeDefXsd.String); + + Throws(() => _sut.FillOutElement(element, null!)); + } + + [Fact] + public void FillOutElement_NoMatchingHandler_ThrowsException() + { + var element = new Property(idShort: "Prop", valueType: DataTypeDefXsd.String); + var values = new SemanticLeafNode("test", "val", DataType.String, Cardinality.One); + + var ex = Throws(() => _sut.FillOutElement(element, values)); + Equal("Internal Server Error.", ex.Message); + } + + [Fact] + public void FillOutElement_WithMatchingHandler_DelegatesToHandler() + { + var element = new Property(idShort: "Prop", valueType: DataTypeDefXsd.String); + var values = new SemanticLeafNode("test", "val", DataType.String, Cardinality.One); + + var handler = Substitute.For(); + handler.CanHandle(element).Returns(true); + _handlers.Add(handler); + + _sut.FillOutElement(element, values); + + handler.Received(1).FillOut(element, values, Arg.Any, SemanticTreeNode, bool>>()); + } + + [Fact] + public void FillOutSubmodelElementValue_NoMatchingValueNode_PreservesElements() + { + var property = new Property(idShort: "Prop", valueType: DataTypeDefXsd.String, value: "original"); + var elements = new List { property }; + var values = new SemanticBranchNode("root", Cardinality.Unknown); + _resolver.ExtractSemanticId(property).Returns("http://test/prop"); + + _sut.FillOutSubmodelElementValue(elements, values, false); + + Single(elements); + Equal("original", ((Property)elements[0]).Value); + } +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/ReferenceHelperTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/ReferenceHelperTests.cs new file mode 100644 index 00000000..b8d214ab --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/ReferenceHelperTests.cs @@ -0,0 +1,221 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.Config; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using NSubstitute; + +using static Xunit.Assert; + +namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; + +public class ReferenceHelperTests +{ + private readonly ReferenceHelper _sut; + private readonly ISemanticIdResolver _resolver; + private readonly ILogger _logger; + + public ReferenceHelperTests() + { + var semantics = Options.Create(new Semantics + { + MultiLanguageSemanticPostfixSeparator = "_", + SubmodelElementIndexContextPrefix = "_aastwinengineindex_" + }); + _resolver = new SemanticIdResolver(semantics); + _logger = Substitute.For>(); + _sut = new ReferenceHelper(_resolver, _logger); + } + + [Fact] + public void ExtractReferenceKeys_WithKeys_ReturnsBranchNode() + { + var reference = new Reference( + ReferenceTypes.ModelReference, + [ + new Key(KeyTypes.Submodel, "submodel-value"), + new Key(KeyTypes.Property, "prop-value"), + ] + ); + + var result = _sut.ExtractReferenceKeys(reference, "http://test/ref", Cardinality.One); + + NotNull(result); + Equal("http://test/ref", result!.SemanticId); + Equal(2, result.Children.Count); + var submodelLeaf = IsType(result.Children[0]); + Equal("http://test/ref_Submodel", submodelLeaf.SemanticId); + var propLeaf = IsType(result.Children[1]); + Equal("http://test/ref_Property", propLeaf.SemanticId); + } + + [Fact] + public void ExtractReferenceKeys_WithMultipleSameType_IncludesIndex() + { + var reference = new Reference( + ReferenceTypes.ModelReference, + [ + new Key(KeyTypes.SubmodelElementCollection, "col0"), + new Key(KeyTypes.SubmodelElementCollection, "col1"), + ] + ); + + var result = _sut.ExtractReferenceKeys(reference, "http://test/ref", Cardinality.One); + + NotNull(result); + Equal(2, result!.Children.Count); + var leaf0 = IsType(result.Children[0]); + Equal("http://test/ref_SubmodelElementCollection_0", leaf0.SemanticId); + var leaf1 = IsType(result.Children[1]); + Equal("http://test/ref_SubmodelElementCollection_1", leaf1.SemanticId); + } + + [Fact] + public void ExtractReferenceKeys_EmptyKeys_ReturnsNull() + { + var reference = new Reference(ReferenceTypes.ModelReference, []); + + var result = _sut.ExtractReferenceKeys(reference, "http://test/ref", Cardinality.One); + + Null(result); + } + + [Fact] + public void PopulateReferenceKeys_WithMatchingLeafNodes_UpdatesKeyValues() + { + var reference = new Reference( + ReferenceTypes.ModelReference, + [ + new Key(KeyTypes.Submodel, ""), + new Key(KeyTypes.Property, ""), + ] + ); + + var branchNode = new SemanticBranchNode("http://test/ref", Cardinality.One); + branchNode.AddChild(new SemanticLeafNode("http://test/ref_Submodel", "NewSubmodelValue", DataType.String, Cardinality.One)); + branchNode.AddChild(new SemanticLeafNode("http://test/ref_Property", "NewPropValue", DataType.String, Cardinality.One)); + + _sut.PopulateReferenceKeys(reference, branchNode, "http://test/ref"); + + Equal("NewSubmodelValue", reference.Keys[0].Value); + Equal("NewPropValue", reference.Keys[1].Value); + } + + [Fact] + public void PopulateReferenceKeys_WithNonBranchNode_LogsWarning() + { + var reference = new Reference( + ReferenceTypes.ModelReference, + [new Key(KeyTypes.Submodel, "original")] + ); + var leafNode = new SemanticLeafNode("http://test/ref", "val", DataType.String, Cardinality.One); + + _sut.PopulateReferenceKeys(reference, leafNode, "http://test/ref"); + + Equal("original", reference.Keys[0].Value); + _logger.Received(1).Log( + LogLevel.Warning, + Arg.Any(), + Arg.Is(state => state.ToString()!.Contains("Expected SemanticBranchNode")), + null, + Arg.Any>()! + ); + } + + [Fact] + public void PopulateReferenceKeys_EmptyKeys_LogsInfo() + { + var reference = new Reference(ReferenceTypes.ModelReference, []); + var branchNode = new SemanticBranchNode("http://test/ref", Cardinality.One); + + _sut.PopulateReferenceKeys(reference, branchNode, "http://test/ref"); + + _logger.Received(1).Log( + LogLevel.Information, + Arg.Any(), + Arg.Is(state => state.ToString()!.Contains("has no keys")), + null, + Arg.Any>()! + ); + } + + [Fact] + public void PopulateReferenceKeys_MissingLeafNode_LogsWarning() + { + var reference = new Reference( + ReferenceTypes.ModelReference, + [new Key(KeyTypes.Submodel, "original")] + ); + var branchNode = new SemanticBranchNode("http://test/ref", Cardinality.One); + + _sut.PopulateReferenceKeys(reference, branchNode, "http://test/ref"); + + Equal("original", reference.Keys[0].Value); + _logger.Received(1).Log( + LogLevel.Warning, + Arg.Any(), + Arg.Is(state => state.ToString()!.Contains("No matching leaf node")), + null, + Arg.Any>()! + ); + } + + [Fact] + public void PopulateRelationshipReference_ExternalReference_DoesNotModify() + { + var reference = new Reference( + ReferenceTypes.ExternalReference, + [new Key(KeyTypes.GlobalReference, "original")] + ); + var tree = new SemanticBranchNode("http://test", Cardinality.One); + + _sut.PopulateRelationshipReference(reference, tree, "http://test", "_first"); + + Equal("original", reference.Keys[0].Value); + } + + [Fact] + public void PopulateRelationshipReference_ModelReference_WithMatchingNode_PopulatesKeys() + { + var reference = new Reference( + ReferenceTypes.ModelReference, + [new Key(KeyTypes.Submodel, "")] + ); + + var firstBranch = new SemanticBranchNode("http://test_first", Cardinality.One); + firstBranch.AddChild(new SemanticLeafNode("http://test_first_Submodel", "NewValue", DataType.String, Cardinality.One)); + + var tree = new SemanticBranchNode("http://test", Cardinality.One); + tree.AddChild(firstBranch); + + _sut.PopulateRelationshipReference(reference, tree, "http://test", "_first"); + + Equal("NewValue", reference.Keys[0].Value); + } + + [Fact] + public void PopulateRelationshipReference_ModelReference_NoMatchingNode_LogsWarning() + { + var reference = new Reference( + ReferenceTypes.ModelReference, + [new Key(KeyTypes.Submodel, "original")] + ); + var tree = new SemanticBranchNode("http://test", Cardinality.One); + + _sut.PopulateRelationshipReference(reference, tree, "http://test", "_first"); + + Equal("original", reference.Keys[0].Value); + _logger.Received(1).Log( + LogLevel.Warning, + Arg.Any(), + Arg.Is(state => state.ToString()!.Contains("No matching node")), + null, + Arg.Any>()! + ); + } +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolverTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolverTests.cs new file mode 100644 index 00000000..8b6acd7b --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolverTests.cs @@ -0,0 +1,303 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.Config; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using Microsoft.Extensions.Options; + +using NSubstitute; + +using static Xunit.Assert; + +using File = AasCore.Aas3_0.File; +using Range = AasCore.Aas3_0.Range; + +namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; + +public class SemanticIdResolverTests +{ + private readonly SemanticIdResolver _sut; + private readonly IOptions _semantics; + + public SemanticIdResolverTests() + { + _semantics = Options.Create(new Semantics + { + MultiLanguageSemanticPostfixSeparator = "_", + SubmodelElementIndexContextPrefix = "_aastwinengineindex_", + InternalSemanticId = "InternalSemanticId" + }); + _sut = new SemanticIdResolver(_semantics); + } + + [Fact] + public void Constructor_NullOptions_ThrowsException() + { + var options = Options.Create(null!); + + _ = Throws(() => new SemanticIdResolver(options)); + } + + [Fact] + public void GetSemanticId_WithSemanticId_ReturnsValue() + { + var element = CreateElementWithSemanticId("http://example.com/semantic-id"); + + var result = _sut.GetSemanticId(element); + + Equal("http://example.com/semantic-id", result); + } + + [Fact] + public void GetSemanticId_WithNullSemanticId_ReturnsEmpty() + { + var element = Substitute.For(); + element.SemanticId.Returns((Reference)null!); + + var result = _sut.GetSemanticId(element); + + Equal(string.Empty, result); + } + + [Fact] + public void GetSemanticId_WithEmptyKeys_ReturnsEmpty() + { + var reference = Substitute.For(); + reference.Keys.Returns(new List()); + var element = Substitute.For(); + element.SemanticId.Returns(reference); + + var result = _sut.GetSemanticId(element); + + Equal(string.Empty, result); + } + + [Fact] + public void ExtractSemanticId_WithInternalSemanticIdQualifier_ReturnsQualifierValue() + { + var element = Substitute.For(); + element.SemanticId.Returns(CreateReference("http://original-semantic-id")); + var qualifier = Substitute.For(); + qualifier.Type.Returns("InternalSemanticId"); + qualifier.Value.Returns("http://internal-semantic-id"); + element.Qualifiers.Returns(new List { qualifier }); + + var result = _sut.ExtractSemanticId(element); + + Equal("http://internal-semantic-id", result); + } + + [Fact] + public void ExtractSemanticId_WithoutInternalSemanticIdQualifier_ReturnsSemantId() + { + var element = Substitute.For(); + element.SemanticId.Returns(CreateReference("http://original-semantic-id")); + var qualifier = Substitute.For(); + qualifier.Type.Returns("SomeOtherQualifier"); + qualifier.Value.Returns("http://other-value"); + element.Qualifiers.Returns(new List { qualifier }); + + var result = _sut.ExtractSemanticId(element); + + Equal("http://original-semantic-id", result); + } + + [Fact] + public void ExtractSemanticId_WithNullQualifiers_ReturnsSemanticId() + { + var element = Substitute.For(); + element.SemanticId.Returns(CreateReference("http://original-semantic-id")); + element.Qualifiers.Returns((List?)null); + + var result = _sut.ExtractSemanticId(element); + + Equal("http://original-semantic-id", result); + } + + [Fact] + public void ResolveSemanticId_WithoutIndex_ReturnsBaseSemanticId() + { + var element = CreateElementWithSemanticId("http://example.com/semantic-id"); + + var result = _sut.ResolveSemanticId(element, "MyElement"); + + Equal("http://example.com/semantic-id", result); + } + + [Fact] + public void ResolveSemanticId_WithTrailingIndex_AppendsIndex() + { + var element = CreateElementWithSemanticId("http://example.com/semantic-id"); + + var result = _sut.ResolveSemanticId(element, "MyElement42"); + + Equal("http://example.com/semantic-id_aastwinengineindex_42", result); + } + + [Fact] + public void ResolveElementSemanticId_WithoutIndex_ReturnsBaseSemanticId() + { + var element = Substitute.For(); + element.SemanticId.Returns(CreateReference("http://example.com/semantic-id")); + element.Qualifiers.Returns(new List()); + + var result = _sut.ResolveElementSemanticId(element, "ContactList"); + + Equal("http://example.com/semantic-id", result); + } + + [Fact] + public void ResolveElementSemanticId_WithTrailingDigits_AppendsIndex() + { + var element = Substitute.For(); + element.SemanticId.Returns(CreateReference("http://example.com/semantic-id")); + element.Qualifiers.Returns(new List()); + + var result = _sut.ResolveElementSemanticId(element, "ContactList01"); + + Equal("http://example.com/semantic-id_aastwinengineindex_01", result); + } + + [Theory] + [InlineData("One", Cardinality.One)] + [InlineData("ZeroToOne", Cardinality.ZeroToOne)] + [InlineData("ZeroToMany", Cardinality.ZeroToMany)] + [InlineData("OneToMany", Cardinality.OneToMany)] + [InlineData("", Cardinality.Unknown)] + public void GetCardinality_VariousQualifierValues_ReturnsExpected(string? qualifierValue, Cardinality expected) + { + var qualifier = Substitute.For(); + qualifier.Value.Returns(qualifierValue); + var element = Substitute.For(); + element.Qualifiers.Returns(new List { qualifier }); + + var actual = _sut.GetCardinality(element); + + Equal(expected, actual); + } + + [Fact] + public void GetCardinality_QualifiersNull_ReturnsUnknown() + { + var element = Substitute.For(); + element.Qualifiers.Returns((List?)null); + + var actual = _sut.GetCardinality(element); + + Equal(Cardinality.Unknown, actual); + } + + [Fact] + public void GetCardinality_EmptyQualifiers_ReturnsUnknown() + { + var element = Substitute.For(); + element.Qualifiers.Returns(new List()); + + var actual = _sut.GetCardinality(element); + + Equal(Cardinality.Unknown, actual); + } + + [Theory] + [InlineData(DataTypeDefXsd.DateTime, DataType.String)] + [InlineData(DataTypeDefXsd.UnsignedShort, DataType.Integer)] + [InlineData(DataTypeDefXsd.Double, DataType.Number)] + [InlineData(DataTypeDefXsd.Boolean, DataType.Boolean)] + [InlineData((DataTypeDefXsd)999, DataType.Unknown)] + [InlineData(DataTypeDefXsd.AnyUri, DataType.String)] + [InlineData(DataTypeDefXsd.Duration, DataType.String)] + [InlineData(DataTypeDefXsd.NonNegativeInteger, DataType.Integer)] + [InlineData(DataTypeDefXsd.GYearMonth, DataType.String)] + [InlineData(DataTypeDefXsd.Float, DataType.Number)] + [InlineData(DataTypeDefXsd.HexBinary, DataType.String)] + [InlineData(DataTypeDefXsd.PositiveInteger, DataType.Integer)] + [InlineData(DataTypeDefXsd.Decimal, DataType.Number)] + public void GetValueType_PropertyValueType_ReturnsExpected(DataTypeDefXsd valueType, DataType expected) + { + var prop = new Property( + idShort: "MyProp", + valueType: valueType, + value: "", + semanticId: new Reference(ReferenceTypes.ExternalReference, + [new Key(KeyTypes.Property, "http://example.com/test")]), + qualifiers: [] + ); + + var actual = _sut.GetValueType(prop); + + Equal(expected, actual); + } + + [Fact] + public void GetValueType_RangeElement_ReturnsExpectedType() + { + var range = new Range(valueType: DataTypeDefXsd.Double, idShort: "TestRange"); + + var actual = _sut.GetValueType(range); + + Equal(DataType.Number, actual); + } + + [Fact] + public void GetValueType_FileElement_ReturnsString() + { + var file = new File(contentType: "image/png", idShort: "TestFile"); + + var actual = _sut.GetValueType(file); + + Equal(DataType.String, actual); + } + + [Fact] + public void GetValueType_BlobElement_ReturnsString() + { + var blob = new Blob(contentType: "application/octet-stream", idShort: "TestBlob"); + + var actual = _sut.GetValueType(blob); + + Equal(DataType.String, actual); + } + + [Fact] + public void GetValueType_UnsupportedElement_ReturnsUnknown() + { + var element = Substitute.For(); + + var actual = _sut.GetValueType(element); + + Equal(DataType.Unknown, actual); + } + + [Fact] + public void BuildReferenceKeySemanticId_SingleKey_OmitsIndex() + { + var result = _sut.BuildReferenceKeySemanticId("http://base", KeyTypes.Submodel, 0, 1); + + Equal("http://base_Submodel", result); + } + + [Fact] + public void BuildReferenceKeySemanticId_MultipleKeys_IncludesIndex() + { + var result = _sut.BuildReferenceKeySemanticId("http://base", KeyTypes.SubmodelElementCollection, 1, 3); + + Equal("http://base_SubmodelElementCollection_1", result); + } + + [Fact] + public void MlpPostFixSeparator_ReturnsConfiguredValue() + { + Equal("_", _sut.MlpPostFixSeparator); + } + + private static IHasSemantics CreateElementWithSemanticId(string semanticId) + { + var element = Substitute.For(); + element.SemanticId.Returns(CreateReference(semanticId)); + return element; + } + + private static Reference CreateReference(string value) => + new(ReferenceTypes.ExternalReference, [new Key(KeyTypes.GlobalReference, value)]); +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigatorTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigatorTests.cs new file mode 100644 index 00000000..d6f07f71 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigatorTests.cs @@ -0,0 +1,154 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using static Xunit.Assert; + +namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; + +public class SemanticTreeNavigatorTests +{ + [Fact] + public void FindBranchNodesBySemanticId_ReturnsMatchingChildren() + { + var root = new SemanticBranchNode("root", Cardinality.Unknown); + var child1 = new SemanticBranchNode("target", Cardinality.One); + var child2 = new SemanticBranchNode("target", Cardinality.ZeroToOne); + var child3 = new SemanticBranchNode("other", Cardinality.One); + root.AddChild(child1); + root.AddChild(child2); + root.AddChild(child3); + + var result = SemanticTreeNavigator.FindBranchNodesBySemanticId(root, "target").ToList(); + + Equal(2, result.Count); + Contains(child1, result); + Contains(child2, result); + } + + [Fact] + public void FindBranchNodesBySemanticId_NoMatch_ReturnsEmpty() + { + var root = new SemanticBranchNode("root", Cardinality.Unknown); + root.AddChild(new SemanticBranchNode("other", Cardinality.One)); + + var result = SemanticTreeNavigator.FindBranchNodesBySemanticId(root, "nonexistent").ToList(); + + Empty(result); + } + + [Fact] + public void FindBranchNodesBySemanticId_LeafNode_ReturnsEmpty() + { + var leaf = new SemanticLeafNode("leaf", "value", DataType.String, Cardinality.One); + + var result = SemanticTreeNavigator.FindBranchNodesBySemanticId(leaf, "leaf").ToList(); + + Empty(result); + } + + [Fact] + public void FindNodeBySemanticId_ReturnsMatchingNode_AtRoot() + { + var root = new SemanticBranchNode("target", Cardinality.Unknown); + + var result = SemanticTreeNavigator.FindNodeBySemanticId(root, "target").ToList(); + + Single(result); + Same(root, result[0]); + } + + [Fact] + public void FindNodeBySemanticId_ReturnsMatchingNodes_InNestedTree() + { + var root = new SemanticBranchNode("root", Cardinality.Unknown); + var child = new SemanticBranchNode("branch", Cardinality.One); + var grandchild = new SemanticLeafNode("target", "val", DataType.String, Cardinality.One); + child.AddChild(grandchild); + root.AddChild(child); + + var result = SemanticTreeNavigator.FindNodeBySemanticId(root, "target").ToList(); + + Single(result); + Same(grandchild, result[0]); + } + + [Fact] + public void FindNodeBySemanticId_ReturnsMultipleMatches_AcrossTree() + { + var root = new SemanticBranchNode("root", Cardinality.Unknown); + var leaf1 = new SemanticLeafNode("target", "v1", DataType.String, Cardinality.One); + var branch = new SemanticBranchNode("branch", Cardinality.One); + var leaf2 = new SemanticLeafNode("target", "v2", DataType.String, Cardinality.One); + branch.AddChild(leaf2); + root.AddChild(leaf1); + root.AddChild(branch); + + var result = SemanticTreeNavigator.FindNodeBySemanticId(root, "target").ToList(); + + Equal(2, result.Count); + } + + [Fact] + public void FindNodeBySemanticId_NoMatch_ReturnsEmpty() + { + var root = new SemanticBranchNode("root", Cardinality.Unknown); + root.AddChild(new SemanticLeafNode("other", "val", DataType.String, Cardinality.One)); + + var result = SemanticTreeNavigator.FindNodeBySemanticId(root, "nonexistent").ToList(); + + Empty(result); + } + + [Fact] + public void AreAllNodesOfSameType_EmptyList_ReturnsTrueWithNullType() + { + var result = SemanticTreeNavigator.AreAllNodesOfSameType([], out var nodeType); + + True(result); + Null(nodeType); + } + + [Fact] + public void AreAllNodesOfSameType_AllBranchNodes_ReturnsTrue() + { + var nodes = new List + { + new SemanticBranchNode("a", Cardinality.One), + new SemanticBranchNode("b", Cardinality.ZeroToOne), + }; + + var result = SemanticTreeNavigator.AreAllNodesOfSameType(nodes, out var nodeType); + + True(result); + Equal(typeof(SemanticBranchNode), nodeType); + } + + [Fact] + public void AreAllNodesOfSameType_AllLeafNodes_ReturnsTrue() + { + var nodes = new List + { + new SemanticLeafNode("a", "v1", DataType.String, Cardinality.One), + new SemanticLeafNode("b", "v2", DataType.Integer, Cardinality.ZeroToOne), + }; + + var result = SemanticTreeNavigator.AreAllNodesOfSameType(nodes, out var nodeType); + + True(result); + Equal(typeof(SemanticLeafNode), nodeType); + } + + [Fact] + public void AreAllNodesOfSameType_MixedNodes_ReturnsFalse() + { + var nodes = new List + { + new SemanticBranchNode("a", Cardinality.One), + new SemanticLeafNode("b", "v2", DataType.String, Cardinality.One), + }; + + var result = SemanticTreeNavigator.AreAllNodesOfSameType(nodes, out _); + + False(result); + } +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelperTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelperTests.cs new file mode 100644 index 00000000..652da14d --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelperTests.cs @@ -0,0 +1,290 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.Config; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; + +using AasCore.Aas3_0; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using NSubstitute; + +using static Xunit.Assert; + +using File = AasCore.Aas3_0.File; + +namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; + +public class SubmodelElementHelperTests +{ + private readonly SubmodelElementHelper _sut; + private readonly ILogger _logger; + + public SubmodelElementHelperTests() + { + _logger = Substitute.For>(); + var mlpSettings = Options.Create(new MultiLanguagePropertySettings { DefaultLanguages = null }); + _sut = new SubmodelElementHelper(_logger, mlpSettings); + } + + [Fact] + public void CloneElement_Property_ReturnsDeepCopy() + { + var original = new Property( + idShort: "TestProp", + valueType: DataTypeDefXsd.String, + value: "original" + ); + + var cloned = _sut.CloneElement(original); + + NotSame(original, cloned); + Equal("TestProp", cloned.IdShort); + var clonedProp = IsType(cloned); + Equal("original", clonedProp.Value); + } + + [Fact] + public void CloneElement_Collection_ReturnsDeepCopyWithChildren() + { + var original = new SubmodelElementCollection( + idShort: "TestCollection", + value: [new Property(idShort: "Child", valueType: DataTypeDefXsd.String, value: "childVal")] + ); + + var cloned = _sut.CloneElement(original); + + NotSame(original, cloned); + var clonedCollection = IsType(cloned); + Single(clonedCollection.Value!); + Equal("Child", clonedCollection.Value![0].IdShort); + } + + [Fact] + public void GetElementByIdShort_MatchingElement_ReturnsElement() + { + var elements = new List + { + new Property(idShort: "First", valueType: DataTypeDefXsd.String), + new Property(idShort: "Second", valueType: DataTypeDefXsd.String), + }; + + var result = _sut.GetElementByIdShort(elements, "Second"); + + NotNull(result); + Equal("Second", result!.IdShort); + } + + [Fact] + public void GetElementByIdShort_NoMatch_ReturnsNull() + { + var elements = new List + { + new Property(idShort: "First", valueType: DataTypeDefXsd.String), + }; + + var result = _sut.GetElementByIdShort(elements, "NonExistent"); + + Null(result); + } + + [Fact] + public void GetElementByIdShort_NullCollection_ReturnsNull() + { + var result = _sut.GetElementByIdShort(null, "Any"); + + Null(result); + } + + [Fact] + public void GetElementByIdShort_WithBracketIndex_ReturnsListElement() + { + var listElement = new SubmodelElementList( + idShort: "MyList", + typeValueListElement: AasSubmodelElements.Property, + value: [ + new Property(idShort: "Item0", valueType: DataTypeDefXsd.String, value: "zero"), + new Property(idShort: "Item1", valueType: DataTypeDefXsd.String, value: "one"), + ] + ); + var elements = new List { listElement }; + + var result = _sut.GetElementByIdShort(elements, "MyList[1]"); + + NotNull(result); + Equal("Item1", result!.IdShort); + } + + [Fact] + public void GetElementByIdShort_WithEncodedBracketIndex_ReturnsListElement() + { + var listElement = new SubmodelElementList( + idShort: "MyList", + typeValueListElement: AasSubmodelElements.Property, + value: [ + new Property(idShort: "Item0", valueType: DataTypeDefXsd.String, value: "zero"), + ] + ); + var elements = new List { listElement }; + + var result = _sut.GetElementByIdShort(elements, "MyList%5B0%5D"); + + NotNull(result); + Equal("Item0", result!.IdShort); + } + + [Fact] + public void GetElementFromListByIndex_ValidIndex_ReturnsElement() + { + var list = new SubmodelElementList( + idShort: "TestList", + typeValueListElement: AasSubmodelElements.Property, + value: [ + new Property(idShort: "Item0", valueType: DataTypeDefXsd.String), + new Property(idShort: "Item1", valueType: DataTypeDefXsd.String), + ] + ); + var elements = new List { list }; + + var result = _sut.GetElementFromListByIndex(elements, "TestList", 1); + + Equal("Item1", result.IdShort); + } + + [Fact] + public void GetElementFromListByIndex_OutOfBounds_ThrowsException() + { + var list = new SubmodelElementList( + idShort: "TestList", + typeValueListElement: AasSubmodelElements.Property, + value: [new Property(idShort: "Item0", valueType: DataTypeDefXsd.String)] + ); + var elements = new List { list }; + + Throws(() => _sut.GetElementFromListByIndex(elements, "TestList", 5)); + } + + [Fact] + public void GetElementFromListByIndex_NotAList_ThrowsException() + { + var elements = new List + { + new Property(idShort: "NotAList", valueType: DataTypeDefXsd.String), + }; + + Throws(() => _sut.GetElementFromListByIndex(elements, "NotAList", 0)); + } + + [Fact] + public void GetChildElements_Collection_ReturnsValue() + { + var child = new Property(idShort: "Child", valueType: DataTypeDefXsd.String); + var collection = new SubmodelElementCollection(idShort: "Col", value: [child]); + + var result = _sut.GetChildElements(collection); + + NotNull(result); + Single(result!); + Same(child, result[0]); + } + + [Fact] + public void GetChildElements_List_ReturnsValue() + { + var child = new Property(idShort: "Child", valueType: DataTypeDefXsd.String); + var list = new SubmodelElementList( + idShort: "List", + typeValueListElement: AasSubmodelElements.Property, + value: [child] + ); + + var result = _sut.GetChildElements(list); + + NotNull(result); + Single(result!); + } + + [Fact] + public void GetChildElements_Entity_ReturnsStatements() + { + var statement = new Property(idShort: "Statement", valueType: DataTypeDefXsd.String); + var entity = new Entity(idShort: "Ent", entityType: EntityType.SelfManagedEntity, statements: [statement]); + + var result = _sut.GetChildElements(entity); + + NotNull(result); + Single(result!); + } + + [Fact] + public void GetChildElements_Property_ReturnsNull() + { + var property = new Property(idShort: "Prop", valueType: DataTypeDefXsd.String); + + var result = _sut.GetChildElements(property); + + Null(result); + } + + [Fact] + public void ResolveLanguages_WithValues_ReturnsLanguagesFromValues() + { + var mlp = new MultiLanguageProperty( + idShort: "TestMlp", + value: [ + new LangStringTextType("en", "English"), + new LangStringTextType("de", "German"), + ] + ); + + var result = _sut.ResolveLanguages(mlp); + + Equal(2, result.Count); + Contains("en", result); + Contains("de", result); + } + + [Fact] + public void ResolveLanguages_WithNullValue_ReturnsEmpty() + { + var mlp = new MultiLanguageProperty(idShort: "TestMlp", value: null); + + var result = _sut.ResolveLanguages(mlp); + + Empty(result); + } + + [Fact] + public void ResolveLanguages_WithDefaultLanguages_MergesWithDefaults() + { + var mlpSettings = Options.Create(new MultiLanguagePropertySettings { DefaultLanguages = ["en", "fr"] }); + var sut = new SubmodelElementHelper(Substitute.For>(), mlpSettings); + + var mlp = new MultiLanguageProperty( + idShort: "TestMlp", + value: [new LangStringTextType("en", "English"), new LangStringTextType("de", "German")] + ); + + var result = sut.ResolveLanguages(mlp); + + Equal(3, result.Count); + Contains("en", result); + Contains("de", result); + Contains("fr", result); + } + + [Fact] + public void ResolveLanguages_WithOnlyDefaultLanguages_ReturnsDefaults() + { + var mlpSettings = Options.Create(new MultiLanguagePropertySettings { DefaultLanguages = ["en", "fr"] }); + var sut = new SubmodelElementHelper(Substitute.For>(), mlpSettings); + + var mlp = new MultiLanguageProperty(idShort: "TestMlp", value: null); + + var result = sut.ResolveLanguages(mlp); + + Equal(2, result.Count); + Contains("en", result); + Contains("fr", result); + } +} diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs index 0beea9f6..e032483b 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs @@ -1,21 +1,19 @@ -/*using System.Reflection; - -using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.Config; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Extraction; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.FillOut; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; +using MongoDB.Bson; + using AasCore.Aas3_0; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MongoDB.Bson; - using NSubstitute; using static Xunit.Assert; @@ -31,7 +29,6 @@ public class SemanticIdHandlerTests private readonly ILogger _fillerLogger; private readonly IOptions _mlpSettings; private readonly IOptions _semantics; - private readonly ISemanticIdResolver _resolver; public SemanticIdHandlerTests() { @@ -42,7 +39,6 @@ public SemanticIdHandlerTests() _semantics = Substitute.For>(); _ = _semantics.Value.Returns(new Semantics { MultiLanguageSemanticPostfixSeparator = "_", SubmodelElementIndexContextPrefix = "_aastwinengineindex_" }); - _resolver = new SemanticIdResolver(_semantics); _sut = CreateSut(_semantics, _mlpSettings); } @@ -62,14 +58,6 @@ public void Extract_TemplateNull_ThrowsException() [Fact] public void FillOutTemplate_TemplateNull_ThrowsException() => _ = Throws(() => _sut.FillOutTemplate(submodelTemplate: null!, TestData.SubmodelTreeNode)); - [Fact] - public void SemanticIdHandler_NullSemantics_ThrowsException() - { - var options = Options.Create(options: null!); - - _ = Throws(() => new SemanticIdResolver(options)); - } - [Fact] public void Extract_Submodel_ReturnsSemanticTreeNode() { @@ -276,7 +264,7 @@ public void Extract_MultiLanguageProperty_WithDefaultLanguagesAs_En_De_Fr() } [Fact] - public void Extract_EmptySubmodelElementCollection_LogsWarningAndReturnsNode() + public void Extract_EmptySubmodelElementCollection_ReturnsNodeWithEmptyChildren() { var mlp = TestData.CreateSubmodelWithContactInformationWithOutElements(); @@ -287,15 +275,10 @@ public void Extract_EmptySubmodelElementCollection_LogsWarningAndReturnsNode() var contactInformationNode = node.Children[0] as SemanticBranchNode; Equal("http://example.com/idta/digital-nameplate/contact-information", contactInformationNode?.SemanticId); Empty(contactInformationNode!.Children); - _extractorLogger.Received(1).Log(LogLevel.Warning, Arg.Any(), - Arg.Is(state => state.ToString()! - .Contains("No elements defined in SubmodelElementCollection ContactInformation")), - null, - Arg.Any>()!); } [Fact] - public void Extract_EmptySubmodelElementList_LogsWarningAndReturnsNode() + public void Extract_EmptySubmodelElementList_ReturnsNodeWithEmptyChildren() { var mlp = TestData.CreateSubmodelWithContactListWithOutElements(); @@ -306,11 +289,6 @@ public void Extract_EmptySubmodelElementList_LogsWarningAndReturnsNode() var contactInformationNode = node.Children[0] as SemanticBranchNode; Equal("http://example.com/idta/digital-nameplate/contact-list", contactInformationNode?.SemanticId); Empty(contactInformationNode!.Children); - _extractorLogger.Received(1).Log(LogLevel.Warning, Arg.Any(), - Arg.Is(state => state.ToString()! - .Contains("No elements defined in SubmodelElementList ContactList")), - null, - Arg.Any>()!); } [Fact] @@ -401,85 +379,6 @@ public void Extract_ThrowsNotFoundException_WhenElementNotFound() Throws(() => _sut.Extract(submodel, Path)); } - [Theory] - [InlineData("One", Cardinality.One)] - [InlineData("ZeroToOne", Cardinality.ZeroToOne)] - [InlineData("ZeroToMany", Cardinality.ZeroToMany)] - [InlineData("OneToMany", Cardinality.OneToMany)] - [InlineData("", Cardinality.Unknown)] - public void GetCardinality_VariousQualifierValues_ReturnsExpected(string? qualifierValue, Cardinality expected) - { - var qualifier = Substitute.For(); - qualifier.Value.Returns(qualifierValue); - var element = Substitute.For(); - element.Qualifiers.Returns([qualifier]); - - var actual = _resolver.GetCardinality(element); - - Equal(expected, actual); - } - - [Fact] - public void GetCardinality_QualifiersNull_ReturnsUnknown() - { - var element = Substitute.For(); - element.Qualifiers.Returns((List?)null); - - var actual = _resolver.GetCardinality(element); - - Equal(Cardinality.Unknown, actual); - } - - [Fact] - public void GetCardinality_EmptyQualifiers_ReturnsUnknown() - { - var element = Substitute.For(); - element.Qualifiers.Returns([]); - - var actual = _resolver.GetCardinality(element); - - Equal(Cardinality.Unknown, actual); - } - - [Theory] - [InlineData(DataTypeDefXsd.DateTime, DataType.String)] - [InlineData(DataTypeDefXsd.UnsignedShort, DataType.Integer)] - [InlineData(DataTypeDefXsd.Double, DataType.Number)] - [InlineData(DataTypeDefXsd.Boolean, DataType.Boolean)] - [InlineData((DataTypeDefXsd)999, DataType.Unknown)] - [InlineData(DataTypeDefXsd.AnyUri, DataType.String)] - [InlineData(DataTypeDefXsd.Duration, DataType.String)] - [InlineData(DataTypeDefXsd.NonNegativeInteger, DataType.Integer)] - [InlineData(DataTypeDefXsd.GYearMonth, DataType.String)] - [InlineData(DataTypeDefXsd.Float, DataType.Number)] - [InlineData(DataTypeDefXsd.HexBinary, DataType.String)] - [InlineData(DataTypeDefXsd.PositiveInteger, DataType.Integer)] - [InlineData(DataTypeDefXsd.Decimal, DataType.Number)] - public void GetValueType_PropertyValueType_ReturnsExpected(DataTypeDefXsd valueType, DataType expected) - { - var prop = new Property( - idShort: "MyProp", - valueType: valueType, - value: "", - semanticId: TestData.CreateContactName().SemanticId, - qualifiers: [] - ); - - var actual = _resolver.GetValueType(prop); - - Equal(expected, actual); - } - - [Fact] - public void GetValueType_ElementWithoutValueProperty_ReturnsUnknown() - { - var element = Substitute.For(); - - var actual = _resolver.GetValueType(element); - - Equal(DataType.Unknown, actual); - } - [Theory] [InlineData("ContactList01", "http://example.com/idta/digital-nameplate/contact-list_aastwinengineindex_01")] [InlineData("ContactList42", "http://example.com/idta/digital-nameplate/contact-list_aastwinengineindex_42")] @@ -844,13 +743,6 @@ public void FillOutTemplate_EmptyMultiLanguageProperty_WithDefaultLanguagesAs_En Equal(3, mlp.Value!.Count); var languages = mlp.Value.Select(v => v.Language).OrderBy(l => l).ToList(); Equal(["de", "en", "fr"], languages); - _fillerLogger.Received(3).Log( - LogLevel.Information, - Arg.Any(), - Arg.Is(state => state.ToString()!.Contains("Added language")), - null, - Arg.Any>()! - ); } [Fact] @@ -877,13 +769,6 @@ public void FillOutTemplate_MultiLanguageProperty_WithDefaultLanguagesAs_En_De_F var frValue = mlp.Value.FirstOrDefault(v => v.Language == "fr"); NotNull(frValue); Equal("Exemple de test Fabricant", frValue.Text); - _fillerLogger.Received(1).Log( - LogLevel.Information, - Arg.Any(), - Arg.Is(state => state.ToString()!.Contains("Added language 'fr'")), - null, - Arg.Any>()! - ); } [Fact] @@ -970,12 +855,25 @@ private static Submodel CreateSubmodelWithSubmodelElement(ISubmodelElement submo private SemanticIdHandler CreateSut(IOptions semantics, IOptions mlpSettings) { var resolver = new SemanticIdResolver(semantics); - var navigator = new SemanticTreeNavigator(); - var helper = new SubmodelElementHelper(Substitute.For>()); - var multiLanguageHelper = new MultiLanguageHelper(mlpSettings); - var referenceHelper = new ReferenceHelper(resolver, navigator, Substitute.For>()); - var extractor = new SemanticTreeExtractor(resolver, helper, multiLanguageHelper, referenceHelper, _extractorLogger); - var filler = new SubmodelFiller(resolver, navigator, helper, multiLanguageHelper, referenceHelper, _fillerLogger); + var helper = new SubmodelElementHelper(Substitute.For>(), mlpSettings); + var referenceHelper = new ReferenceHelper(resolver, Substitute.For>()); + + var handlers = new List + { + new PropertyHandler(resolver), + new CollectionHandler(resolver, Substitute.For>()), + new ListHandler(resolver, Substitute.For>()), + new MultiLanguagePropertyHandler(resolver, helper, Substitute.For>()), + new RangeHandler(resolver), + new FileHandler(resolver), + new BlobHandler(resolver), + new EntityHandler(resolver, Substitute.For>()), + new ReferenceElementHandler(resolver, referenceHelper, Substitute.For>()), + new RelationshipElementHandler(resolver, referenceHelper), + }; + + var extractor = new SemanticTreeExtractor(resolver, helper, handlers, _extractorLogger); + var filler = new SubmodelFiller(resolver, helper, handlers, _fillerLogger); return new SemanticIdHandler(extractor, filler); } @@ -990,4 +888,4 @@ private static IOptions CreateMlpSettings(List specificAssetIds = new List -{ + public static List _specificAssetIds = +[ new SpecificAssetId( name: "Manufacturer", value: "ExampleCorp", @@ -1339,7 +1339,7 @@ public static RelationshipElement CreateFilledRelationshipElementWithBothModelRe ] ) } -}; +]; public static Submodel CreateFilledSubmodel() => new( id: "http://example.com/idta/digital-nameplate", diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/BlobHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/BlobHandler.cs new file mode 100644 index 00000000..e4d4a1cb --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/BlobHandler.cs @@ -0,0 +1,25 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class BlobHandler(ISemanticIdResolver semanticIdResolver) : ISubmodelElementTypeHandler +{ + public bool CanHandle(ISubmodelElement element) => element is Blob; + + public SemanticTreeNode? Extract(ISubmodelElement element, Func extractChild) + { + var semanticId = semanticIdResolver.ResolveElementSemanticId(element, element.IdShort!); + return new SemanticLeafNode(semanticId, string.Empty, semanticIdResolver.GetValueType(element), semanticIdResolver.GetCardinality(element)); + } + + public void FillOut(ISubmodelElement element, SemanticTreeNode values, Action, SemanticTreeNode, bool> fillOutChildren) + { + if (values is SemanticLeafNode leafValueNode) + { + ((Blob)element).Value = Convert.FromBase64String(leafValueNode.Value); + } + } +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/CollectionHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/CollectionHandler.cs new file mode 100644 index 00000000..7d36bcbb --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/CollectionHandler.cs @@ -0,0 +1,49 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class CollectionHandler( + ISemanticIdResolver semanticIdResolver, + ILogger logger) : ISubmodelElementTypeHandler +{ + public bool CanHandle(ISubmodelElement element) => element is SubmodelElementCollection; + + public SemanticTreeNode? Extract(ISubmodelElement element, Func extractChild) + { + var collection = (SubmodelElementCollection)element; + var node = new SemanticBranchNode(semanticIdResolver.ResolveElementSemanticId(collection, collection.IdShort!), semanticIdResolver.GetCardinality(collection)); + + if (collection.Value?.Count > 0) + { + foreach (var child in collection.Value.Where(_ => true)) + { + var childNode = extractChild(child); + if (childNode != null) + { + node.AddChild(childNode); + } + } + } + else + { + logger.LogWarning("No elements defined in SubmodelElementCollection {CollectionIdShort}", collection.IdShort); + } + + return node; + } + + public void FillOut(ISubmodelElement element, SemanticTreeNode values, Action, SemanticTreeNode, bool> fillOutChildren) + { + var collection = (SubmodelElementCollection)element; + + if (collection?.Value == null || collection.Value.Count == 0) + { + return; + } + + fillOutChildren(collection.Value, values, true); + } +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/EntityHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/EntityHandler.cs new file mode 100644 index 00000000..2e2ed78f --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/EntityHandler.cs @@ -0,0 +1,111 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class EntityHandler( + ISemanticIdResolver semanticIdResolver, + ILogger logger) : ISubmodelElementTypeHandler +{ + public bool CanHandle(ISubmodelElement element) => element is Entity; + + public SemanticTreeNode? Extract(ISubmodelElement element, Func extractChild) + { + var entity = (Entity)element; + var semanticId = semanticIdResolver.ResolveElementSemanticId(entity, entity.IdShort!); + var node = new SemanticBranchNode(semanticId, semanticIdResolver.GetCardinality(entity)); + + if (entity.EntityType == EntityType.SelfManagedEntity) + { + var globalAssetIdNode = new SemanticLeafNode(semanticId + SemanticIdResolver.EntityGlobalAssetIdPostFix, string.Empty, DataType.String, Cardinality.One); + node.AddChild(globalAssetIdNode); + + if (entity.SpecificAssetIds != null) + { + foreach (var specificAssetId in entity.SpecificAssetIds) + { + IHasSemantics specificAsset = specificAssetId; + if (specificAsset.SemanticId == null) + { + continue; + } + + var specificAssetIdNode = new SemanticLeafNode(semanticIdResolver.GetSemanticId(specificAssetId), string.Empty, DataType.String, Cardinality.One); + node.AddChild(specificAssetIdNode); + } + } + } + + if (entity.Statements?.Count > 0) + { + foreach (var child in entity.Statements.Select(extractChild).OfType()) + { + node.AddChild(child); + } + } + else + { + logger.LogWarning("No elements defined in Entity {EntityIdShort}", entity.IdShort); + } + + return node; + } + + public void FillOut(ISubmodelElement element, SemanticTreeNode values, Action, SemanticTreeNode, bool> fillOutChildren) + { + var entity = (Entity)element; + + if (entity.EntityType == EntityType.SelfManagedEntity) + { + FillOutSelfManagedEntity(entity, values); + } + + if (entity?.Statements == null || entity.Statements.Count == 0) + { + return; + } + + fillOutChildren(entity.Statements, values, true); + } + + private void FillOutSelfManagedEntity(Entity entity, SemanticTreeNode values) + { + var semanticId = semanticIdResolver.ResolveElementSemanticId(entity, entity.IdShort!); + + if (SemanticTreeNavigator.FindNodeBySemanticId(values, semanticId).FirstOrDefault() is not SemanticBranchNode valueNode) + { + return; + } + + var globalAssetSemanticId = semanticId + SemanticIdResolver.EntityGlobalAssetIdPostFix; + + var globalAssetNode = valueNode.Children + .OfType() + .FirstOrDefault(c => c.SemanticId == globalAssetSemanticId); + + if (globalAssetNode != null) + { + entity.GlobalAssetId = globalAssetNode.Value; + } + + if (entity.SpecificAssetIds != null) + { + foreach (var specificAssetId in entity.SpecificAssetIds) + { + var specSemanticId = semanticIdResolver.GetSemanticId(specificAssetId); + + var specNode = valueNode.Children + .OfType() + .FirstOrDefault(c => c.SemanticId == specSemanticId); + + if (specNode != null) + { + specificAssetId.Value = specNode.Value; + } + } + } + } +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/FileHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/FileHandler.cs new file mode 100644 index 00000000..8dab50d1 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/FileHandler.cs @@ -0,0 +1,27 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using File = AasCore.Aas3_0.File; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class FileHandler(ISemanticIdResolver semanticIdResolver) : ISubmodelElementTypeHandler +{ + public bool CanHandle(ISubmodelElement element) => element is File; + + public SemanticTreeNode? Extract(ISubmodelElement element, Func extractChild) + { + var semanticId = semanticIdResolver.ResolveElementSemanticId(element, element.IdShort!); + return new SemanticLeafNode(semanticId, string.Empty, semanticIdResolver.GetValueType(element), semanticIdResolver.GetCardinality(element)); + } + + public void FillOut(ISubmodelElement element, SemanticTreeNode values, Action, SemanticTreeNode, bool> fillOutChildren) + { + if (values is SemanticLeafNode leafValueNode) + { + ((File)element).Value = leafValueNode.Value; + } + } +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ISubmodelElementTypeHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ISubmodelElementTypeHandler.cs new file mode 100644 index 00000000..c47663ae --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ISubmodelElementTypeHandler.cs @@ -0,0 +1,14 @@ +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public interface ISubmodelElementTypeHandler +{ + bool CanHandle(ISubmodelElement element); + + SemanticTreeNode? Extract(ISubmodelElement element, Func extractChild); + + void FillOut(ISubmodelElement element, SemanticTreeNode values, Action, SemanticTreeNode, bool> fillOutChildren); +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ListHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ListHandler.cs new file mode 100644 index 00000000..9141b916 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ListHandler.cs @@ -0,0 +1,45 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class ListHandler( + ISemanticIdResolver semanticIdResolver, + ILogger logger) : ISubmodelElementTypeHandler +{ + public bool CanHandle(ISubmodelElement element) => element is SubmodelElementList; + + public SemanticTreeNode? Extract(ISubmodelElement element, Func extractChild) + { + var list = (SubmodelElementList)element; + var node = new SemanticBranchNode(semanticIdResolver.ResolveElementSemanticId(list, list.IdShort!), semanticIdResolver.GetCardinality(list)); + + if (list.Value?.Count > 0) + { + foreach (var childNode in list.Value.Select(extractChild).OfType()) + { + node.AddChild(childNode); + } + } + else + { + logger.LogWarning("No elements defined in SubmodelElementList {ListIdShort}", list.IdShort); + } + + return node; + } + + public void FillOut(ISubmodelElement element, SemanticTreeNode values, Action, SemanticTreeNode, bool> fillOutChildren) + { + var list = (SubmodelElementList)element; + + if (list?.Value == null || list.Value.Count == 0) + { + return; + } + + fillOutChildren(list.Value, values, false); + } +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/MultiLanguagePropertyHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/MultiLanguagePropertyHandler.cs new file mode 100644 index 00000000..c628c5d8 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/MultiLanguagePropertyHandler.cs @@ -0,0 +1,83 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class MultiLanguagePropertyHandler( + ISemanticIdResolver semanticIdResolver, + ISubmodelElementHelper elementHelper, + ILogger logger) : ISubmodelElementTypeHandler +{ + public bool CanHandle(ISubmodelElement element) => element is MultiLanguageProperty; + + public SemanticTreeNode? Extract(ISubmodelElement element, Func extractChild) + { + var mlp = (MultiLanguageProperty)element; + var semanticId = semanticIdResolver.ExtractSemanticId(mlp); + var node = new SemanticBranchNode(semanticId, semanticIdResolver.GetCardinality(mlp)); + + var languages = elementHelper.ResolveLanguages(mlp); + + if (mlp.Value is not { Count: > 0 }) + { + logger.LogInformation("No languages defined in template for MultiLanguageProperty {MlpIdShort}", mlp.IdShort); + } + + var mlpSeparator = semanticIdResolver.MlpPostFixSeparator; + foreach (var langSemanticId in languages.Select(language => string.Concat(semanticId, mlpSeparator, language))) + { + node.AddChild(new SemanticLeafNode(langSemanticId, string.Empty, DataType.String, Cardinality.ZeroToOne)); + } + + return node; + } + + public void FillOut(ISubmodelElement element, SemanticTreeNode values, Action, SemanticTreeNode, bool> fillOutChildren) + { + var mlp = (MultiLanguageProperty)element; + var semanticId = semanticIdResolver.ExtractSemanticId(mlp); + + if (SemanticTreeNavigator.FindNodeBySemanticId(values, semanticId).FirstOrDefault() is not SemanticBranchNode valueNode) + { + logger.LogInformation("No value node found for MultiLanguageProperty {MlpIdShort}", mlp.IdShort); + return; + } + + mlp.Value ??= []; + + var languageValueMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var langValue in mlp.Value) + { + languageValueMap[langValue.Language] = (LangStringTextType)langValue; + } + + var languages = elementHelper.ResolveLanguages(mlp); + + var mlpSeparator = semanticIdResolver.MlpPostFixSeparator; + foreach (var language in languages) + { + if (!languageValueMap.TryGetValue(language, out var languageValue)) + { + languageValue = new LangStringTextType(language, string.Empty); + mlp.Value.Add(languageValue); + languageValueMap[language] = languageValue; + + logger.LogInformation("Added language '{Language}' to MultiLanguageProperty {MlpIdShort}", language, mlp.IdShort); + } + + var languageSemanticId = semanticId + mlpSeparator + language; + + var leafNode = valueNode.Children + .OfType() + .FirstOrDefault(child => child.SemanticId.Equals(languageSemanticId, StringComparison.Ordinal)); + + if (leafNode != null) + { + languageValue.Text = leafNode.Value; + } + } + } +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/PropertyHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/PropertyHandler.cs new file mode 100644 index 00000000..cfe3b423 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/PropertyHandler.cs @@ -0,0 +1,25 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class PropertyHandler(ISemanticIdResolver semanticIdResolver) : ISubmodelElementTypeHandler +{ + public bool CanHandle(ISubmodelElement element) => element is Property; + + public SemanticTreeNode? Extract(ISubmodelElement element, Func extractChild) + { + var semanticId = semanticIdResolver.ResolveElementSemanticId(element, element.IdShort!); + return new SemanticLeafNode(semanticId, string.Empty, semanticIdResolver.GetValueType(element), semanticIdResolver.GetCardinality(element)); + } + + public void FillOut(ISubmodelElement element, SemanticTreeNode values, Action, SemanticTreeNode, bool> fillOutChildren) + { + if (values is SemanticLeafNode leafValueNode) + { + ((Property)element).Value = leafValueNode.Value; + } + } +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RangeHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RangeHandler.cs new file mode 100644 index 00000000..1ee208f0 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RangeHandler.cs @@ -0,0 +1,47 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +using Range = AasCore.Aas3_0.Range; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class RangeHandler(ISemanticIdResolver semanticIdResolver) : ISubmodelElementTypeHandler +{ + public bool CanHandle(ISubmodelElement element) => element is Range; + + public SemanticTreeNode? Extract(ISubmodelElement element, Func extractChild) + { + var range = (Range)element; + var semanticId = semanticIdResolver.ExtractSemanticId(range); + var valueType = semanticIdResolver.GetValueType(range); + var node = new SemanticBranchNode(semanticId, semanticIdResolver.GetCardinality(range)); + + node.AddChild(new SemanticLeafNode(semanticId + SemanticIdResolver.RangeMinimumPostFixSeparator, string.Empty, valueType, Cardinality.ZeroToOne)); + node.AddChild(new SemanticLeafNode(semanticId + SemanticIdResolver.RangeMaximumPostFixSeparator, string.Empty, valueType, Cardinality.ZeroToOne)); + + return node; + } + + public void FillOut(ISubmodelElement element, SemanticTreeNode values, Action, SemanticTreeNode, bool> fillOutChildren) + { + var range = (Range)element; + + if (values is not SemanticBranchNode branchNode) + { + return; + } + + var leafNodes = branchNode.Children.OfType().ToList(); + + range.Min = leafNodes.FirstOrDefault(n => n.SemanticId + .EndsWith(SemanticIdResolver.RangeMinimumPostFixSeparator, StringComparison.Ordinal))? + .Value; + + range.Max = leafNodes.FirstOrDefault(n => n.SemanticId + .EndsWith(SemanticIdResolver.RangeMaximumPostFixSeparator, StringComparison.Ordinal))? + .Value; + } +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ReferenceElementHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ReferenceElementHandler.cs new file mode 100644 index 00000000..cd195553 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ReferenceElementHandler.cs @@ -0,0 +1,42 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class ReferenceElementHandler( + ISemanticIdResolver semanticIdResolver, + IReferenceHelper referenceHelper, + ILogger logger) : ISubmodelElementTypeHandler +{ + public bool CanHandle(ISubmodelElement element) => element is ReferenceElement; + + public SemanticTreeNode? Extract(ISubmodelElement element, Func extractChild) + { + var referenceElement = (ReferenceElement)element; + + if (referenceElement.Value == null || referenceElement.Value.Type == ReferenceTypes.ExternalReference) + { + return null; + } + + return referenceHelper.ExtractReferenceKeys( + referenceElement.Value, + semanticIdResolver.ResolveElementSemanticId(referenceElement, referenceElement.IdShort!), + semanticIdResolver.GetCardinality(referenceElement)); + } + + public void FillOut(ISubmodelElement element, SemanticTreeNode values, Action, SemanticTreeNode, bool> fillOutChildren) + { + var referenceElement = (ReferenceElement)element; + + if (referenceElement?.Value?.Type != ReferenceTypes.ModelReference) + { + logger.LogInformation("ReferenceElement does not contain a ModelReference for SemanticId '{SemanticId}'. Skipping population.", semanticIdResolver.GetSemanticId(referenceElement!)); + return; + } + + referenceHelper.PopulateReferenceKeys(referenceElement.Value, values, semanticIdResolver.GetSemanticId(referenceElement)); + } +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RelationshipElementHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RelationshipElementHandler.cs new file mode 100644 index 00000000..71c7b295 --- /dev/null +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RelationshipElementHandler.cs @@ -0,0 +1,58 @@ +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; + +using AasCore.Aas3_0; + +namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; + +public class RelationshipElementHandler( + ISemanticIdResolver semanticIdResolver, + IReferenceHelper referenceHelper) : ISubmodelElementTypeHandler +{ + public bool CanHandle(ISubmodelElement element) => element is RelationshipElement; + + public SemanticTreeNode? Extract(ISubmodelElement element, Func extractChild) + { + var relationshipElement = (RelationshipElement)element; + + if (relationshipElement.First.Type == ReferenceTypes.ExternalReference && relationshipElement.Second.Type == ReferenceTypes.ExternalReference) + { + return null; + } + + var semanticId = semanticIdResolver.GetSemanticId(relationshipElement); + var cardinality = semanticIdResolver.GetCardinality(relationshipElement); + var relationshipElementNode = new SemanticBranchNode(semanticId, cardinality); + + if (relationshipElement.First.Type == ReferenceTypes.ModelReference) + { + var referenceNode = referenceHelper.ExtractReferenceKeys(relationshipElement.First, $"{semanticId}{SemanticIdResolver.RelationshipElementFirstPostFixSeparator}", cardinality); + if (referenceNode != null) + { + relationshipElementNode.AddChild(referenceNode); + } + } + + if (relationshipElement.Second.Type == ReferenceTypes.ModelReference) + { + var referenceNode = referenceHelper.ExtractReferenceKeys(relationshipElement.Second, $"{semanticId}{SemanticIdResolver.RelationshipElementSecondPostFixSeparator}", cardinality); + if (referenceNode != null) + { + relationshipElementNode.AddChild(referenceNode); + } + } + + return relationshipElementNode; + } + + public void FillOut(ISubmodelElement element, SemanticTreeNode values, Action, SemanticTreeNode, bool> fillOutChildren) + { + var relationshipElement = (RelationshipElement)element; + var semanticId = values.SemanticId; + + referenceHelper.PopulateRelationshipReference(relationshipElement.First, values, semanticId, SemanticIdResolver.RelationshipElementFirstPostFixSeparator); + + referenceHelper.PopulateRelationshipReference(relationshipElement.Second, values, semanticId, SemanticIdResolver.RelationshipElementSecondPostFixSeparator); + } +} diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractor.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractor.cs index 2c314dca..0b542eed 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractor.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractor.cs @@ -1,19 +1,16 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; using AasCore.Aas3_0; -using Range = AasCore.Aas3_0.Range; - namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Extraction; public class SemanticTreeExtractor( ISemanticIdResolver semanticIdResolver, ISubmodelElementHelper elementHelper, - IReferenceHelper referenceHelper, - ILogger logger) : ISemanticTreeExtractor + IEnumerable handlers) : ISemanticTreeExtractor { public SemanticTreeNode Extract(ISubmodel submodelTemplate) { @@ -59,179 +56,14 @@ public ISubmodelElement Extract(ISubmodel submodelTemplate, string idShortPath) throw new InternalDataProcessingException(); } - private SemanticTreeNode? ExtractElement(ISubmodelElement submodelElementTemplate) - { - ArgumentNullException.ThrowIfNull(submodelElementTemplate); - - return submodelElementTemplate switch - { - SubmodelElementCollection collection => ExtractCollection(collection), - SubmodelElementList list => ExtractList(list), - MultiLanguageProperty mlp => ExtractMultiLanguageProperty(mlp), - Range range => ExtractRange(range), - ReferenceElement re => ExtractReferenceElement(re), - RelationshipElement relationshipElement => ExtractRelationshipElement(relationshipElement), - Entity entity => ExtractEntity(entity), - _ => CreateLeafNode(submodelElementTemplate) - }; - } - - private SemanticBranchNode ExtractList(SubmodelElementList list) - { - var node = new SemanticBranchNode(semanticIdResolver.ResolveElementSemanticId(list, list.IdShort!), semanticIdResolver.GetCardinality(list)); - if (list.Value?.Count > 0) - { - foreach (var element in list.Value) - { - var child = ExtractElement(element); - if (child != null) - { - node.AddChild(child); - } - } - } - else - { - logger.LogWarning("No elements defined in SubmodelElementList {ListIdShort}", list.IdShort); - } - - return node; - } - - private SemanticBranchNode ExtractCollection(SubmodelElementCollection collection) + internal SemanticTreeNode? ExtractElement(ISubmodelElement element) { - var node = new SemanticBranchNode(semanticIdResolver.ResolveElementSemanticId(collection, collection.IdShort!), semanticIdResolver.GetCardinality(collection)); - if (collection.Value?.Count > 0) - { - foreach (var element in collection.Value.Where(_ => true)) - { - var child = ExtractElement(element); - if (child != null) - { - node.AddChild(child); - } - } - } - else - { - logger.LogWarning("No elements defined in SubmodelElementCollection {CollectionIdShort}", collection.IdShort); - } - - return node; - } - - private SemanticBranchNode? ExtractReferenceElement(ReferenceElement referenceElement) - { - if (referenceElement.Value == null || referenceElement.Value.Type == ReferenceTypes.ExternalReference) - { - return null; - } - - return referenceHelper.ExtractReferenceKeys(referenceElement.Value, semanticIdResolver.ResolveElementSemanticId(referenceElement, referenceElement.IdShort!), semanticIdResolver.GetCardinality(referenceElement)); - } - - private SemanticBranchNode? ExtractRelationshipElement(RelationshipElement relationshipElement) - { - if (relationshipElement.First.Type == ReferenceTypes.ExternalReference && relationshipElement.Second.Type == ReferenceTypes.ExternalReference) - { - return null; - } - - var semanticId = semanticIdResolver.GetSemanticId(relationshipElement); - var cardinality = semanticIdResolver.GetCardinality(relationshipElement); - var relationshipElementNode = new SemanticBranchNode(semanticId, cardinality); - - if (relationshipElement.First.Type == ReferenceTypes.ModelReference) - { - var referenceNode = referenceHelper.ExtractReferenceKeys(relationshipElement.First, $"{semanticId}{SemanticIdResolver.RelationshipElementFirstPostFixSeparator}", cardinality); - if (referenceNode != null) - { - relationshipElementNode.AddChild(referenceNode); - } - } - - if (relationshipElement.Second.Type == ReferenceTypes.ModelReference) - { - var referenceNode = referenceHelper.ExtractReferenceKeys(relationshipElement.Second, $"{semanticId}{SemanticIdResolver.RelationshipElementSecondPostFixSeparator}", cardinality); - if (referenceNode != null) - { - relationshipElementNode.AddChild(referenceNode); - } - } - - return relationshipElementNode; - } - - private SemanticBranchNode ExtractEntity(Entity entity) - { - var semanticId = semanticIdResolver.ResolveElementSemanticId(entity, entity.IdShort!); - var node = new SemanticBranchNode(semanticId, semanticIdResolver.GetCardinality(entity)); - if (entity.EntityType == EntityType.SelfManagedEntity) - { - var globalAssetIdNode = new SemanticLeafNode(semanticId + SemanticIdResolver.EntityGlobalAssetIdPostFix, string.Empty, DataType.String, Cardinality.One); - node.AddChild(globalAssetIdNode); - if (entity.SpecificAssetIds != null) - { - foreach (var specificAssetId in entity.SpecificAssetIds) - { - IHasSemantics specificAsset = specificAssetId; - if (specificAsset.SemanticId == null) - { - continue; - } - - var specificAssetIdNode = new SemanticLeafNode(semanticIdResolver.GetSemanticId(specificAssetId), string.Empty, DataType.String, Cardinality.One); - node.AddChild(specificAssetIdNode); - } - } - } - - if (entity.Statements?.Count > 0) - { - foreach (var child in entity.Statements.Select(ExtractElement).OfType()) - { - node.AddChild(child); - } - } - else - { - logger.LogWarning("No elements defined in Entity {EntityIdShort}", entity.IdShort); - } - - return node; - } - - private SemanticBranchNode? ExtractMultiLanguageProperty(MultiLanguageProperty mlp) - { - var semanticId = semanticIdResolver.ExtractSemanticId(mlp); - var node = new SemanticBranchNode(semanticId, semanticIdResolver.GetCardinality(mlp)); - - var languages = elementHelper.ResolveLanguages(mlp); - - if (mlp.Value is not { Count: > 0 }) - { - logger.LogInformation("No languages defined in template for MultiLanguageProperty {MlpIdShort}", mlp.IdShort); - } - - var mlpSeparator = semanticIdResolver.MlpPostFixSeparator; - foreach (var langSemanticId in languages.Select(language => string.Concat(semanticId, mlpSeparator, language))) - { - node.AddChild(new SemanticLeafNode(langSemanticId, string.Empty, DataType.String, Cardinality.ZeroToOne)); - } - - return node; - } - - private SemanticBranchNode ExtractRange(Range range) - { - var semanticId = semanticIdResolver.ExtractSemanticId(range); - var valueType = semanticIdResolver.GetValueType(range); - var node = new SemanticBranchNode(semanticId, semanticIdResolver.GetCardinality(range)); - - node.AddChild(new SemanticLeafNode(semanticId + SemanticIdResolver.RangeMinimumPostFixSeparator, string.Empty, valueType, Cardinality.ZeroToOne)); - node.AddChild(new SemanticLeafNode(semanticId + SemanticIdResolver.RangeMaximumPostFixSeparator, string.Empty, valueType, Cardinality.ZeroToOne)); + ArgumentNullException.ThrowIfNull(element); - return node; + var handler = handlers.FirstOrDefault(h => h.CanHandle(element)); + return handler != null + ? handler.Extract(element, ExtractElement) + : CreateLeafNode(element); } private SemanticLeafNode CreateLeafNode(ISubmodelElement element) diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFiller.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFiller.cs index 129a1f12..6173551d 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFiller.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFiller.cs @@ -1,22 +1,19 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; using AasCore.Aas3_0; -using File = AasCore.Aas3_0.File; -using Range = AasCore.Aas3_0.Range; - namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.FillOut; public class SubmodelFiller( ISemanticIdResolver semanticIdResolver, ISubmodelElementHelper elementHelper, - IReferenceHelper referenceHelper, + IEnumerable handlers, ILogger logger) : ISubmodelFiller { - public ISubmodel FillOutTemplate(ISubmodel submodelTemplate, SemanticTreeNode values) { ArgumentNullException.ThrowIfNull(submodelTemplate); @@ -79,82 +76,23 @@ private void HandleSingleMatchingNode( submodelTemplate.SubmodelElements?.Add(element); } - private ISubmodelElement FillOutElement(ISubmodelElement submodelElementTemplate, SemanticTreeNode values) + internal ISubmodelElement FillOutElement(ISubmodelElement element, SemanticTreeNode values) { - ArgumentNullException.ThrowIfNull(submodelElementTemplate); + ArgumentNullException.ThrowIfNull(element); ArgumentNullException.ThrowIfNull(values); - switch (submodelElementTemplate) - { - case SubmodelElementCollection collection: - FillOutSubmodelElementCollection(collection, values); - break; - - case SubmodelElementList list: - FillOutSubmodelElementList(list, values); - break; - - case MultiLanguageProperty mlp: - FillOutMultiLanguageProperty(mlp, values); - break; - - case Property property: - FillOutProperty(property, values); - break; - - case File file: - FillOutFile(file, values); - break; - - case Blob blob: - FillOutBlob(blob, values); - break; - - case RelationshipElement relationship: - FillOutRelationshipElement(relationship, values); - break; - - case ReferenceElement reference: - FillOutReferenceElement(reference, values); - break; - - case Range range: - FillOutRange(range, values); - break; - - case Entity entity: - FillOutEntity(entity, values); - break; - - default: - logger.LogError("InValid submodelElementTemplate Type. IdShort : {IdShort}", submodelElementTemplate.IdShort); - throw new InternalDataProcessingException(); - } - - return submodelElementTemplate; - } - - private void FillOutSubmodelElementList(SubmodelElementList list, SemanticTreeNode values) - { - if (list?.Value == null || list.Value.Count == 0) - { - return; - } - - FillOutSubmodelElementValue(list.Value, values, false); - } - - private void FillOutSubmodelElementCollection(SubmodelElementCollection collection, SemanticTreeNode values) - { - if (collection?.Value == null || collection.Value.Count == 0) + var handler = handlers.FirstOrDefault(h => h.CanHandle(element)); + if (handler == null) { - return; + logger.LogError("InValid submodelElementTemplate Type. IdShort : {IdShort}", element.IdShort); + throw new InternalDataProcessingException(); } - FillOutSubmodelElementValue(collection.Value, values); + handler.FillOut(element, values, FillOutSubmodelElementValue); + return element; } - private void FillOutSubmodelElementValue(List elements, SemanticTreeNode values, bool updateIdShort = true) + internal void FillOutSubmodelElementValue(List elements, SemanticTreeNode values, bool updateIdShort) { var originalElements = elements.ToList(); foreach (var element in originalElements) @@ -193,168 +131,8 @@ private void FillOutSubmodelElementValue(List elements, Semant } else { - FillOutElement(element, semanticTreeNodes[0]); + _ = FillOutElement(element, semanticTreeNodes[0]); } } } - - private void FillOutMultiLanguageProperty(MultiLanguageProperty mlp, SemanticTreeNode values) - { - var semanticId = semanticIdResolver.ExtractSemanticId(mlp); - - if (SemanticTreeNavigator.FindNodeBySemanticId(values, semanticId).FirstOrDefault() is not SemanticBranchNode valueNode) - { - logger.LogInformation("No value node found for MultiLanguageProperty {MlpIdShort}", mlp.IdShort); - return; - } - - mlp.Value ??= []; - - var languageValueMap = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var langValue in mlp.Value) - { - languageValueMap[langValue.Language] = (LangStringTextType)langValue; - } - - var languages = elementHelper.ResolveLanguages(mlp); - - var mlpSeparator = semanticIdResolver.MlpPostFixSeparator; - foreach (var language in languages) - { - if (!languageValueMap.TryGetValue(language, out var languageValue)) - { - languageValue = new LangStringTextType(language, string.Empty); - mlp.Value.Add(languageValue); - languageValueMap[language] = languageValue; - - logger.LogInformation("Added language '{Language}' to MultiLanguageProperty {MlpIdShort}", language, mlp.IdShort); - } - - var languageSemanticId = semanticId + mlpSeparator + language; - - var leafNode = valueNode.Children - .OfType() - .FirstOrDefault(child => child.SemanticId.Equals(languageSemanticId, StringComparison.Ordinal)); - - if (leafNode != null) - { - languageValue.Text = leafNode.Value; - } - } - } - - private void FillOutEntity(Entity entity, SemanticTreeNode values) - { - if (entity.EntityType == EntityType.SelfManagedEntity) - { - FillOutSelfManagedEntity(entity, values); - } - - if (entity?.Statements == null || entity.Statements.Count == 0) - { - return; - } - - FillOutSubmodelElementValue(entity.Statements, values); - } - - private void FillOutSelfManagedEntity(Entity entity, SemanticTreeNode values) - { - var semanticId = semanticIdResolver.ResolveElementSemanticId(entity, entity.IdShort!); - - if (SemanticTreeNavigator.FindNodeBySemanticId(values, semanticId).FirstOrDefault() is not SemanticBranchNode valueNode) - { - return; - } - - var globalAssetSemanticId = semanticId + SemanticIdResolver.EntityGlobalAssetIdPostFix; - - var globalAssetNode = valueNode.Children - .OfType() - .FirstOrDefault(c => c.SemanticId == globalAssetSemanticId); - - if (globalAssetNode != null) - { - entity.GlobalAssetId = globalAssetNode.Value; - } - - if (entity.SpecificAssetIds != null) - { - foreach (var specificAssetId in entity.SpecificAssetIds) - { - var specSemanticId = semanticIdResolver.GetSemanticId(specificAssetId); - - var specNode = valueNode.Children - .OfType() - .FirstOrDefault(c => c.SemanticId == specSemanticId); - - if (specNode != null) - { - specificAssetId.Value = specNode.Value; - } - } - } - } - - private static void FillOutProperty(Property valueElement, SemanticTreeNode values) - { - if (values is SemanticLeafNode leafValueNode) - { - valueElement.Value = leafValueNode.Value; - } - } - - private static void FillOutFile(File valueElement, SemanticTreeNode values) - { - if (values is SemanticLeafNode leafValueNode) - { - valueElement.Value = leafValueNode.Value; - } - } - - private static void FillOutBlob(Blob valueElement, SemanticTreeNode values) - { - if (values is SemanticLeafNode leafValueNode) - { - valueElement.Value = Convert.FromBase64String(leafValueNode.Value); - } - } - - private static void FillOutRange(Range valueElement, SemanticTreeNode values) - { - if (values is not SemanticBranchNode branchNode) - { - return; - } - - var leafNodes = branchNode.Children.OfType().ToList(); - - valueElement.Min = leafNodes.FirstOrDefault(n => n.SemanticId - .EndsWith(SemanticIdResolver.RangeMinimumPostFixSeparator, StringComparison.Ordinal))? - .Value; - - valueElement.Max = leafNodes.FirstOrDefault(n => n.SemanticId - .EndsWith(SemanticIdResolver.RangeMaximumPostFixSeparator, StringComparison.Ordinal))? - .Value; - } - - private void FillOutReferenceElement(ReferenceElement referenceElement, SemanticTreeNode semanticNode) - { - if (referenceElement?.Value?.Type != ReferenceTypes.ModelReference) - { - logger.LogInformation("ReferenceElement does not contain a ModelReference for SemanticId '{SemanticId}'. Skipping population.", semanticIdResolver.GetSemanticId(referenceElement!)); - return; - } - - referenceHelper.PopulateReferenceKeys(referenceElement.Value, semanticNode, semanticIdResolver.GetSemanticId(referenceElement)); - } - - private void FillOutRelationshipElement(RelationshipElement relationshipElement, SemanticTreeNode semanticTreeNode) - { - var semanticId = semanticTreeNode.SemanticId; - - referenceHelper.PopulateRelationshipReference(relationshipElement.First, semanticTreeNode, semanticId, SemanticIdResolver.RelationshipElementFirstPostFixSeparator); - - referenceHelper.PopulateRelationshipReference(relationshipElement.Second, semanticTreeNode, semanticId, SemanticIdResolver.RelationshipElementSecondPostFixSeparator); - } } diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigator.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigator.cs index a2a4ab33..69c03914 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigator.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigator.cs @@ -2,7 +2,7 @@ namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; -public class SemanticTreeNavigator +public static class SemanticTreeNavigator { public static IEnumerable FindBranchNodesBySemanticId(SemanticTreeNode tree, string semanticId) { @@ -34,7 +34,7 @@ public static IEnumerable FindNodeBySemanticId(SemanticTreeNod } } - public static bool AreAllNodesOfSameType(List nodes, out Type? nodeType) + public static bool AreAllNodesOfSameType(IList nodes, out Type? nodeType) { if (nodes.Count == 0) { diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelper.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelper.cs index 04d4db4c..069e8b25 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelper.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SubmodelElementHelper.cs @@ -13,7 +13,7 @@ namespace AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository public partial class SubmodelElementHelper(ILogger logger, IOptions mlpSettings) : ISubmodelElementHelper { - private readonly HashSet? _defaultLanguagesSet = mlpSettings.Value.DefaultLanguages != null && mlpSettings.Value.DefaultLanguages.Count > 0 + private readonly HashSet? _defaultLanguagesSet = mlpSettings.Value.DefaultLanguages is { Count: > 0 } ? new HashSet(mlpSettings.Value.DefaultLanguages, StringComparer.OrdinalIgnoreCase) : null; diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelRepositoryService.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelRepositoryService.cs index 093e9161..d2a3384b 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelRepositoryService.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelRepositoryService.cs @@ -1,4 +1,6 @@ -using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using System.Diagnostics; + +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin; @@ -40,13 +42,30 @@ public async Task GetSubmodelElementAsync(string submodelId, s private async Task BuildSubmodelWithValuesAsync(ISubmodel template, string submodelId, CancellationToken cancellationToken) { + var stopwatch1 = new Stopwatch(); + var stopwatch2 = new Stopwatch(); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + stopwatch1.Start(); var semanticIds = semanticIdHandler.Extract(template); + stopwatch1.Stop(); var pluginManifests = pluginManifestConflictHandler.Manifests; var values = await pluginDataHandler.TryGetValuesAsync(pluginManifests, semanticIds, submodelId, cancellationToken).ConfigureAwait(false); - return semanticIdHandler.FillOutTemplate(template, values); + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + stopwatch2.Start(); + var result = semanticIdHandler.FillOutTemplate(template, values); + stopwatch2.Stop(); + + var time = stopwatch1.ElapsedMilliseconds + stopwatch2.ElapsedMilliseconds; + result.Kind = ModellingKind.Instance; + return result; } private static async Task ExecuteWithExceptionHandlingAsync(Func> action) diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/ApplicationDependencyInjectionExtensions.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/ApplicationDependencyInjectionExtensions.cs index 40fe3bda..1c8e58b4 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/ApplicationDependencyInjectionExtensions.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/ApplicationDependencyInjectionExtensions.cs @@ -9,6 +9,7 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRegistry; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Extraction; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.FillOut; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; @@ -38,6 +39,16 @@ public static void ConfigureApplication(this IServiceCollection services, IConfi _ = services.AddScoped(); _ = services.AddScoped(); _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); _ = services.AddScoped(); _ = services.AddScoped(); _ = services.AddScoped(); diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs index e132051e..17533a6d 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs @@ -42,16 +42,13 @@ public static void ConfigureInfrastructure(this IServiceCollection services, ICo var aasEnvironment = configuration.GetSection(AasEnvironmentConfig.Section).Get(); var plugins = configuration.GetSection(PluginConfig.Section).Get(); - _ = services.AddHttpClientWithResilience(configuration, AasEnvironmentConfig.AasEnvironmentRepoHttpClientName, HttpRetryPolicyOptions.TemplateProvider, aasEnvironment?.AasEnvironmentRepositoryBaseUrl!); _ = services.AddHttpClientWithResilience(configuration, AasEnvironmentConfig.AasRegistryHttpClientName, HttpRetryPolicyOptions.TemplateProvider, aasEnvironment?.AasRegistryBaseUrl!); _ = services.AddHttpClientWithResilience(configuration, AasEnvironmentConfig.SubmodelRegistryHttpClientName, HttpRetryPolicyOptions.SubmodelDescriptorProvider, aasEnvironment?.SubModelRegistryBaseUrl!); - _ = services.AddOptions() .Bind(configuration.GetSection(MultiLanguagePropertySettings.Section)) .ValidateOnStart(); _ = services.AddSingleton, MultiLanguagePropertySettingsValidator>(); - foreach (var plugin in plugins.Plugins) { _ = services.AddHttpClientWithResilience(configuration, PluginConfig.HttpClientNamePrefix + plugin.PluginName, HttpRetryPolicyOptions.PluginDataProvider, plugin?.PluginUrl); From 4ad124197915029f48a69f93e5e4984c0d3011a7 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Wed, 18 Mar 2026 23:52:11 +0530 Subject: [PATCH 04/16] Refactor the code to remove warnings --- .../Extraction/SemanticTreeExtractorTests.cs | 35 +++++++------------ .../SemanticId/FillOut/SubmodelFillerTests.cs | 18 ++-------- .../SemanticIdHandlerTests.cs | 4 +-- .../AasRegistry/ShellDescriptorService.cs | 2 +- .../Extraction/SemanticTreeExtractor.cs | 2 +- .../SemanticId/FillOut/SubmodelFiller.cs | 2 +- .../SubmodelRepositoryService.cs | 15 -------- 7 files changed, 18 insertions(+), 60 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs index b81680ef..57bc2546 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs @@ -1,4 +1,4 @@ -using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Extraction; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; @@ -19,30 +19,25 @@ public class SemanticTreeExtractorTests private readonly SemanticTreeExtractor _sut; private readonly ISemanticIdResolver _resolver; private readonly ISubmodelElementHelper _elementHelper; - private readonly ILogger _logger; private readonly List _handlers; public SemanticTreeExtractorTests() { _resolver = Substitute.For(); _elementHelper = Substitute.For(); - _logger = Substitute.For>(); _handlers = []; - _sut = new SemanticTreeExtractor(_resolver, _elementHelper, _handlers, _logger); + _sut = new SemanticTreeExtractor(_resolver, _elementHelper, _handlers); } [Fact] - public void Extract_NullSubmodel_ThrowsArgumentNullException() - { - Throws(() => _sut.Extract(null!)); - } + public void Extract_NullSubmodel_ThrowsArgumentNullException() => Throws(() => _sut.Extract(null!)); [Fact] public void Extract_SubmodelWithNoElements_ReturnsRootNodeWithNoChildren() { var submodel = Substitute.For(); submodel.IdShort.Returns("TestSubmodel"); - submodel.SubmodelElements.Returns(new List()); + submodel.SubmodelElements.Returns([]); _resolver.ResolveSemanticId(submodel, "TestSubmodel").Returns("http://test/root"); var result = _sut.Extract(submodel) as SemanticBranchNode; @@ -58,7 +53,7 @@ public void Extract_SubmodelWithElements_DelegatesToHandlers() var property = new Property(idShort: "Prop", valueType: DataTypeDefXsd.String); var submodel = Substitute.For(); submodel.IdShort.Returns("Test"); - submodel.SubmodelElements.Returns(new List { property }); + submodel.SubmodelElements.Returns([property]); _resolver.ResolveSemanticId(submodel, "Test").Returns("http://test/root"); var handler = Substitute.For(); @@ -81,7 +76,7 @@ public void Extract_ElementWithNoHandler_CreatesLeafNodeFallback() element.IdShort.Returns("UnknownElement"); var submodel = Substitute.For(); submodel.IdShort.Returns("Test"); - submodel.SubmodelElements.Returns(new List { element }); + submodel.SubmodelElements.Returns([element]); _resolver.ResolveSemanticId(submodel, "Test").Returns("http://test/root"); _resolver.ResolveElementSemanticId(element, "UnknownElement").Returns("http://test/unknown"); _resolver.GetValueType(element).Returns(DataType.Unknown); @@ -97,10 +92,7 @@ public void Extract_ElementWithNoHandler_CreatesLeafNodeFallback() } [Fact] - public void Extract_ByIdShortPath_NullSubmodel_ThrowsArgumentNullException() - { - Throws(() => _sut.Extract(null!, "path")); - } + public void Extract_ByIdShortPath_NullSubmodel_ThrowsArgumentNullException() => Throws(() => _sut.Extract(null!, "path")); [Fact] public void Extract_ByIdShortPath_NullPath_ThrowsArgumentNullException() @@ -114,7 +106,7 @@ public void Extract_ByIdShortPath_SingleSegment_ReturnsMatchingElement() { var property = new Property(idShort: "MyProp", valueType: DataTypeDefXsd.String, value: "test"); var submodel = Substitute.For(); - submodel.SubmodelElements.Returns(new List { property }); + submodel.SubmodelElements.Returns([property]); _elementHelper.GetElementByIdShort(Arg.Any>(), "MyProp").Returns(property); var result = _sut.Extract(submodel, "MyProp"); @@ -128,7 +120,7 @@ public void Extract_ByIdShortPath_NestedPath_ReturnsNestedElement() var childProp = new Property(idShort: "ChildProp", valueType: DataTypeDefXsd.String); var collection = new SubmodelElementCollection(idShort: "Parent", value: [childProp]); var submodel = Substitute.For(); - submodel.SubmodelElements.Returns(new List { collection }); + submodel.SubmodelElements.Returns([collection]); _elementHelper.GetElementByIdShort(Arg.Any>(), "Parent").Returns(collection); _elementHelper.GetChildElements(collection).Returns(collection.Value); _elementHelper.GetElementByIdShort(collection.Value, "ChildProp").Returns(childProp); @@ -142,7 +134,7 @@ public void Extract_ByIdShortPath_NestedPath_ReturnsNestedElement() public void Extract_ByIdShortPath_ElementNotFound_ThrowsException() { var submodel = Substitute.For(); - submodel.SubmodelElements.Returns(new List()); + submodel.SubmodelElements.Returns([]); _elementHelper.GetElementByIdShort(Arg.Any>(), "NonExistent").Returns((ISubmodelElement?)null); Throws(() => _sut.Extract(submodel, "NonExistent")); @@ -153,7 +145,7 @@ public void Extract_ByIdShortPath_ChildElementsNull_ThrowsException() { var property = new Property(idShort: "Prop", valueType: DataTypeDefXsd.String); var submodel = Substitute.For(); - submodel.SubmodelElements.Returns(new List { property }); + submodel.SubmodelElements.Returns([property]); _elementHelper.GetElementByIdShort(Arg.Any>(), "Prop").Returns(property); _elementHelper.GetChildElements(property).Returns((IList?)null); @@ -161,10 +153,7 @@ public void Extract_ByIdShortPath_ChildElementsNull_ThrowsException() } [Fact] - public void ExtractElement_NullElement_ThrowsArgumentNullException() - { - Throws(() => _sut.ExtractElement(null!)); - } + public void ExtractElement_NullElement_ThrowsArgumentNullException() => Throws(() => _sut.ExtractElement(null!)); [Fact] public void ExtractElement_HandlerReturnsNull_CreatesFallbackLeaf() diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFillerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFillerTests.cs index b42dd023..bf407fd0 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFillerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFillerTests.cs @@ -1,4 +1,4 @@ -using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.ElementHandlers; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.FillOut; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; @@ -43,7 +43,7 @@ public void FillOutTemplate_NullSubmodel_ThrowsArgumentNullException() public void FillOutTemplate_NullValues_ThrowsArgumentNullException() { var submodel = Substitute.For(); - submodel.SubmodelElements.Returns(new List()); + submodel.SubmodelElements.Returns([]); Throws(() => _sut.FillOutTemplate(submodel, null!)); } @@ -114,18 +114,4 @@ public void FillOutElement_WithMatchingHandler_DelegatesToHandler() handler.Received(1).FillOut(element, values, Arg.Any, SemanticTreeNode, bool>>()); } - - [Fact] - public void FillOutSubmodelElementValue_NoMatchingValueNode_PreservesElements() - { - var property = new Property(idShort: "Prop", valueType: DataTypeDefXsd.String, value: "original"); - var elements = new List { property }; - var values = new SemanticBranchNode("root", Cardinality.Unknown); - _resolver.ExtractSemanticId(property).Returns("http://test/prop"); - - _sut.FillOutSubmodelElementValue(elements, values, false); - - Single(elements); - Equal("original", ((Property)elements[0]).Value); - } } diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs index e032483b..250b98f1 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs @@ -25,14 +25,12 @@ namespace AAS.TwinEngine.DataEngine.UnitTests.ApplicationLogic.Services.Submodel public class SemanticIdHandlerTests { private readonly SemanticIdHandler _sut; - private readonly ILogger _extractorLogger; private readonly ILogger _fillerLogger; private readonly IOptions _mlpSettings; private readonly IOptions _semantics; public SemanticIdHandlerTests() { - _extractorLogger = Substitute.For>(); _fillerLogger = Substitute.For>(); _mlpSettings = Substitute.For>(); _ = _mlpSettings.Value.Returns(new MultiLanguagePropertySettings { DefaultLanguages = null }); @@ -872,7 +870,7 @@ private SemanticIdHandler CreateSut(IOptions semantics, IOptions GetSubmodelElementAsync(string submodelId, s private async Task BuildSubmodelWithValuesAsync(ISubmodel template, string submodelId, CancellationToken cancellationToken) { - var stopwatch1 = new Stopwatch(); - var stopwatch2 = new Stopwatch(); - - GC.Collect(); - GC.WaitForPendingFinalizers(); - GC.Collect(); - stopwatch1.Start(); var semanticIds = semanticIdHandler.Extract(template); - stopwatch1.Stop(); var pluginManifests = pluginManifestConflictHandler.Manifests; var values = await pluginDataHandler.TryGetValuesAsync(pluginManifests, semanticIds, submodelId, cancellationToken).ConfigureAwait(false); - GC.Collect(); - GC.WaitForPendingFinalizers(); - GC.Collect(); - stopwatch2.Start(); var result = semanticIdHandler.FillOutTemplate(template, values); - stopwatch2.Stop(); - var time = stopwatch1.ElapsedMilliseconds + stopwatch2.ElapsedMilliseconds; - result.Kind = ModellingKind.Instance; return result; } From 040b9ca4b2e806345a9ca2df229602dc4b0fcf61 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Thu, 19 Mar 2026 00:17:33 +0530 Subject: [PATCH 05/16] refactor to remove warnings --- .../Extraction/SemanticTreeExtractorTests.cs | 2 -- .../ElementHandlers/EntityHandler.cs | 4 +-- .../SemanticId/FillOut/SubmodelFiller.cs | 29 ++++++++++++++----- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs index 57bc2546..1fc5ae0b 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs @@ -6,8 +6,6 @@ using AasCore.Aas3_0; -using Microsoft.Extensions.Logging; - using NSubstitute; using static Xunit.Assert; diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/EntityHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/EntityHandler.cs index 2e2ed78f..787624ce 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/EntityHandler.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/EntityHandler.cs @@ -1,4 +1,4 @@ -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.SubmodelRepository.SemanticId.Helpers.Interfaces; using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; @@ -63,7 +63,7 @@ public void FillOut(ISubmodelElement element, SemanticTreeNode values, Action
  • elements, Seman var originalElements = elements.ToList(); foreach (var element in originalElements) { - var valueNode = SemanticTreeNavigator.FindNodeBySemanticId(values, semanticIdResolver.ExtractSemanticId(element)); - var semanticTreeNodes = valueNode?.ToList(); + var semanticTreeNodes = GetSemanticNodes(element, values); if (semanticTreeNodes == null || semanticTreeNodes.Count == 0) { continue; } - if (!SemanticTreeNavigator.AreAllNodesOfSameType(semanticTreeNodes, out _)) + if (HasMixedNodeTypes(semanticTreeNodes, element, elements)) { - logger.LogWarning("Mixed node types found for element '{IdShort}' with SemanticId '{SemanticId}'. Expected all nodes to be either SemanticBranchNode or SemanticLeafNode. Removing element.", - element.IdShort, - semanticIdResolver.ExtractSemanticId(element)); - _ = elements.Remove(element); continue; } @@ -135,4 +130,24 @@ internal void FillOutSubmodelElementValue(List elements, Seman } } } + + private List? GetSemanticNodes( ISubmodelElement element, SemanticTreeNode values) + { + var valueNode = SemanticTreeNavigator.FindNodeBySemanticId(values, semanticIdResolver.ExtractSemanticId(element)); + + return valueNode?.ToList(); + } + + private bool HasMixedNodeTypes(List nodes, ISubmodelElement element, List elements) + { + if (SemanticTreeNavigator.AreAllNodesOfSameType(nodes, out _)) + { + return false; + } + + logger.LogWarning("Mixed node types found for element '{IdShort}' with SemanticId '{SemanticId}'. Removing element.", element.IdShort, semanticIdResolver.ExtractSemanticId(element)); + + _ = elements.Remove(element); + return true; + } } From c75f9d62b76664047427fc614de4a68c1e74ea90 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Thu, 19 Mar 2026 00:18:16 +0530 Subject: [PATCH 06/16] remove internal methods test cases --- .../Extraction/SemanticTreeExtractorTests.cs | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs index 1fc5ae0b..1e732063 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/Extraction/SemanticTreeExtractorTests.cs @@ -149,25 +149,4 @@ public void Extract_ByIdShortPath_ChildElementsNull_ThrowsException() Throws(() => _sut.Extract(submodel, "Prop.Child")); } - - [Fact] - public void ExtractElement_NullElement_ThrowsArgumentNullException() => Throws(() => _sut.ExtractElement(null!)); - - [Fact] - public void ExtractElement_HandlerReturnsNull_CreatesFallbackLeaf() - { - var element = Substitute.For(); - element.IdShort.Returns("Test"); - _resolver.ResolveElementSemanticId(element, "Test").Returns("http://test/element"); - _resolver.GetValueType(element).Returns(DataType.String); - _resolver.GetCardinality(element).Returns(Cardinality.ZeroToOne); - - var result = _sut.ExtractElement(element); - - NotNull(result); - var leaf = IsType(result); - Equal("http://test/element", leaf.SemanticId); - Equal(DataType.String, leaf.DataType); - Equal(Cardinality.ZeroToOne, leaf.Cardinality); - } } From 91a1d0e781b51a8ca28c8119daef444d3738ca4a Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Thu, 19 Mar 2026 00:24:26 +0530 Subject: [PATCH 07/16] change dataengine image --- example/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/docker-compose.yml b/example/docker-compose.yml index a7343777..196ce776 100644 --- a/example/docker-compose.yml +++ b/example/docker-compose.yml @@ -23,7 +23,7 @@ services: - twinengine-network twinengine-dataengine: - image: dataengine:1.0.0 + image: ghcr.io/aas-twinengine/dataengine:v1.0.0 container_name: twinengine-dataengine depends_on: dpp-plugin: From 1501c54a9178dba423d28336e13ac06c2afa3fec Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Thu, 19 Mar 2026 00:30:53 +0530 Subject: [PATCH 08/16] remove cognitive complexity inside submodel filler --- example/docker-compose.yml | 2 +- .../SemanticId/FillOut/SubmodelFiller.cs | 49 ++++++++++--------- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/example/docker-compose.yml b/example/docker-compose.yml index 196ce776..1c02d944 100644 --- a/example/docker-compose.yml +++ b/example/docker-compose.yml @@ -23,7 +23,7 @@ services: - twinengine-network twinengine-dataengine: - image: ghcr.io/aas-twinengine/dataengine:v1.0.0 + image: ghcr.io/aas-twinengine/dataengine:v1.0.0 container_name: twinengine-dataengine depends_on: dpp-plugin: diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFiller.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFiller.cs index d16b2b12..1cbb9b21 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFiller.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFiller.cs @@ -99,39 +99,26 @@ internal void FillOutSubmodelElementValue(List elements, Seman { var semanticTreeNodes = GetSemanticNodes(element, values); - if (semanticTreeNodes == null || semanticTreeNodes.Count == 0) + if (ShouldSkipElement(semanticTreeNodes, element, elements)) { continue; } - if (HasMixedNodeTypes(semanticTreeNodes, element, elements)) + if (ShouldCloneElements(semanticTreeNodes, element)) { + ReplaceWithClones(elements, element, semanticTreeNodes, updateIdShort); continue; } - if (semanticTreeNodes.Count > 1 && element is not Property && element is not ReferenceElement) - { - _ = elements.Remove(element); - for (var i = 0; i < semanticTreeNodes.Count; i++) - { - var cloned = elementHelper.CloneElement(element); - if (updateIdShort) - { - cloned.IdShort = $"{cloned.IdShort}{i}"; - } - - _ = FillOutElement(cloned, semanticTreeNodes[i]); - elements.Add(cloned); - } - } - else - { - _ = FillOutElement(element, semanticTreeNodes[0]); - } + _ = FillOutElement(element, semanticTreeNodes[0]); } } - private List? GetSemanticNodes( ISubmodelElement element, SemanticTreeNode values) + private bool ShouldSkipElement(List? nodes, ISubmodelElement element, List elements) => nodes == null || nodes.Count == 0 || HasMixedNodeTypes(nodes, element, elements); + + private static bool ShouldCloneElements(List nodes, ISubmodelElement element) => nodes.Count > 1 && element is not Property && element is not ReferenceElement; + + private List? GetSemanticNodes(ISubmodelElement element, SemanticTreeNode values) { var valueNode = SemanticTreeNavigator.FindNodeBySemanticId(values, semanticIdResolver.ExtractSemanticId(element)); @@ -150,4 +137,22 @@ private bool HasMixedNodeTypes(List nodes, ISubmodelElement el _ = elements.Remove(element); return true; } + + private void ReplaceWithClones(List elements, ISubmodelElement element, List nodes, bool updateIdShort) + { + _ = elements.Remove(element); + + for (var i = 0; i < nodes.Count; i++) + { + var cloned = elementHelper.CloneElement(element); + + if (updateIdShort) + { + cloned.IdShort = $"{cloned.IdShort}{i}"; + } + + _ = FillOutElement(cloned, nodes[i]); + elements.Add(cloned); + } + } } From 30fc78ebbd45cc6df7c32f5d76c27cc257694726 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Thu, 19 Mar 2026 00:32:27 +0530 Subject: [PATCH 09/16] remove unused using --- .../Services/SubmodelRepository/SubmodelRepositoryService.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelRepositoryService.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelRepositoryService.cs index f4e77d61..875b86b2 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelRepositoryService.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelRepositoryService.cs @@ -1,6 +1,4 @@ -using System.Diagnostics; - -using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Application; using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin; From b7ee7c5b72e405943bb2628b76b09e84ac71e059 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Fri, 20 Mar 2026 13:27:55 +0530 Subject: [PATCH 10/16] Added internal semantic id removal logic from submodel qualifiers --- .../ReferenceElementHandlerTests.cs | 6 +- .../RelationshipElementHandlerTests.cs | 4 +- .../SemanticId/FillOut/SubmodelFillerTests.cs | 333 ++++++++++++++++++ .../ReferenceElementHandler.cs | 4 +- .../RelationshipElementHandler.cs | 2 +- .../SemanticId/FillOut/SubmodelFiller.cs | 48 ++- .../Helpers/Interfaces/ISemanticIdResolver.cs | 2 + .../SemanticId/Helpers/SemanticIdResolver.cs | 6 +- .../Helpers/SemanticTreeNavigator.cs | 4 +- .../SubmodelRepository/SemanticIdHandler.cs | 2 +- 10 files changed, 388 insertions(+), 23 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ReferenceElementHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ReferenceElementHandlerTests.cs index e56a58d0..5fb61b36 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ReferenceElementHandlerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ReferenceElementHandlerTests.cs @@ -90,7 +90,7 @@ public void FillOut_WithModelReference_DelegatesToReferenceHelper() var modelRef = new Reference(ReferenceTypes.ModelReference, [new Key(KeyTypes.Submodel, "")]); var refElement = new ReferenceElement(idShort: "Test", value: modelRef); - _resolver.GetSemanticId(refElement).Returns("http://test/ref"); + _resolver.ExtractSemanticId(refElement).Returns("http://test/ref"); var values = new SemanticBranchNode("http://test/ref", Cardinality.One); @@ -103,7 +103,7 @@ public void FillOut_WithModelReference_DelegatesToReferenceHelper() public void FillOut_WithNullValue_LogsInfoAndSkips() { var refElement = new ReferenceElement(idShort: "Test", value: null); - _resolver.GetSemanticId(refElement).Returns("http://test/ref"); + _resolver.ExtractSemanticId(refElement).Returns("http://test/ref"); var values = new SemanticBranchNode("http://test/ref", Cardinality.One); @@ -119,7 +119,7 @@ public void FillOut_WithExternalReference_LogsInfoAndSkips() var externalRef = new Reference(ReferenceTypes.ExternalReference, [new Key(KeyTypes.GlobalReference, "http://external")]); var refElement = new ReferenceElement(idShort: "Test", value: externalRef); - _resolver.GetSemanticId(refElement).Returns("http://test/ref"); + _resolver.ExtractSemanticId(refElement).Returns("http://test/ref"); var values = new SemanticBranchNode("http://test/ref", Cardinality.One); diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RelationshipElementHandlerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RelationshipElementHandlerTests.cs index 27f40c61..655fda68 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RelationshipElementHandlerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/RelationshipElementHandlerTests.cs @@ -64,7 +64,7 @@ public void Extract_FirstModelReference_ExtractsFirstAndDelegatesToReferenceHelp var firstRef = new Reference(ReferenceTypes.ModelReference, [new Key(KeyTypes.Submodel, "sub")]); var secondRef = new Reference(ReferenceTypes.ExternalReference, [new Key(KeyTypes.GlobalReference, "ext")]); var rel = new RelationshipElement(first: firstRef, second: secondRef, idShort: "Test"); - _resolver.GetSemanticId(rel).Returns("http://test/rel"); + _resolver.ExtractSemanticId(rel).Returns("http://test/rel"); _resolver.GetCardinality(rel).Returns(Cardinality.One); var firstNode = new SemanticBranchNode("http://test/rel_first", Cardinality.One); @@ -88,7 +88,7 @@ public void Extract_BothModelReferences_ExtractsBoth() var firstRef = new Reference(ReferenceTypes.ModelReference, [new Key(KeyTypes.Submodel, "sub1")]); var secondRef = new Reference(ReferenceTypes.ModelReference, [new Key(KeyTypes.Submodel, "sub2")]); var rel = new RelationshipElement(first: firstRef, second: secondRef, idShort: "Test"); - _resolver.GetSemanticId(rel).Returns("http://test/rel"); + _resolver.ExtractSemanticId(rel).Returns("http://test/rel"); _resolver.GetCardinality(rel).Returns(Cardinality.One); var firstNode = new SemanticBranchNode("http://test/rel_first", Cardinality.One); diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFillerTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFillerTests.cs index bf407fd0..f6741025 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFillerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticId/FillOut/SubmodelFillerTests.cs @@ -114,4 +114,337 @@ public void FillOutElement_WithMatchingHandler_DelegatesToHandler() handler.Received(1).FillOut(element, values, Arg.Any, SemanticTreeNode, bool>>()); } + + [Fact] + public void FillOutTemplate_RemovesInternalSemanticIdQualifier_FromProperty() + { + var property = new Property( + idShort: "Prop", + valueType: DataTypeDefXsd.String, + qualifiers: [ + new Qualifier(type: "InternalSemanticId", valueType: DataTypeDefXsd.String, value: "http://test/internal") + ]); + var submodel = Substitute.For(); + var elements = new List { property }; + submodel.SubmodelElements.Returns(elements); + _resolver.ExtractSemanticId(property).Returns("http://test/prop"); + _resolver.InternalSemanticIdType.Returns("InternalSemanticId"); + + var values = new SemanticBranchNode("root", Cardinality.Unknown); + + _sut.FillOutTemplate(submodel, values); + + Empty(property.Qualifiers!); + } + + [Fact] + public void FillOutTemplate_PreservesNonInternalQualifiers_WhenRemovingInternalSemanticId() + { + var property = new Property( + idShort: "Prop", + valueType: DataTypeDefXsd.String, + qualifiers: [ + new Qualifier(type: "ExternalReference", valueType: DataTypeDefXsd.String, value: "ZeroToOne"), + new Qualifier(type: "InternalSemanticId", valueType: DataTypeDefXsd.String, value: "http://test/internal") + ]); + var submodel = Substitute.For(); + var elements = new List { property }; + submodel.SubmodelElements.Returns(elements); + _resolver.ExtractSemanticId(property).Returns("http://test/prop"); + _resolver.InternalSemanticIdType.Returns("InternalSemanticId"); + + var values = new SemanticBranchNode("root", Cardinality.Unknown); + + _sut.FillOutTemplate(submodel, values); + + Single(property.Qualifiers!); + Equal("ExternalReference", property.Qualifiers[0].Type); + } + + [Fact] + public void FillOutTemplate_RemovesInternalSemanticIdQualifier_FromNestedCollection() + { + var innerProperty = new Property( + idShort: "InnerProp", + valueType: DataTypeDefXsd.String, + qualifiers: [ + new Qualifier(type: "InternalSemanticId", valueType: DataTypeDefXsd.String, value: "http://test/inner") + ]); + var collection = new SubmodelElementCollection( + idShort: "Collection", + qualifiers: [ + new Qualifier(type: "InternalSemanticId", valueType: DataTypeDefXsd.String, value: "http://test/collection") + ], + value: [innerProperty]); + + var submodel = Substitute.For(); + var elements = new List { collection }; + submodel.SubmodelElements.Returns(elements); + _resolver.ExtractSemanticId(collection).Returns("http://test/collection"); + _resolver.InternalSemanticIdType.Returns("InternalSemanticId"); + + var values = new SemanticBranchNode("root", Cardinality.Unknown); + + _sut.FillOutTemplate(submodel, values); + + Empty(collection.Qualifiers!); + Empty(innerProperty.Qualifiers!); + } + + [Fact] + public void FillOutTemplate_RemovesInternalSemanticIdQualifier_FromNestedSubmodelElementList() + { + var innerProperty = new Property( + idShort: "ListItem", + valueType: DataTypeDefXsd.String, + qualifiers: [ + new Qualifier(type: "InternalSemanticId", valueType: DataTypeDefXsd.String, value: "http://test/item") + ]); + var list = new SubmodelElementList( + AasSubmodelElements.Property, + idShort: "List", + qualifiers: [ + new Qualifier(type: "InternalSemanticId", valueType: DataTypeDefXsd.String, value: "http://test/list") + ], + value: [innerProperty]); + + var submodel = Substitute.For(); + var elements = new List { list }; + submodel.SubmodelElements.Returns(elements); + _resolver.ExtractSemanticId(list).Returns("http://test/list"); + _resolver.InternalSemanticIdType.Returns("InternalSemanticId"); + + var values = new SemanticBranchNode("root", Cardinality.Unknown); + + _sut.FillOutTemplate(submodel, values); + + Empty(list.Qualifiers!); + Empty(innerProperty.Qualifiers!); + } + + [Fact] + public void FillOutTemplate_RemovesInternalSemanticIdQualifier_FromNestedEntity() + { + var statement = new Property( + idShort: "Statement", + valueType: DataTypeDefXsd.String, + qualifiers: [ + new Qualifier(type: "InternalSemanticId", valueType: DataTypeDefXsd.String, value: "http://test/statement") + ]); + var entity = new Entity( + EntityType.CoManagedEntity, + idShort: "Entity", + qualifiers: [ + new Qualifier(type: "InternalSemanticId", valueType: DataTypeDefXsd.String, value: "http://test/entity") + ], + statements: [statement]); + + var submodel = Substitute.For(); + var elements = new List { entity }; + submodel.SubmodelElements.Returns(elements); + _resolver.ExtractSemanticId(entity).Returns("http://test/entity"); + _resolver.InternalSemanticIdType.Returns("InternalSemanticId"); + + var values = new SemanticBranchNode("root", Cardinality.Unknown); + + _sut.FillOutTemplate(submodel, values); + + Empty(entity.Qualifiers!); + Empty(statement.Qualifiers!); + } + + [Fact] + public void FillOutTemplate_ElementWithNoQualifiers_DoesNotThrow() + { + var property = new Property(idShort: "Prop", valueType: DataTypeDefXsd.String); + var submodel = Substitute.For(); + var elements = new List { property }; + submodel.SubmodelElements.Returns(elements); + _resolver.ExtractSemanticId(property).Returns("http://test/prop"); + _resolver.InternalSemanticIdType.Returns("InternalSemanticId"); + + var values = new SemanticBranchNode("root", Cardinality.Unknown); + + var result = _sut.FillOutTemplate(submodel, values); + + NotNull(result); + Null(property.Qualifiers); + } + + [Fact] + public void FillOutTemplate_ElementWithOnlyNonInternalQualifiers_PreservesAll() + { + var property = new Property( + idShort: "Prop", + valueType: DataTypeDefXsd.String, + qualifiers: [ + new Qualifier(type: "ExternalReference", valueType: DataTypeDefXsd.String, value: "ZeroToOne"), + new Qualifier(type: "OtherQualifier", valueType: DataTypeDefXsd.String, value: "SomeValue") + ]); + var submodel = Substitute.For(); + var elements = new List { property }; + submodel.SubmodelElements.Returns(elements); + _resolver.ExtractSemanticId(property).Returns("http://test/prop"); + _resolver.InternalSemanticIdType.Returns("InternalSemanticId"); + + var values = new SemanticBranchNode("root", Cardinality.Unknown); + + _sut.FillOutTemplate(submodel, values); + + Equal(2, property.Qualifiers!.Count); + } + + [Fact] + public void FillOutTemplate_DeeplyNestedElements_RemovesInternalSemanticIdAtAllLevels() + { + var deepProperty = new Property( + idShort: "DeepProp", + valueType: DataTypeDefXsd.String, + qualifiers: [ + new Qualifier(type: "InternalSemanticId", valueType: DataTypeDefXsd.String, value: "http://test/deep") + ]); + var innerCollection = new SubmodelElementCollection( + idShort: "InnerCollection", + qualifiers: [ + new Qualifier(type: "InternalSemanticId", valueType: DataTypeDefXsd.String, value: "http://test/inner") + ], + value: [deepProperty]); + var outerCollection = new SubmodelElementCollection( + idShort: "OuterCollection", + qualifiers: [ + new Qualifier(type: "InternalSemanticId", valueType: DataTypeDefXsd.String, value: "http://test/outer") + ], + value: [innerCollection]); + + var submodel = Substitute.For(); + var elements = new List { outerCollection }; + submodel.SubmodelElements.Returns(elements); + _resolver.ExtractSemanticId(outerCollection).Returns("http://test/outer"); + _resolver.InternalSemanticIdType.Returns("InternalSemanticId"); + + var values = new SemanticBranchNode("root", Cardinality.Unknown); + + _sut.FillOutTemplate(submodel, values); + + Empty(outerCollection.Qualifiers!); + Empty(innerCollection.Qualifiers!); + Empty(deepProperty.Qualifiers!); + } + + [Fact] + public void FillOutTemplate_MultipleElementsWithInternalSemanticId_RemovesFromAll() + { + var prop1 = new Property( + idShort: "Prop1", + valueType: DataTypeDefXsd.String, + qualifiers: [ + new Qualifier(type: "InternalSemanticId", valueType: DataTypeDefXsd.String, value: "http://test/1") + ]); + var prop2 = new Property( + idShort: "Prop2", + valueType: DataTypeDefXsd.String, + qualifiers: [ + new Qualifier(type: "InternalSemanticId", valueType: DataTypeDefXsd.String, value: "http://test/2") + ]); + var submodel = Substitute.For(); + var elements = new List { prop1, prop2 }; + submodel.SubmodelElements.Returns(elements); + _resolver.ExtractSemanticId(prop1).Returns("http://test/1"); + _resolver.ExtractSemanticId(prop2).Returns("http://test/2"); + _resolver.InternalSemanticIdType.Returns("InternalSemanticId"); + + var values = new SemanticBranchNode("root", Cardinality.Unknown); + + _sut.FillOutTemplate(submodel, values); + + Empty(prop1.Qualifiers!); + Empty(prop2.Qualifiers!); + } + + [Fact] + public void FillOutTemplate_TwoQualifiers_RemovesOnlyInternalSemanticId() + { + var property = new Property( + idShort: "Prop", + valueType: DataTypeDefXsd.String, + qualifiers: [ + new Qualifier(type: "InternalSemanticId", valueType: DataTypeDefXsd.String, value: "http://test/internal"), + new Qualifier(type: "ExternalReference", valueType: DataTypeDefXsd.String, value: "ZeroToOne") + ]); + var submodel = Substitute.For(); + var elements = new List { property }; + submodel.SubmodelElements.Returns(elements); + _resolver.ExtractSemanticId(property).Returns("http://test/prop"); + _resolver.InternalSemanticIdType.Returns("InternalSemanticId"); + + var values = new SemanticBranchNode("root", Cardinality.Unknown); + + _sut.FillOutTemplate(submodel, values); + + Single(property.Qualifiers!); + Equal("ExternalReference", property.Qualifiers[0].Type); + Equal("ZeroToOne", property.Qualifiers[0].Value); + DoesNotContain(property.Qualifiers, q => q.Type == "InternalSemanticId"); + } + + [Fact] + public void FillOutTemplate_ReferenceElementWithTwoQualifiers_RemovesOnlyInternalSemanticId() + { + var refElement = new ReferenceElement( + idShort: "RefElement", + qualifiers: [ + new Qualifier(type: "InternalSemanticId", valueType: DataTypeDefXsd.String, value: "http://test/ref-internal"), + new Qualifier(type: "ExternalReference", valueType: DataTypeDefXsd.String, value: "ZeroToOne") + ], + value: new Reference( + ReferenceTypes.ExternalReference, + [new Key(KeyTypes.GlobalReference, "http://example.com/ref")])); + + var submodel = Substitute.For(); + var elements = new List { refElement }; + submodel.SubmodelElements.Returns(elements); + _resolver.ExtractSemanticId(refElement).Returns("http://test/ref"); + _resolver.InternalSemanticIdType.Returns("InternalSemanticId"); + + var values = new SemanticBranchNode("root", Cardinality.Unknown); + + _sut.FillOutTemplate(submodel, values); + + Single(refElement.Qualifiers!); + Equal("ExternalReference", refElement.Qualifiers![0].Type); + Equal("ZeroToOne", refElement.Qualifiers[0].Value); + DoesNotContain(refElement.Qualifiers, q => q.Type == "InternalSemanticId"); + } + + [Fact] + public void FillOutTemplate_RelationshipElementWithTwoQualifiers_RemovesOnlyInternalSemanticId() + { + var relationship = new RelationshipElement( + first: new Reference( + ReferenceTypes.ExternalReference, + [new Key(KeyTypes.GlobalReference, "http://example.com/first")]), + second: new Reference( + ReferenceTypes.ExternalReference, + [new Key(KeyTypes.GlobalReference, "http://example.com/second")]), + idShort: "RelElement", + qualifiers: [ + new Qualifier(type: "InternalSemanticId", valueType: DataTypeDefXsd.String, value: "http://test/rel-internal"), + new Qualifier(type: "ExternalReference", valueType: DataTypeDefXsd.String, value: "One") + ]); + + var submodel = Substitute.For(); + var elements = new List { relationship }; + submodel.SubmodelElements.Returns(elements); + _resolver.ExtractSemanticId(relationship).Returns("http://test/rel"); + _resolver.InternalSemanticIdType.Returns("InternalSemanticId"); + + var values = new SemanticBranchNode("root", Cardinality.Unknown); + + _sut.FillOutTemplate(submodel, values); + + Single(relationship.Qualifiers!); + Equal("ExternalReference", relationship.Qualifiers![0].Type); + Equal("One", relationship.Qualifiers[0].Value); + DoesNotContain(relationship.Qualifiers, q => q.Type == "InternalSemanticId"); + } } diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ReferenceElementHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ReferenceElementHandler.cs index cd195553..5491caf9 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ReferenceElementHandler.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/ElementHandlers/ReferenceElementHandler.cs @@ -33,10 +33,10 @@ public void FillOut(ISubmodelElement element, SemanticTreeNode values, Action
  • matchingNodes, - ISubmodelElement baseElement, - ISubmodel submodelTemplate) + private void RemoveInternalSemanticIdQualifiers(IEnumerable? elements) + { + if (elements == null) + { + return; + } + + foreach (var element in elements) + { + if (element.Qualifiers != null) + { + var internalQualifiers = element.Qualifiers + .Where(q => q.Type == semanticIdResolver.InternalSemanticIdType) + .ToList(); + + foreach (var qualifier in internalQualifiers) + { + _ = element.Qualifiers.Remove(qualifier); + } + } + + switch (element) + { + case SubmodelElementCollection collection: + RemoveInternalSemanticIdQualifiers(collection.Value); + break; + case SubmodelElementList list: + RemoveInternalSemanticIdQualifiers(list.Value); + break; + case Entity entity: + RemoveInternalSemanticIdQualifiers(entity.Statements); + break; + } + } + } + + private void HandleMultipleMatchingNodes(List matchingNodes, ISubmodelElement baseElement, ISubmodel submodelTemplate) { for (var i = 0; i < matchingNodes.Count; i++) { @@ -67,10 +102,7 @@ private void HandleMultipleMatchingNodes( } } - private void HandleSingleMatchingNode( - SemanticTreeNode node, - ISubmodelElement element, - ISubmodel submodelTemplate) + private void HandleSingleMatchingNode(SemanticTreeNode node, ISubmodelElement element, ISubmodel submodelTemplate) { _ = FillOutElement(element, node); submodelTemplate.SubmodelElements?.Add(element); diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/Interfaces/ISemanticIdResolver.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/Interfaces/ISemanticIdResolver.cs index 379365ac..b664d33c 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/Interfaces/ISemanticIdResolver.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/Interfaces/ISemanticIdResolver.cs @@ -8,6 +8,8 @@ public interface ISemanticIdResolver { string MlpPostFixSeparator { get; } + string InternalSemanticIdType { get; } + string GetSemanticId(IHasSemantics hasSemantics); string ExtractSemanticId(ISubmodelElement element); diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolver.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolver.cs index eb9990a6..df91ff9e 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolver.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticIdResolver.cs @@ -20,12 +20,12 @@ public partial class SemanticIdResolver(IOptions semantics) : ISemant public const string EntityGlobalAssetIdPostFix = "_globalAssetId"; public const string RelationshipElementFirstPostFixSeparator = "_first"; public const string RelationshipElementSecondPostFixSeparator = "_second"; - - private readonly string _internalSemanticId = semantics.Value.InternalSemanticId; private readonly string _submodelElementIndexContextPrefix = semantics.Value.SubmodelElementIndexContextPrefix; public string MlpPostFixSeparator { get; } = semantics.Value.MultiLanguageSemanticPostfixSeparator; + public string InternalSemanticIdType { get; } = semantics.Value.InternalSemanticId; + private static readonly HashSet StringTypes = [ DataTypeDefXsd.String, DataTypeDefXsd.AnyUri, DataTypeDefXsd.Byte, DataTypeDefXsd.Date, @@ -56,7 +56,7 @@ public string ExtractSemanticId(ISubmodelElement element) return GetSemanticId(element); } - var qualifier = element.Qualifiers.FirstOrDefault(q => q.Type == _internalSemanticId); + var qualifier = element.Qualifiers.FirstOrDefault(q => q.Type == InternalSemanticIdType); return qualifier != null ? qualifier.Value! : GetSemanticId(element); } diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigator.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigator.cs index 69c03914..aa20ac5f 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigator.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticId/Helpers/SemanticTreeNavigator.cs @@ -8,9 +8,7 @@ public static IEnumerable FindBranchNodesBySemanticId(Semantic { var node = tree as SemanticBranchNode; - return node?.Children! - .Where(child => child.SemanticId.Equals(semanticId, StringComparison.Ordinal)) - ?? []; + return node?.Children!.Where(child => child.SemanticId.Equals(semanticId, StringComparison.Ordinal)) ?? []; } public static IEnumerable FindNodeBySemanticId(SemanticTreeNode tree, string semanticId) diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandler.cs index 8711f051..50f41348 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandler.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandler.cs @@ -15,4 +15,4 @@ public class SemanticIdHandler( public ISubmodelElement Extract(ISubmodel submodelTemplate, string idShortPath) => extractor.Extract(submodelTemplate, idShortPath); public ISubmodel FillOutTemplate(ISubmodel submodelTemplate, SemanticTreeNode values) => filler.FillOutTemplate(submodelTemplate, values); -} +} \ No newline at end of file From c8df6feccde61232d5ddfefa05706fda5a4cdd38 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Fri, 20 Mar 2026 13:59:18 +0530 Subject: [PATCH 11/16] remove unused var --- .../Services/SubmodelRepository/SubmodelRepositoryService.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelRepositoryService.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelRepositoryService.cs index 875b86b2..5a69ef25 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelRepositoryService.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelRepositoryService.cs @@ -48,9 +48,7 @@ private async Task BuildSubmodelWithValuesAsync(ISubmodel template, s var values = await pluginDataHandler.TryGetValuesAsync(pluginManifests, semanticIds, submodelId, cancellationToken).ConfigureAwait(false); - var result = semanticIdHandler.FillOutTemplate(template, values); - - return result; + return semanticIdHandler.FillOutTemplate(template, values); } private static async Task ExecuteWithExceptionHandlingAsync(Func> action) From dd01f18b4de3166c9a94c4d6619097c8fe33a7f4 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Fri, 20 Mar 2026 14:45:03 +0530 Subject: [PATCH 12/16] remove internal semantic from test data --- ...ntactInfo_ContactInformation_Expected.json | 18 +----------------- .../GetSubmodel_ContactInfo_Expected.json | 19 ++----------------- .../GetSubmodel_Nameplate_Expected.json | 18 +----------------- 3 files changed, 4 insertions(+), 51 deletions(-) diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json index b55a8eeb..b481d47c 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json @@ -64,29 +64,13 @@ } ] }, - "qualifiers": [ + "qualifiers": { "kind": "ConceptQualifier", "type": "Multiplicity", "valueType": "xs:string", "value": "ZeroToOne" }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/AddressOfAdditionalLink" - } - ], "valueType": "xs:string", "value": "https://www.mm-software.com/more-the-newsroom/", "modelType": "Property" diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json index 656b7c2a..6f55a3dc 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json @@ -78,29 +78,14 @@ } ] }, - "qualifiers": [ + "qualifiers": { "kind": "ConceptQualifier", "type": "Multiplicity", "valueType": "xs:string", "value": "ZeroToOne" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/AddressOfAdditionalLink" } - ], + , "valueType": "xs:string", "value": "https://www.mm-software.com/mobile-arbeitsmaschinen/", "modelType": "Property" diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json index 4a5ed6c2..824cf788 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json @@ -1312,7 +1312,7 @@ } ] }, - "qualifiers": [ + "qualifiers": { "semanticId": { "type": "ExternalReference", @@ -1328,22 +1328,6 @@ "valueType": "xs:string", "value": "ZeroToMany" }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/AssetSpecificProperties/ArbitraryMLP" - } - ], "value": [ { "language": "en", From f2b936214757853082f3fc3d72fe68f0699fd622 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Fri, 20 Mar 2026 14:49:07 +0530 Subject: [PATCH 13/16] Revert "remove internal semantic from test data" This reverts commit dd01f18b4de3166c9a94c4d6619097c8fe33a7f4. --- ...ntactInfo_ContactInformation_Expected.json | 18 +++++++++++++++++- .../GetSubmodel_ContactInfo_Expected.json | 19 +++++++++++++++++-- .../GetSubmodel_Nameplate_Expected.json | 18 +++++++++++++++++- 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json index b481d47c..b55a8eeb 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json @@ -64,13 +64,29 @@ } ] }, - "qualifiers": + "qualifiers": [ { "kind": "ConceptQualifier", "type": "Multiplicity", "valueType": "xs:string", "value": "ZeroToOne" }, + { + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://mm-software.com/twinengine/qualifier" + } + ] + }, + "kind": "TemplateQualifier", + "type": "InternalSemanticId", + "valueType": "xs:string", + "value": "https://mm-software.com/AddressOfAdditionalLink" + } + ], "valueType": "xs:string", "value": "https://www.mm-software.com/more-the-newsroom/", "modelType": "Property" diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json index 6f55a3dc..656b7c2a 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json @@ -78,14 +78,29 @@ } ] }, - "qualifiers": + "qualifiers": [ { "kind": "ConceptQualifier", "type": "Multiplicity", "valueType": "xs:string", "value": "ZeroToOne" + }, + { + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://mm-software.com/twinengine/qualifier" + } + ] + }, + "kind": "TemplateQualifier", + "type": "InternalSemanticId", + "valueType": "xs:string", + "value": "https://mm-software.com/AddressOfAdditionalLink" } - , + ], "valueType": "xs:string", "value": "https://www.mm-software.com/mobile-arbeitsmaschinen/", "modelType": "Property" diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json index 824cf788..4a5ed6c2 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json @@ -1312,7 +1312,7 @@ } ] }, - "qualifiers": + "qualifiers": [ { "semanticId": { "type": "ExternalReference", @@ -1328,6 +1328,22 @@ "valueType": "xs:string", "value": "ZeroToMany" }, + { + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://mm-software.com/twinengine/qualifier" + } + ] + }, + "kind": "TemplateQualifier", + "type": "InternalSemanticId", + "valueType": "xs:string", + "value": "https://mm-software.com/AssetSpecificProperties/ArbitraryMLP" + } + ], "value": [ { "language": "en", From 60432c8749345d2fa7986a140a9f61a6ea92869d Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Fri, 20 Mar 2026 14:50:26 +0530 Subject: [PATCH 14/16] remove internal semantic id from qualifires in testdata --- ..._ContactInfo_ContactInformation_Expected.json | 15 --------------- .../GetSubmodel_ContactInfo_Expected.json | 15 --------------- .../TestData/GetSubmodel_Nameplate_Expected.json | 16 +--------------- 3 files changed, 1 insertion(+), 45 deletions(-) diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json index b55a8eeb..c77d421e 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json @@ -70,21 +70,6 @@ "type": "Multiplicity", "valueType": "xs:string", "value": "ZeroToOne" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/AddressOfAdditionalLink" } ], "valueType": "xs:string", diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json index 656b7c2a..225cb784 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json @@ -84,21 +84,6 @@ "type": "Multiplicity", "valueType": "xs:string", "value": "ZeroToOne" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/AddressOfAdditionalLink" } ], "valueType": "xs:string", diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json index 4a5ed6c2..9bd42c47 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json @@ -6,6 +6,7 @@ "text": "Contains the nameplate information attached to the product" } ], + "administration": { "administration": { "version": "3", "revision": "0" @@ -1327,21 +1328,6 @@ "type": "SMT/Cardinality", "valueType": "xs:string", "value": "ZeroToMany" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/AssetSpecificProperties/ArbitraryMLP" } ], "value": [ From 54fe06ddca23adb70f8913c592c7828c91d2fa36 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Fri, 20 Mar 2026 15:32:42 +0530 Subject: [PATCH 15/16] remove internal semantic ids from testdata of playwrite tests --- ...ntactInfo_ContactInformation_Expected.json | 60 -------- .../GetSubmodel_ContactInfo_Expected.json | 135 ------------------ .../GetSubmodel_Nameplate_Expected.json | 45 ------ 3 files changed, 240 deletions(-) diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json index c77d421e..2e628d84 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodelElement_ContactInfo_ContactInformation_Expected.json @@ -356,21 +356,6 @@ "type": "Multiplicity", "valueType": "xs:string", "value": "One" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/ipCommunication/AddressOfAdditionalLink" } ], "valueType": "xs:string", @@ -417,21 +402,6 @@ "type": "Multiplicity", "valueType": "xs:string", "value": "ZeroToOne" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/IPCommunication/AvailableTime" } ], "value": [ @@ -515,21 +485,6 @@ "type": "Multiplicity", "valueType": "xs:string", "value": "ZeroToOne" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/Phone/AvailableTime" } ], "value": [ @@ -642,21 +597,6 @@ "type": "Multiplicity", "valueType": "xs:string", "value": "ZeroToOne" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/Phone/AvailableTime" } ], "value": [ diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json index 225cb784..5a66e771 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_ContactInfo_Expected.json @@ -370,21 +370,6 @@ "type": "Multiplicity", "valueType": "xs:string", "value": "One" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/ipCommunication/AddressOfAdditionalLink" } ], "valueType": "xs:string", @@ -431,21 +416,6 @@ "type": "Multiplicity", "valueType": "xs:string", "value": "ZeroToOne" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/IPCommunication/AvailableTime" } ], "value": [ @@ -529,21 +499,6 @@ "type": "Multiplicity", "valueType": "xs:string", "value": "ZeroToOne" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/Phone/AvailableTime" } ], "value": [ @@ -656,21 +611,6 @@ "type": "Multiplicity", "valueType": "xs:string", "value": "ZeroToOne" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/Phone/AvailableTime" } ], "value": [ @@ -788,21 +728,6 @@ "type": "Multiplicity", "valueType": "xs:string", "value": "ZeroToOne" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/AddressOfAdditionalLink" } ], "valueType": "xs:string", @@ -1089,21 +1014,6 @@ "type": "Multiplicity", "valueType": "xs:string", "value": "One" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/ipCommunication/AddressOfAdditionalLink" } ], "valueType": "xs:string", @@ -1150,21 +1060,6 @@ "type": "Multiplicity", "valueType": "xs:string", "value": "ZeroToOne" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/IPCommunication/AvailableTime" } ], "value": [ @@ -1248,21 +1143,6 @@ "type": "Multiplicity", "valueType": "xs:string", "value": "ZeroToOne" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/Phone/AvailableTime" } ], "value": [ @@ -1375,21 +1255,6 @@ "type": "Multiplicity", "valueType": "xs:string", "value": "ZeroToOne" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/Phone/AvailableTime" } ], "value": [ diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json index 9bd42c47..ca8527d6 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json @@ -1378,21 +1378,6 @@ "type": "SMT/Cardinality", "valueType": "xs:string", "value": "ZeroToMany" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "ConceptQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/AssetSpecificProperties/ArbitraryFile" } ], "value": "https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf", @@ -1576,21 +1561,6 @@ "type": "SMT/Cardinality", "valueType": "xs:string", "value": "ZeroToMany" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "ConceptQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/GuidelineSpecificProperties/ArbitraryFile" } ], "value": "https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/checkmark.png", @@ -1637,21 +1607,6 @@ "type": "SMT/Cardinality", "valueType": "xs:string", "value": "ZeroToMany" - }, - { - "semanticId": { - "type": "ExternalReference", - "keys": [ - { - "type": "GlobalReference", - "value": "https://mm-software.com/twinengine/qualifier" - } - ] - }, - "kind": "TemplateQualifier", - "type": "InternalSemanticId", - "valueType": "xs:string", - "value": "https://mm-software.com/GuidelineSpecificProperties/ArbitraryMLP" } ], "value": [ From d4149721cf3bebdd4d07f76abfa003cd049d5a51 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Fri, 20 Mar 2026 15:42:20 +0530 Subject: [PATCH 16/16] remove extra property added in testdata json --- .../TestData/GetSubmodel_Nameplate_Expected.json | 1 - 1 file changed, 1 deletion(-) diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json index ca8527d6..3b37fde8 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.PlaywrightTests/SubmodelRepository/TestData/GetSubmodel_Nameplate_Expected.json @@ -6,7 +6,6 @@ "text": "Contains the nameplate information attached to the product" } ], - "administration": { "administration": { "version": "3", "revision": "0"