From eb6ba2ef3b6d4bb8b7fee59f4bfea82d45e58eb2 Mon Sep 17 00:00:00 2001 From: Kevalkumar Date: Thu, 5 Mar 2026 14:45:09 +0530 Subject: [PATCH 01/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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 6f2f922c8a6fcb9468866cb5326ece04e4a5c173 Mon Sep 17 00:00:00 2001 From: Hardi Shah Date: Wed, 25 Mar 2026 10:40:53 +0530 Subject: [PATCH 10/14] Enhance semantic logic to handle same antacId for different Types of Node Expanded HandoverDocumentation.xml to support richer, standards-based, multi-language metadata for document handover, including new properties, preview files, and concept descriptions aligned with VDI 2770 and IEC 61360. Updated database schema to model languages and many-to-many relationships between document versions and languages. Refactored semantic tree handling logic for type-safe node selection, removed mixed-type skipping, and improved unit tests to ensure correct handling of branch/leaf node ambiguities and multi-language scenarios. --- example/aas/HandoverDocumentation.xml | 6647 ++++++++--------- .../postgres/04_handoverdocumentation.sql.inc | 63 +- .../SemanticId/FillOut/SubmodelFillerTests.cs | 111 + .../Helpers/SemanticTreeNavigatorTests.cs | 61 +- .../SemanticIdHandlerTests.cs | 4 +- .../SemanticId/FillOut/SubmodelFiller.cs | 27 +- .../Helpers/SemanticTreeNavigator.cs | 20 +- 7 files changed, 3510 insertions(+), 3423 deletions(-) diff --git a/example/aas/HandoverDocumentation.xml b/example/aas/HandoverDocumentation.xml index e1fdf491..85cf5e58 100644 --- a/example/aas/HandoverDocumentation.xml +++ b/example/aas/HandoverDocumentation.xml @@ -1,3324 +1,3323 @@ - - - - - HandoverDocumentationAAS - https://admin-shell.io/idta/aas/HandoverDocumentation/2/0 - - Type - https://admin-shell.io/idta/asset/HandoverDocumentation/2/0 - Type - - - - ModelReference - - - Submodel - https://admin-shell.io/idta/SubmodelTemplate/HandoverDocumentation/2/0 - - - - - - - - - HandoverDocumentation - - - en - The Submodel defines a set meta data for the handover of documentation from the manufacturer to the operator for industrial equipment - - - - 2 - 0 - https://admin-shell.io/idta-02004-2-0 - - https://admin-shell.io/idta/SubmodelTemplate/HandoverDocumentation/2/0 - Template - - ModelReference - - - Submodel - 0173-1#01-AHF578#003 - - - - - - ExternalReference - - - GlobalReference - https://api.eclass-cdp.com/0173-1-01-AHF578-003 - - - - - - - Documents - - - en - Documents (handover documentation) - - - de - Dokumente (Übergabedokumentation) - - - - ExternalReference - - - GlobalReference - 0173-1#02-ABI500#003 - - - - - - ExternalReference - - - GlobalReference - https://api.eclass-cdp.com/0173-1-02-ABI500-003 - - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - One - - - true - - ExternalReference - - - GlobalReference - 0173-1#02-ABI500#003/0173-1#01-AHF579#003 - - - - SubmodelElementCollection - - - Document - - - en - This SubmodelElementCollection holds the information for a VDI 2770 Document entity - - - - ExternalReference - - - GlobalReference - 0173-1#02-ABI500#003/0173-1#01-AHF579#003 - - - - - - ExternalReference - - - GlobalReference - 0173-1#02-ABI500#003~0/0173-1#01-AHF579#003 - - - GlobalReference - https://api.eclass-cdp.com/0173-1-02-ABI500-003/0173-1-01-AHF579-003 - - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - OneToMany - - - - - DocumentIds - - - en - Document identifyers - - - de - Dokumentidentifikatoren - - - - ExternalReference - - - GlobalReference - 0173-1#02-ABI501#003 - - - - - - ExternalReference - - - GlobalReference - https://api.eclass-cdp.com/0173-1-02-ABI501-003 - - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - One - - - true - - ExternalReference - - - GlobalReference - 0173-1#02-ABI501#003/0173-1#01-AHF580#003 - - - - SubmodelElementCollection - - - DocumentId - - - en - Document identificator - - - de - Dokumentidentifikator - - - - - en - This SubmodelElementCollection holds the information for a VDI 2770 Document entity - - - - ExternalReference - - - GlobalReference - 0173-1#02-ABI501#003/0173-1#01-AHF580#003 - - - - - - ExternalReference - - - GlobalReference - 0173-1#02-ABI501#003~0/0173-1#01-AHF580#003 - - - GlobalReference - https://api.eclass-cdp.com/0173-1-02-ABI501-003/0173-1-01-AHF580-003 - - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - OneToMany - - - - - PARAMETER - DocumentDomainId - - - en - document domain identificator - - - de - Document Domain Identifikator - - - - ExternalReference - - - GlobalReference - 0173-1#02-ABH994#003 - - - - - - ExternalReference - - - GlobalReference - https://api.eclass-cdp.com/0173-1-02-ABH994-003 - - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - One - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 - - - - ConceptQualifier - ExampleValue - xs:string - https://domain.com/... - - - xs:string - - - PARAMETER - DocumentIdentifier - - - en - Document Identifyer - - - de - Dokumentennummer - - - - ExternalReference - - - GlobalReference - 0173-1#02-AAO099#004 - - - - - - ExternalReference - - - GlobalReference - https://api.eclass-cdp.com/0173-1-02-AAO099-004 - - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - One - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 - - - - ConceptQualifier - ExampleValue - xs:string - XF90-884 - - - xs:string - - - PARAMETER - DocumentIsPrimary - - - en - Document is primary - - - de - Dokument ist primär - - - - ExternalReference - - - GlobalReference - 0173-1#02-ABH995#003 - - - - - - ExternalReference - - - GlobalReference - https://api.eclass-cdp.com/0173-1-02-ABH995-003 - - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - ZeroToOne - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 - - - - ConceptQualifier - ExampleValue - xs:string - true - - - xs:boolean - - - - - - - DocumentClassifications - - - en - Document classifications - - - de - Dokumentklassifikationen - - - - ExternalReference - - - GlobalReference - 0173-1#02-ABI502#003 - - - - - - ExternalReference - - - GlobalReference - https://api.eclass-cdp.com/0173-1-02-ABI502-003 - - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - One - - - true - - ExternalReference - - - GlobalReference - 0173-1#02-ABI502#003/0173-1#01-AHF581#003 - - - - SubmodelElementCollection - - - DocumentClassification - - - en - Document classification - - - de - Dokumentklassifikation - - - - - en - Set of information for describing the classification of the Document according to a ClassificationSystem - - - - ExternalReference - - - GlobalReference - 0173-1#02-ABI502#003/0173-1#01-AHF581#003 - - - - - - ExternalReference - - - GlobalReference - 0173-1#02-ABI502#003~0/0173-1#01-AHF581#003 - - - GlobalReference - https://api.eclass-cdp.com/0173-1-02-ABI502-003/0173-1-01-AHF581-003 - - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - OneToMany - - - - - PARAMETER - ClassId - - - en - Class identificator - - - de - Klassenidentifikator - - - - ExternalReference - - - GlobalReference - 0173-1#02-ABH996#003 - - - - - - ExternalReference - - - GlobalReference - https://api.eclass-cdp.com/0173-1-02-ABH996-003 - - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - One - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 - - - - ConceptQualifier - ExampleValue - xs:string - 03-02 - - - xs:string - - - PARAMETER - ClassificationSystem - - - en - Classification system - - - de - Klassifizierungssystem - - - - ExternalReference - - - GlobalReference - 0173-1#02-ABH997#003 - - - - - - ExternalReference - - - GlobalReference - https://api.eclass-cdp.com/0173-1-02-ABH997-003 - - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - One - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 - - - - ConceptQualifier - ExampleValue - xs:string - VDI2770:2020 - - - xs:string - - - PARAMETER - ClassName - - - en - Class Name - - - de - Klassenname - - - - ExternalReference - - - GlobalReference - 0173-1#02-ABJ219#002 - - - - - - ExternalReference - - - GlobalReference - https://api.eclass-cdp.com/0173-1-02-ABJ219-002 - - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - One - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 - - - - ConceptQualifier - ExampleValue - xs:string - Operation@en - - - - - en - - - - de - - - - - - - - - - DocumentVersions - - - en - Document versions - - - de - Dokumentenversionen - - - - ExternalReference - - - GlobalReference - 0173-1#02-ABI503#003 - - - - - - ExternalReference - - - GlobalReference - https://api.eclass-cdp.com/0173-1-02-ABI503-003 - - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - One - - - true - - ExternalReference - - - GlobalReference - 0173-1#02-ABI503#003/0173-1#01-AHF582#003 - - - - SubmodelElementCollection - - - DocumentVersion - - - en - Document version - - - de - Document version - - - - - en - Set of information for describing the classification of the Document according to a ClassificationSystem - - - - ExternalReference - - - GlobalReference - 0173-1#02-ABI503#003/0173-1#01-AHF582#003 - - - - - - ExternalReference - - - GlobalReference - 0173-1#02-ABI503#003~0/0173-1#01-AHF582#003 - - - GlobalReference - https://api.eclass-cdp.com/0173-1-02-ABI503-003/0173-1-01-AHF582-003 - - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - OneToMany - - - - - Language - - - en - Language - - - de - Sprache - - - - ExternalReference - - - GlobalReference - 0173-1#02-AAN468#008 - - - - - - ExternalReference - - - GlobalReference - https://api.eclass-cdp.com/0173-1-02-AAN468-008 - - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - One - - - true - Property - xs:string - - - language - - - en - en (English) - - - de - en (Englisch) - - - - ExternalReference - - - GlobalReference - 0173-1#02-AAN468#009 - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - OneToMany - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 - - - - ConceptQualifier - ExampleValue - xs:string - en - - - xs:string - en - - ExternalReference - - - GlobalReference - 0173-1#07-AAS045#003 - - - - - - - - RefersToEntities - - - en - Reference to other documents - - - de - Referenz zu anderen Dokumenten - - - - ExternalReference - - - GlobalReference - 0173-1#02-ABK288#002 - - - - - - ExternalReference - - - GlobalReference - https://api.eclass-cdp.com/0173-1-02-ABK288-002 - - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - ZeroToOne - - - true - ReferenceElement - - - BasedOnReferences - - - en - Based on other documents - - - de - Basiert auf anderen Dokumenten - - - - ExternalReference - - - GlobalReference - 0173-1#02-ABK289#002 - - - - - - ExternalReference - - - GlobalReference - https://api.eclass-cdp.com/0173-1-02-ABK289-002 - - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - ZeroToOne - - - true - ReferenceElement - - - TranslationOfEntities - - - en - Translation of other documents - - - de - Übersetzung von anderen Elementen - - - - ExternalReference - - - GlobalReference - 0173-1#02-ABK290#002 - - - - - - ExternalReference - - - GlobalReference - https://api.eclass-cdp.com/0173-1-02-ABK290-002 - - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - ZeroToOne - - - true - ReferenceElement - - - DigitalFiles - - - en - Digital files - - - de - Digitale Dateien - - - - ExternalReference - - - GlobalReference - 0173-1#02-ABK126#002 - - - - - - ExternalReference - - - GlobalReference - https://api.eclass-cdp.com/0173-1-02-ABK126-002 - - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - One - - - true - File - - - DigitalFile - - - en - Name of the specific digital file@en - - - de - Name der spezifischen digitalen Datei@de - - - - ExternalReference - - - GlobalReference - 0173-1#02-ABK126#003 - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - OneToMany - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 - - - - ConceptQualifier - ExampleValue - xs:string - docu_cecc_fullmanual_DE.PDF - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/AllowedIdShort/1/0 - - - - ConceptQualifier - AllowedIdShort - xs:string - DigitalFile[\d{2,3}] - - - application/pdf - - - - - PARAMETER - Version - - - en - Document version - - - de - Dokumentenversion - - - - ExternalReference - - - GlobalReference - 0173-1#02-AAP003#005 - - - - - - ExternalReference - - - GlobalReference - https://api.eclass-cdp.com/0173-1-02-AAP003-005 - - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - One - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 - - - - ConceptQualifier - ExampleValue - xs:string - V1.2 - - - xs:string - - - PARAMETER - StatusSetDate - - - en - Document status set date - - - de - Datum der Einstellung des Dokumentenstatus - - - - ExternalReference - - - GlobalReference - 0173-1#02-ABI000#003 - - - - - - ExternalReference - - - GlobalReference - https://api.eclass-cdp.com/0173-1-02-ABI000-003 - - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - One - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 - - - - ConceptQualifier - ExampleValue - xs:string - 2020-02-06 - - - xs:date - - - PARAMETER - StatusValue - - - en - Document status - - - de - Dokumentstatus - - - - ExternalReference - - - GlobalReference - 0173-1#02-ABI001#003 - - - - - - ExternalReference - - - GlobalReference - https://api.eclass-cdp.com/0173-1-02-ABI001-003 - - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - One - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 - - - - ConceptQualifier - ExampleValue - xs:string - Released - - - xs:string - - - PARAMETER - OrganizationShortName - - - en - Organization short name - - - de - Kurzname der Organisation - - - - ExternalReference - - - GlobalReference - https://api.eclass-cdp.com/0173-1-02-ABI002-003 - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - One - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 - - - - ConceptQualifier - ExampleValue - xs:string - Example company - - - xs:string - - - PARAMETER - OrganizationOfficialName - - - en - Organization official name - - - de - Offizieller Name der Organisation - - - - ExternalReference - - - GlobalReference - 0173-1#02-ABI004#003 - - - - - - ExternalReference - - - GlobalReference - https://api.eclass-cdp.com/0173-1-02-ABI004-003 - - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - One - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 - - - - ConceptQualifier - ExampleValue - xs:string - Example company Ltd. - - - xs:string - - - PARAMETER - Title - - - en - Document title - - - de - Dokumententitel - - - - ExternalReference - - - GlobalReference - 0173-1#02-ABG940#003 - - - - - - ExternalReference - - - GlobalReference - https://api.eclass-cdp.com/0173-1-02-ABG940-003 - - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - One - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 - - - - ConceptQualifier - ExampleValue - xs:string - Examplary title@en - - - - - en - - - - de - - - - - - PARAMETER - Subtitle - - - en - Subtitle - - - de - Untertitel - - - - ExternalReference - - - GlobalReference - 0173-1#02-ABH998#003 - - - - - - ExternalReference - - - GlobalReference - https://api.eclass-cdp.com/0173-1-02-ABH998-003 - - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - ZeroToOne - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 - - - - ConceptQualifier - ExampleValue - xs:string - Examplary subtitle@en - - - - - en - s - - - de - s - - - - - PARAMETER - Description - - - en - Document description - - - de - Dokumentenbeschreibung - - - - ExternalReference - - - GlobalReference - 0173-1#02-AAN466#004 - - - - - - ExternalReference - - - GlobalReference - https://api.eclass-cdp.com/0173-1-02-AAN466-004 - - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - One - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 - - - - ConceptQualifier - ExampleValue - xs:string - Abstract@en - - - - - en - s - - - de - s - - - - - PARAMETER - KeyWords - - - en - Keywords - - - de - Stichworte - - - - ExternalReference - - - GlobalReference - 0173-1#02-ABH999#003 - - - - - - ExternalReference - - - GlobalReference - https://api.eclass-cdp.com/0173-1-02-ABH999-003 - - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - ZeroToOne - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 - - - - ConceptQualifier - ExampleValue - xs:string - Examplary keywords@en - - - - - en - s - - - de - s - - - - - PARAMETER - PreviewFile - - - en - Preview file - - - de - Vorschaudatei - - - - ExternalReference - - - GlobalReference - 0173-1#02-ABK127#002 - - - - - - ExternalReference - - - GlobalReference - https://api.eclass-cdp.com/0173-1-02-ABK127-002 - - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - ZeroToOne - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 - - - - ConceptQualifier - ExampleValue - xs:string - docu_cecc_fullmanual_DE.jpg - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/AllowedIdShort/1/0 - - - - ConceptQualifier - AllowedIdShort - xs:string - PreviewFile[\d{2,3}] - - - image/jpeg - - - - - - - DocumentedEntities - - ExternalReference - - - GlobalReference - https://admin-shell.io/vdi/2770/1/0/Document/DocumentedEntities - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - ZeroToOne - - - true - ReferenceElement - - - - - - - Entities - - ExternalReference - - - GlobalReference - https://admin-shell.io/vdi/2770/1/0/EntitiesForDocumentation - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - ZeroToOne - - - true - Entity - - - - - - - KeyWords - - - en - Keywords - - - de - Stichworte - - - 0173-1#02-ABH999#003 - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 - - - - - - - - en - Keywords - - - de - Stichworte - - - - - en - Keywords - - - en - Stichworte - - - STRING_TRANSLATABLE - - - en - List of language-dependent keywords of the document - - - de - Liste der sprachabhängigen Schlüsselwörter des Dokuments - - - - - - - - - DocumentDomainId - - - en - document domain identificator - - - de - Document Domain Identifikator - - - 0173-1#02-ABH994#003 - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 - - - - - - - - en - Document domain id - - - de - Dokument Domain Identifikator - - - - - en - DocDomainId - - - de - DokDomainId - - - STRING - - - en - Identification of the domain in which the given DocumentId is unique. The domain ID can e.g., be the name or acronym of the providing organisation - - - de - Identifikation der Domäne, in der die angegebene DocumentId eindeutig ist. Die Domain-ID kann z. B. der Name oder das Akronym der bereitstellenden Organisation sein - - - - - - - - - DocumentVersion - - - en - Document version - - - de - Document version - - - 0173-1#02-ABI503#003/0173-1#01-AHF582#003 - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 - - - - - - - - en - Document version - - - en - Document version - - - - - en - DocuVersion - - - en - DokuVersion - - - - - en - Information about a document version entity - - - en - Information für eine Dokumentenversdions-Entität - - - - - - - - - StatusValue - - - en - Document status - - - de - Dokumentstatus - - - 0173-1#02-ABI001#003 - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 - - - - - - - - en - Document status - - - de - Dokumentstatus - - - - - en - DocStatus - - - de - DokStatus - - - - - en - Each document version represents a point in time in the document life cycle. This status value refers to the milestones in the document life cycle. The following two values should be used for the application of this guideline: InReview (under review), Released (released) - - - de - Jede Dokumentversion repräsentiert einen Zeitpunkt im Dokumentlebenszyklus. Dieser Statuswert bezieht sich auf die Meilensteine ​​im Dokumentenlebenszyklus. Für die Anwendung dieser Richtlinie sollten die folgenden zwei Werte verwendet werden: InReview (in Überprüfung), Released (freigegeben) - - - - - - - - - Description - - - en - Document description - - - de - Dokumentenbeschreibung - - - 0173-1#02-AAN466#004 - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 - - - - - - - - en - Document description - - - de - Dokumentenbeschreibung - - - - - en - DocDescr - - - de - DokBeschreib - - - STRING_TRANSLATABLE - - - en - Plain text characterizing the content of the document - - - de - Klartext, der den Inhalt des Dokuments kennzeichnet - - - - - - - - - ClassificationSystem - - - en - Classification system - - - en - Klassifizierungssystem - - - 0173-1#02-ABH997#003 - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 - - - - - - - - en - Classification system - - - de - Klassifizierungssystem - - - - - en - ClassSystem - - - de - KlassSystem - - - STRING - - - en - Identification of the classification system - - - en - Identifikation des Klassifikationssystems - - - - - - - - - ClassId - - - en - Class identificator - - - de - Klassenidentifikator - - - 0173-1#02-ABH996#003 - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 - - - - - - - - en - Class identifyer - - - de - Klassenidentifikator - - - - - en - ClassId - - - de - KlassenId - - - STRING - - - en - Unique ID of the document class within a classficationsystem - - - de - Eindeutige ID der Dokumentenklasse innerhalb eines Klassifikationsystems - - - - - - - - - Title - - - en - Document title - - - de - Dokumententitel - - - 0173-1#02-ABG940#003 - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 - - - - - - - - en - Document name - - - de - Dokumentenname - - - - - en - DocName - - - de - DokName - - - STRING_TRANSLATABLE - - - en - Name of the document - - - de - Name des Dokuments - - - - - - - - - Document - - - en - Document - - - de - Dokument - - - - - en - This SubmodelElementCollection holds the information for a VDI 2770 Document entity - - - 0173-1#02-ABI500#003/0173-1#01-AHF579#003 - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 - - - - - - - - en - Document (handover documentation) - - - de - Dokument (Übergabedokumentation) - - - - - en - Document - - - en - Dokument - - - - - en - Each SubmodelElementCollection describes a document by standard, which is associated to the particular Asset Administration Shell - - - de - Jede SubmodelElementCollection beschreibt ein Dokument (siehe IEC 82045-1 und IEC 8245-2), das der jeweiligen Asset Administration Shell zugeordnet ist - - - - - - - - - DocumentIdentifier - - - en - Document identifyer - - - 0173-1#02-AAO099#004 - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 - - - - - - - - en - DocumentIdentifier - - - be - Dokumentennummer - - - - - en - DocNumber - - - de - DokNummer - - - STRING - - - en - alphanumeric character sequence uniquely identifying a document - - - de - alphanumerische Zeichenfolge, die ein Dokument eindeutig identifiziert - - - - - - - - - HandoverDocumentation - - - en - The Submodel defines a set meta data for the handover of documentation from the manufacturer to the operator for industrial equipment - - - 0173-1#01-AHF578#003 - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 - - - - - - - - en - HandoverDocumentation - - - - - en - The Submodel defines a set meta data for the handover of documentation from the manufacturer to the operator for industrial equipment - - - - - - - - - ClassName - - - en - Class Name - - - de - Klassenname - - - 0173-1#02-ABJ219#002 - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 - - - - - - - - en - Class name - - - de - Klassenname - - - - - en - ClassName - - - en - KlassName - - - STRING - - - en - Name of the class in the classification system - - - de - Name der Klasse im Klassifikationssystem - - - - - - - - - OrganizationOfficialName - - - en - Organization official name - - - de - Offizieller Name der Organisation - - - 0173-1#02-ABI004#003 - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 - - - - - - - - en - Organization official name - - - de - Offizieller Name der Organisation - - - - - en - OfficialName - - - de - OffiziellerName - - - STRING - - - en - Official name of the organization of the author of the document - - - de - Offizieller Name der Organisation des Autors des Dokuments - - - - - - - - - DocumentIsPrimary - - - en - Document is primary - - - de - Dokument ist primär - - - 0173-1#02-ABH995#003 - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 - - - - - - - - en - DocumentIsPrimary - - - de - Dokument ist primär - - - - - en - DocPrimary - - - de - DokPrimär - - - BOOLEAN - - - en - Flag indicating that a DocumentId within a collection of at least two DocumentId`s is the ‘primary’ identifier for the document. This is the preferred ID of the document (commonly from the point of view of the owner of the asset) - - - de - Flag, das angibt, dass eine DocumentId innerhalb einer Sammlung von mindestens zwei DocumentIds die „primäre“ Kennung für das Dokument ist. Dies ist die bevorzugte ID des Dokuments (üblicherweise aus Sicht des Eigentümers des Assets) - - - - - - - - - DocumentId - - - en - Document identificator - - - de - Dokumentidentifikator - - - 0173-1#02-ABI501#003/0173-1#01-AHF580#003 - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 - - - - - - - - en - Document identificator - - - de - Dokumentidentifikator - - - - - en - DocuId - - - de - DokuId - - - - - en - Information about a document identification entity - - - de - Information für eine Dokumentenidentifikations-Entität - - - - - - - - - Subtitle - - - en - Subtitle - - - de - Untertitel - - - 0173-1#02-ABH998#003 - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 - - - - - - - - en - Subtitle - - - de - Untertitel - - - - - en - Subtitle - - - de - Untertitel - - - STRING_TRANSLATABLE - - - en - List of language-dependent subtitles of the document - - - de - Liste der sprachabhängigen Untertitel des Dokuments - - - - - - - - - StatusSetDate - - - en - Document status set date - - - de - Datum der Einstellung des Dokumentenstatus - - - 0173-1#02-ABI000#003 - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 - - - - - - - - en - Document status set date - - - de - Datum der Einstellung des Dokumentenstatus - - - - - en - SetDate - - - de - SetDatum - - - - - en - Date when the document status was set - - - de - Datum, an dem der Dokumentenstatus gesetzt wurde - - - - - - - - - Version - - - en - Document version - - - de - Dokumentenversion - - - 0173-1#02-AAP003#005 - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 - - - - - - - - en - Document version - - - de - Dokumentenversion - - - - - en - DocVersion - - - de - DokVersion - - - STRING - - - en - Design that partly deviates from the previous - - - de - Ausführung, die in einigen Punkten von der vorhergehenden abweicht - - - - - - - - - DocumentClassification - - - en - Document classification - - - de - Dokumentklassifikation - - - 0173-1#02-ABI502#003/0173-1#01-AHF581#003 - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 - - - - - - - - en - Document classification - - - en - Dokumentklassifikation - - - - - en - DocuClass - - - en - DokuKlass - - - - - en - Information about a document classification entity - - - de - Information für eine Dokumentenklassifikations-Entität - - - - - - - - - OrganizationShortName - https://api.eclass-cdp.com/0173-1-02-ABI002-003 - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 - - - - - - - - en - Organization Short Name - - - - - en - Short name of the organization - - - - - - - - - + + + + HandoverDocumentationAAS + https://admin-shell.io/idta/aas/HandoverDocumentation/2/0 + + Type + https://admin-shell.io/idta/asset/HandoverDocumentation/2/0 + Type + + + + ModelReference + + + Submodel + https://admin-shell.io/idta/SubmodelTemplate/HandoverDocumentation/2/0 + + + + + + + + + HandoverDocumentation + + + en + The Submodel defines a set meta data for the handover of documentation from the manufacturer to the operator for industrial equipment + + + + 2 + 0 + https://admin-shell.io/idta-02004-2-0 + + https://admin-shell.io/idta/SubmodelTemplate/HandoverDocumentation/2/0 + Template + + ModelReference + + + Submodel + 0173-1#01-AHF578#003 + + + + + + ExternalReference + + + GlobalReference + https://api.eclass-cdp.com/0173-1-01-AHF578-003 + + + + + + + Documents + + + en + Documents (handover documentation) + + + de + Dokumente (Übergabedokumentation) + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABI500#003 + + + + + + ExternalReference + + + GlobalReference + https://api.eclass-cdp.com/0173-1-02-ABI500-003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + true + + ExternalReference + + + GlobalReference + 0173-1#02-ABI500#003/0173-1#01-AHF579#003 + + + + SubmodelElementCollection + + + Document + + + en + This SubmodelElementCollection holds the information for a VDI 2770 Document entity + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABI500#003/0173-1#01-AHF579#003 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABI500#003~0/0173-1#01-AHF579#003 + + + GlobalReference + https://api.eclass-cdp.com/0173-1-02-ABI500-003/0173-1-01-AHF579-003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + OneToMany + + + + + DocumentIds + + + en + Document identifyers + + + de + Dokumentidentifikatoren + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABI501#003 + + + + + + ExternalReference + + + GlobalReference + https://api.eclass-cdp.com/0173-1-02-ABI501-003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + true + + ExternalReference + + + GlobalReference + 0173-1#02-ABI501#003/0173-1#01-AHF580#003 + + + + SubmodelElementCollection + + + DocumentId + + + en + Document identificator + + + de + Dokumentidentifikator + + + + + en + This SubmodelElementCollection holds the information for a VDI 2770 Document entity + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABI501#003/0173-1#01-AHF580#003 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABI501#003~0/0173-1#01-AHF580#003 + + + GlobalReference + https://api.eclass-cdp.com/0173-1-02-ABI501-003/0173-1-01-AHF580-003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + OneToMany + + + + + PARAMETER + DocumentDomainId + + + en + document domain identificator + + + de + Document Domain Identifikator + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABH994#003 + + + + + + ExternalReference + + + GlobalReference + https://api.eclass-cdp.com/0173-1-02-ABH994-003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 + + + + ConceptQualifier + ExampleValue + xs:string + https://domain.com/... + + + xs:string + + + PARAMETER + DocumentIdentifier + + + en + Document Identifyer + + + de + Dokumentennummer + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAO099#004 + + + + + + ExternalReference + + + GlobalReference + https://api.eclass-cdp.com/0173-1-02-AAO099-004 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 + + + + ConceptQualifier + ExampleValue + xs:string + XF90-884 + + + xs:string + + + PARAMETER + DocumentIsPrimary + + + en + Document is primary + + + de + Dokument ist primär + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABH995#003 + + + + + + ExternalReference + + + GlobalReference + https://api.eclass-cdp.com/0173-1-02-ABH995-003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 + + + + ConceptQualifier + ExampleValue + xs:string + true + + + xs:boolean + + + + + + + DocumentClassifications + + + en + Document classifications + + + de + Dokumentklassifikationen + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABI502#003 + + + + + + ExternalReference + + + GlobalReference + https://api.eclass-cdp.com/0173-1-02-ABI502-003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + true + + ExternalReference + + + GlobalReference + 0173-1#02-ABI502#003/0173-1#01-AHF581#003 + + + + SubmodelElementCollection + + + DocumentClassification + + + en + Document classification + + + de + Dokumentklassifikation + + + + + en + Set of information for describing the classification of the Document according to a ClassificationSystem + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABI502#003/0173-1#01-AHF581#003 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABI502#003~0/0173-1#01-AHF581#003 + + + GlobalReference + https://api.eclass-cdp.com/0173-1-02-ABI502-003/0173-1-01-AHF581-003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + OneToMany + + + + + PARAMETER + ClassId + + + en + Class identificator + + + de + Klassenidentifikator + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABH996#003 + + + + + + ExternalReference + + + GlobalReference + https://api.eclass-cdp.com/0173-1-02-ABH996-003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 + + + + ConceptQualifier + ExampleValue + xs:string + 03-02 + + + xs:string + + + PARAMETER + ClassificationSystem + + + en + Classification system + + + de + Klassifizierungssystem + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABH997#003 + + + + + + ExternalReference + + + GlobalReference + https://api.eclass-cdp.com/0173-1-02-ABH997-003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 + + + + ConceptQualifier + ExampleValue + xs:string + VDI2770:2020 + + + xs:string + + + PARAMETER + ClassName + + + en + Class Name + + + de + Klassenname + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABJ219#002 + + + + + + ExternalReference + + + GlobalReference + https://api.eclass-cdp.com/0173-1-02-ABJ219-002 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 + + + + ConceptQualifier + ExampleValue + xs:string + Operation@en + + + + + en + + + + de + + + + + + + + + + DocumentVersions + + + en + Document versions + + + de + Dokumentenversionen + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABI503#003 + + + + + + ExternalReference + + + GlobalReference + https://api.eclass-cdp.com/0173-1-02-ABI503-003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + true + + ExternalReference + + + GlobalReference + 0173-1#02-ABI503#003/0173-1#01-AHF582#003 + + + + SubmodelElementCollection + + + DocumentVersion + + + en + Document version + + + de + Document version + + + + + en + Set of information for describing the classification of the Document according to a ClassificationSystem + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABI503#003/0173-1#01-AHF582#003 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABI503#003~0/0173-1#01-AHF582#003 + + + GlobalReference + https://api.eclass-cdp.com/0173-1-02-ABI503-003/0173-1-01-AHF582-003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + OneToMany + + + + + Language + + + en + Language + + + de + Sprache + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAN468#008 + + + + + + ExternalReference + + + GlobalReference + https://api.eclass-cdp.com/0173-1-02-AAN468-008 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + true + Property + xs:string + + + language + + + en + en (English) + + + de + en (Englisch) + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAN468#008 + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + OneToMany + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 + + + + ConceptQualifier + ExampleValue + xs:string + en + + + xs:string + en + + ExternalReference + + + GlobalReference + 0173-1#07-AAS045#003 + + + + + + + + RefersToEntities + + + en + Reference to other documents + + + de + Referenz zu anderen Dokumenten + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABK288#002 + + + + + + ExternalReference + + + GlobalReference + https://api.eclass-cdp.com/0173-1-02-ABK288-002 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + true + ReferenceElement + + + BasedOnReferences + + + en + Based on other documents + + + de + Basiert auf anderen Dokumenten + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABK289#002 + + + + + + ExternalReference + + + GlobalReference + https://api.eclass-cdp.com/0173-1-02-ABK289-002 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + true + ReferenceElement + + + TranslationOfEntities + + + en + Translation of other documents + + + de + Übersetzung von anderen Elementen + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABK290#002 + + + + + + ExternalReference + + + GlobalReference + https://api.eclass-cdp.com/0173-1-02-ABK290-002 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + true + ReferenceElement + + + DigitalFiles + + + en + Digital files + + + de + Digitale Dateien + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABK126#002 + + + + + + ExternalReference + + + GlobalReference + https://api.eclass-cdp.com/0173-1-02-ABK126-002 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + true + File + + + DigitalFile + + + en + Name of the specific digital file@en + + + de + Name der spezifischen digitalen Datei@de + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABK126#002 + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + OneToMany + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 + + + + ConceptQualifier + ExampleValue + xs:string + docu_cecc_fullmanual_DE.PDF + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/AllowedIdShort/1/0 + + + + ConceptQualifier + AllowedIdShort + xs:string + DigitalFile[\d{2,3}] + + + application/pdf + + + + + PARAMETER + Version + + + en + Document version + + + de + Dokumentenversion + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAP003#005 + + + + + + ExternalReference + + + GlobalReference + https://api.eclass-cdp.com/0173-1-02-AAP003-005 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 + + + + ConceptQualifier + ExampleValue + xs:string + V1.2 + + + xs:string + + + PARAMETER + StatusSetDate + + + en + Document status set date + + + de + Datum der Einstellung des Dokumentenstatus + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABI000#003 + + + + + + ExternalReference + + + GlobalReference + https://api.eclass-cdp.com/0173-1-02-ABI000-003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 + + + + ConceptQualifier + ExampleValue + xs:string + 2020-02-06 + + + xs:date + + + PARAMETER + StatusValue + + + en + Document status + + + de + Dokumentstatus + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABI001#003 + + + + + + ExternalReference + + + GlobalReference + https://api.eclass-cdp.com/0173-1-02-ABI001-003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 + + + + ConceptQualifier + ExampleValue + xs:string + Released + + + xs:string + + + PARAMETER + OrganizationShortName + + + en + Organization short name + + + de + Kurzname der Organisation + + + + ExternalReference + + + GlobalReference + https://api.eclass-cdp.com/0173-1-02-ABI002-003 + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 + + + + ConceptQualifier + ExampleValue + xs:string + Example company + + + xs:string + + + PARAMETER + OrganizationOfficialName + + + en + Organization official name + + + de + Offizieller Name der Organisation + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABI004#003 + + + + + + ExternalReference + + + GlobalReference + https://api.eclass-cdp.com/0173-1-02-ABI004-003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 + + + + ConceptQualifier + ExampleValue + xs:string + Example company Ltd. + + + xs:string + + + PARAMETER + Title + + + en + Document title + + + de + Dokumententitel + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABG940#003 + + + + + + ExternalReference + + + GlobalReference + https://api.eclass-cdp.com/0173-1-02-ABG940-003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 + + + + ConceptQualifier + ExampleValue + xs:string + Examplary title@en + + + + + en + + + + de + + + + + + PARAMETER + Subtitle + + + en + Subtitle + + + de + Untertitel + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABH998#003 + + + + + + ExternalReference + + + GlobalReference + https://api.eclass-cdp.com/0173-1-02-ABH998-003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 + + + + ConceptQualifier + ExampleValue + xs:string + Examplary subtitle@en + + + + + en + s + + + de + s + + + + + PARAMETER + Description + + + en + Document description + + + de + Dokumentenbeschreibung + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAN466#004 + + + + + + ExternalReference + + + GlobalReference + https://api.eclass-cdp.com/0173-1-02-AAN466-004 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 + + + + ConceptQualifier + ExampleValue + xs:string + Abstract@en + + + + + en + s + + + de + s + + + + + PARAMETER + KeyWords + + + en + Keywords + + + de + Stichworte + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABH999#003 + + + + + + ExternalReference + + + GlobalReference + https://api.eclass-cdp.com/0173-1-02-ABH999-003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 + + + + ConceptQualifier + ExampleValue + xs:string + Examplary keywords@en + + + + + en + s + + + de + s + + + + + PARAMETER + PreviewFile + + + en + Preview file + + + de + Vorschaudatei + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABK127#002 + + + + + + ExternalReference + + + GlobalReference + https://api.eclass-cdp.com/0173-1-02-ABK127-002 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 + + + + ConceptQualifier + ExampleValue + xs:string + docu_cecc_fullmanual_DE.jpg + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/AllowedIdShort/1/0 + + + + ConceptQualifier + AllowedIdShort + xs:string + PreviewFile[\d{2,3}] + + + image/jpeg + + + + + + + DocumentedEntities + + ExternalReference + + + GlobalReference + https://admin-shell.io/vdi/2770/1/0/Document/DocumentedEntities + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + true + ReferenceElement + + + + + + + Entities + + ExternalReference + + + GlobalReference + https://admin-shell.io/vdi/2770/1/0/EntitiesForDocumentation + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + true + Entity + + + + + + + KeyWords + + + en + Keywords + + + de + Stichworte + + + 0173-1#02-ABH999#003 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + Keywords + + + de + Stichworte + + + + + en + Keywords + + + en + Stichworte + + + STRING_TRANSLATABLE + + + en + List of language-dependent keywords of the document + + + de + Liste der sprachabhängigen Schlüsselwörter des Dokuments + + + + + + + + + DocumentDomainId + + + en + document domain identificator + + + de + Document Domain Identifikator + + + 0173-1#02-ABH994#003 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + Document domain id + + + de + Dokument Domain Identifikator + + + + + en + DocDomainId + + + de + DokDomainId + + + STRING + + + en + Identification of the domain in which the given DocumentId is unique. The domain ID can e.g., be the name or acronym of the providing organisation + + + de + Identifikation der Domäne, in der die angegebene DocumentId eindeutig ist. Die Domain-ID kann z. B. der Name oder das Akronym der bereitstellenden Organisation sein + + + + + + + + + DocumentVersion + + + en + Document version + + + de + Document version + + + 0173-1#02-ABI503#003/0173-1#01-AHF582#003 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + Document version + + + en + Document version + + + + + en + DocuVersion + + + en + DokuVersion + + + + + en + Information about a document version entity + + + en + Information für eine Dokumentenversdions-Entität + + + + + + + + + StatusValue + + + en + Document status + + + de + Dokumentstatus + + + 0173-1#02-ABI001#003 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + Document status + + + de + Dokumentstatus + + + + + en + DocStatus + + + de + DokStatus + + + + + en + Each document version represents a point in time in the document life cycle. This status value refers to the milestones in the document life cycle. The following two values should be used for the application of this guideline: InReview (under review), Released (released) + + + de + Jede Dokumentversion repräsentiert einen Zeitpunkt im Dokumentlebenszyklus. Dieser Statuswert bezieht sich auf die Meilensteine ​​im Dokumentenlebenszyklus. Für die Anwendung dieser Richtlinie sollten die folgenden zwei Werte verwendet werden: InReview (in Überprüfung), Released (freigegeben) + + + + + + + + + Description + + + en + Document description + + + de + Dokumentenbeschreibung + + + 0173-1#02-AAN466#004 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + Document description + + + de + Dokumentenbeschreibung + + + + + en + DocDescr + + + de + DokBeschreib + + + STRING_TRANSLATABLE + + + en + Plain text characterizing the content of the document + + + de + Klartext, der den Inhalt des Dokuments kennzeichnet + + + + + + + + + ClassificationSystem + + + en + Classification system + + + en + Klassifizierungssystem + + + 0173-1#02-ABH997#003 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + Classification system + + + de + Klassifizierungssystem + + + + + en + ClassSystem + + + de + KlassSystem + + + STRING + + + en + Identification of the classification system + + + en + Identifikation des Klassifikationssystems + + + + + + + + + ClassId + + + en + Class identificator + + + de + Klassenidentifikator + + + 0173-1#02-ABH996#003 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + Class identifyer + + + de + Klassenidentifikator + + + + + en + ClassId + + + de + KlassenId + + + STRING + + + en + Unique ID of the document class within a classficationsystem + + + de + Eindeutige ID der Dokumentenklasse innerhalb eines Klassifikationsystems + + + + + + + + + Title + + + en + Document title + + + de + Dokumententitel + + + 0173-1#02-ABG940#003 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + Document name + + + de + Dokumentenname + + + + + en + DocName + + + de + DokName + + + STRING_TRANSLATABLE + + + en + Name of the document + + + de + Name des Dokuments + + + + + + + + + Document + + + en + Document + + + de + Dokument + + + + + en + This SubmodelElementCollection holds the information for a VDI 2770 Document entity + + + 0173-1#02-ABI500#003/0173-1#01-AHF579#003 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + Document (handover documentation) + + + de + Dokument (Übergabedokumentation) + + + + + en + Document + + + en + Dokument + + + + + en + Each SubmodelElementCollection describes a document by standard, which is associated to the particular Asset Administration Shell + + + de + Jede SubmodelElementCollection beschreibt ein Dokument (siehe IEC 82045-1 und IEC 8245-2), das der jeweiligen Asset Administration Shell zugeordnet ist + + + + + + + + + DocumentIdentifier + + + en + Document identifyer + + + 0173-1#02-AAO099#004 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + DocumentIdentifier + + + be + Dokumentennummer + + + + + en + DocNumber + + + de + DokNummer + + + STRING + + + en + alphanumeric character sequence uniquely identifying a document + + + de + alphanumerische Zeichenfolge, die ein Dokument eindeutig identifiziert + + + + + + + + + HandoverDocumentation + + + en + The Submodel defines a set meta data for the handover of documentation from the manufacturer to the operator for industrial equipment + + + 0173-1#01-AHF578#003 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + HandoverDocumentation + + + + + en + The Submodel defines a set meta data for the handover of documentation from the manufacturer to the operator for industrial equipment + + + + + + + + + ClassName + + + en + Class Name + + + de + Klassenname + + + 0173-1#02-ABJ219#002 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + Class name + + + de + Klassenname + + + + + en + ClassName + + + en + KlassName + + + STRING + + + en + Name of the class in the classification system + + + de + Name der Klasse im Klassifikationssystem + + + + + + + + + OrganizationOfficialName + + + en + Organization official name + + + de + Offizieller Name der Organisation + + + 0173-1#02-ABI004#003 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + Organization official name + + + de + Offizieller Name der Organisation + + + + + en + OfficialName + + + de + OffiziellerName + + + STRING + + + en + Official name of the organization of the author of the document + + + de + Offizieller Name der Organisation des Autors des Dokuments + + + + + + + + + DocumentIsPrimary + + + en + Document is primary + + + de + Dokument ist primär + + + 0173-1#02-ABH995#003 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + DocumentIsPrimary + + + de + Dokument ist primär + + + + + en + DocPrimary + + + de + DokPrimär + + + BOOLEAN + + + en + Flag indicating that a DocumentId within a collection of at least two DocumentId`s is the ‘primary’ identifier for the document. This is the preferred ID of the document (commonly from the point of view of the owner of the asset) + + + de + Flag, das angibt, dass eine DocumentId innerhalb einer Sammlung von mindestens zwei DocumentIds die „primäre“ Kennung für das Dokument ist. Dies ist die bevorzugte ID des Dokuments (üblicherweise aus Sicht des Eigentümers des Assets) + + + + + + + + + DocumentId + + + en + Document identificator + + + de + Dokumentidentifikator + + + 0173-1#02-ABI501#003/0173-1#01-AHF580#003 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + Document identificator + + + de + Dokumentidentifikator + + + + + en + DocuId + + + de + DokuId + + + + + en + Information about a document identification entity + + + de + Information für eine Dokumentenidentifikations-Entität + + + + + + + + + Subtitle + + + en + Subtitle + + + de + Untertitel + + + 0173-1#02-ABH998#003 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + Subtitle + + + de + Untertitel + + + + + en + Subtitle + + + de + Untertitel + + + STRING_TRANSLATABLE + + + en + List of language-dependent subtitles of the document + + + de + Liste der sprachabhängigen Untertitel des Dokuments + + + + + + + + + StatusSetDate + + + en + Document status set date + + + de + Datum der Einstellung des Dokumentenstatus + + + 0173-1#02-ABI000#003 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + Document status set date + + + de + Datum der Einstellung des Dokumentenstatus + + + + + en + SetDate + + + de + SetDatum + + + + + en + Date when the document status was set + + + de + Datum, an dem der Dokumentenstatus gesetzt wurde + + + + + + + + + Version + + + en + Document version + + + de + Dokumentenversion + + + 0173-1#02-AAP003#005 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + Document version + + + de + Dokumentenversion + + + + + en + DocVersion + + + de + DokVersion + + + STRING + + + en + Design that partly deviates from the previous + + + de + Ausführung, die in einigen Punkten von der vorhergehenden abweicht + + + + + + + + + DocumentClassification + + + en + Document classification + + + de + Dokumentklassifikation + + + 0173-1#02-ABI502#003/0173-1#01-AHF581#003 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + Document classification + + + en + Dokumentklassifikation + + + + + en + DocuClass + + + en + DokuKlass + + + + + en + Information about a document classification entity + + + de + Information für eine Dokumentenklassifikations-Entität + + + + + + + + + OrganizationShortName + https://api.eclass-cdp.com/0173-1-02-ABI002-003 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + Organization Short Name + + + + + en + Short name of the organization + + + + + + + + + \ No newline at end of file diff --git a/example/postgres/04_handoverdocumentation.sql.inc b/example/postgres/04_handoverdocumentation.sql.inc index 14d0e895..8306fa84 100644 --- a/example/postgres/04_handoverdocumentation.sql.inc +++ b/example/postgres/04_handoverdocumentation.sql.inc @@ -55,7 +55,6 @@ CREATE TABLE "DocumentDocumentClassification" ( CREATE TABLE "DocumentVersion" ( "Id" INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, "Index" INT, - "en" TEXT, "DigitalFile" TEXT, "Version" TEXT, "StatusSetDate" DATE, @@ -73,6 +72,18 @@ CREATE TABLE "DocumentVersion" ( "PreviewFile" TEXT ); +CREATE TABLE "Languages" ( + "Id" INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + "Index" INT, + "Language" TEXT +); + +CREATE TABLE "DocumentVersionLanguages" ( + "Id" INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + "DocumentVersionId" INT REFERENCES "DocumentVersion"("Id") ON DELETE CASCADE, + "LanguageId" INT REFERENCES "Languages"("Id") ON DELETE CASCADE +); + CREATE TABLE "DocumentDocumentVersion" ( "Id" INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, "DocumentId" INT REFERENCES "Document"("Id") ON DELETE CASCADE, @@ -166,47 +177,77 @@ INSERT INTO "DocumentDocumentClassification" ("DocumentId","DocumentClassificati -- ============================================================ INSERT INTO "DocumentVersion" ( - "Index","en","DigitalFile","Version","StatusSetDate","StatusValue","OrganizationShortName", + "Index","DigitalFile","Version","StatusSetDate","StatusValue","OrganizationShortName", "OrganizationOfficialName","Title_en","Title_de","Subtitle_en","Subtitle_de","Description_en","Description_de", "KeyWords_en","KeyWords_de","PreviewFile" ) VALUES -(0,'en','https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf','1','2023-01-01','Released','M&M','M&M Germany', +(0,'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf','1','2023-01-01','Released','M&M','M&M Germany', 'User Guide – DSLR Camera Model X100','Benutzerhandbuch – DSLR-Kamera Modell X100','Complete Instructions for Professional Photography','Vollständige Anleitung für professionelle Fotografie', 'Detailed instructions for operating the X100 DSLR camera, including setup and troubleshooting.','Detaillierte Anweisungen zur Bedienung der DSLR-Kamera X100, einschließlich Einrichtung und Fehlerbehebung', 'DSLR, Camera, Photography, User Guide, Setup','DSLR, Kamera, Fotografie, Benutzerhandbuch, Einrichtung','https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.jpg'), -(1,'en','https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf','1.1','2024-05-05','InReview','M&M','M&M India', +(1,'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf','1.1','2024-05-05','InReview','M&M','M&M India', 'Technical Specification – Mirrorless Camera Z-Series','Technische Spezifikation – Systemkamera der Z-Serie','Detailed Specs for Advanced Imaging','Detaillierte Spezifikationen für fortschrittliche Bildgebung', 'Comprehensive technical details of the Z-Series mirrorless camera, covering sensor and performance.','Umfassende technische Details der spiegellosen Kamera Z-Serie, einschließlich Sensor und Leistung.','Mirrorless, Camera, Specs, Imaging, Performance','Spiegellos, Kamera, Spezifikationen, Bildgebung, Leistung','https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.jpg'), -(0,'en','https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf','2.1','2026-01-01','Released','M&M','M&M China', +(0,'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf','2.1','2026-01-01','Released','M&M','M&M China', 'Maintenance Manual – Professional Camera Lens 50mm','Wartungshandbuch – Professionelles Kameraobjektiv 50 mm','Care and Cleaning Procedures','Pflege und Reinigungsverfahren', 'Guidelines for cleaning and maintaining the 50mm professional lens for optimal performance.','Richtlinien zur Reinigung und Wartung des professionellen 50-mm-Objektivs für optimale Leistung.','Lens, Maintenance, Cleaning, Professional, Care','Objektiv, Wartung, Reinigung, Professionell, Pflege','https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.jpg'), -(0,'en','https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf','2.3','2025-10-10','InReview','M&M','M&M Germany', +(0,'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf','2.3','2025-10-10','InReview','M&M','M&M Germany', 'Installation Guide – Wide-Angle Lens Kit','Installationsanleitung – Weitwinkel-Objektiv-Kit','Step-by-Step Setup Instructions','Schritt-für-Schritt-Installationsanleitung', 'Step-by-step instructions for installing and configuring the wide-angle lens kit.','Schritt-für-Schritt-Anleitung zur Installation und Konfiguration des Weitwinkel-Objektivsets.','Wide-Angle, Lens, Installation, Setup, Kit','Weitwinkel, Objektiv, Installation, Einrichtung, Set','https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.jpg'), -(0,'en','https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf','0.9','2024-01-01','Released','M&M','M&M India', +(0,'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf','0.9','2024-01-01','Released','M&M','M&M India', 'Product Data Sheet – Telephoto Lens 200mm','Produktdatenblatt – Teleobjektiv 200 mm','Technical Data and Performance Metrics','Technische Daten und Leistungskennzahlen', 'Technical data and compatibility details for the 200mm telephoto lens.','Technische Daten und Kompatibilitätsdetails für das 200-mm-Teleobjektiv.','Telephoto, Lens, Data Sheet, Specifications, Optics','Teleobjektiv, Objektiv, Datenblatt, Spezifikationen, Optik','https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.jpg'), -(0,'en','https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf','1.2','2024-03-03','InReview','M&M','M&M China', +(0,'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf','1.2','2024-03-03','InReview','M&M','M&M China', 'Safety Instructions – Digital Camera Accessories','Sicherheitsanweisungen – Zubehör für Digitalkameras','Guidelines for Safe Usage','Richtlinien für sichere Verwendung', 'Safety guidelines for handling batteries, chargers, and other camera accessories.','Sicherheitsrichtlinien für den Umgang mit Batterien, Ladegeräten und anderem Kamera-Zubehör.','Safety, Camera, Accessories, Guidelines, Handling','Sicherheit, Kamera, Zubehör, Richtlinien, Handhabung','https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.jpg'), -(1,'en','https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf','1.4','2023-01-01','Released','M&M','M&M Germany', +(1,'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf','1.4','2023-01-01','Released','M&M','M&M Germany', 'Perfume Catalog – Luxury Fragrance Collection 2025','Parfümkatalog – Luxusduftkollektion 2025','Explore Elegant Scents for Every Occasion','Entdecken Sie elegante Düfte für jeden Anlass', 'A curated catalog showcasing premium perfumes with scent profiles and packaging details.','Ein kuratierter Katalog mit Premium-Parfums, Duftprofilen und Verpackungsdetails.','Perfume, Fragrance, Luxury, Catalog, Collection','Parfum, Duft, Luxus, Katalog, Kollektion','https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.jpg'), -(2,'en','https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf','2','2024-03-03','InReview','M&M','M&M India', +(2,'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf','2','2024-03-03','InReview','M&M','M&M India', 'Quality Assurance Report – Eau de Parfum Series A','Qualitätssicherungsbericht – Eau de Parfum Serie A','Verified Standards and Testing Results','Geprüfte Standards und Testergebnisse', 'Report detailing quality checks and compliance standards for Series A perfumes.','Bericht mit Qualitätsprüfungen und Konformitätsstandards für Parfums der Serie A.','Packaging, Perfume, Bottles, Caps, Compliance','Verpackung, Parfum, Flaschen, Verschlüsse, Konformität','https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.jpg'), -(0,'en','https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf','1','2022-02-02','Released','M&M','M&M Germany', +(0,'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf','1','2022-02-02','Released','M&M','M&M Germany', 'Packaging Standards – Perfume Bottles and Caps','Verpackungsstandards – Parfümflaschen und Verschlüsse','Design and Material Compliance Guidelines','Richtlinien für Design und Materialkonformität', 'Design and material compliance guidelines for perfume packaging.','Richtlinien für Design und Materialkonformität bei Parfumverpackungen.','Perfume, Fragrance, Luxury, Catalog, Collection','Parfum, Duft, Luxus, Katalog, Kollektion','https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.jpg'); +-- ============================================================ +-- Languages +-- ============================================================ + +INSERT INTO "Languages" ("Index","Language") VALUES +(0,'en'), +(0,'de'), +(0,'en'), +(0,'de'), +(0,'de'), +(0,'en'), +(0,'de'), +(0,'hi'), +(0,'en'); + +-- ============================================================ +-- DocumentVersion-Language Relationships +-- ============================================================ + +INSERT INTO "DocumentVersionLanguages" ("DocumentVersionId","LanguageId") VALUES +(1,1), +(2,2), +(3,3), +(4,4), +(5,5), +(6,6), +(7,7), +(8,8), +(9,9), + -- ============================================================ -- Document-Version Relationships -- ============================================================ 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 f6741025..ff379107 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 @@ -447,4 +447,115 @@ [new Key(KeyTypes.GlobalReference, "http://example.com/second")]), Equal("One", relationship.Qualifiers[0].Value); DoesNotContain(relationship.Qualifiers, q => q.Type == "InternalSemanticId"); } + + [Fact] + public void FillOutTemplate_WhenPropertySemanticIdHasBothBranchAndLeaf_UsesLeafNode() + { + const string collectionSemanticId = "urn:test:collection"; + const string propertySemanticId = "urn:test:property"; + + var property = new Property(idShort: "Language", valueType: DataTypeDefXsd.String, value: string.Empty); + var collection = new SubmodelElementCollection(idShort: "DocVersion", value: [property]); + + var submodel = Substitute.For(); + var elements = new List { collection }; + submodel.SubmodelElements.Returns(elements); + + _resolver.ExtractSemanticId(collection).Returns(collectionSemanticId); + _resolver.ExtractSemanticId(property).Returns(propertySemanticId); + + var collectionHandler = Substitute.For(); + collectionHandler.CanHandle(Arg.Any()).Returns(call => call.Arg() is SubmodelElementCollection); + collectionHandler + .When(h => h.FillOut(Arg.Any(), Arg.Any(), Arg.Any, SemanticTreeNode, bool>>())) + .Do(call => + { + var element = (SubmodelElementCollection)call.ArgAt(0); + var node = call.ArgAt(1); + var fillChildren = call.ArgAt, SemanticTreeNode, bool>>(2); + fillChildren(element.Value!, node, false); + }); + + var propertyHandler = Substitute.For(); + propertyHandler.CanHandle(Arg.Any()).Returns(call => call.Arg() is Property); + propertyHandler + .When(h => h.FillOut(Arg.Any(), Arg.Any(), Arg.Any, SemanticTreeNode, bool>>())) + .Do(call => + { + var element = (Property)call.ArgAt(0); + var valueNode = call.ArgAt(1); + element.Value = (valueNode as SemanticLeafNode)?.Value; + }); + + _handlers.Add(collectionHandler); + _handlers.Add(propertyHandler); + + var root = new SemanticBranchNode("root", Cardinality.Unknown); + var collectionNode = new SemanticBranchNode(collectionSemanticId, Cardinality.One); + collectionNode.AddChild(new SemanticBranchNode(propertySemanticId, Cardinality.One)); + collectionNode.AddChild(new SemanticLeafNode(propertySemanticId, "en", DataType.String, Cardinality.One)); + root.AddChild(collectionNode); + + _ = _sut.FillOutTemplate(submodel, root); + + Equal("en", property.Value); + } + + [Fact] + public void FillOutTemplate_WhenCollectionSemanticIdHasBothBranchAndLeaf_UsesBranchNode() + { + const string parentSemanticId = "urn:test:parent"; + const string childCollectionSemanticId = "urn:test:child-collection"; + const string childPropertySemanticId = "urn:test:child-property"; + + var childProperty = new Property(idShort: "Language", valueType: DataTypeDefXsd.String, value: string.Empty); + var childCollection = new SubmodelElementCollection(idShort: "Languages", value: [childProperty]); + var parentCollection = new SubmodelElementCollection(idShort: "DocumentVersion", value: [childCollection]); + + var submodel = Substitute.For(); + var elements = new List { parentCollection }; + submodel.SubmodelElements.Returns(elements); + + _resolver.ExtractSemanticId(parentCollection).Returns(parentSemanticId); + _resolver.ExtractSemanticId(childCollection).Returns(childCollectionSemanticId); + _resolver.ExtractSemanticId(childProperty).Returns(childPropertySemanticId); + + var collectionHandler = Substitute.For(); + collectionHandler.CanHandle(Arg.Any()).Returns(call => call.Arg() is SubmodelElementCollection); + collectionHandler + .When(h => h.FillOut(Arg.Any(), Arg.Any(), Arg.Any, SemanticTreeNode, bool>>())) + .Do(call => + { + var element = (SubmodelElementCollection)call.ArgAt(0); + var node = call.ArgAt(1); + var fillChildren = call.ArgAt, SemanticTreeNode, bool>>(2); + fillChildren(element.Value!, node, true); + }); + + var propertyHandler = Substitute.For(); + propertyHandler.CanHandle(Arg.Any()).Returns(call => call.Arg() is Property); + propertyHandler + .When(h => h.FillOut(Arg.Any(), Arg.Any(), Arg.Any, SemanticTreeNode, bool>>())) + .Do(call => + { + var element = (Property)call.ArgAt(0); + var valueNode = call.ArgAt(1); + element.Value = (valueNode as SemanticLeafNode)?.Value; + }); + + _handlers.Add(collectionHandler); + _handlers.Add(propertyHandler); + + var root = new SemanticBranchNode("root", Cardinality.Unknown); + var parentNode = new SemanticBranchNode(parentSemanticId, Cardinality.One); + var childCollectionNode = new SemanticBranchNode(childCollectionSemanticId, Cardinality.One); + childCollectionNode.AddChild(new SemanticLeafNode(childPropertySemanticId, "de", DataType.String, Cardinality.One)); + parentNode.AddChild(childCollectionNode); + parentNode.AddChild(new SemanticLeafNode(childCollectionSemanticId, "ignore-me", DataType.String, Cardinality.One)); + root.AddChild(parentNode); + + _ = _sut.FillOutTemplate(submodel, root); + + Equal("de", childProperty.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 index d6f07f71..05783ef5 100644 --- 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 @@ -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.DomainModel.SubmodelRepository; using static Xunit.Assert; @@ -100,55 +100,20 @@ public void FindNodeBySemanticId_NoMatch_ReturnsEmpty() } [Fact] - public void AreAllNodesOfSameType_EmptyList_ReturnsTrueWithNullType() + public void FindNodeBySemanticId_Generic_WhenSameSemanticIdOnBothBranchAndLeaf_ReturnsOnlyRequestedType() { - 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 root = new SemanticBranchNode("root", Cardinality.Unknown); + var branch = new SemanticBranchNode("shared", Cardinality.One); + var leaf = new SemanticLeafNode("shared", "val", DataType.String, Cardinality.One); + root.AddChild(branch); + root.AddChild(leaf); - var result = SemanticTreeNavigator.AreAllNodesOfSameType(nodes, out _); + var branchResults = SemanticTreeNavigator.FindNodeBySemanticId(root, "shared").ToList(); + var leafResults = SemanticTreeNavigator.FindNodeBySemanticId(root, "shared").ToList(); - False(result); + Single(branchResults); + Same(branch, branchResults[0]); + Single(leafResults); + Same(leaf, leafResults[0]); } } 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 250b98f1..9d7d1ac7 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs @@ -502,7 +502,7 @@ public void FillOutTemplate_SubmodelOfComplexData_ReturnsSubmodelWithValue() Equal("ComplexData1", submodelWithValues.SubmodelElements[1].IdShort); var complexData0 = GetSubmodelElementCollection(submodelWithValues, 0); var complexData1 = GetSubmodelElementCollection(submodelWithValues, 1); - Equal(3, complexData1.Value!.Count); + Equal(5, complexData1.Value!.Count); AssertMultiLanguageProperty(complexData0, "Test Example Manufacturer", "Test Beispiel Hersteller"); AssertMultiLanguageProperty(complexData1, "Test1 Example Manufacturer", "Test1 Beispiel Hersteller"); AssertModelType(complexData0, 1, "22.47"); @@ -510,6 +510,8 @@ public void FillOutTemplate_SubmodelOfComplexData_ReturnsSubmodelWithValue() AssertContactList(complexData0, 2, "Test John Doe", "Test Example Model"); AssertContactInfo(complexData0, 3, "Test John Doe"); AssertContactInfo(complexData1, 2, "Test1 John Doe"); + AssertContactList(complexData1, 3, "Test1 John Doe", "Test1 Example Model"); + AssertContactList(complexData1, 4, "Test2 John Doe", "Test2 Example Model"); } [Fact] 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 09ef8318..30360ec1 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 @@ -131,11 +131,6 @@ internal void FillOutSubmodelElementValue(List elements, Seman { var semanticTreeNodes = GetSemanticNodes(element, values); - if (ShouldSkipElement(semanticTreeNodes, element, elements)) - { - continue; - } - if (ShouldCloneElements(semanticTreeNodes, element)) { ReplaceWithClones(elements, element, semanticTreeNodes, updateIdShort); @@ -146,29 +141,19 @@ internal void FillOutSubmodelElementValue(List elements, Seman } } - 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)); + var semanticId = semanticIdResolver.ExtractSemanticId(element); - return valueNode?.ToList(); + return IsBranchElement(element) + ? SemanticTreeNavigator.FindNodeBySemanticId(values, semanticId).Cast().ToList() + : SemanticTreeNavigator.FindNodeBySemanticId(values, semanticId).Cast().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; - } + private static bool IsBranchElement(ISubmodelElement element) => + element is not (Property or AasCore.Aas3_0.File or Blob); private void ReplaceWithClones(List elements, ISubmodelElement element, List nodes, bool updateIdShort) { 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 aa20ac5f..28482ea3 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 @@ -32,22 +32,6 @@ public static IEnumerable FindNodeBySemanticId(SemanticTreeNod } } - public static bool AreAllNodesOfSameType(IList 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); - } + public static IEnumerable FindNodeBySemanticId(SemanticTreeNode tree, string semanticId) where T : SemanticTreeNode + => FindNodeBySemanticId(tree, semanticId).OfType(); } From b5abc0ce17cd20c86e98c71b25a4c8ae368dd5fb Mon Sep 17 00:00:00 2001 From: Hardi Shah Date: Wed, 25 Mar 2026 10:44:08 +0530 Subject: [PATCH 11/14] Update HandoverDocumentation.xml --- example/aas/HandoverDocumentation.xml | 187 -------------------------- 1 file changed, 187 deletions(-) diff --git a/example/aas/HandoverDocumentation.xml b/example/aas/HandoverDocumentation.xml index 85cf5e58..64d8ab13 100644 --- a/example/aas/HandoverDocumentation.xml +++ b/example/aas/HandoverDocumentation.xml @@ -1071,162 +1071,6 @@ - - RefersToEntities - - - en - Reference to other documents - - - de - Referenz zu anderen Dokumenten - - - - ExternalReference - - - GlobalReference - 0173-1#02-ABK288#002 - - - - - - ExternalReference - - - GlobalReference - https://api.eclass-cdp.com/0173-1-02-ABK288-002 - - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - ZeroToOne - - - true - ReferenceElement - - - BasedOnReferences - - - en - Based on other documents - - - de - Basiert auf anderen Dokumenten - - - - ExternalReference - - - GlobalReference - 0173-1#02-ABK289#002 - - - - - - ExternalReference - - - GlobalReference - https://api.eclass-cdp.com/0173-1-02-ABK289-002 - - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - ZeroToOne - - - true - ReferenceElement - - - TranslationOfEntities - - - en - Translation of other documents - - - de - Übersetzung von anderen Elementen - - - - ExternalReference - - - GlobalReference - 0173-1#02-ABK290#002 - - - - - - ExternalReference - - - GlobalReference - https://api.eclass-cdp.com/0173-1-02-ABK290-002 - - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - ZeroToOne - - - true - ReferenceElement - DigitalFiles @@ -2065,37 +1909,6 @@ - - DocumentedEntities - - ExternalReference - - - GlobalReference - https://admin-shell.io/vdi/2770/1/0/Document/DocumentedEntities - - - - - - - ExternalReference - - - GlobalReference - https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 - - - - ConceptQualifier - SMT/Cardinality - xs:string - ZeroToOne - - - true - ReferenceElement - From 5535eabb58b9ff3f38a127adb31900a4a9c2c513 Mon Sep 17 00:00:00 2001 From: Hardi Shah Date: Wed, 25 Mar 2026 12:07:55 +0530 Subject: [PATCH 12/14] Refactor SQL and C# for clarity and minor logic fixes Reformatted SQL inserts for readability and fixed a minor syntax issue in DocumentVersionLanguages. Improved SubmodelFiller logic to skip elements with no semantic nodes and refactored methods for conciseness. Removed unused field and clarified property naming in SemanticIdResolver. Simplified result handling in SubmodelRepositoryService. These changes enhance code clarity, maintainability, and correctness. --- .../postgres/04_handoverdocumentation.sql.inc | 226 +++++++++++++++--- .../SemanticId/FillOut/SubmodelFiller.cs | 14 +- .../SemanticId/Helpers/SemanticIdResolver.cs | 1 - .../SubmodelRepositoryService.cs | 4 +- 4 files changed, 200 insertions(+), 45 deletions(-) diff --git a/example/postgres/04_handoverdocumentation.sql.inc b/example/postgres/04_handoverdocumentation.sql.inc index 8306fa84..0c210e4d 100644 --- a/example/postgres/04_handoverdocumentation.sql.inc +++ b/example/postgres/04_handoverdocumentation.sql.inc @@ -181,42 +181,194 @@ INSERT INTO "DocumentVersion" ( "OrganizationOfficialName","Title_en","Title_de","Subtitle_en","Subtitle_de","Description_en","Description_de", "KeyWords_en","KeyWords_de","PreviewFile" ) VALUES -(0,'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf','1','2023-01-01','Released','M&M','M&M Germany', - 'User Guide – DSLR Camera Model X100','Benutzerhandbuch – DSLR-Kamera Modell X100','Complete Instructions for Professional Photography','Vollständige Anleitung für professionelle Fotografie', - 'Detailed instructions for operating the X100 DSLR camera, including setup and troubleshooting.','Detaillierte Anweisungen zur Bedienung der DSLR-Kamera X100, einschließlich Einrichtung und Fehlerbehebung', - 'DSLR, Camera, Photography, User Guide, Setup','DSLR, Kamera, Fotografie, Benutzerhandbuch, Einrichtung','https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.jpg'), - -(1,'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf','1.1','2024-05-05','InReview','M&M','M&M India', - 'Technical Specification – Mirrorless Camera Z-Series','Technische Spezifikation – Systemkamera der Z-Serie','Detailed Specs for Advanced Imaging','Detaillierte Spezifikationen für fortschrittliche Bildgebung', - 'Comprehensive technical details of the Z-Series mirrorless camera, covering sensor and performance.','Umfassende technische Details der spiegellosen Kamera Z-Serie, einschließlich Sensor und Leistung.','Mirrorless, Camera, Specs, Imaging, Performance','Spiegellos, Kamera, Spezifikationen, Bildgebung, Leistung','https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.jpg'), - -(0,'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf','2.1','2026-01-01','Released','M&M','M&M China', - 'Maintenance Manual – Professional Camera Lens 50mm','Wartungshandbuch – Professionelles Kameraobjektiv 50 mm','Care and Cleaning Procedures','Pflege und Reinigungsverfahren', - 'Guidelines for cleaning and maintaining the 50mm professional lens for optimal performance.','Richtlinien zur Reinigung und Wartung des professionellen 50-mm-Objektivs für optimale Leistung.','Lens, Maintenance, Cleaning, Professional, Care','Objektiv, Wartung, Reinigung, Professionell, Pflege','https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.jpg'), - -(0,'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf','2.3','2025-10-10','InReview','M&M','M&M Germany', - 'Installation Guide – Wide-Angle Lens Kit','Installationsanleitung – Weitwinkel-Objektiv-Kit','Step-by-Step Setup Instructions','Schritt-für-Schritt-Installationsanleitung', - 'Step-by-step instructions for installing and configuring the wide-angle lens kit.','Schritt-für-Schritt-Anleitung zur Installation und Konfiguration des Weitwinkel-Objektivsets.','Wide-Angle, Lens, Installation, Setup, Kit','Weitwinkel, Objektiv, Installation, Einrichtung, Set','https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.jpg'), - -(0,'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf','0.9','2024-01-01','Released','M&M','M&M India', - 'Product Data Sheet – Telephoto Lens 200mm','Produktdatenblatt – Teleobjektiv 200 mm','Technical Data and Performance Metrics','Technische Daten und Leistungskennzahlen', - 'Technical data and compatibility details for the 200mm telephoto lens.','Technische Daten und Kompatibilitätsdetails für das 200-mm-Teleobjektiv.','Telephoto, Lens, Data Sheet, Specifications, Optics','Teleobjektiv, Objektiv, Datenblatt, Spezifikationen, Optik','https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.jpg'), - -(0,'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf','1.2','2024-03-03','InReview','M&M','M&M China', - 'Safety Instructions – Digital Camera Accessories','Sicherheitsanweisungen – Zubehör für Digitalkameras','Guidelines for Safe Usage','Richtlinien für sichere Verwendung', - 'Safety guidelines for handling batteries, chargers, and other camera accessories.','Sicherheitsrichtlinien für den Umgang mit Batterien, Ladegeräten und anderem Kamera-Zubehör.','Safety, Camera, Accessories, Guidelines, Handling','Sicherheit, Kamera, Zubehör, Richtlinien, Handhabung','https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.jpg'), - -(1,'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf','1.4','2023-01-01','Released','M&M','M&M Germany', - 'Perfume Catalog – Luxury Fragrance Collection 2025','Parfümkatalog – Luxusduftkollektion 2025','Explore Elegant Scents for Every Occasion','Entdecken Sie elegante Düfte für jeden Anlass', - 'A curated catalog showcasing premium perfumes with scent profiles and packaging details.','Ein kuratierter Katalog mit Premium-Parfums, Duftprofilen und Verpackungsdetails.','Perfume, Fragrance, Luxury, Catalog, Collection','Parfum, Duft, Luxus, Katalog, Kollektion','https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.jpg'), - -(2,'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf','2','2024-03-03','InReview','M&M','M&M India', - 'Quality Assurance Report – Eau de Parfum Series A','Qualitätssicherungsbericht – Eau de Parfum Serie A','Verified Standards and Testing Results','Geprüfte Standards und Testergebnisse', - 'Report detailing quality checks and compliance standards for Series A perfumes.','Bericht mit Qualitätsprüfungen und Konformitätsstandards für Parfums der Serie A.','Packaging, Perfume, Bottles, Caps, Compliance','Verpackung, Parfum, Flaschen, Verschlüsse, Konformität','https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.jpg'), - -(0,'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf','1','2022-02-02','Released','M&M','M&M Germany', - 'Packaging Standards – Perfume Bottles and Caps','Verpackungsstandards – Parfümflaschen und Verschlüsse','Design and Material Compliance Guidelines','Richtlinien für Design und Materialkonformität', - 'Design and material compliance guidelines for perfume packaging.','Richtlinien für Design und Materialkonformität bei Parfumverpackungen.','Perfume, Fragrance, Luxury, Catalog, Collection','Parfum, Duft, Luxus, Katalog, Kollektion','https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.jpg'); +( + 0, + 'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/' + 'AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf', + '1', + '2023-01-01', + 'Released', + 'M&M', + 'M&M Germany', + 'User Guide – DSLR Camera Model X100', + 'Benutzerhandbuch – DSLR-Kamera Modell X100', + 'Complete Instructions for Professional Photography', + 'Vollständige Anleitung für professionelle Fotografie', + 'Detailed instructions for operating the X100 DSLR camera, including setup and troubleshooting.', + 'Detaillierte Anweisungen zur Bedienung der DSLR-Kamera X100, einschließlich Einrichtung und Fehlerbehebung', + 'DSLR, Camera, Photography, User Guide, Setup', + 'DSLR, Kamera, Fotografie, Benutzerhandbuch, Einrichtung', + 'https://raw.githubusercontent.com/' + 'AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.jpg' +), + +( + 1, + 'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/' + 'AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf', + '1.1', + '2024-05-05', + 'InReview', + 'M&M', + 'M&M India', + 'Technical Specification – Mirrorless Camera Z-Series', + 'Technische Spezifikation – Systemkamera der Z-Serie', + 'Detailed Specs for Advanced Imaging', + 'Detaillierte Spezifikationen für fortschrittliche Bildgebung', + 'Comprehensive technical details of the Z-Series mirrorless camera, covering sensor and performance.', + 'Umfassende technische Details der spiegellosen Kamera Z-Serie, einschließlich Sensor und Leistung.', + 'Mirrorless, Camera, Specs, Imaging, Performance', + 'Spiegellos, Kamera, Spezifikationen, Bildgebung, Leistung', + 'https://raw.githubusercontent.com/' + 'AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.jpg' +), + +( + 0, + 'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/' + 'AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf', + '2.1', + '2026-01-01', + 'Released', + 'M&M', + 'M&M China', + 'Maintenance Manual – Professional Camera Lens 50mm', + 'Wartungshandbuch – Professionelles Kameraobjektiv 50 mm', + 'Care and Cleaning Procedures', + 'Pflege und Reinigungsverfahren', + 'Guidelines for cleaning and maintaining the 50mm professional lens for optimal performance.', + 'Richtlinien zur Reinigung und Wartung des professionellen 50-mm-Objektivs für optimale Leistung.', + 'Lens, Maintenance, Cleaning, Professional, Care', + 'Objektiv, Wartung, Reinigung, Professionell, Pflege', + 'https://raw.githubusercontent.com/' + 'AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.jpg' +), + +( + 0, + 'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/' + 'AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf', + '2.3', + '2025-10-10', + 'InReview', + 'M&M', + 'M&M Germany', + 'Installation Guide – Wide-Angle Lens Kit', + 'Installationsanleitung – Weitwinkel-Objektiv-Kit', + 'Step-by-Step Setup Instructions', + 'Schritt-für-Schritt-Installationsanleitung', + 'Step-by-step instructions for installing and configuring the wide-angle lens kit.', + 'Schritt-für-Schritt-Anleitung zur Installation und Konfiguration des Weitwinkel-Objektivsets.', + 'Wide-Angle, Lens, Installation, Setup, Kit', + 'Weitwinkel, Objektiv, Installation, Einrichtung, Set', + 'https://raw.githubusercontent.com/' + 'AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.jpg' +), + +( + 0, + 'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/' + 'AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf', + '0.9', + '2024-01-01', + 'Released', + 'M&M', + 'M&M India', + 'Product Data Sheet – Telephoto Lens 200mm', + 'Produktdatenblatt – Teleobjektiv 200 mm', + 'Technical Data and Performance Metrics', + 'Technische Daten und Leistungskennzahlen', + 'Technical data and compatibility details for the 200mm telephoto lens.', + 'Technische Daten und Kompatibilitätsdetails für das 200-mm-Teleobjektiv.', + 'Telephoto, Lens, Data Sheet, Specifications, Optics', + 'Teleobjektiv, Objektiv, Datenblatt, Spezifikationen, Optik', + 'https://raw.githubusercontent.com/' + 'AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.jpg' +), + +( + 0, + 'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/' + 'AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf', + '1.2', + '2024-03-03', + 'InReview', + 'M&M', + 'M&M China', + 'Safety Instructions – Digital Camera Accessories', + 'Sicherheitsanweisungen – Zubehör für Digitalkameras', + 'Guidelines for Safe Usage', + 'Richtlinien für sichere Verwendung', + 'Safety guidelines for handling batteries, chargers, and other camera accessories.', + 'Sicherheitsrichtlinien für den Umgang mit Batterien, Ladegeräten und anderem Kamera-Zubehör.', + 'Safety, Camera, Accessories, Guidelines, Handling', + 'Sicherheit, Kamera, Zubehör, Richtlinien, Handhabung', + 'https://raw.githubusercontent.com/' + 'AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.jpg' +), + +( + 1, + 'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/' + 'AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf', + '1.4', + '2023-01-01', + 'Released', + 'M&M', + 'M&M Germany', + 'Perfume Catalog – Luxury Fragrance Collection 2025', + 'Parfümkatalog – Luxusduftkollektion 2025', + 'Explore Elegant Scents for Every Occasion', + 'Entdecken Sie elegante Düfte für jeden Anlass', + 'A curated catalog showcasing premium perfumes with scent profiles and packaging details.', + 'Ein kuratierter Katalog mit Premium-Parfums, Duftprofilen und Verpackungsdetails.', + 'Perfume, Fragrance, Luxury, Catalog, Collection', + 'Parfum, Duft, Luxus, Katalog, Kollektion', + 'https://raw.githubusercontent.com/' + 'AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.jpg' +), + +( + 2, + 'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/' + 'AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf', + '2', + '2024-03-03', + 'InReview', + 'M&M', + 'M&M India', + 'Quality Assurance Report – Eau de Parfum Series A', + 'Qualitätssicherungsbericht – Eau de Parfum Serie A', + 'Verified Standards and Testing Results', + 'Geprüfte Standards und Testergebnisse', + 'Report detailing quality checks and compliance standards for Series A perfumes.', + 'Bericht mit Qualitätsprüfungen und Konformitätsstandards für Parfums der Serie A.', + 'Packaging, Perfume, Bottles, Caps, Compliance', + 'Verpackung, Parfum, Flaschen, Verschlüsse, Konformität', + 'https://raw.githubusercontent.com/' + 'AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.jpg' +), + +( + 0, + 'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/' + 'AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf', + '1', + '2022-02-02', + 'Released', + 'M&M', + 'M&M Germany', + 'Packaging Standards – Perfume Bottles and Caps', + 'Verpackungsstandards – Parfümflaschen und Verschlüsse', + 'Design and Material Compliance Guidelines', + 'Richtlinien für Design und Materialkonformität', + 'Design and material compliance guidelines for perfume packaging.', + 'Richtlinien für Design und Materialkonformität bei Parfumverpackungen.', + 'Perfume, Fragrance, Luxury, Catalog, Collection', + 'Parfum, Duft, Luxus, Katalog, Kollektion', + 'https://raw.githubusercontent.com/' + 'AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.jpg' +); -- ============================================================ -- Languages @@ -246,7 +398,7 @@ INSERT INTO "DocumentVersionLanguages" ("DocumentVersionId","LanguageId") VALUES (6,6), (7,7), (8,8), -(9,9), +(9,9); -- ============================================================ -- Document-Version Relationships 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 30360ec1..36140471 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 @@ -131,6 +131,11 @@ internal void FillOutSubmodelElementValue(List elements, Seman { var semanticTreeNodes = GetSemanticNodes(element, values); + if (ShouldSkipElement(semanticTreeNodes)) + { + continue; + } + if (ShouldCloneElements(semanticTreeNodes, element)) { ReplaceWithClones(elements, element, semanticTreeNodes, updateIdShort); @@ -141,6 +146,8 @@ internal void FillOutSubmodelElementValue(List elements, Seman } } + private static bool ShouldSkipElement(List? nodes) => nodes == null || nodes.Count == 0; + 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) @@ -148,12 +155,11 @@ internal void FillOutSubmodelElementValue(List elements, Seman var semanticId = semanticIdResolver.ExtractSemanticId(element); return IsBranchElement(element) - ? SemanticTreeNavigator.FindNodeBySemanticId(values, semanticId).Cast().ToList() - : SemanticTreeNavigator.FindNodeBySemanticId(values, semanticId).Cast().ToList(); + ? [.. SemanticTreeNavigator.FindNodeBySemanticId(values, semanticId).Cast()] + : [.. SemanticTreeNavigator.FindNodeBySemanticId(values, semanticId).Cast()]; } - private static bool IsBranchElement(ISubmodelElement element) => - element is not (Property or AasCore.Aas3_0.File or Blob); + private static bool IsBranchElement(ISubmodelElement element) => element is not (Property or AasCore.Aas3_0.File or Blob); private void ReplaceWithClones(List elements, ISubmodelElement element, List nodes, bool updateIdShort) { 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 13c58cfb..1131f0f5 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 @@ -21,7 +21,6 @@ public partial class SemanticIdResolver(IOptions semantics) : ISemant 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; 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 da5fa552def65f6115beebec6624942185d6c55c Mon Sep 17 00:00:00 2001 From: Hardi Shah Date: Wed, 25 Mar 2026 12:15:30 +0530 Subject: [PATCH 13/14] Remove extra spaces form files --- .../SubmodelRepository/SemanticId/Helpers/SemanticIdResolver.cs | 1 - 1 file changed, 1 deletion(-) 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 1131f0f5..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,7 +20,6 @@ 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 _submodelElementIndexContextPrefix = semantics.Value.SubmodelElementIndexContextPrefix; public string MlpPostFixSeparator { get; } = semantics.Value.MultiLanguageSemanticPostfixSeparator; From e045474ef517a8e7947426e61c4414468c7a19d4 Mon Sep 17 00:00:00 2001 From: Hardi Shah Date: Thu, 26 Mar 2026 11:42:56 +0530 Subject: [PATCH 14/14] Refactor element skipping logic in SubmodelFiller Changed FillOutSubmodelElementValue from internal to private. Inlined the element skipping check, removing the ShouldSkipElement method for improved clarity and encapsulation. --- .../SubmodelRepository/SemanticId/FillOut/SubmodelFiller.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 36140471..b7234cdb 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 @@ -124,14 +124,14 @@ public ISubmodelElement FillOutElement(ISubmodelElement element, SemanticTreeNod return element; } - internal void FillOutSubmodelElementValue(List elements, SemanticTreeNode values, bool updateIdShort) + private void FillOutSubmodelElementValue(List elements, SemanticTreeNode values, bool updateIdShort) { var originalElements = elements.ToList(); foreach (var element in originalElements) { var semanticTreeNodes = GetSemanticNodes(element, values); - if (ShouldSkipElement(semanticTreeNodes)) + if (semanticTreeNodes == null || semanticTreeNodes.Count == 0) { continue; } @@ -146,8 +146,6 @@ internal void FillOutSubmodelElementValue(List elements, Seman } } - private static bool ShouldSkipElement(List? nodes) => nodes == null || nodes.Count == 0; - 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)