From 9d7e284a0fb952a1d87a45b17b622f7e7270a483 Mon Sep 17 00:00:00 2001 From: mm-rer <86965447+mm-rer@users.noreply.github.com> Date: Fri, 19 Dec 2025 16:12:02 +0100 Subject: [PATCH 1/3] Initial version copied from contributor-covenant.org --- CODE_OF_CONDUCT.md | 128 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..0d88745 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at TBD +. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. From d969522f9f6a8303f04049ad7da6fc66a27259d5 Mon Sep 17 00:00:00 2001 From: mm-rer <86965447+mm-rer@users.noreply.github.com> Date: Fri, 19 Dec 2025 16:59:06 +0100 Subject: [PATCH 2/3] Delete CODE_OF_CONDUCT.md --- CODE_OF_CONDUCT.md | 128 --------------------------------------------- 1 file changed, 128 deletions(-) delete mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index 0d88745..0000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,128 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -We as members, contributors, and leaders pledge to make participation in our -community a harassment-free experience for everyone, regardless of age, body -size, visible or invisible disability, ethnicity, sex characteristics, gender -identity and expression, level of experience, education, socio-economic status, -nationality, personal appearance, race, religion, or sexual identity -and orientation. - -We pledge to act and interact in ways that contribute to an open, welcoming, -diverse, inclusive, and healthy community. - -## Our Standards - -Examples of behavior that contributes to a positive environment for our -community include: - -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -* Focusing on what is best not just for us as individuals, but for the - overall community - -Examples of unacceptable behavior include: - -* The use of sexualized language or imagery, and sexual attention or - advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email - address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Enforcement Responsibilities - -Community leaders are responsible for clarifying and enforcing our standards of -acceptable behavior and will take appropriate and fair corrective action in -response to any behavior that they deem inappropriate, threatening, offensive, -or harmful. - -Community leaders have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, and will communicate reasons for moderation -decisions when appropriate. - -## Scope - -This Code of Conduct applies within all community spaces, and also applies when -an individual is officially representing the community in public spaces. -Examples of representing our community include using an official e-mail address, -posting via an official social media account, or acting as an appointed -representative at an online or offline event. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at TBD -. -All complaints will be reviewed and investigated promptly and fairly. - -All community leaders are obligated to respect the privacy and security of the -reporter of any incident. - -## Enforcement Guidelines - -Community leaders will follow these Community Impact Guidelines in determining -the consequences for any action they deem in violation of this Code of Conduct: - -### 1. Correction - -**Community Impact**: Use of inappropriate language or other behavior deemed -unprofessional or unwelcome in the community. - -**Consequence**: A private, written warning from community leaders, providing -clarity around the nature of the violation and an explanation of why the -behavior was inappropriate. A public apology may be requested. - -### 2. Warning - -**Community Impact**: A violation through a single incident or series -of actions. - -**Consequence**: A warning with consequences for continued behavior. No -interaction with the people involved, including unsolicited interaction with -those enforcing the Code of Conduct, for a specified period of time. This -includes avoiding interactions in community spaces as well as external channels -like social media. Violating these terms may lead to a temporary or -permanent ban. - -### 3. Temporary Ban - -**Community Impact**: A serious violation of community standards, including -sustained inappropriate behavior. - -**Consequence**: A temporary ban from any sort of interaction or public -communication with the community for a specified period of time. No public or -private interaction with the people involved, including unsolicited interaction -with those enforcing the Code of Conduct, is allowed during this period. -Violating these terms may lead to a permanent ban. - -### 4. Permanent Ban - -**Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an -individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within -the community. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], -version 2.0, available at -https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. - -Community Impact Guidelines were inspired by [Mozilla's code of conduct -enforcement ladder](https://github.com/mozilla/diversity). - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see the FAQ at -https://www.contributor-covenant.org/faq. Translations are available at -https://www.contributor-covenant.org/translations. From bd4a1fca199e8a7c38fb20d3290f2a374b3d2ab8 Mon Sep 17 00:00:00 2001 From: Kevalkumar Ghelani Date: Tue, 27 Jan 2026 16:47:09 +0530 Subject: [PATCH 3/3] Release (#26) --- .github/dependabot.yml | 49 + .github/workflows/codeql.yml | 100 + .github/workflows/dependency-review.yml | 99 + .github/workflows/docker-publish.yml | 125 + .github/workflows/dotnet.yml | 141 + .github/workflows/scorecard.yml | 78 + README.md | 103 +- apiCollection/environments/develop.bru | 3 - example/README.md | 172 + example/aas/CarbonFootprint.xml | 2597 +++++++++++++ example/aas/ContactInformationAAS.xml | 2139 +++++++++++ example/aas/HandoverDocumentation.xml | 3324 +++++++++++++++++ example/aas/Nameplate.xml | 2071 ++++++++++ example/aas/TechnicalData.xml | 1659 ++++++++ .../Aas Registry/Get All ShellDescriptors.bru | 0 .../Get Shell Descriptor By Id - Product1.bru | 25 + .../Get Shell Descriptor By Id - Product2.bru | 25 + .../Get Shell Descriptor By Id - Product3.bru | 25 + example/apiCollection/Aas Registry/folder.bru | 8 + .../Product1/Get Asset Information By Id.bru | 20 + .../Product1/Get Shell By Id.bru | 20 + .../Product1/Get Submodel Ref By Id.bru | 25 + .../Aas Repository/Product1/folder.bru | 8 + .../Product2/Get Asset Information By Id.bru | 20 + .../Product2/Get Shell By Id.bru | 20 + .../Product2/Get Submodel Ref By Id.bru | 25 + .../Aas Repository/Product2/folder.bru | 8 + .../Product3/Get Asset Information By Id.bru | 20 + .../Product3/Get Shell By Id.bru | 20 + .../Product3/Get Submodel Ref By Id.bru | 25 + .../Aas Repository/Product3/folder.bru | 8 + .../apiCollection/Aas Repository/folder.bru | 8 + example/apiCollection/README.md | 160 + ...del Descriptor By Id - CarbonFootprint.bru | 20 + ...et Submodel Descriptor By Id - Contact.bru | 20 + ...scriptor By Id - HandoverDocumentation.bru | 20 + ... Submodel Descriptor By Id - Nameplate.bru | 20 + ...model Descriptor By Id - TechnicalData.bru | 20 + .../Submodel Registry/folder.bru | 8 + ...t appropriate serialization - Product1.bru | 26 + .../Serialization/folder.bru | 0 ...Get Submodel Element - CarbonFootprint.bru | 21 + .../Get Submodel Element - ContactInfo.bru | 21 + ...bmodel Element - HandoverDocumentation.bru | 21 + .../Get Submodel Element - Nameplate.bru | 21 + .../Get Submodel Element - TechnicalData.bru | 21 + .../Submodel Element/folder.bru | 8 + .../Submodel/Get Submodel - ContactInfo.bru | 20 + .../Get Submodel - HandoverDocumentation.bru | 20 + .../Submodel/Get Submodel - Nameplate.bru | 20 + .../Submodel/Get Submodel - TechnicalData.bru | 20 + .../Get Submodel -CarbonFootprint.bru | 15 + .../Submodel Repository/Submodel/folder.bru | 0 .../Submodel Repository/folder.bru | 8 + example/apiCollection/bruno.json | 9 + example/apiCollection/collection.bru | 38 + example/apiCollection/environments/local.bru | 3 + example/basyx/aas-env.properties | 16 + example/data/checkmark.png | Bin 0 -> 153823 bytes example/data/dummy_document.jpg | Bin 0 -> 12688 bytes example/data/dummy_document.pdf | Bin 0 -> 1579 bytes example/data/product1.jpg | Bin 0 -> 114423 bytes example/data/product2.jpg | Bin 0 -> 92622 bytes example/data/product3.jpg | Bin 0 -> 121068 bytes example/docker-compose.yml | 222 ++ example/logo/MM_Logo.svg | 51 + example/nginx/default.conf.template | 133 + example/postgres/01_core_asset_tables.sql.inc | 118 + ...late_carbonfootprint_technicaldata.sql.inc | 116 + .../postgres/03_contactinformations.sql.inc | 221 ++ .../postgres/04_handoverdocumentation.sql.inc | 224 ++ example/postgres/init.sql | 27 + .../ShellDescriptorServiceTests.cs | 105 +- .../SemanticIdHandlerTests.cs | 25 + .../SubmodelTemplateServiceTests.cs | 53 +- .../Services/SubmodelRepository/TestData.cs | 109 + .../Providers/ProviderTestData.cs | 6 +- .../Services/TemplateProviderTests.cs | 2 +- source/AAS.TwinEngine.DataEngine.sln | 12 + .../AasRegistry/ShellDescriptorService.cs | 56 +- .../SubmodelRepository/SemanticIdHandler.cs | 56 +- .../SubmodelTemplateService.cs | 53 +- source/AAS.TwinEngine.DataEngine/Dockerfile | 44 +- .../Services/MultiPluginDataHandler.cs | 10 +- .../Services/PluginDataHandler.cs | 3 +- .../Services/ShellTemplateMappingProvider.cs | 2 +- .../Shared/JsonSerializationOptions.cs | 5 + source/AAS.TwinEngine.DataEngine/Program.cs | 4 +- .../appsettings.development.json | 16 +- .../appsettings.json | 29 +- ...nEngine.Plugin.TestPlugin.UnitTests.csproj | 34 + .../Manifest/Handler/ManifestHandlerTests.cs | 42 + .../Api/Manifest/ManifestControllerTests.cs | 30 + .../ManifestMappingProfileTests.cs | 37 + .../MetaData/Handler/MetaDataHandlerTests.cs | 101 + .../AssetMappingProfileTests.cs | 69 + .../ShellDescriptorProfileTests.cs | 73 + .../ShellDescriptorsProfileTests.cs | 56 + .../Api/MetaData/MetaDataControllerTests.cs | 148 + .../Submodel/Handler/SubmodelHandlerTests.cs | 143 + .../Services/JsonSchemaParserTests.cs | 389 ++ .../Services/JsonSchemaValidatorTests.cs | 180 + .../Services/SemanticTreeHandlerTests.cs | 304 ++ .../Api/Submodel/SubmodelControllerTests.cs | 77 + .../Services/Manifest/ManifestServiceTests.cs | 36 + .../Services/MetaData/MetaDataServiceTests.cs | 77 + .../Services/Submodel/SubmodelServiceTests.cs | 51 + .../CleanArchitectureTests.cs | 129 + .../AssetMappingProfileTests.cs | 46 + .../ShellDescriptorMappingProfileTests.cs | 62 + .../Helper/JsonConverterTests.cs | 150 + .../ManifestProvider/ManifestProviderTests.cs | 82 + .../MetaDataProvider/Helper/PaginatorTests.cs | 121 + .../MetaDataProvider/MetaDataProviderTests.cs | 149 + .../Providers/MockDataInitializerTests.cs | 87 + .../SubmodelProviderTests.cs | 253 ++ .../Infrastructure/Providers/TestData.cs | 157 + .../AAS.TwinEngine.Plugin.TestPlugin.csproj | 38 + .../Api/Manifest/Handler/IManifestHandler.cs | 8 + .../Api/Manifest/Handler/ManifestHandler.cs | 18 + .../Api/Manifest/ManifestController.cs | 28 + .../MappingProfiles/ManifestMappingProfile.cs | 20 + .../Api/Manifest/Responses/ManifestDto.cs | 21 + .../Api/MetaData/Handler/IMetaDataHandler.cs | 13 + .../Api/MetaData/Handler/MetaDataHandler.cs | 57 + .../MappingProfiles/AssetMappingProfile.cs | 27 + .../MappingProfiles/ShellDescriptorProfile.cs | 22 + .../ShellDescriptorsProfile.cs | 21 + .../Api/MetaData/MetaDataController.cs | 63 + .../Api/MetaData/Requests/GetAssetRequest.cs | 3 + .../Requests/GetShellDescriptorRequest.cs | 3 + .../Requests/GetShellDescriptorsRequest.cs | 3 + .../Api/MetaData/Responses/AssetDto.cs | 24 + .../MetaData/Responses/ShellDescriptorDto.cs | 28 + .../MetaData/Responses/ShellDescriptorsDto.cs | 18 + .../Api/Submodel/Handler/ISubmodelHandler.cs | 10 + .../Api/Submodel/Handler/SubmodelHandler.cs | 29 + .../Requests/GetSubmodelDataRequest.cs | 5 + .../Submodel/Services/IJsonSchemaParser.cs | 13 + .../Submodel/Services/IJsonSchemaValidator.cs | 8 + .../Submodel/Services/ISemanticTreeHandler.cs | 15 + .../Api/Submodel/Services/JsonSchemaParser.cs | 166 + .../Submodel/Services/JsonSchemaValidator.cs | 232 ++ .../Submodel/Services/SemanticTreeHandler.cs | 165 + .../Api/Submodel/SubmodelController.cs | 40 + .../Constants/ExceptionMessages.cs | 16 + .../Exceptions/BadRequestException.cs | 33 + .../Exceptions/CustomException.cs | 33 + .../Exceptions/ForbiddenException.cs | 26 + .../Exceptions/GlobalExceptionHandler.cs | 39 + .../Exceptions/InternalServerException.cs | 26 + .../Exceptions/NotFoundException.cs | 26 + .../Responses/ServiceErrorResponse.cs | 40 + .../Exceptions/UnauthorizedAccessException.cs | 23 + .../Services/Manifest/IManifestProvider.cs | 8 + .../Services/Manifest/IManifestService.cs | 8 + .../Services/Manifest/ManifestService.cs | 8 + .../Services/MetaData/IMetaDataProvider.cs | 12 + .../Services/MetaData/IMetaDataService.cs | 12 + .../Services/MetaData/MetaDataService.cs | 14 + .../Services/Submodel/Config/Semantics.cs | 12 + .../Services/Submodel/ISubmodelProvider.cs | 11 + .../Services/Submodel/ISubmodelService.cs | 11 + .../Services/Submodel/SubmodelService.cs | 10 + .../Authorization/.gitkeep | 0 .../PaginationValidationExtensions.cs | 18 + .../Common/Extensions/StringExtensions.cs | 32 + .../Data/mock-metadata.json | 38 + .../Data/mock-submodel-data.json | 648 ++++ .../Dockerfile | 15 + .../DomainModel/Manifest/ManifestData.cs | 21 + .../DomainModel/MetaData/AssetData.cs | 17 + .../MetaData/ShellDescriptorData.cs | 15 + .../MetaData/ShellDescriptorsData.cs | 18 + .../DomainModel/Submodel/DataType.cs | 13 + .../DomainModel/Submodel/SemanticTreeNode.cs | 31 + .../Example/README.md | 54 + .../Example/aas/ContactInformationAAS.aas.xml | 1791 +++++++++ .../Example/aas/DigitalNameplateAAS.aas.xml | 2395 ++++++++++++ .../Example/aas/Reliability.xml | 1471 ++++++++ .../Aas Registry/Get All ShellDescriptors.bru | 21 + .../Get Shell Descriptor By Id.bru | 0 .../apiCollection}/Aas Registry/folder.bru | 0 .../Get Asset Information By Id.bru | 0 .../Aas Repository/Get Shell By Id.bru | 0 .../Aas Repository/Get Submodel Ref By Id.bru | 0 .../apiCollection}/Aas Repository/folder.bru | 0 .../Example/apiCollection}/README.md | 28 +- .../Get Submodel Descriptor By Id.bru | 0 .../Submodel Registry/folder.bru | 0 .../Get appropriate serialization.bru | 0 .../Serialization/folder.bru | 8 + .../Get Submodel Element - ContactInfo.bru | 0 .../Get Submodel Element - Markings.bru | 16 + .../Get Submodel Element - Nameplate.bru | 0 .../Get Submodel Element - Reliability.bru | 0 .../Submodel Element/folder.bru | 0 .../Submodel/Get Submodel - ContactInfo.bru | 0 .../Submodel/Get Submodel - Nameplate.bru | 0 .../Submodel/Get Submodel - Reliability.bru | 0 .../Submodel Repository/Submodel/folder.bru | 8 + .../Submodel Repository/folder.bru | 0 .../Example/apiCollection}/bruno.json | 0 .../Example/apiCollection}/collection.bru | 0 .../apiCollection/environments/local.bru | 3 + .../apiCollection/environments/local_net.bru | 0 .../Example/apiCollection}/health.bru | 0 .../Example/basyx/aas-env.properties | 16 + .../Example/docker-compose.yml | 183 + .../Example/logo/MM_Logo.svg | 51 + .../Example/nginx/default.conf.template | 134 + .../Example/plugin/mock-metadata.json | 38 + .../Example/plugin/mock-submodel-data.json | 656 ++++ .../DataAccess/Entity/MetaDataEntity.cs | 48 + .../MapperProfiles/AssetMappingProfile.cs | 23 + .../ShellDescriptorMappingProfile.cs | 25 + .../ManifestProvider/Config/Capabilities.cs | 14 + .../ManifestProvider/Helper/JsonConverter.cs | 147 + .../ManifestProvider/ManifestProvider.cs | 60 + .../MetaDataProvider/Helper/Paginator.cs | 39 + .../MetaDataProvider/MetaDataProvider.cs | 99 + .../Infrastructure/Providers/MockData.cs | 9 + .../Providers/MockDataInitializer.cs | 45 + .../SubmodelProviders/Helper/JsonNodeInfo.cs | 10 + .../SubmodelProviders/SubmodelProvider.cs | 274 ++ .../Program.cs | 59 + .../Properties/launchSettings.json | 39 + ...pplicationDependencyInjectionExtensions.cs | 33 + .../Config/OpenTelemetrySettings.cs | 9 + ...astructureDependencyInjectionExtensions.cs | 24 + .../LoggingConfigurationExtension.cs | 65 + .../appsettings.Development.json | 56 + .../appsettings.json | 55 + .../config.nswag | 64 + 234 files changed, 28526 insertions(+), 207 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/dependency-review.yml create mode 100644 .github/workflows/docker-publish.yml create mode 100644 .github/workflows/dotnet.yml create mode 100644 .github/workflows/scorecard.yml delete mode 100644 apiCollection/environments/develop.bru create mode 100644 example/README.md create mode 100644 example/aas/CarbonFootprint.xml create mode 100644 example/aas/ContactInformationAAS.xml create mode 100644 example/aas/HandoverDocumentation.xml create mode 100644 example/aas/Nameplate.xml create mode 100644 example/aas/TechnicalData.xml rename {apiCollection => example/apiCollection}/Aas Registry/Get All ShellDescriptors.bru (100%) create mode 100644 example/apiCollection/Aas Registry/Get Shell Descriptor By Id - Product1.bru create mode 100644 example/apiCollection/Aas Registry/Get Shell Descriptor By Id - Product2.bru create mode 100644 example/apiCollection/Aas Registry/Get Shell Descriptor By Id - Product3.bru create mode 100644 example/apiCollection/Aas Registry/folder.bru create mode 100644 example/apiCollection/Aas Repository/Product1/Get Asset Information By Id.bru create mode 100644 example/apiCollection/Aas Repository/Product1/Get Shell By Id.bru create mode 100644 example/apiCollection/Aas Repository/Product1/Get Submodel Ref By Id.bru create mode 100644 example/apiCollection/Aas Repository/Product1/folder.bru create mode 100644 example/apiCollection/Aas Repository/Product2/Get Asset Information By Id.bru create mode 100644 example/apiCollection/Aas Repository/Product2/Get Shell By Id.bru create mode 100644 example/apiCollection/Aas Repository/Product2/Get Submodel Ref By Id.bru create mode 100644 example/apiCollection/Aas Repository/Product2/folder.bru create mode 100644 example/apiCollection/Aas Repository/Product3/Get Asset Information By Id.bru create mode 100644 example/apiCollection/Aas Repository/Product3/Get Shell By Id.bru create mode 100644 example/apiCollection/Aas Repository/Product3/Get Submodel Ref By Id.bru create mode 100644 example/apiCollection/Aas Repository/Product3/folder.bru create mode 100644 example/apiCollection/Aas Repository/folder.bru create mode 100644 example/apiCollection/README.md create mode 100644 example/apiCollection/Submodel Registry/Get Submodel Descriptor By Id - CarbonFootprint.bru create mode 100644 example/apiCollection/Submodel Registry/Get Submodel Descriptor By Id - Contact.bru create mode 100644 example/apiCollection/Submodel Registry/Get Submodel Descriptor By Id - HandoverDocumentation.bru create mode 100644 example/apiCollection/Submodel Registry/Get Submodel Descriptor By Id - Nameplate.bru create mode 100644 example/apiCollection/Submodel Registry/Get Submodel Descriptor By Id - TechnicalData.bru create mode 100644 example/apiCollection/Submodel Registry/folder.bru create mode 100644 example/apiCollection/Submodel Repository/Serialization/Get appropriate serialization - Product1.bru rename {apiCollection => example/apiCollection}/Submodel Repository/Serialization/folder.bru (100%) create mode 100644 example/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - CarbonFootprint.bru create mode 100644 example/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - ContactInfo.bru create mode 100644 example/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - HandoverDocumentation.bru create mode 100644 example/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - Nameplate.bru create mode 100644 example/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - TechnicalData.bru create mode 100644 example/apiCollection/Submodel Repository/Submodel Element/folder.bru create mode 100644 example/apiCollection/Submodel Repository/Submodel/Get Submodel - ContactInfo.bru create mode 100644 example/apiCollection/Submodel Repository/Submodel/Get Submodel - HandoverDocumentation.bru create mode 100644 example/apiCollection/Submodel Repository/Submodel/Get Submodel - Nameplate.bru create mode 100644 example/apiCollection/Submodel Repository/Submodel/Get Submodel - TechnicalData.bru create mode 100644 example/apiCollection/Submodel Repository/Submodel/Get Submodel -CarbonFootprint.bru rename {apiCollection => example/apiCollection}/Submodel Repository/Submodel/folder.bru (100%) create mode 100644 example/apiCollection/Submodel Repository/folder.bru create mode 100644 example/apiCollection/bruno.json create mode 100644 example/apiCollection/collection.bru create mode 100644 example/apiCollection/environments/local.bru create mode 100644 example/basyx/aas-env.properties create mode 100644 example/data/checkmark.png create mode 100644 example/data/dummy_document.jpg create mode 100644 example/data/dummy_document.pdf create mode 100644 example/data/product1.jpg create mode 100644 example/data/product2.jpg create mode 100644 example/data/product3.jpg create mode 100644 example/docker-compose.yml create mode 100644 example/logo/MM_Logo.svg create mode 100644 example/nginx/default.conf.template create mode 100644 example/postgres/01_core_asset_tables.sql.inc create mode 100644 example/postgres/02_nameplate_carbonfootprint_technicaldata.sql.inc create mode 100644 example/postgres/03_contactinformations.sql.inc create mode 100644 example/postgres/04_handoverdocumentation.sql.inc create mode 100644 example/postgres/init.sql create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/AAS.TwinEngine.Plugin.TestPlugin.UnitTests.csproj create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Manifest/Handler/ManifestHandlerTests.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Manifest/ManifestControllerTests.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Manifest/MappingProfiles/ManifestMappingProfileTests.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/MetaData/Handler/MetaDataHandlerTests.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/MetaData/MappingProfiles/AssetMappingProfileTests.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/MetaData/MappingProfiles/ShellDescriptorProfileTests.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/MetaData/MappingProfiles/ShellDescriptorsProfileTests.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/MetaData/MetaDataControllerTests.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Handler/SubmodelHandlerTests.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaParserTests.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaValidatorTests.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/SemanticTreeHandlerTests.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/SubmodelControllerTests.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/ApplicationLogic/Services/Manifest/ManifestServiceTests.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/ApplicationLogic/Services/MetaData/MetaDataServiceTests.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/ApplicationLogic/Services/Submodel/SubmodelServiceTests.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/CleanArchitectureTests.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/DataAccess/MapperProfiles/AssetMappingProfileTests.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/DataAccess/MapperProfiles/ShellDescriptorMappingProfileTests.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/Providers/ManifestProvider/Helper/JsonConverterTests.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/Providers/ManifestProvider/ManifestProviderTests.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/Providers/MetaDataProvider/Helper/PaginatorTests.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/Providers/MetaDataProvider/MetaDataProviderTests.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/Providers/MockDataInitializerTests.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/Providers/SubmodelProviders/SubmodelProviderTests.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/Providers/TestData.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/AAS.TwinEngine.Plugin.TestPlugin.csproj create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Api/Manifest/Handler/IManifestHandler.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Api/Manifest/Handler/ManifestHandler.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Api/Manifest/ManifestController.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Api/Manifest/MappingProfiles/ManifestMappingProfile.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Api/Manifest/Responses/ManifestDto.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/Handler/IMetaDataHandler.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/Handler/MetaDataHandler.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/MappingProfiles/AssetMappingProfile.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/MappingProfiles/ShellDescriptorProfile.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/MappingProfiles/ShellDescriptorsProfile.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/MetaDataController.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/Requests/GetAssetRequest.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/Requests/GetShellDescriptorRequest.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/Requests/GetShellDescriptorsRequest.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/Responses/AssetDto.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/Responses/ShellDescriptorDto.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/Responses/ShellDescriptorsDto.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Handler/ISubmodelHandler.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Handler/SubmodelHandler.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Requests/GetSubmodelDataRequest.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/IJsonSchemaParser.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/IJsonSchemaValidator.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/ISemanticTreeHandler.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaParser.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaValidator.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/SemanticTreeHandler.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/SubmodelController.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Constants/ExceptionMessages.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Exceptions/BadRequestException.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Exceptions/CustomException.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Exceptions/ForbiddenException.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Exceptions/GlobalExceptionHandler.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Exceptions/InternalServerException.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Exceptions/NotFoundException.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Exceptions/Responses/ServiceErrorResponse.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Exceptions/UnauthorizedAccessException.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/Manifest/IManifestProvider.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/Manifest/IManifestService.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/Manifest/ManifestService.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/MetaData/IMetaDataProvider.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/MetaData/IMetaDataService.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/MetaData/MetaDataService.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/Submodel/Config/Semantics.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/Submodel/ISubmodelProvider.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/Submodel/ISubmodelService.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/Submodel/SubmodelService.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Authorization/.gitkeep create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Common/Extensions/PaginationValidationExtensions.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Common/Extensions/StringExtensions.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Data/mock-metadata.json create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Data/mock-submodel-data.json create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Dockerfile create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/DomainModel/Manifest/ManifestData.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/DomainModel/MetaData/AssetData.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/DomainModel/MetaData/ShellDescriptorData.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/DomainModel/MetaData/ShellDescriptorsData.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/DomainModel/Submodel/DataType.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/DomainModel/Submodel/SemanticTreeNode.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Example/README.md create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Example/aas/ContactInformationAAS.aas.xml create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Example/aas/DigitalNameplateAAS.aas.xml create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Example/aas/Reliability.xml create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Aas Registry/Get All ShellDescriptors.bru rename {apiCollection => source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection}/Aas Registry/Get Shell Descriptor By Id.bru (100%) rename {apiCollection => source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection}/Aas Registry/folder.bru (100%) rename {apiCollection => source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection}/Aas Repository/Get Asset Information By Id.bru (100%) rename {apiCollection => source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection}/Aas Repository/Get Shell By Id.bru (100%) rename {apiCollection => source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection}/Aas Repository/Get Submodel Ref By Id.bru (100%) rename {apiCollection => source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection}/Aas Repository/folder.bru (100%) rename {apiCollection => source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection}/README.md (85%) rename {apiCollection => source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection}/Submodel Registry/Get Submodel Descriptor By Id.bru (100%) rename {apiCollection => source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection}/Submodel Registry/folder.bru (100%) rename {apiCollection => source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection}/Submodel Repository/Serialization/Get appropriate serialization.bru (100%) create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Repository/Serialization/folder.bru rename {apiCollection => source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection}/Submodel Repository/Submodel Element/Get Submodel Element - ContactInfo.bru (100%) create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - Markings.bru rename {apiCollection => source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection}/Submodel Repository/Submodel Element/Get Submodel Element - Nameplate.bru (100%) rename {apiCollection => source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection}/Submodel Repository/Submodel Element/Get Submodel Element - Reliability.bru (100%) rename {apiCollection => source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection}/Submodel Repository/Submodel Element/folder.bru (100%) rename {apiCollection => source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection}/Submodel Repository/Submodel/Get Submodel - ContactInfo.bru (100%) rename {apiCollection => source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection}/Submodel Repository/Submodel/Get Submodel - Nameplate.bru (100%) rename {apiCollection => source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection}/Submodel Repository/Submodel/Get Submodel - Reliability.bru (100%) create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Repository/Submodel/folder.bru rename {apiCollection => source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection}/Submodel Repository/folder.bru (100%) rename {apiCollection => source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection}/bruno.json (100%) rename {apiCollection => source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection}/collection.bru (100%) create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/environments/local.bru rename apiCollection/environments/local.bru => source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/environments/local_net.bru (100%) rename {apiCollection => source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection}/health.bru (100%) create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Example/basyx/aas-env.properties create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Example/docker-compose.yml create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Example/logo/MM_Logo.svg create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Example/nginx/default.conf.template create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Example/plugin/mock-metadata.json create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Example/plugin/mock-submodel-data.json create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/DataAccess/Entity/MetaDataEntity.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/DataAccess/MapperProfiles/AssetMappingProfile.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/DataAccess/MapperProfiles/ShellDescriptorMappingProfile.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/ManifestProvider/Config/Capabilities.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/ManifestProvider/Helper/JsonConverter.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/ManifestProvider/ManifestProvider.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/MetaDataProvider/Helper/Paginator.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/MetaDataProvider/MetaDataProvider.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/MockData.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/MockDataInitializer.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/SubmodelProviders/Helper/JsonNodeInfo.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/SubmodelProviders/SubmodelProvider.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Program.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/Properties/launchSettings.json create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/ServiceConfiguration/ApplicationDependencyInjectionExtensions.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/ServiceConfiguration/Config/OpenTelemetrySettings.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/ServiceConfiguration/LoggingConfigurationExtension.cs create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/appsettings.Development.json create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/appsettings.json create mode 100644 source/AAS.TwinEngine.Plugin.TestPlugin/config.nswag diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..c29e6ce --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,49 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "nuget" + target-branch: "develop" + directory: "/source" + groups: + major-minor-patch: + applies-to: version-updates + update-types: + - "major" + - "minor" + - "patch" + schedule: + interval: "weekly" + cooldown: + default-days: 3 + + - package-ecosystem: "docker" + target-branch: "develop" + directory: "/source/AAS.TwinEngine.DataEngine" + groups: + major-minor-patch: + applies-to: version-updates + update-types: + - "major" + - "minor" + - "patch" + schedule: + interval: "daily" + + - package-ecosystem: "github-actions" + target-branch: "develop" + directory: "/" + groups: + major-minor-patch: + applies-to: version-updates + update-types: + - "major" + - "minor" + - "patch" + schedule: + interval: "weekly" + cooldown: + default-days: 3 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..9c05865 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,100 @@ +name: "CodeQL Advanced" + +on: + push: + branches: + - 'main' + - 'develop' + - 'release/**' + - 'hotfix/**' + pull_request: + branches: + - 'main' + - 'develop' + - 'release/**' + - 'hotfix/**' + schedule: + - cron: '30 19 * * 0' + +permissions: read-all + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: csharp + build-mode: none + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - name: Run manual build steps + if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..c57ba33 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,99 @@ +# Dependency Review Action +# +# This Action will scan dependency manifest files that change as part of a Pull Request, +# surfacing known-vulnerable versions of the packages declared or updated in the PR. +# Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable +# packages will be blocked from merging. +# +# Source repository: https://github.com/actions/dependency-review-action +# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement +name: 'Dependency review' +on: + pull_request: + branches: + - 'main' + - 'develop' + - 'release/**' + - 'hotfix/**' + +# If using a dependency submission action in this workflow this permission will need to be set to: +# +# permissions: +# contents: write +# +# https://docs.github.com/en/enterprise-cloud@latest/code-security/supply-chain-security/understanding-your-software-supply-chain/using-the-dependency-submission-api +permissions: + contents: read + # Write permissions for pull-requests are required for using the `comment-summary-in-pr` option, comment out if you aren't using this option + pull-requests: write + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: 'Checkout repository' + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - name: 'Dependency Review' + uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2 + # Commonly enabled options, see https://github.com/actions/dependency-review-action#configuration-options for all available options. + with: + retry-on-snapshot-warnings: true + retry-on-snapshot-warnings-timeout: 60 + warn-on-openssf-scorecard-level: 5 + comment-summary-in-pr: always + allow-dependencies-licenses: | + pkg:nuget/AasCore.Aas3_0, + pkg:nuget/AasCore.Aas3.Package + allow-licenses: | + Apache-1.0, + Apache-1.1, + Apache-2.0, + BSL-1.0, + BSD-1-Clause, + BSD-2-Clause, + BSD-2-Clause-FreeBSD, + BSD-2-Clause-NetBSD, + BSD-3-Clause, + BSD-3-Clause-Clear, + BSD-3-Clause-No-Nuclear-License, + BSD-3-Clause-No-Nuclear-License-2014, + BSD-3-Clause-No-Nuclear-Warranty, + BSD-3-Clause-Open-MPI, + BSD-4-Clause, + BSD-Protection, + BSD-Source-Code, + BSD-3-Clause-Attribution, + 0BSD, + BSD-2-Clause-Patent, + BSD-4-Clause-UC, + MIT-CMU, + CC-BY-3.0, + CC-BY-SA-1.0, + CC-BY-SA-2.0, + CC-BY-SA-2.5, + CC-BY-SA-3.0, + CC-BY-SA-4.0, + CC0-1.0, + WTFPL, + MIT-enna, + MIT-feh, + ISC, + JSON, + BSD-3-Clause-LBNL, + MITNFA, + MIT, + MIT-0, + UPL-1.0, + NCSA, + X11, + Xerox, + BlueOak-1.0.0, + CC-BY-4.0, + MS-PL, + PostgreSQL, + Python-2.0, + SSPL-1.0, + OFL-1.1, + Unlicense, + Unicode-DFS-2016, + Unicode-3.0 diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..113c32f --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,125 @@ +name: Docker + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +on: + workflow_dispatch: + push: + branches: + - 'develop' + - 'release/**' + - 'hotfix/**' + # Publish semver tags as releases. + tags: [ 'v**' ] + pull_request: + branches: + - 'main' + - 'develop' + - 'release/**' + - 'hotfix/**' + +permissions: + contents: read + +env: + # Use docker.io for Docker Hub if empty + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository_owner }}/dataengine + DOCKERFILE_PATH: source/AAS.TwinEngine.DataEngine/Dockerfile + BUILD_CONTEXT: source + + +jobs: + build: + + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + # Set up BuildKit Docker container builder to be able to build + # multi-platform images and export cache + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 + + # Login against a Docker registry except on PR + # https://github.com/docker/login-action + - name: Log into registry ${{ env.REGISTRY }} + if: github.event_name != 'pull_request' + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Extract metadata (tags, labels) for Docker + # https://github.com/docker/metadata-action + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + flavor: | + latest=false + tags: | + type=semver,pattern={{raw}} + type=raw,value=develop-{{sha}},enable=${{startsWith(github.ref, 'refs/heads/develop')}} + type=raw,value=rc-{{branch}}-{{sha}},enable=${{startsWith(github.ref, 'refs/heads/release/')}} + type=raw,value={{branch}}-{{sha}},enable=${{startsWith(github.ref, 'refs/heads/hotfix/')}} + type=ref,event=pr + type=raw,value=manual-{{branch}}-{{sha}},enable=${{github.event_name == 'workflow_dispatch'}} + + # Build and push Docker image with Buildx (don't push on PR) + # https://github.com/docker/build-push-action + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + with: + sbom: true + provenance: mode=max + context: ${{ env.BUILD_CONTEXT }} + file: ${{ env.DOCKERFILE_PATH }} + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + + # Extract the pure application SBOM from the artifact stage, we want to handle it separately from the container SBOM + # This automaticaly re-uses the previously generated stage from cache, so we get the exact sbom from previous build step + - name: Export Application SBOM from artifact stage + if: ${{ github.event_name != 'pull_request' }} + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + with: + context: ${{ env.BUILD_CONTEXT }} + file: ${{ env.DOCKERFILE_PATH }} + target: app-sbom-artifact + push: false + outputs: type=local,dest=sbom-output + + # Generate container SBOM. + - name: Run Trivy in GitHub SBOM mode to generate CycloneDX SBOM for container + if: ${{ github.event_name != 'pull_request' }} + uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1 + with: + scan-type: 'image' + format: 'cyclonedx' + output: 'sbom-output/sbom_container.cyclonedx.json' + image-ref: ${{ steps.meta.outputs.tags }} + skip-dirs: '/App' # Skip the /app directory as we handle the content of the application in a seperate SBOM for easier vulnerability management and because trivy misses important fields + + - name: Upload trivy/container AND application SBOMs as a Github artifact + if: ${{ github.event_name != 'pull_request' }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: sbom + path: '${{ github.workspace }}/sbom-output/' diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 0000000..1af6d85 --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,141 @@ +name: .NET + +on: + workflow_dispatch: + pull_request: + branches: + - 'main' + - 'develop' + - 'release/**' + - 'hotfix/**' + push: + branches: + - 'main' + - 'develop' + - 'release/**' + - 'hotfix/**' + +permissions: + contents: read + +env: + BUILD_CONFIGURATION: "Release" + SOLUTION_PATH: source/AAS.TwinEngine.DataEngine.sln + TEST_PROJECT: source/AAS.TwinEngine.DataEngine.UnitTests/AAS.TwinEngine.DataEngine.UnitTests.csproj + MODULE_TEST_PROJECT: source/AAS.TwinEngine.DataEngine.ModuleTests/AAS.TwinEngine.DataEngine.ModuleTests.csproj + +jobs: + + build-test: + name: Build & Test + runs-on: ubuntu-latest + + permissions: + contents: read + checks: write + pull-requests: write + + steps: + - name: Checkout code + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Setup .NET + uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 + with: + dotnet-version: "8.0.x" + + - name: Restore + run: dotnet restore ${{ env.SOLUTION_PATH }} --locked-mode + + - name: Build + run: dotnet build ${{ env.SOLUTION_PATH }} --configuration ${{ env.BUILD_CONFIGURATION }} --no-restore + + - name: Run Unit Tests + run: dotnet test ${{ env.TEST_PROJECT }} --configuration Release --no-build --logger "trx;LogFileName=unit_test_results.trx" --collect:"XPlat Code Coverage" --settings source/coverlet.runsettings + + - name: Run Module Tests + run: dotnet test ${{ env.MODULE_TEST_PROJECT }} --configuration Release --no-build --logger "trx;LogFileName=module_test_results.trx" --collect:"XPlat Code Coverage" --settings source/coverlet.runsettings + + - name: Publish Test Results + uses: dorny/test-reporter@fe45e9537387dac839af0d33ba56eed8e24189e8 # v2.3.0 + id: test-results + if: github.event.pull_request.head.repo.fork == false + with: + name: Test Results (Unit & Module) + path: "**/*_test_results.trx" + reporter: dotnet-trx + + - name: Combine Reports (Test Results) + if: github.event.pull_request.head.repo.fork == false + run: | + echo "# Test & Coverage Report" > results.md + echo "" >> results.md + echo "## Test Results Summary" >> results.md + echo "" >> results.md + echo "| Metric | Count |" >> results.md + echo "|--------|-------|" >> results.md + echo "| ✅ Passed | ${{ steps.test-results.outputs.passed }} |" >> results.md + echo "| ❌ Failed | ${{ steps.test-results.outputs.failed }} |" >> results.md + echo "| ⏭️ Skipped | ${{ steps.test-results.outputs.skipped }} |" >> results.md + echo "" >> results.md + echo "[View Detailed Test Results](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> results.md + echo "" >> results.md + echo "---" >> results.md + echo "" >> results.md + + - name: Code Coverage Report - Unit Tests + if: github.event.pull_request.head.repo.fork == false + uses: irongut/CodeCoverageSummary@51cc3a756ddcd398d447c044c02cb6aa83fdae95 # v1.3.0 + with: + filename: "source/AAS.TwinEngine.DataEngine.UnitTests/TestResults/*/coverage.cobertura.xml" + badge: false + fail_below_min: true + format: markdown + hide_branch_rate: false + hide_complexity: false + indicators: true + output: both + thresholds: "80 80" + + - name: Combine Reports (Unit Test Coverage Results) + if: github.event.pull_request.head.repo.fork == false + run: | + echo "## Code Coverage" >> results.md + echo "" >> results.md + echo "### Unit Tests Coverage" >> results.md + cat code-coverage-results.md >> results.md + echo "" >> results.md + + - name: Code Coverage Report - Module Tests + if: github.event.pull_request.head.repo.fork == false + uses: irongut/CodeCoverageSummary@51cc3a756ddcd398d447c044c02cb6aa83fdae95 # v1.3.0 + with: + filename: "source/AAS.TwinEngine.DataEngine.ModuleTests/TestResults/*/coverage.cobertura.xml" + badge: false + fail_below_min: false + format: markdown + hide_branch_rate: false + hide_complexity: false + indicators: true + output: both + + - name: Combine Reports (Module Test Coverage Results) + if: github.event.pull_request.head.repo.fork == false + run: | + echo "### Module Tests Coverage" >> results.md + cat code-coverage-results.md >> results.md + + + - name: Add PR Comment + if: github.event.pull_request.head.repo.fork == false + uses: marocchino/sticky-pull-request-comment@331f8f5b4215f0445d3c07b4967662a32a2d3e31 # v2.9.0 + with: + recreate: true + path: results.md + + - name: Upload TRX as artifact (Fork PR fallback) + if: github.event.pull_request.head.repo.fork == true + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + with: + name: test-results + path: "**/*_test_results.trx" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 0000000..0d586b6 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,78 @@ +# This workflow uses actions that are not certified by GitHub. They are provided +# by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: Scorecard supply-chain security +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '26 2 * * *' + push: + branches: [ "main" ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + # `publish_results: true` only works when run from the default branch. conditional can be removed if disabled. + if: github.event.repository.default_branch == github.ref_name || github.event_name == 'pull_request' + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + # Uncomment the permissions below if installing in a private repository. + # contents: read + # actions: read + + steps: + - name: "Checkout code" + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 + with: + results_file: results.sarif + results_format: sarif + # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: + # - you want to enable the Branch-Protection check on a *public* repository, or + # - you are installing Scorecard on a *private* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. + # repo_token: ${{ secrets.SCORECARD_TOKEN }} + + # Public repositories: + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories: + # - `publish_results` will always be set to `false`, regardless + # of the value entered here. + publish_results: false + + # (Optional) Uncomment file_mode if you have a .gitattributes with files marked export-ignore + # file_mode: git + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard (optional). + # Commenting out will disable upload of results to your repo's Code Scanning dashboard + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6 + with: + sarif_file: results.sarif diff --git a/README.md b/README.md index 3dc74ed..ab6f7c3 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,101 @@ # DataEngine -**DataEngine** is a .NET-based service that dynamically generates complete **Asset Administration Shell (AAS)** submodels by combining standardized templates with real-time data. -It integrates with **Eclipse BaSyx** and follows **IDTA specifications** to ensure interoperability. -When a submodel is requested, DataEngine retrieves its template, queries the **Plugin** for semantic ID values, and populates the structure automatically. -It supports nested and hierarchical data models, providing ready-to-use submodels for visualization or API consumption. -In short, DataEngine acts as the **core orchestration layer** that transforms static AAS templates into live digital representations. +[![Made by M&M Software](https://img.shields.io/badge/Made_by_M%26M_Software-364955?style=flat-square)](https://www.mm-software.com/) +[![Apache License](https://img.shields.io/badge/License-Apache-364955.svg?style=flat-square)](https://www.apache.org/licenses/) +[![.NET 8.0](https://img.shields.io/badge/.NET-8.0-512BD4)](https://learn.microsoft.com/en-us/aspnet/core/web-api/?view=aspnetcore-10.0&WT.mc_id=dotnet-35129-website) + +## Overview + +## DataEngine Overview + +**DataEngine** is an API service that dynamically generates complete **Asset Administration Shell (AAS) submodels** by combining standardized AAS templates with provided data from database via plugins. + +It acts as the **core orchestration layer** in the TwinEngine ecosystem, transforming static AAS templates into **live, ready-to-consume AAS representations** through a **plugin-based value resolution mechanism**. + +DataEngine integrates seamlessly with **Eclipse BaSyx** components and is also capable of orchestrating other **open-source AAS-related components**, enabling a **flexible, vendor-neutral AAS infrastructure**. + +--- + +## Features Overview + +- **Template-based Dynamic AAS and Submodel Generation** + Dynamically builds complete AAS structures using standardized, reusable templates (containing semantic IDs without values). + - For more info : [Template-driven submodel generation](https://github.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/wiki/Feature-Overview#template-driven-submodel-generation) + +- **Schema-driven Plugin Integration** + Decouples data access through external Plugin APIs using JSON Schema contracts for type-safe communication. + - For more info : [Plugin-based value resolution](https://github.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/wiki/Feature-Overview#plugin-based-value-resolution) + +- **IDTA-aligned AAS REST Endpoints** + Multiple API endpoints that align with official IDTA AAS specifications, supporting shell descriptor, submodel, and submodel element operations for seamless interoperability. + - For more info : [Idta-aligned-endpoints](https://github.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/wiki/Feature-Overview#idta-aligned-endpoints) + +- **Multi-Plugin Orchestration** + DataEngine supports orchestration across multiple Plugin APIs to resolve runtime data simultaneously. + - For more info : [Multi-plugin orchestration](https://github.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/wiki/Feature-Overview#multi-plugin-orchestration) + +- **Nested and Hierarchical Submodel Support** + Handles complex Submodels with deeply nested and structured SubmodelElements. + - For more info : [Hierarchical & nested models](https://github.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/wiki/Feature-Overview#hierarchical--nested-models) + +- **Comprehensive SubmodelElement Type Support** + Supports a broad range of SubmodelElement types with semantic preservation during population. + - For more info : [Support for SubmodelElement types](https://github.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/wiki/Feature-Overview#support-for-submodelelement-types) + +## DataEngine - AAS Generation Flow + +```mermaid +sequenceDiagram + actor A as Client + participant B as DataEngine + participant C as Template Repository + participant D as Plugins + + A->>B: GET /submodels/{submodelIdentifier} + B->>C: GET / Submodel Templates + C-->>B: Submodel Template (Semantic IDs) + B->>B: Extract semantic IDs + B->>D: POST /data/{submodelId} + D-->D: Resolve semantic IDs + D-->>B: Semantic Id with Values + B->>B: Populate Template + B-->>A: Filled submodel +``` + +The DataEngine transforms **static AAS templates** into **live digital representations**. + +### Step-by-Step Process + +When a client requests AAS data (shell descriptor, submodel, or submodel element): + +1. **Fetch Template** - DataEngine retrieves the required AAS/Submodel template from the AAS Template Registry +2. **Extract Semantic IDs** - Identifies all semantic IDs within the template that need values +3. **Request Data via Schema** - Sends a JSON Schema to the Plugin API describing the structure and semantic IDs needed +4. **Receive Values** - Plugin queries its database and responds with populated values for the requested semantic IDs +5. **Populate Template** - DataEngine injects the received values into the template structure +6. **Return AAS submodel Response** - Responds to the client with a complete, ready-to-use AAS structure + + +## **Quick Start Guide** + +### Running the Setup With DPP Plugin + +1. **Clone or extract this repository:** + ```bash + git clone https://github.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine.git + cd AAS.TwinEngine.DataEngine/example + ``` + +2. **Start all services:** + ```bash + docker-compose up -d + ``` + +3. **Access the Web UI:** + Open your browser and navigate to: + ``` + http://localhost:8080/aas-ui/ + ``` +- For more info : [TwinEngine Demonstrator Setup](https://github.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/blob/develop/example/README.md) +--- diff --git a/apiCollection/environments/develop.bru b/apiCollection/environments/develop.bru deleted file mode 100644 index 2b22af1..0000000 --- a/apiCollection/environments/develop.bru +++ /dev/null @@ -1,3 +0,0 @@ -vars { - DataEngineBaseUrl: https://twinengine-dataengine-dev-euw-ca.icystone-61e99dc5.westeurope.azurecontainerapps.io -} diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..6776b04 --- /dev/null +++ b/example/README.md @@ -0,0 +1,172 @@ +# TwinEngine Demonstrator Setup + +## Overview + +This folder provides a complete, containerized setup to demonstrate how **TwinEngine.DataEngine** can be integrated and run locally. It creates a fully functional environment for managing Asset Administration Shells (AAS), submodels, and related digital asset components using Docker Compose. + +The setup includes a complete tech stack with services for AAS registry, repository, submodel management, data persistence, UI access, and a plugin system—all orchestrated through Docker containers on a shared network. + +## Included Submodel Templates + +This example includes 5 standardized submodel templates from the **Digital Product Passport for Industry 4.0**: + +- **Nameplate** +- **ContactInformation** +- **TechnicalData** +- **CarbonFootprint** +- **HandoverDocumentation** + +## Quick Start + +### Prerequisites + +Before running the demonstrator, ensure you have installed: + +- **Docker** (v20.10+) — [Install Docker](https://docs.docker.com/get-docker/) +- **Docker Compose** (v1.29+) — Usually included with Docker Desktop +- **Available Ports** — The following ports must be available on your machine: + - `8080` — Main API Gateway (nginx) + - `8081` - PGAdmin + +### Running the Setup + +1. **Clone or extract this repository:** + ```bash + git clone https://github.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine.git + cd AAS.TwinEngine.DataEngine + ``` +2. **Go Inside example Folder** + +```bash +cd AAS.TwinEngine.DataEngine\example +``` + +2. **Start all services:** + ```bash + docker-compose up -d + ``` + +3. **Access the Web UI:** + Open your browser and navigate to: + ``` + http://localhost:8080/aas-ui/ + ``` + +4. **Stop all services:** + ```bash + docker-compose down + ``` + +## Architecture & Services + +The docker-compose setup includes the following services, all running on a shared `twinengine-network`: + +### Core Services + +| Service | Port | Image | Purpose | +|---------|------|-------|----------| +| **nginx** | 8080 | `nginx:trixie-perl` | API Gateway & Web UI proxy | +| **twinengine-dataengine** | - | `ghcr.io/aas-twinengine/dataengine:1.0.0` | Main TwinEngine DataEngine service | +| **template-repository** | - | `eclipsebasyx/aas-environment:2.0.0-SNAPSHOT` | AAS Environment & Submodel repository | +| **aas-template-registry** | - | `eclipsebasyx/aas-registry-log-mongodb:2.0.0-SNAPSHOT` | AAS Shell Descriptor Registry | +| **sm-template-registry** | - | `eclipsebasyx/submodel-registry-log-mongodb:2.0.0-SNAPSHOT` | Submodel Descriptor Registry | +| **dpp-plugin** | - | `ghcr.io/aas-twinengine/plugindpp:1.0.0` | Digital Product Passport Plugin | +| **aas-web-ui** | — | `eclipsebasyx/aas-gui:SNAPSHOT` | Web User Interface (served via nginx) | + +### Infrastructure Services + +| Service | Port | Image | Purpose | +|---------|------|-------|----------| +| **postgres** | - | `postgres:16-alpine` | Relational database for plugin data | +| **pgadmin** | 8081 | `dpage/pgadmin4:snapshot` | Web UI for managing PostgreSQL database | +| **mongo** | - | `mongo:6.0` | NoSQL database for registry metadata | + +## Creating/Changing Your AAS-Data + +### Using PGAdmin + +PGAdmin provides a web-based interface to manage the PostgreSQL database without writing SQL queries. + +**Access PGAdmin:** +1. Navigate to `http://localhost:8081` +2. Login with: + - **Email:** admin@example.com + - **Password:** admin + +**Connect to PostgreSQL Server:** +1. In PGAdmin, click **"Add New Server"** +2. Fill in the connection details: + - **Name:** twinengine + - **Host name:** postgres + - **Port:** 5432 + - **Username:** postgres + - **Password:** admin + - **Database:** twinengine +3. Click **"Save"** + +**Browse and Modify Data:** +- In the left sidebar, navigate to: **Servers → twinengine → Databases → twinengine → Schemas → public → Tables** +- Right-click any table and select **"View/Edit Data"** to manage records +- Create new records or modify existing ones directly through the UI + +**How changes affect the Plugin:** +- Updates to application data (e.g., shell records, submodels, submodel element values) are reflected in what the Plugin serves. +- Submodel and shell templates are managed by BaSyx services and are not modified via PostgreSQL. + +-- + +## Additional Notes + +### PostgreSQL Database (Plugin) + +If desired, you can edit credentials in `docker-compose.yml`: +```yaml +POSTGRES_PASSWORD: admin +``` + +Update plugin connection string to match. Edit `example/postgres/init.sql` for custom schema/data. + +**Using an External Database:** +To use your own database instead: +1. Change `RelationalDatabaseConfiguration__ConnectionString` in the plugin service environment variables +2. Remove the postgres container from `docker-compose.yml` + +**Database Initialization:** +The initial database script is located in `postgres/init.sql`. Modify this file as needed for your requirements. + +**Security and Production Notice** + +Change all default passwords before any use beyond local development. Default credentials (postgres: admin) are for **development** only. + +In production, hosting and managing the PostgreSQL database is the customer's responsibility, not the DataEngine's. Use a managed or self-hosted, production-grade PostgreSQL instance and configure the plugin connection string accordingly. + + +### Port Changes + +Modify port mappings in `docker-compose.yml`. Update corresponding environment variables in affected services. + +### Security Note + +**Change default passwords before any use beyond local development.** Default credentials (postgres: admin) are for development only. + +In production: use a secure API gateway (Azure API Management, AWS API Gateway, Kong), and manage database security (encryption, access control, backups) is a customer responsibility. +*Do not use this Docker Compose configuration in production.* +--- + +## Troubleshooting + +**UI not loading:** `docker-compose logs nginx` - Verify ports 8080-8086 are available. + +**Port conflicts:** `netstat -ano | findstr :8080` (Windows) to find conflicts. Change ports in `docker-compose.yml`. + +**Startup issues:** Run `docker-compose pull` followed by `docker-compose up -d --force-recreate` + +**Database errors:** Check `docker-compose ps` for health status. Verify connection strings match credentials. + +**PGAdmin not accessible:** Verify the postgres service is healthy with `docker-compose ps`. Check port mappings are correctly configured. + + + +## Additional Resources + +- [TwinEngine Documentation](https://github.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/wiki) diff --git a/example/aas/CarbonFootprint.xml b/example/aas/CarbonFootprint.xml new file mode 100644 index 0000000..a99092d --- /dev/null +++ b/example/aas/CarbonFootprint.xml @@ -0,0 +1,2597 @@ + + + + + AasTemplate + https://admin-shell.io/idta/aas/CarbonFootprintAAS/1/0 + + Type + https://admin-shell.io/idta/asset/CarbonFootprintAAS/1/0 + + /2b6b0bd3.png + image/png + + Type + + + + ModelReference + + + Submodel + https://admin-shell.io/idta/SubmodelTemplate/CarbonFootprint/1/0 + + + + + + + + + CarbonFootprint + + + en + The Submodel provides the means to access the Carbon Footprint of the asset. + + + + 1 + 0 + https://admin-shell.io/IDTA 02023-1-0 + + https://admin-shell.io/idta/SubmodelTemplate/CarbonFootprint/1/0 + Template + + ExternalReference + + + GlobalReference + https://admin-shell.io/idta/CarbonFootprint/CarbonFootprint/1/0 + + + + + + ProductCarbonFootprints + + + de + Produkt CO2-Fußabdruck + + + en + Product carbon footprint + + + + + en + Balance of greenhouse gas emissions along the entire life cycle of a product in a defined application and in relation to a defined unit of use. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/idta/CarbonFootprint/ProductCarbonFootprints/1/0 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + true + + ExternalReference + + + GlobalReference + https://admin-shell.io/idta/CarbonFootprint/ProductCarbonFootprint/1/0 + + + + SubmodelElementCollection + + + ProductCarbonFootprint + + + de + Produkt CO2-Fußabdruck + + + en + Product carbon footprint + + + + + en + Balance of greenhouse gas emissions along the entire life cycle of a product in a defined application and in relation to a defined unit of use. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/idta/CarbonFootprint/ProductCarbonFootprint/1/0 + + + + + + PcfCalculationMethods + + + de + Folgenabschätzungsmethoden + + + en + impact assessment methods + + + + + de + Normen, Standards, Verfahren zur Ermittlung der Treibhausgas-Emissionen eines Produkts + + + en + Standards, methods for determining the greenhouse gas emissions of a product. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/idta/CarbonFootprint/PcfCalculationMethods/1/0 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + true + + ExternalReference + + + GlobalReference + 0173-1#02-ABG854#003 + + + + Property + xs:string + + + PARAMETER + PcfCalculationMethod + + + de + Folgenabschätzungsmethode / Berechnungsmethode + + + en + impact assessment method / calculation method + + + + + en + Standard, method for determining the greenhouse gas emissions of a product. + + + de + Norm, Standard, Verfahren zur Ermittlung der Treibhausgas-Emissionen eines Produkts + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABG854#003 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + xs:string + + + + + + LifeCyclePhases + + + de + Lebenszyklusphasen + + + en + life cycle phases + + + + + en + List of life cycle stages of the product according to the quantification requirements of the standard to which the PCF carbon footprint statement refers + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/idta/CarbonFootprint/LifeCyclePhases/1/0 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + true + + ExternalReference + + + GlobalReference + 0173-1#02-ABG858#003 + + + + Property + xs:string + + + PARAMETER + LifeCyclePhase + + + de + Lebenszyklusphase + + + en + life cycle phase + + + + + en + Life cycle stages of the product according to the quantification requirements of the standard to which the PCF carbon footprint statement refers + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABG858#003 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + xs:string + + + + + + PARAMETER + PcfCO2eq + + + de + CO2-Äquivalent + + + en + CO2 equivalent + + + + + en + Sum of all greenhouse gas emissions of a product according to the quantification requirements of the standard. + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABG855#003 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + xs:decimal + + + PARAMETER + ReferenceImpactUnitForCalculation + + + de + Referenzeinheit für die Berechnung + + + en + Reference value for calculation + + + + + en + Quantity unit of the product to which the PCF information on the CO2 footprint refers. + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABG856#003 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + xs:string + + + PARAMETER + QuantityOfMeasureForCalculation + + + de + Mengenangabe für die Berechnung + + + en + quantity of measure for calculation + + + + + en + provides the quantity number of pieces or mass or volume to compute the impact of climate change or product carbon footprint. + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABG857#003 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + xs:double + + + PARAMETER + PublicationDate + + + de + Veröffentlichungsdatum + + + en + Publication date + + + + + en + The UTC timestamp on which a Product Carbon Footprint (PCF) - a calculation of a product's total greenhouse gas emissions - was created and published + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/idta/CarbonFootprint/PublicationDate/1/0 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + xs:dateTime + + + PARAMETER + ExpirationDate + + + de + Ablaufdatum + + + en + Expiration date + + + + + en + End date up to which a study or data collection for calculating an ecological footprint is considered current and valid before an update or new calculation is required. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/idta/CarbonFootprint/ExpirationDate/1/0 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + xs:dateTime + + + PARAMETER + ExplanatoryStatement + + + de + Erklärung + + + en + Explanatory statement + + + + + en + Explanation required or provided to ensure that a footprint communication can be properly understood by a purchaser, potential purchaser, or user of the product. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/idta/CarbonFootprint/ExplanatoryStatement/1/0 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + application/pdf + + + GoodsHandoverAddress + + + de + Warenübergabeadresse + + + en + goods address hand-over + + + + + en + Indicates the hand-over address of the goods transport + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/AddressInformation + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/smt-dropin/smt-dropin-use/1/0 + + + + + ExternalReference + + + GlobalReference + 0112/2///61360_7#AAS002#001 + + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAQ837#008 + + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + + + + + + + ProductOrSectorSpecificCarbonFootprints + + + en + Product Or Sector Specific Carbon Footprints + + + + + en + Product Carbon Footprints, which is determined in accordance with sector or product group-specific rules or guidelines and covers the life cycle or parts of a product life cycle. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/idta/CarbonFootprint/ProductOrSectorSpecificCarbonFootprints/1/0 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + true + + ExternalReference + + + GlobalReference + https://admin-shell.io/idta/CarbonFootprint/ProductOrSectorSpecificCarbonFootprint/1/0 + + + + SubmodelElementCollection + + + ProductOrSectorSpecificCarbonFootprint + + + de + Produkt CO2-Fußabdruck + + + en + Product carbon footprint + + + + + en + Product Carbon Footprint, which is determined in accordance with sector or product group-specific rules or guidelines and covers the life cycle or parts of a product life cycle. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/idta/CarbonFootprint/ProductOrSectorSpecificCarbonFootprint/1/0 + + + + + + PcfCalculationMethods + + + de + Folgenabschätzungsmethoden + + + en + impact assessment methods + + + + + de + Normen, Standards, Verfahren zur Ermittlung der Treibhausgas-Emissionen eines Produkts + + + en + Standards, methods for determining the greenhouse gas emissions of a product. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/idta/CarbonFootprint/PcfCalculationMethods/1/0 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + true + + ExternalReference + + + GlobalReference + 0173-1#02-ABG854#003 + + + + Property + xs:string + + + PARAMETER + PcfCalculationMethod + + + de + Folgenabschätzungsmethode / Berechnungsmethode + + + en + impact assessment method / calculation method + + + + + en + Standard, method for determining the greenhouse gas emissions of a product. + + + de + Norm, Standard, Verfahren zur Ermittlung der Treibhausgas-Emissionen eines Produkts + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABG854#003 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + xs:string + + + + + + ProductOrSectorSpecificRule + + + de + Produktspezifische oder sektorspezifische Regel + + + en + Product or Sector Specific Rule + + + + + de + Beinhaltet weiterführende Informationen zur produktspezifischen oder sektorspezifischen Regel, welche zur Berechnung des CO2-Fußabdrucks eingesetzt wird. + + + en + Contains further information on the product-specific or sector-specific rule used to calculate the carbon footprint. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/idta/CarbonFootprint/ProductOrSectorSpecificRule/1/0 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + + + PcfRuleOperator + + + de + Herausgeber der PCF Berechnungsmethode + + + en + Operator of the PCF calculation method + + + + + de + Einrichtung, welche spezifische Anweisungen und Methoden zur Berechnung und Überwachung des CO2-Fußabdrucks eines Produkts oder Sektors definiert und umsetzt. + + + en + Organization that defines and implements specific instructions and methods for calculating and monitoring the carbon footprint of a product or sector. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/idta/CarbonFootprint/ProductOrSectorSpecificRule/Operator/1/0 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + xs:string + + + PcfRuleName + + + de + Name der PCF Berechnungsmethode + + + en + Name of the PCF calculation method + + + + + de + Folgenabschätzungsmethode / Berechnungsmethode + + + en + Standard, method for determining the greenhouse gas emissions of a product + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/idta/CarbonFootprint/ProductOrSectorSpecificRule/Name/1/0 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + xs:string + + + PcfRuleVersion + + + de + Version der PCF Berechnungsmethode + + + en + Version of the PCF calculation method + + + + + de + Spezifische Ausgabe oder Revision der Regel, die zur Berechnung des CO2-Fußabdrucks eines Produkts verwendet wird. + + + en + Specific version or revision of the rule used to calculate the carbon footprint of a product. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/idta/CarbonFootprint/ProductOrSectorSpecificRule/Version/1/0 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + xs:string + + + PcfRuleOnlineReference + + + de + Online Referenz zur PCF Berechnungsmethode + + + en + Online reference to the PCF calculation method + + + + + de + Online-Referenz zur PCF-Berechnungsmethodik, die detaillierte Anweisungen und Richtlinien zur Berechnung des CO2-Fußabdrucks eines Produkts bereitstellt. + + + en + Online PCF calculation methodology reference that provides detailed instructions and guidelines for calculating a product's carbon footprint. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/idta/CarbonFootprint/ProductOrSectorSpecificRule/OnlineReference/1/0 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + application/pdf + + + + + ExternalPcfApi + + + de + Externe API für PCF Informationen + + + en + External API for PCF information + + + + + de + Ein externer Dienst, der über eine Schnittstelle Informationen zum CO2-Fußabdruck bereitstellt und den Abruf dieser Daten auf Abruf ermöglicht. + + + en + An external service that provides carbon footprint information via an interface, allowing on-demand retrieval of this data when required. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/idta/CarbonFootprint/ExternalPcfApi/1/0 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + + + PcfApiEndpoint + + + de + Endpunkt + + + en + Endpoint + + + + + de + Spezifische URL oder Adresse, über die Daten zur Berechnung des CO2-Fußabdrucks eines Produkts von externen Quellen abgerufen werden können. + + + en + Specific URL or address that can be used to retrieve data from external sources to calculate the carbon footprint of a product. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/idta/CarbonFootprint/ExternalPcfApi/Endpoint/1/0 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + xs:anyURI + + + PcfApiQuery + + + de + Abfrage + + + en + Query + + + + + de + Spezifische Abfrage, über die Daten zur Berechnung des CO2-Fußabdrucks eines Produkts von externen Quellen abgerufen werden können. + + + en + Specific query that can be used to retrieve data from external sources to calculate the carbon footprint of a product. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/idta/CarbonFootprint/ExternalPcfApi/Query/1/0 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + xs:string + + + + + PcfInformation + + + de + Ein Abschnitt, in dem weitere Inhalte entsprechend der Berechnungsmethode zum Product Carbon Footprint aufgeführt werden. + + + en + A section in which further content is listed according to the calculation method for the Product Carbon Footprint. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/idta/CarbonFootprint/PcfInformation/1/0 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + + + + + + + + + de + C02 Footprint + + + en + Carbon Footprint + + + + + + + PcfApiEndpoint + + 1 + 0 + + https://admin-shell.io/idta/CarbonFootprint/ExternalPcfApi/Endpoint/1/0 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + Endpoint + + + de + Endpunkt + + + STRING + + + de + Spezifische URL oder Adresse, über die Daten zur Berechnung des CO2-Fußabdrucks eines Produkts von externen Quellen abgerufen werden können. + + + en + Specific URL or address that can be used to retrieve data from external sources to calculate the carbon footprint of a product. + + + + + + + + + PcfRuleVersion + + 1 + 0 + + https://admin-shell.io/idta/CarbonFootprint/ProductOrSectorSpecificRule/Version/1/0 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + Version of the PCF calculation method + + + de + Version der PCF Berechnungsmethode + + + + + en + Version + + + de + Version + + + STRING + + + de + Spezifische Ausgabe oder Revision der Regel, die zur Berechnung des CO2-Fußabdrucks eines Produkts verwendet wird. + + + en + Specific version or revision of the rule used to calculate the carbon footprint of a product. + + + 2.0 + + + + + + + PcfCalculationMethod + 0173-1#02-ABG854#003 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + impact assessment method / calculation method + + + de + Folgenabschätzungsmethode / Berechnungsmethode + + + STRING + + + de + Norm, Standard, Verfahren zur Ermittlung der Treibhausgas-Emissionen eines Produkts + + + en + standard, method for determining the greenhouse gas emissions of a product + + + + + + EN 15804 + + ExternalReference + + + GlobalReference + 0173-1#07-ABU223#003 + + + + + + GHG Protocol + + ExternalReference + + + GlobalReference + 0173-1#07-ABU221#003 + + + + + + IEC TS 63058 + + ExternalReference + + + GlobalReference + 0173-1#07-ABU222#003 + + + + + + IEC 63366 + + ExternalReference + + + GlobalReference + 0173-1#07-ACA792#002 + + + + + + ISO 14040, ISO 14044 + + ExternalReference + + + GlobalReference + 0173-1#07-ABV505#003 + + + + + + ISO 14067 + + ExternalReference + + + GlobalReference + 0173-1#07-ABU218#003 + + + + + + PEP Ecopassport + + ExternalReference + + + GlobalReference + 0173-1#07-ABU220#003 + + + + + + PACT v1.0.1 + + ExternalReference + + + GlobalReference + 0173-1#07-ACC004#001 + + + + + + PACT v2.0.0 + + ExternalReference + + + GlobalReference + 0173-1#07-ACC003#001 + + + + + + PACT v3.0.0 + + ExternalReference + + + GlobalReference + 0173-1#07-ACC012#001 + + + + + + TFS v2 + + ExternalReference + + + GlobalReference + 0173-1#07-ACC005#001 + + + + + + TFS v3 + + ExternalReference + + + GlobalReference + 0173-1#07-ACC010#001 + + + + + + Catena-X v1 + + ExternalReference + + + GlobalReference + 0173-1#07-ACC007#001 + + + + + + Catena-X v2 + + ExternalReference + + + GlobalReference + 0173-1#07-ACC006#001 + + + + + + Catena-X v3 + + ExternalReference + + + GlobalReference + 0173-1#07-ACC011#001 + + + + + + BS PAS 2050 + + ExternalReference + + + GlobalReference + 0173-1#07-ACC008#001 + + + + + + IEC 63372 + + ExternalReference + + + GlobalReference + 0173-1#07-ACC019#001 + + + + + + + + + + + + + PublicationDate + + 1 + 0 + + https://admin-shell.io/idta/CarbonFootprint/PublicationDate/1/0 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + de + Veröffentlichungsdatum + + + en + Publication date + + + + + en + EMPTY + + + TIMESTAMP + + + de + Der UTC Zeitstempel, an dem ein Product Carbon Footprint (PCF) – eine Berechnung der gesamten Treibhausgasemissionen eines Produkts – erstellt und veröffentlicht wurde + + + en + The UTC timestamp on which a Product Carbon Footprint (PCF) - a calculation of a product's total greenhouse gas emissions - was created and published + + + + + + + + + ExpirationDate + + 1 + 0 + + https://admin-shell.io/idta/CarbonFootprint/ExpirationDate/1/0 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + de + Ablaufdatum + + + en + Expiration Date + + + TIMESTAMP + + + de + Enddatum, bis zu dem eine Studie oder Datenerhebung zur Berechnung eines ökologischen Fußabdrucks als aktuell und gültig betrachtet wird, bevor eine Aktualisierung oder neue Berechnung erforderlich ist. + + + en + End date up to which a study or data collection for calculating an ecological footprint is considered current and valid before an update or new calculation is required. + + + + + + + + + PcfRuleOperator + + 1 + 0 + + https://admin-shell.io/idta/CarbonFootprint/ProductOrSectorSpecificRule/Operator/1/0 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + Publisher of the PCF calculation method + + + de + Herausgeber der PCF Berechnungsmethode + + + STRING + + + de + Einrichtung, welche spezifische Anweisungen und Methoden zur Berechnung und Überwachung des CO2-Fußabdrucks eines Produkts oder Sektors definiert und umsetzt. + + + en + Organization that defines and implements specific instructions and methods for calculating and monitoring the carbon footprint of a product or sector. + + + + + + + + + ProductOrSectorSpecificRule + + + de + Produktspezifische oder sektorspezifische Regel + + + en + Product or Sector Specific Rule + + + + 1 + 0 + + https://admin-shell.io/idta/CarbonFootprint/ProductOrSectorSpecificRule/1/0 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + Product or Sector Specific Rule + + + de + Produktspezifische oder sektorspezifische Regel + + + + + de + Beinhaltet weiterführende Informationen zur produktspezifischen oder sektorspezifischen Regel, welche zur Berechnung des CO2-Fußabdrucks eingesetzt wird. + + + en + Contains further information on the product-specific or sector-specific rule used to calculate the carbon footprint. + + + + + + + + + ReferenceImpactUnitForCalculation + 0173-1#02-ABG856#003 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + Reference value for calculation + + + de + Referenzeinheit für die Berechnung + + + STRING + + + en + Quantity unit of the product to which the PCF information on the CO2 footprint refers + + + de + Mengeneinheit des Produktes, auf das sich die PCF-Angabe zum CO2-Fußabdruck bezieht + + + + + + g + + ExternalReference + + + GlobalReference + 0173-1#07-ABZ596#003 + + + + + + kg + + ExternalReference + + + GlobalReference + 0173-1#07-ABZ597#003 + + + + + + t + + ExternalReference + + + GlobalReference + 0173-1#07-ABZ598#003 + + + + + + ml + + ExternalReference + + + GlobalReference + 0173-1#07-ABZ599#003 + + + + + + l + + ExternalReference + + + GlobalReference + 0173-1#07-ABZ600#003 + + + + + + cbm + + ExternalReference + + + GlobalReference + 0173-1#07-ABZ601#003 + + + + + + qm + + ExternalReference + + + GlobalReference + 0173-1#07-ABZ602#003 + + + + + + piece + + ExternalReference + + + GlobalReference + 0173-1#07-ABZ603#003 + + + + + + kWh + + ExternalReference + + + GlobalReference + 0173-1#07-ACB997#001 + + + + + + + + + + + + + PcfApiQuery + + + en + Query + + + de + Abfrage + + + + + de + Spezifische Abfrage, über die Daten zur Berechnung des CO2-Fußabdrucks eines Produkts von externen Quellen abgerufen werden können. + + + en + Specific query that can be used to retrieve data from external sources to calculate the carbon footprint of a product. + + + + 1 + 0 + + https://admin-shell.io/idta/CarbonFootprint/ExternalPcfApi/Query/1/0 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + Query + + + de + Abfrage + + + STRING + + + en + Specific query that can be used to retrieve data from external sources to calculate the carbon footprint of a product. + + + de + Spezifische Abfrage, über die Daten zur Berechnung des CO2-Fußabdrucks eines Produkts von externen Quellen abgerufen werden können. + + + + + + + + + ExternalPcfApi + + 1 + 0 + + https://admin-shell.io/idta/CarbonFootprint/ExternalPcfApi/1/0 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + An external service that provisions carbon footprint information via an interface, allowing on-demand retrieval of this data. + + + de + Ein externer Dienst, der über eine Schnittstelle Informationen zum CO2-Fußabdruck bereitstellt und den Abruf dieser Daten auf Abruf ermöglicht. + + + + + en + An external service that provisions carbon footprint information via an interface, allowing on-demand retrieval of this data. + + + de + Ein externer Dienst, der über eine Schnittstelle Informationen zum CO2-Fußabdruck bereitstellt und den Abruf dieser Daten auf Abruf ermöglicht. + + + + + + + + + LifeCyclePhase + 0173-1#02-ABG858#003 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + life cycle phase (acc. EN 15804) + + + de + Lebenszyklusphase (nach EN 15804) + + + + + en + life cycle phase + + + de + Lebenszyklusphase + + + STRING + + + de + Lebenszyklusphase des Produktes gemäß den Quantifizierungsvorgaben der Norm, auf den sich die PCF-Angabe zum CO2-Fußabdruck bezieht + + + en + life cycle stages of the product according to the quantification requirements of the standard to which the PCF carbon footprint statement refers + + + + + + A1 - raw material supply (and upstream production) + + ExternalReference + + + GlobalReference + 0173-1#07-ABU208#003 + + + + + + A2 - cradle-to-gate transport to factory + + ExternalReference + + + GlobalReference + 0173-1#07-ABU209#003 + + + + + + A3 - production + + ExternalReference + + + GlobalReference + 0173-1#07-ABU210#003 + + + + + + A1-A3 + + ExternalReference + + + GlobalReference + 0173-1#07-ABZ789#003 + + + + + + A4 - transport to final destination + + ExternalReference + + + GlobalReference + 0173-1#07-ABU211#003 + + + + + + A4-A5 + + ExternalReference + + + GlobalReference + 0173-1#07-ACC013#001 + + + + + + A5 - Installation + + ExternalReference + + + GlobalReference + 0173-1#07-ACC016#001 + + + + + + B1 - usage phase + + ExternalReference + + + GlobalReference + 0173-1#07-ABU212#003 + + + + + + B1-B7 + + ExternalReference + + + GlobalReference + 0173-1#07-ACC014#001 + + + + + + B2 - maintenance + + ExternalReference + + + GlobalReference + 0173-1#07-ABV498#003 + + + + + + B3 - repair + + ExternalReference + + + GlobalReference + 0173-1#07-ABV497#003 + + + + + + B4 - Replacement + + ExternalReference + + + GlobalReference + 0173-1#07-ACC017#001 + + + + + + B5 - update/upgrade, refurbishing + + ExternalReference + + + GlobalReference + 0173-1#07-ABV499#003 + + + + + + B6 - usage energy consumption + + ExternalReference + + + GlobalReference + 0173-1#07-ABV500#003 + + + + + + B7 - usage water consumption + + ExternalReference + + + GlobalReference + 0173-1#07-ABV501#003 + + + + + + C1 - reassembly + + ExternalReference + + + GlobalReference + 0173-1#07-ABV502#003 + + + + + + C1-C4 + + ExternalReference + + + GlobalReference + 0173-1#07-ACC015#001 + + + + + + C2 - transport to recycler + + ExternalReference + + + GlobalReference + 0173-1#07-ABU213#003 + + + + + + C2-C4 + + ExternalReference + + + GlobalReference + 0173-1#07-ACC018#001 + + + + + + C3 - recycling, waste treatment + + ExternalReference + + + GlobalReference + 0173-1#07-ABV503#003 + + + + + + C4 - landfill + + ExternalReference + + + GlobalReference + 0173-1#07-ABV504#003 + + + + + + D - reuse + + ExternalReference + + + GlobalReference + 0173-1#07-ABU214#003 + + + + + + + + + + + + + ProductOrSectorSpecificCarbonFootprint + + + en + Product Carbon Footprint, which is determined in accordance with sector or product group-specific rules or guidelines and covers the life cycle or parts of a product life cycle. + + + + 1 + 0 + + https://admin-shell.io/idta/CarbonFootprint/ProductOrSectorSpecificCarbonFootprint/1/0 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + Product Or Sector Specific Carbon Footprint + + + + + en + Product Carbon Footprint, which is determined in accordance with sector or product group-specific rules or guidelines and covers the life cycle or parts of a product life cycle. + + + + + + + + + ProductCarbonFootprint + + + de + Produkt CO2-Fußabdruck + + + en + Product carbon footprint + + + + 1 + 0 + + https://admin-shell.io/idta/CarbonFootprint/ProductCarbonFootprint/1/0 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + Product Carbon Footprint + + + de + Produkt CO2-Fußabdruck + + + + + en + Balance of greenhouse gas emissions along the entire life cycle of a product in a defined application and in relation to a defined unit of use + + + + + + + + + PcfInformation + + + de + PCF Informationen + + + en + PCF Informationen + + + + 1 + 0 + + https://admin-shell.io/idta/CarbonFootprint/PcfInformation/1/0 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + PCF Information + + + de + PCF Informationen + + + + + en + A section in which further content is listed according to the calculation method for the Product Carbon Footprint. + + + de + Ein Abschnitt, in dem weitere Inhalte entsprechend der Berechnungsmethode zum Product Carbon Footprint aufgeführt werden. + + + + + + + + + PcfCO2eq + 0173-1#02-ABG855#003 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + CO2 equivalent - total (for PCF and LCA) + + + de + CO2-Äquivalent - total (für PCF und LCA) + + + CO2eq + REAL_MEASURE + + + de + Summe aller Treibhausgas-Emissionen eines Produkts gemäß der Quantifizierungsvorgaben der Norm + + + en + sum of all greenhouse gas emissions of a product according to the quantification requirements of the standard + + + + + + + + + PcfRuleName + + + de + Name der PCF Berechnungsmethode + + + en + Name of the PCF calculation method + + + + 1 + 0 + + https://admin-shell.io/idta/CarbonFootprint/ProductOrSectorSpecificRule/Name/1/0 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + Name of the PCF calculation method + + + de + Name der PCF Berechnungsmethode + + + STRING + + + de + Folgenabschätzungsmethode / Berechnungsmethode + + + en + Standard, method for determining the greenhouse gas emissions of a product + + + + + + + + + QuantityOfMeasureForCalculation + + + de + Mengenangabe für die Berechnung + + + en + quantity of measure for calculation + + + 0173-1#02-ABG857#003 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + quantity of measure for calculation + + + de + Mengenangabe für die Berechnung + + + REAL_COUNT + + + de + Angabe der Stückzahl, Masse oder des Volumens zur Berechnung der Auswirkungen des Klimawandels oder des CO2-Fußabdrucks des Produktes + + + en + provides the quantity number of pieces or mass or volume to compute the impact of climate change or product carbon footprint + + + + + + + + + diff --git a/example/aas/ContactInformationAAS.xml b/example/aas/ContactInformationAAS.xml new file mode 100644 index 0000000..b0f8973 --- /dev/null +++ b/example/aas/ContactInformationAAS.xml @@ -0,0 +1,2139 @@ + + + + ContactInformationAAS + https://admin-shell.io/idta/aas/ContactInformation/1/0 + + Type + https://admin-shell.io/idta/asset/ContactInformation/1/0 + Type + + + + ModelReference + + + Submodel + https://admin-shell.io/idta/SubmodelTemplate/ContactInformation/1/0 + + + + + + + + + ContactInformations + https://admin-shell.io/idta/SubmodelTemplate/ContactInformation/1/0 + Template + + ModelReference + + + Submodel + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations + + + + + + ContactInformation + + + en + The SMC “ContactInformation” contains information on how to contact the manufacturer or an authorised service provider, e.g. when a maintenance service is required + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation + + + + + + ConceptQualifier + Multiplicity + xs:string + OneToMany + + + + + RoleOfContactPerson + + + en + enumeration: 0173-1#07-AAS927#001 (administrativ contact), 0173-1#07-AAS928#001 (commercial contact), 0173-1#07-AAS929#001 (other contact), 0173-1#07-AAS930#001 (hazardous goods contact), 0173-1#07-AAS931#001 (technical contact). Note: the above mentioned ECLASS enumeration should be declared as “open” for further addition. ECLASS enumeration IRDI is preferable. If no IRDI available, custom input as String may also be accepted. + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAO204#003 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + xs:string + 0173-1#07-AAS931#001 + + + Language + + + en + Note: language codes defined accord. to ISO 639-1. Note: as per ECLASS definition, Expression and representation of thoughts, information, feelings, ideas through characters. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Language + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToMany + + + xs:string + de + + + TimeZone + + + en + Note: notation accord. to ISO 8601 Note: for time in UTC the zone designator “Z” is to be used + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/TimeZone + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + xs:string + Z + + + AddressOfAdditionalLink + + ExternalReference + + + GlobalReference + 0173-1#02-AAQ326#002 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + xs:string + + + NationalCode + + + en + Note: country codes defined accord. to ISO 3166-1. Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice. + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAO134#002 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + + en + DE + + + + + CityTown + + ExternalReference + + + GlobalReference + 0173-1#02-AAO132#002 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + + de + Musterstadt + + + + + Company + + ExternalReference + + + GlobalReference + 0173-1#02-AAW001#001 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + + en + ABC Company + + + + + Department + + ExternalReference + + + GlobalReference + 0173-1#02-AAO127#003 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + + de + Vertrieb + + + + + Street + + ExternalReference + + + GlobalReference + 0173-1#02-AAO128#002 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + + de + Musterstraße 1 + + + + + Zipcode + + + en + Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice. + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAO129#002 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + + de + 12345 + + + + + POBox + + ExternalReference + + + GlobalReference + 0173-1#02-AAO130#002 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + + en + PF 1234 + + + + + ZipCodeOfPOBox + + + en + Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice. + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAO131#002 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + + en + 12345 + + + + + StateCounty + + ExternalReference + + + GlobalReference + 0173-1#02-AAO133#002 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + + de + Muster-Bundesland + + + + + NameOfContact + + ExternalReference + + + GlobalReference + 0173-1#02-AAO205#002 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + + en + + + + de + + + + + + FirstName + + ExternalReference + + + GlobalReference + 0173-1#02-AAO206#002 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + + en + + + + de + + + + + + MiddleNames + + ExternalReference + + + GlobalReference + 0173-1#02-AAO207#002 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + + en + + + + de + + + + + + Title + + ExternalReference + + + GlobalReference + 0173-1#02-AAO208#003 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + + en + a + + + de + a + + + + + AcademicTitle + + ExternalReference + + + GlobalReference + 0173-1#02-AAO209#003 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + + en + + + + de + + + + + + FurtherDetailsOfContact + + ExternalReference + + + GlobalReference + 0173-1#02-AAO210#002 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + + en + + + + de + + + + + + Phone + + ExternalReference + + + GlobalReference + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + + TelephoneNumber + + + en + Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice. + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAO136#002 + + + + + + ConceptQualifier + Multiplicity + xs:string + One + + + + + en + +491234567890 + + + + + AvailableTime + + ExternalReference + + + GlobalReference + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/ + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + + de + Montag – Freitag 08:00 bis 16:00 + + + + + TypeOfTelephone + + + en + enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS755#001 (office mobile), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home), 0173-1#07-AAS759#001 (private mobile) + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAO137#003 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + xs:string + 0173-1#07-AAS754#001 + + + + + Fax + + ExternalReference + + + GlobalReference + 0173-1#02-AAQ834#005 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + + FaxNumber + + + en + Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice. + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAO195#002 + + + + + + ConceptQualifier + Multiplicity + xs:string + One + + + + + en + +491234567890 + + + + + TypeOfFaxNumber + + + en + enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS758#001 (home) + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAO196#003 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + xs:string + 0173-1#07-AAS754#001 + + + + + Email + + ExternalReference + + + GlobalReference + 0173-1#02-AAQ836#005 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + + EmailAddress + + ExternalReference + + + GlobalReference + 0173-1#02-AAO198#002 + + + + + + ConceptQualifier + Multiplicity + xs:string + One + + + xs:string + email@muster-ag.de + + + TypeOfEmailAddress + + + en + enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home) + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAO199#003 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + xs:string + 0173-1#07-AAS754#001 + + + PublicKey + + + en + Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice. + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAO200#002 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + + en + o + + + de + a + + + + + TypeOfPublicKey + + ExternalReference + + + GlobalReference + 0173-1#02-AAO201#002 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + + en + a + + + de + a + + + + + + + IPCommunication__00__ + + ExternalReference + + + GlobalReference + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/ + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToMany + + + + + AddressOfAdditionalLink + + ExternalReference + + + GlobalReference + 0173-1#02-AAQ326#002 + + + + + + ConceptQualifier + Multiplicity + xs:string + One + + + xs:string + + + TypeOfCommunication + + ExternalReference + + + GlobalReference + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + xs:string + Chat + + + AvailableTime + + ExternalReference + + + GlobalReference + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/ + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + + de + Montag – Freitag 08:00 bis 16:00 + + + + + + + + + + + + + NationalCode + 0173-1#02-AAO134#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + NationalCode + + + + + en + code of a country + + + + + + + + + Company + 0173-1#02-AAW001#001 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + Company + + + + + en + name of the company + + + + + + + + + Fax + 0173-1#02-AAQ834#005 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + Fax + + + + + en + Fax number including type + + + + + + + + + TimeZone + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/TimeZone + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + TimeZone + + + + + en + offsets from Coordinated Universal Time (UTC) + + + + + + + + + FurtherDetailsOfContact + 0173-1#02-AAO210#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + FurtherDetailsOfContact + + + + + en + additional information of the contact person + + + + + + + + + TypeOfEmailAddress + 0173-1#02-AAO199#003 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + TypeOfEmailAddress + + + + + en + characterization of an e-mail address according to its location or usage + + + + + + + + + MiddleNames + 0173-1#02-AAO207#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + MiddleNames + + + + + en + middle names of contact person + + + + + + + + + TypeOfPublicKey + 0173-1#02-AAO201#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + TypeOfPublicKey + + + + + en + characterization of a public key according to its encryption process + + + + + + + + + AvailableTime + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/ + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + AvailableTime + + + + + en + Specification of the available time window + + + + + + + + + Department + 0173-1#02-AAO127#003 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + Department + + + + + en + administrative section within an organisation where a business partner is located + + + + + + + + + ContactInformation + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + ContactInformation + + + + + en + The SMC “ContactInformation” contains information on how to contact the manufacturer or an authorised service provider, e.g. when a maintenance service is required + + + + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAQ837#005 + + + + + + + StateCounty + 0173-1#02-AAO133#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + StateCounty + + + + + en + federal state a part of a state + + + + + + + + + PublicKey + 0173-1#02-AAO200#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + PublicKey + + + + + en + public part of an unsymmetrical key pair to sign or encrypt text or messages + + + + + + + + + EmailAddress + 0173-1#02-AAO198#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + EmailAddress + + + + + en + electronic mail address of a business partner + + + + + + + + + Street + 0173-1#02-AAO128#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + Street + + + + + en + street name and house number + + + + + + + + + TypeOfTelephone + 0173-1#02-AAO137#003 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + TypeOfTelephone + + + + + en + characterization of a telephone according to its location or usage + + + + + + + + + Title + 0173-1#02-AAO208#003 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + Title + + + + + en + common, formal, religious, or other title preceding a contact person's name + + + + + + + + + RoleOfContactPerson + 0173-1#02-AAO204#003 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + RoleOfContactPerson + + + + + en + function of a contact person in a process + + + + + + + + + AcademicTitle + 0173-1#02-AAO209#003 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + AcademicTitle + + + + + en + academic title preceding a contact person's name + + + + + + + + + Language + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Language + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + Language + + + + + en + Available language + + + + + + + + + ModelReference + + + Submodel + 0173-1#02-AAO895#003 + + + + + + + CityTown + 0173-1#02-AAO132#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + CityTown + + + + + en + town or city + + + + + + + + + Email + 0173-1#02-AAQ836#005 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + Email + + + + + en + E-mail address and encryption method + + + + + + + + + TelephoneNumber + 0173-1#02-AAO136#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + TelephoneNumber + + + + + en + complete telephone number to be called to reach a business partner + + + + + + + + + TypeOfFaxNumber + 0173-1#02-AAO196#003 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + TypeOfFaxNumber + + + + + en + characterization of the fax according its location or usage + + + + + + + + + Zipcode + 0173-1#02-AAO129#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + Zipcode + + + + + en + ZIP code of address + + + + + + + + + NameOfContact + 0173-1#02-AAO205#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + NameOfContact + + + + + en + surname of a contact person + + + + + + + + + Phone + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + Phone + + + + + en + Phone number including type + + + + + + + + + AddressOfAdditionalLink + 0173-1#02-AAQ326#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + AddressOfAdditionalLink + + + + + en + web site address where information about the product or contact is given + + + + + + + + + FaxNumber + 0173-1#02-AAO195#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + FaxNumber + + + + + en + complete telephone number to be called to reach a business partner's fax machine + + + + + + + + + ZipCodeOfPOBox + 0173-1#02-AAO131#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + ZipCodeOfPOBox + + + + + en + ZIP code of P.O. box address + + + + + + + + + POBox + 0173-1#02-AAO130#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + POBox + + + + + en + P.O. box number + + + + + + + + + ContactInformations + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + ContactInformations + + + + + en + The Submodel “ContactInformations” is the collection for various contact information. + + + + + + + + + FirstName + 0173-1#02-AAO206#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + FirstName + + + + + en + first name of a contact person + + + + + + + + + \ No newline at end of file diff --git a/example/aas/HandoverDocumentation.xml b/example/aas/HandoverDocumentation.xml new file mode 100644 index 0000000..e1fdf49 --- /dev/null +++ b/example/aas/HandoverDocumentation.xml @@ -0,0 +1,3324 @@ + + + + + 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 + + + + + + + + + diff --git a/example/aas/Nameplate.xml b/example/aas/Nameplate.xml new file mode 100644 index 0000000..f428fd1 --- /dev/null +++ b/example/aas/Nameplate.xml @@ -0,0 +1,2071 @@ + + + + + DigitalNameplateAAS + https://admin-shell.io/idta/aas/DigitalNameplate/3/0 + + Type + https://admin-shell.io/idta/asset/DigitalNameplate/3/0 + Type + + + + ModelReference + + + Submodel + https://admin-shell.io/idta/SubmodelTemplate/DigitalNameplate/3/0 + + + + + + + + + Nameplate + + + en + Contains the nameplate information attached to the product + + + + 3 + 0 + https://admin-shell.io/idta-02006-3-0 + + https://admin-shell.io/idta/SubmodelTemplate/DigitalNameplate/3/0 + Template + + ExternalReference + + + GlobalReference + https://admin-shell.io/idta/nameplate/3/0/Nameplate + + + + + + URIOfTheProduct + + ExternalReference + + + GlobalReference + 0112/2///61987#ABN590#002 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABH173#003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + One + + + xs:anyURI + https://www.domain-abc.com/Model-Nr-1234/Serial-Nr-5678 + + + ManufacturerProductType + + ExternalReference + + + GlobalReference + 0112/2///61987#ABA300#008 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAO057#004 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + xs:string + FM-ABC-1234 + + + OrderCodeOfManufacturer + + ExternalReference + + + GlobalReference + 0112/2///61987#ABA950#008 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAO227#004 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + One + + + xs:string + FMABC1234 + + + ProductArticleNumberOfManufacturer + + ExternalReference + + + GlobalReference + 0112/2///61987#ABA581#007 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAO676#005 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + xs:string + FM11-ABC22-123456 + + + SerialNumber + + ExternalReference + + + GlobalReference + 0112/2///61987#ABA951#009 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAM556#004 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + xs:string + 12345678 + + + YearOfConstruction + + ExternalReference + + + GlobalReference + 0112/2///61987#ABP000#002 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAP906#003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + xs:string + 2022 + + + DateOfManufacture + + ExternalReference + + + GlobalReference + 0112/2///61987#ABB757#007 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAR972#004 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + xs:date + 2022-01-01 + + + HardwareVersion + + ExternalReference + + + GlobalReference + 0112/2///61987#ABA926#008 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAN270#004 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + xs:string + 1.0.0 + + + FirmwareVersion + + ExternalReference + + + GlobalReference + 0112/2///61987#ABA302#006 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAM985#004 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + xs:string + 1.0.0 + + + SoftwareVersion + + ExternalReference + + + GlobalReference + 0112/2///61987#ABA601#008 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAM737#004 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + xs:string + 1.0.0 + + + CountryOfOrigin + + + en + Note: Country codes defined accord. to DIN EN ISO 3166-1 alpha-2 codes + + + + ExternalReference + + + GlobalReference + 0112/2///61987#ABP462#001 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAO259#007 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + xs:string + DE + + + UniqueFacilityIdentifier + + ExternalReference + + + GlobalReference + https://admin-shell.io/idta/nameplate/3/0/UniqueFacilityIdentifier + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + xs:string + 987654321 + + + ManufacturerName + + ExternalReference + + + GlobalReference + 0112/2///61987#ABA565#009 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAO677#004 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + One + + + + + de + "Muster AG" + + + + + ManufacturerProductDesignation + + ExternalReference + + + GlobalReference + 0112/2///61987#ABA567#009 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAW338#003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + One + + + + + en + "ABC-123" + + + + + ManufacturerProductRoot + + ExternalReference + + + GlobalReference + 0112/2///61360_7#AAS011#001 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAU732#003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + + + en + "flow meter" + + + + ExternalReference + + + GlobalReference + 0112/2///61987#ABP464#002 + + + + + + ManufacturerProductFamily + + ExternalReference + + + GlobalReference + 0112/2///61987#ABP464#002 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAU731#003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + + + en + "Type ABC" + + + + + AddressInformation + + + en + Note: this set of information is defined by SMT drop-in "Address Information" + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/AddressInformation + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/smt-dropin/smt-dropin-use/1/0 + + + + + ExternalReference + + + GlobalReference + 0112/2///61360_7#AAS002#001 + + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAQ837#008/0173-1#01-ADR448#008 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + One + + + + + AssetSpecificProperties + + ExternalReference + + + GlobalReference + 0173-1#02-ABI218#003/0173-1#01-AGZ672#004 + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + + + GuidelineSpecificProperties + + ExternalReference + + + GlobalReference + 0173-1#02-ABI219#003/0173-1#01-AHD205#004 + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + true + SubmodelElementCollection + + + + ExternalReference + + + GlobalReference + 0173-1#01-AHD205#004 + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + OneToMany + + + + + + + + + CompanyLogo + + ExternalReference + + + GlobalReference + 0112/2///61987#ABP463#001 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABI776#002 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + image/png + + + Markings + + + en + Note: CE marking is declared as mandatory according to EU Blue Guide + + + + ExternalReference + + + GlobalReference + 0112/2///61360_7#AAS006#001 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABI563#003/0173-1#01-AHF849#003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + true + SubmodelElementCollection + + + Marking + + + en + Note: CE marking is declared as mandatory according to the Blue Guide of the EU-Commission + + + + ExternalReference + + + GlobalReference + 0112/2///61360_7#AAS009#001 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABI564#003/0173-1#01-AHF850#003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + OneToMany + + + + + MarkingName + + ExternalReference + + + GlobalReference + 0112/2///61987#ABA231#009 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABI190#003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + One + + + xs:string + 0173-1#07-DAA603#004 + + + DesignationOfCertificateOrApproval + + + en + Note: Approval identifier, reference to the certificate number, to be entered without spaces + + + + ExternalReference + + + GlobalReference + 0112/2///61987#ABH783#003 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABI975#002 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + xs:string + KEMA99IECEX1105/128 + + + IssueDate + + + en + Note: format by lexical representation: CCYY-MM-DD Note: to be specified to the day + + + + ExternalReference + + + GlobalReference + 0112/2///61987#ABO097#001 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABL774#001 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + xs:date + 2022-01-01 + + + ExpiryDate + + + en + Note: format by lexical representation: CCYY-MM-DD Note: to be specified to the day + + + + ExternalReference + + + GlobalReference + 0112/2///61987#ABH830#002 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABL775#001 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + xs:date + 2022-01-01 + + + MarkingAdditionalText + + ExternalReference + + + GlobalReference + 0112/2///61987#ABB146#007 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABI192#003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToMany + + + xs:string + 0044 + + + MarkingFile + + ExternalReference + + + GlobalReference + 0112/2///61987#ABO100#002 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABI191#003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + One + + + image/png + + + + + + + + + + + DateOfManufacture + 0112/2///61987#ABB757#007 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + DateOfManufacture + + + DATE + + + en + date when an item was manufactured + + + + + + + + + MarkingAdditionalText__00__ + 0112/2///61987#ABB146#007 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + MarkingAdditionalText__00__ + + + STRING + + + en + where applicable, additional information on the marking in plain text, e.g. the ID-number of the notified body involved in the conformity process + + + + + + + + + MarkingName + 0112/2///61987#ABA231#009 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + MarkingName + + + STRING + + + en + common name of the marking + + + + + + + + + IssueDate + 0112/2///61987#ABO097#001 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + IssueDate + + + DATE + + + en + date, at which the specified certificate is issued + + + + + + + + + FirmwareVersion + 0112/2///61987#ABA302#006 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + FirmwareVersion + + + STRING + + + en + version of the firmware supplied with the device + + + + + + + + + SerialNumber + 0112/2///61987#ABA951#009 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + SerialNumber + + + STRING + + + en + unique combination of numbers and letters used to identify the device once it has been manufactured + + + + + + + + + ManufacturerName + 0112/2///61987#ABA565#009 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + ManufacturerName + + + STRING_TRANSLATABLE + + + en + legally valid designation of the natural or judicial person which is directly responsible for the design, production, packaging and labeling of a product in respect to its being brought into circulation + + + + + + + + + ManufacturerProductRoot + 0112/2///61360_7#AAS011#001 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + ManufacturerProductRoot + + + STRING_TRANSLATABLE + + + en + top level of a 3 level manufacturer specific product hierarchy + + + + + + + + + YearOfConstruction + 0112/2///61987#ABP000#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + YearOfConstruction + + + STRING + + + en + year in which the manufacturing process is completed + + + + + + + + + UniqueFacilityIdentifier + https://admin-shell.io/idta/nameplate/3/0/UniqueFacilityIdentifier + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + UniqueFacilityIdentifier + + + STRING + + + en + unique string of characters for the identification of locations or buildings involved in a product’s value chain or used by actors involved in a product’s value chain + + + + + + + + + OrderCodeOfManufacturer + 0112/2///61987#ABA950#008 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + OrderCodeOfManufacturer + + + STRING + + + en + unique combination of numbers and letters issued by the manufacturer that is used to identify the device for ordering + + + + + + + + + DesignationOfCertificateOrApproval + 0112/2///61987#ABH783#003 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + DesignationOfCertificateOrApproval + + + STRING + + + en + alphanumeric character sequence identifying a certificate or approval + + + + + + + + + ManufacturerProductType + 0112/2///61987#ABA300#008 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + ManufacturerProductType + + + STRING + + + en + characteristic to differentiate between different products of a product family or special variants + + + + + + + + + ManufacturerProductFamily + 0112/2///61987#ABP464#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + ManufacturerProductFamily + + + STRING_TRANSLATABLE + + + en + second level of a 3 level manufacturer specific product hierarchy + + + + + + + + + SoftwareVersion + 0112/2///61987#ABA601#008 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + SoftwareVersion + + + STRING + + + en + version of the software used by the device + + + + + + + + + ProductArticleNumberOfManufacturer + 0112/2///61987#ABA581#007 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + ProductArticleNumberOfManufacturer + + + STRING + + + en + unique product identifier of the manufacturer + + + + + + + + + URIOfTheProduct + 0112/2///61987#ABN590#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + URIOfTheProduct + + + + + en + URIOfTheProduct + + + STRING + + + en + unique global identification of the product instance using an universal resource identifier (URI) + + + + + + + + + Marking + + + en + Note: CE marking is declared as mandatory according to the Blue Guide of the EU-Commission + + + 0112/2///61360_7#AAS009#001 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + Marking + + + + + en + Single marking information + + + + + + + + + ExpiryDate + 0112/2///61987#ABH830#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + ExpiryDate + + + DATE + + + en + date, at which the specified certificate expires + + + + + + + + + CountryOfOrigin + 0112/2///61987#ABP462#001 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + CountryOfOrigin + + + STRING + + + en + country where the product was manufactured + + + + + + + + + ManufacturerProductDesignation + 0112/2///61987#ABA567#009 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + ManufacturerProductDesignation + + + STRING_TRANSLATABLE + + + en + short description of the product (short text), third or lowest level of a 3 level manufacturer specific product hierarchy + + + + + + + + + HardwareVersion + 0112/2///61987#ABA926#008 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + HardwareVersion + + + STRING + + + en + version of the hardware supplied with the device + + + + + + + + + diff --git a/example/aas/TechnicalData.xml b/example/aas/TechnicalData.xml new file mode 100644 index 0000000..0a3239f --- /dev/null +++ b/example/aas/TechnicalData.xml @@ -0,0 +1,1659 @@ + + + + TechnicalDataAAS + https://admin-shell.io/aas/TechnicalData/1/2 + + ModelReference + + + AssetAdministrationShell + https://admin-shell.io/aas/TechnicalData/1/2 + + + + + Type + https://admin-shell.io/asset/TechnicalData/1/2 + Type + + + + ModelReference + + + Submodel + https://admin-shell.io/ZVEI/TechnicalData/Submodel/1/2 + + + + + + + + + TechnicalData + + + en + Submodel containing techical data of the asset and associated product classificatons. + + + de + Teilmodell, das die technischen Daten der Anlage und die zugehörigen Produktklassifizierungen enthält. + + + + 1 + 2 + + https://admin-shell.io/ZVEI/TechnicalData/Submodel/1/2 + Template + + ModelReference + + + Submodel + https://admin-shell.io/ZVEI/TechnicalData/Submodel/1/2 + + + + + + GeneralInformation + + + en + General information, for example ordering and manufacturer information. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/ZVEI/TechnicalData/GeneralInformation/1/1 + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + Cardinality + xs:string + One + + + + + PARAMETER + ManufacturerName + + + en + Legally valid designation of the natural or judicial body which is directly responsible for the design, production, packaging and labeling of a product in respect to its being brought into the market. + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAO677#002 + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + Cardinality + xs:string + One + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 + + + + ConceptQualifier + ExampleValue + xs:string + Example Company + + + xs:string + + + PARAMETER + ManufacturerArticleNumber + + + en + unique product identifier of the manufacturer + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAO676#003 + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + Cardinality + xs:string + One + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 + + + + ConceptQualifier + ExampleValue + xs:string + A123-456 + + + xs:string + + + PARAMETER + ManufacturerOrderCode + + + en + By manufactures issued unique combination of numbers and letters used to identify the device for ordering + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAO227#002 + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + Cardinality + xs:string + One + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 + + + + ConceptQualifier + ExampleValue + xs:string + EEA-EX-200-S/47-Q3 + + + xs:string + + + ManufacturerLogo + + + en + Imagefile for logo of manufacturer provided in common format (.png, .jpg). + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/ZVEI/TechnicalData/ManufacturerLogo/1/1 + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + Cardinality + xs:string + ZeroToOne + + + image/png + + + ProductImage + + + en + Image file for associated product provided in common format (.png, .jpg). + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/ZVEI/TechnicalData/ProductImage/1/1 + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + Cardinality + xs:string + ZeroToMany + + + image/png + + + PARAMETER + ManufacturerProductDesignation + + + en + Product designation as given by the mnaufacturer. Short description of the product, product group or function (short text) in common language. + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAW338#001 + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + Cardinality + xs:string + One + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 + + + + ConceptQualifier + ExampleValue + xs:string + Electrical energy accelerator@en + + + + + en + a + + + de + a + + + + + + + ProductClassifications + + + en + Product classifications by association of product classes with common classification systems. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/ZVEI/TechnicalData/ProductClassifications/1/1 + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + Cardinality + xs:string + ZeroToOne + + + + + ProductClassificationItem + + + en + Single product classification item by association with product class in a particular classification system or property dictionary. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/ZVEI/TechnicalData/ProductClassificationItem/1/1 + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + Cardinality + xs:string + ZeroToMany + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/AllowedIdShort/1/0 + + + + ConceptQualifier + AllowedIdShort + xs:string + ProductClassificationItem[\d{2,3}] + + + + + PARAMETER + ProductClassificationSystem + + + en + Common name of the classification system. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/ZVEI/TechnicalData/ProductClassificationSystem/1/1 + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + Cardinality + xs:string + One + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 + + + + ConceptQualifier + ExampleValue + xs:string + ECLASS + + + xs:string + + + PARAMETER + ClassificationSystemVersion + + + en + Common version identifier of the used classification system, in order to distinguish different version of the property dictionary. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/ZVEI/TechnicalData/ClassificationSystemVersion/1/1 + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + Cardinality + xs:string + ZeroToOne + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 + + + + ConceptQualifier + ExampleValue + xs:string + 9.0 (BASIC) + + + xs:string + + + PARAMETER + ProductClassId + + + en + Class of the associated product or industrial equipment in the classification system. According to the notation of the system. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/ZVEI/TechnicalData/ProductClassId/1/1 + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + Cardinality + xs:string + One + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 + + + + ConceptQualifier + ExampleValue + xs:string + 27-01-88-77 | 0112/2///61987#ABA827#003 + + + xs:string + + + + + + + TechnicalProperties + + + en + Individual characteristics that describe the product and its technical properties. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/ZVEI/TechnicalData/TechnicalProperties/1/1 + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + Cardinality + xs:string + One + + + + + MainSection + + + en + Main subdivision possibility for properties. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/ZVEI/TechnicalData/MainSection/1/1 + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + Cardinality + xs:string + ZeroToMany + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/AllowedIdShort/1/0 + + + + ConceptQualifier + AllowedIdShort + xs:string + MainSection[\d{2,3}] + + + + + SubSection + + + en + Subordinate subdivision possibility for properties. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/ZVEI/TechnicalData/SubSection/1/1 + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + Cardinality + xs:string + ZeroToMany + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/AllowedIdShort/1/0 + + + + ConceptQualifier + AllowedIdShort + xs:string + SubSection[\d{2,3}] + + + + + + + SubSection + + + en + Subordinate subdivision possibility for properties. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/ZVEI/TechnicalData/SubSection/1/1 + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + Cardinality + xs:string + ZeroToMany + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/AllowedIdShort/1/0 + + + + ConceptQualifier + AllowedIdShort + xs:string + SubSection[\d{2,3}] + + + + + + + FurtherInformation + + + en + Further information on the product, the validity of the information provided and this data record. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/ZVEI/TechnicalData/FurtherInformation/1/1 + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + Cardinality + xs:string + ZeroToOne + + + + + PARAMETER + TextStatement + + + en + Statement by the manufacturer in text form, e.g. scope of validity of the statements, scopes of application, conditions of operation. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/ZVEI/TechnicalData/TextStatement/1/1 + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + Cardinality + xs:string + ZeroToMany + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 + + + + ConceptQualifier + ExampleValue + xs:string + Restricted use@en + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/AllowedIdShort/1/0 + + + + ConceptQualifier + AllowedIdShort + xs:string + TextStatement[\d{2,3}] + + + + + en + a + + + de + a + + + + + PARAMETER + ValidDate + + + en + Denotes a date on which the data specified in the Submodel was valid from for the associated asset. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/ZVEI/TechnicalData/ManufacturerOrderCode/1/1 + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + ConceptQualifier + Cardinality + xs:string + One + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/ExampleValue/1/0 + + + + ConceptQualifier + ExampleValue + xs:string + 5/28/2021 + + + xs:date + + + + + + + + + TechnicalData + + + en + Submodel containing techical data of the asset and associated product classificatons. + + + de + Teilmodell, das die technischen Daten der Anlage und die zugehörigen Produktklassifizierungen enthält. + + + https://admin-shell.io/ZVEI/TechnicalData/Submodel/1/2 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + TechnicalData + + + + + en + Submodel containing techical data of the asset and associated product classificatons. + + + + + + + + + GeneralInformation + + + en + General information, for example ordering and manufacturer information. + + + https://admin-shell.io/ZVEI/TechnicalData/GeneralInformation/1/1 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + GeneralInformation + + + + + en + General information, for example ordering and manufacturer information. + + + + + + + + + ProductClassificationSystem + + + en + Common name of the classification system. + + + https://admin-shell.io/ZVEI/TechnicalData/ProductClassificationSystem/1/1 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + ProductClassificationSystem + + + + + en + Common name of the classification system. + + + + + + + + + ManufacturerName + + + en + Legally valid designation of the natural or judicial body which is directly responsible for the design, production, packaging and labeling of a product in respect to its being brought into the market. + + + 0173-1#02-AAO677#002 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + ManufacturerName + + + + + en + Legally valid designation of the natural or judicial body which is directly responsible for the design, production, packaging and labeling of a product in respect to its being brought into the market. + + + + + + + + + TextStatement + + + en + Statement by the manufacturer in text form, e.g. scope of validity of the statements, scopes of application, conditions of operation. + + + https://admin-shell.io/ZVEI/TechnicalData/TextStatement/1/1 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + TextStatement + + + + + en + Statement by the manufacturer in text form, e.g. scope of validity of the statements, scopes of application, conditions of operation. + + + + + + + + + FurtherInformation + + + en + Further information on the product, the validity of the information provided and this data record. + + + https://admin-shell.io/ZVEI/TechnicalData/FurtherInformation/1/1 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + FurtherInformation + + + + + en + Further information on the product, the validity of the information provided and this data record. + + + + + + + + + ProductClassifications + + + en + Product classifications by association of product classes with common classification systems. + + + https://admin-shell.io/ZVEI/TechnicalData/ProductClassifications/1/1 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + ProductClassifications + + + + + en + Product classifications by association of product classes with common classification systems. + + + + + + + + + SubSection + + + en + Subordinate subdivision possibility for properties. + + + https://admin-shell.io/ZVEI/TechnicalData/SubSection/1/1 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + SubSection + + + + + en + Subordinate subdivision possibility for properties. + + + + + + + + + ManufacturerOrderCode + + + en + By manufactures issued unique combination of numbers and letters used to identify the device for ordering + + + 0173-1#02-AAO227#002 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + ManufacturerOrderCode + + + + + en + By manufactures issued unique combination of numbers and letters used to identify the device for ordering + + + + + + + + + ProductClassificationItem + + + en + Single product classification item by association with product class in a particular classification system or property dictionary. + + + https://admin-shell.io/ZVEI/TechnicalData/ProductClassificationItem/1/1 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + ProductClassificationItem + + + + + en + Single product classification item by association with product class in a particular classification system or property dictionary. + + + + + + + + + ProductClassId + + + en + Class of the associated product or industrial equipment in the classification system. According to the notation of the system. + + + https://admin-shell.io/ZVEI/TechnicalData/ProductClassId/1/1 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + ProductClassId + + + + + en + Class of the associated product or industrial equipment in the classification system. According to the notation of the system. + + + + + + + + + ManufacturerArticleNumber + + + en + unique product identifier of the manufacturer + + + 0173-1#02-AAO676#003 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + ManufacturerArticleNumber + + + + + en + unique product identifier of the manufacturer + + + + + + + + + ValidDate + + + en + Denotes a date on which the data specified in the Submodel was valid from for the associated asset. + + + https://admin-shell.io/ZVEI/TechnicalData/ManufacturerOrderCode/1/1 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + ValidDate + + + + + en + Denotes a date on which the data specified in the Submodel was valid from for the associated asset. + + + + + + + + + ClassificationSystemVersion + + + en + Common version identifier of the used classification system, in order to distinguish different version of the property dictionary. + + + https://admin-shell.io/ZVEI/TechnicalData/ClassificationSystemVersion/1/1 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + ClassificationSystemVersion + + + + + en + Common version identifier of the used classification system, in order to distinguish different version of the property dictionary. + + + + + + + + + MainSection + + + en + Main subdivision possibility for properties. + + + https://admin-shell.io/ZVEI/TechnicalData/MainSection/1/1 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + MainSection + + + + + en + Main subdivision possibility for properties. + + + + + + + + + TechnicalProperties + + + en + Individual characteristics that describe the product and its technical properties. + + + https://admin-shell.io/ZVEI/TechnicalData/TechnicalProperties/1/1 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + TechnicalProperties + + + + + en + Individual characteristics that describe the product and its technical properties. + + + + + + + + + ManufacturerProductDesignation + + + en + Product designation as given by the mnaufacturer. Short description of the product, product group or function (short text) in common language. + + + 0173-1#02-AAW338#001 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + ManufacturerProductDesignation + + + + + en + Product designation as given by the mnaufacturer. Short description of the product, product group or function (short text) in common language. + + + + + + + + + \ No newline at end of file diff --git a/apiCollection/Aas Registry/Get All ShellDescriptors.bru b/example/apiCollection/Aas Registry/Get All ShellDescriptors.bru similarity index 100% rename from apiCollection/Aas Registry/Get All ShellDescriptors.bru rename to example/apiCollection/Aas Registry/Get All ShellDescriptors.bru diff --git a/example/apiCollection/Aas Registry/Get Shell Descriptor By Id - Product1.bru b/example/apiCollection/Aas Registry/Get Shell Descriptor By Id - Product1.bru new file mode 100644 index 0000000..549bcf8 --- /dev/null +++ b/example/apiCollection/Aas Registry/Get Shell Descriptor By Id - Product1.bru @@ -0,0 +1,25 @@ +meta { + name: Get Shell Descriptor By Id - Product1 + type: http + seq: 2 +} + +get { + url: {{DataEngineBaseUrl}}/shell-descriptors/:aasIdentifier + body: none + auth: inherit +} + +params:path { + aasIdentifier: {{aasIdentifier-1}} +} + +script:pre-request { + + +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/example/apiCollection/Aas Registry/Get Shell Descriptor By Id - Product2.bru b/example/apiCollection/Aas Registry/Get Shell Descriptor By Id - Product2.bru new file mode 100644 index 0000000..f4ece14 --- /dev/null +++ b/example/apiCollection/Aas Registry/Get Shell Descriptor By Id - Product2.bru @@ -0,0 +1,25 @@ +meta { + name: Get Shell Descriptor By Id - Product2 + type: http + seq: 3 +} + +get { + url: {{DataEngineBaseUrl}}/shell-descriptors/:aasIdentifier + body: none + auth: inherit +} + +params:path { + aasIdentifier: {{aasIdentifier-2}} +} + +script:pre-request { + + +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/example/apiCollection/Aas Registry/Get Shell Descriptor By Id - Product3.bru b/example/apiCollection/Aas Registry/Get Shell Descriptor By Id - Product3.bru new file mode 100644 index 0000000..69a7745 --- /dev/null +++ b/example/apiCollection/Aas Registry/Get Shell Descriptor By Id - Product3.bru @@ -0,0 +1,25 @@ +meta { + name: Get Shell Descriptor By Id - Product3 + type: http + seq: 4 +} + +get { + url: {{DataEngineBaseUrl}}/shell-descriptors/:aasIdentifier + body: none + auth: inherit +} + +params:path { + aasIdentifier: {{aasIdentifier-3}} +} + +script:pre-request { + + +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/example/apiCollection/Aas Registry/folder.bru b/example/apiCollection/Aas Registry/folder.bru new file mode 100644 index 0000000..4d45dcd --- /dev/null +++ b/example/apiCollection/Aas Registry/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Aas Registry + seq: 2 +} + +auth { + mode: inherit +} diff --git a/example/apiCollection/Aas Repository/Product1/Get Asset Information By Id.bru b/example/apiCollection/Aas Repository/Product1/Get Asset Information By Id.bru new file mode 100644 index 0000000..eb0df9f --- /dev/null +++ b/example/apiCollection/Aas Repository/Product1/Get Asset Information By Id.bru @@ -0,0 +1,20 @@ +meta { + name: Get Asset Information By Id + type: http + seq: 1 +} + +get { + url: {{DataEngineBaseUrl}}/shells/:aasIdentifier/asset-information + body: none + auth: inherit +} + +params:path { + aasIdentifier: {{aasIdentifier-1}} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/example/apiCollection/Aas Repository/Product1/Get Shell By Id.bru b/example/apiCollection/Aas Repository/Product1/Get Shell By Id.bru new file mode 100644 index 0000000..2f49754 --- /dev/null +++ b/example/apiCollection/Aas Repository/Product1/Get Shell By Id.bru @@ -0,0 +1,20 @@ +meta { + name: Get Shell By Id + type: http + seq: 1 +} + +get { + url: {{DataEngineBaseUrl}}/shells/:aasIdentifier + body: none + auth: inherit +} + +params:path { + aasIdentifier: {{aasIdentifier-1}} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/example/apiCollection/Aas Repository/Product1/Get Submodel Ref By Id.bru b/example/apiCollection/Aas Repository/Product1/Get Submodel Ref By Id.bru new file mode 100644 index 0000000..fad3978 --- /dev/null +++ b/example/apiCollection/Aas Repository/Product1/Get Submodel Ref By Id.bru @@ -0,0 +1,25 @@ +meta { + name: Get Submodel Ref By Id + type: http + seq: 2 +} + +get { + url: {{DataEngineBaseUrl}}/shells/:aasIdentifier/submodel-refs?limit&cursor + body: none + auth: inherit +} + +params:query { + limit: + cursor: +} + +params:path { + aasIdentifier: {{aasIdentifier-1}} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/example/apiCollection/Aas Repository/Product1/folder.bru b/example/apiCollection/Aas Repository/Product1/folder.bru new file mode 100644 index 0000000..a64c644 --- /dev/null +++ b/example/apiCollection/Aas Repository/Product1/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Product1 + seq: 1 +} + +auth { + mode: inherit +} diff --git a/example/apiCollection/Aas Repository/Product2/Get Asset Information By Id.bru b/example/apiCollection/Aas Repository/Product2/Get Asset Information By Id.bru new file mode 100644 index 0000000..72d5c9b --- /dev/null +++ b/example/apiCollection/Aas Repository/Product2/Get Asset Information By Id.bru @@ -0,0 +1,20 @@ +meta { + name: Get Asset Information By Id + type: http + seq: 1 +} + +get { + url: {{DataEngineBaseUrl}}/shells/:aasIdentifier/asset-information + body: none + auth: inherit +} + +params:path { + aasIdentifier: {{aasIdentifier-2}} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/example/apiCollection/Aas Repository/Product2/Get Shell By Id.bru b/example/apiCollection/Aas Repository/Product2/Get Shell By Id.bru new file mode 100644 index 0000000..e0901d9 --- /dev/null +++ b/example/apiCollection/Aas Repository/Product2/Get Shell By Id.bru @@ -0,0 +1,20 @@ +meta { + name: Get Shell By Id + type: http + seq: 2 +} + +get { + url: {{DataEngineBaseUrl}}/shells/:aasIdentifier + body: none + auth: inherit +} + +params:path { + aasIdentifier: {{aasIdentifier-2}} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/example/apiCollection/Aas Repository/Product2/Get Submodel Ref By Id.bru b/example/apiCollection/Aas Repository/Product2/Get Submodel Ref By Id.bru new file mode 100644 index 0000000..43cd8ea --- /dev/null +++ b/example/apiCollection/Aas Repository/Product2/Get Submodel Ref By Id.bru @@ -0,0 +1,25 @@ +meta { + name: Get Submodel Ref By Id + type: http + seq: 3 +} + +get { + url: {{DataEngineBaseUrl}}/shells/:aasIdentifier/submodel-refs?limit&cursor + body: none + auth: inherit +} + +params:query { + limit: + cursor: +} + +params:path { + aasIdentifier: {{aasIdentifier-2}} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/example/apiCollection/Aas Repository/Product2/folder.bru b/example/apiCollection/Aas Repository/Product2/folder.bru new file mode 100644 index 0000000..eef6323 --- /dev/null +++ b/example/apiCollection/Aas Repository/Product2/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Product2 + seq: 2 +} + +auth { + mode: inherit +} diff --git a/example/apiCollection/Aas Repository/Product3/Get Asset Information By Id.bru b/example/apiCollection/Aas Repository/Product3/Get Asset Information By Id.bru new file mode 100644 index 0000000..62e52a2 --- /dev/null +++ b/example/apiCollection/Aas Repository/Product3/Get Asset Information By Id.bru @@ -0,0 +1,20 @@ +meta { + name: Get Asset Information By Id + type: http + seq: 1 +} + +get { + url: {{DataEngineBaseUrl}}/shells/:aasIdentifier/asset-information + body: none + auth: inherit +} + +params:path { + aasIdentifier: {{aasIdentifier-3}} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/example/apiCollection/Aas Repository/Product3/Get Shell By Id.bru b/example/apiCollection/Aas Repository/Product3/Get Shell By Id.bru new file mode 100644 index 0000000..1fe6f17 --- /dev/null +++ b/example/apiCollection/Aas Repository/Product3/Get Shell By Id.bru @@ -0,0 +1,20 @@ +meta { + name: Get Shell By Id + type: http + seq: 2 +} + +get { + url: {{DataEngineBaseUrl}}/shells/:aasIdentifier + body: none + auth: inherit +} + +params:path { + aasIdentifier: {{aasIdentifier-3}} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/example/apiCollection/Aas Repository/Product3/Get Submodel Ref By Id.bru b/example/apiCollection/Aas Repository/Product3/Get Submodel Ref By Id.bru new file mode 100644 index 0000000..cf736ec --- /dev/null +++ b/example/apiCollection/Aas Repository/Product3/Get Submodel Ref By Id.bru @@ -0,0 +1,25 @@ +meta { + name: Get Submodel Ref By Id + type: http + seq: 3 +} + +get { + url: {{DataEngineBaseUrl}}/shells/:aasIdentifier/submodel-refs?limit&cursor + body: none + auth: inherit +} + +params:query { + limit: + cursor: +} + +params:path { + aasIdentifier: {{aasIdentifier-3}} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/example/apiCollection/Aas Repository/Product3/folder.bru b/example/apiCollection/Aas Repository/Product3/folder.bru new file mode 100644 index 0000000..a102df2 --- /dev/null +++ b/example/apiCollection/Aas Repository/Product3/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Product3 + seq: 3 +} + +auth { + mode: inherit +} diff --git a/example/apiCollection/Aas Repository/folder.bru b/example/apiCollection/Aas Repository/folder.bru new file mode 100644 index 0000000..3720e72 --- /dev/null +++ b/example/apiCollection/Aas Repository/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Aas Repository + seq: 3 +} + +auth { + mode: inherit +} diff --git a/example/apiCollection/README.md b/example/apiCollection/README.md new file mode 100644 index 0000000..d364248 --- /dev/null +++ b/example/apiCollection/README.md @@ -0,0 +1,160 @@ +# Bruno API Testing Setup – DataEngine (.NET Backend) + +## Overview + +This directory contains the Bruno collection and instructions to test the **AAS.TwinEngine.DataEngine** .NET API using Bruno. The collection includes pre-configured requests and environments to exercise the DataEngine API and its plugin-based data sources. + +--- + +## Quick Summary + +| Item | Description | +|--------------------------|-----------------------------------------------------| +| **API** | `AAS.TwinEngine.DataEngine` (.NET) | +| **Testing Tool** | [Bruno](https://www.usebruno.com/downloads) | +| **Default API URL** | `http://localhost:8080` | +| **SDK Required** | .NET 8 (recommended) | +| **Run docker compose file** | Run `docker-compose-up` [form AasTwin.DataEngine](../README.md) | + +--- + +## Prerequisites + +1. **Install Bruno** + + * Download: [https://www.usebruno.com/downloads](https://www.usebruno.com/downloads) + * Platforms: Windows, macOS, Linux + +2. **Install .NET SDK** + + * Recommended: **.NET 8** (install from Microsoft docs) + +3. **Install docker** + +--- + +## Running the services + + +### 1. Run docker compose file + +Before starting , run twinengine environmnet with dpp-plugin. +[click here for getting starated with docker-compose](../README.md) + + +## Bruno Collection — Quick Start + +1. Open Bruno +2. `Collection -> Open Collection` and choose the Bruno collection folder (`apiCollection`) from the AasTwin.DataEngine repository +3. From the top-right environment dropdown select an environment: `local` +4. Expand folders to find requests, select a request and click **Send** +5. Inspect the request/response in the right panel + +--- + +## Bruno environment & collection variables + +The collection includes a set of environment/collection variables you can edit to point the requests at your local or dev instance. +**Enter these variables in plain text — the collection’s Pre-request script will automatically change value to Base64-encode.** + +| Variable name | Purpose | Example value | +| ------------------------------------------- | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | +| `DataEngineBaseUrl` | Base URL for DataEngine API | `http://localhost:8080` | +| `productId-1` | Product ID for first asset | `000-001` | +| `productId-2` | Product ID for second asset | `000-002` | +| `productId-3` | Product ID for third asset | `001-001` | +| `aasIdentifier-1` | AAS identifier (auto-encoded to Base64 by script) | `https://mm-software.com/ids/aas/000-001` | +| `aasIdentifier-2` | AAS identifier (auto-encoded to Base64 by script) | `https://mm-software.com/ids/aas/000-002` | +| `aasIdentifier-3` | AAS identifier (auto-encoded to Base64 by script) | `https://mm-software.com/ids/aas/001-001` | +| `submodelIdentifierContact-1` | Submodel identifier for ContactInformation (auto-encoded) | `https://mm-software.com/submodel/000-001/ContactInformation` | +| `submodelIdentifierNameplate-1` | Submodel identifier for Nameplate (auto-encoded) | `https://mm-software.com/submodel/000-001/Nameplate` | +| `submodelIdentifierTechnicalData-1` | Submodel identifier for TechnicalData (auto-encoded) | `https://mm-software.com/submodel/000-001/TechnicalData` | +| `submodelIdentifierCarbonFootprint-1` | Submodel identifier for CarbonFootprint (auto-encoded) | `https://mm-software.com/submodel/000-001/CarbonFootprint` | +| `submodelIdentifierHandoverDocumentation-1` | Submodel identifier for HandoverDocumentation (auto-encoded) | `https://mm-software.com/submodel/000-001/HandoverDocumentation` | + +**Note:** All identifier variables (aasIdentifier-*, submodelIdentifier-*) are automatically Base64-encoded by the collection's pre-request script. Enter plain URLs as shown above. + +--- + +## Default api-test configuration + +* The default configuration includes four shell descriptors with these IDs: + + * `https://mm-software.com/ids/aas/000-001` + * `https://mm-software.com/ids/aas/000-002` + * `https://mm-software.com/ids/aas/001-001` + +* Default submodel templates (under `../aas`): + + * `ContactInformation` + * `Nameplate` + * `HandoverDocumentation` + * `CarbonFootprint` + * `TechnicalData` + +* Default shell template used by all 5 shells: + +```json +{ + "id": "https://mm-software.com/aas/aasTemplate", + "assetInformation": { + "assetKind": "Instance" + }, + "submodels": [ + { + "type": "ModelReference", + "keys": [ + { "type": "Submodel", "value": "Nameplate" } + ] + }, + { + "type": "ModelReference", + "keys": [ + { "type": "Submodel", "value": "ContactInformation" } + ] + }, + { + "type": "ModelReference", + "keys": [ + { "type": "Submodel", "value": "HandoverDocumentation" } + ] + }, + { + "type": "ModelReference", + "keys": [ + { "type": "Submodel", "value": "CarbonFootprint" } + ] + }, + { + "type": "ModelReference", + "keys": [ + { "type": "Submodel", "value": "TechnicalData" } + ] + } + ], + "modelType": "AssetAdministrationShell" +} +``` + +--- + +## Useful requests & folders + +* **Aas Registry** — endpoints to get all ShellDescriptors and ShellDescriptor by id +* **Aas Repository** — endpoints to get Shell by id, SubmodelRef by id, Asset Information by id +* **Submodel Registry** — endpoints to get SubmodelDescriptor by id +* **Submodel Repository** — endpoints to get submodel, submodelElement, and serialization + +(Each Bruno request contains example payloads.) + +--- + +## Troubleshooting + +#### Bruno shows `SSL/TLS handshake failed` + +- Run `dotnet dev-certs https --trust` +- Ensure plugin and API endpoints match port and schema (`https://`) + + +--- diff --git a/example/apiCollection/Submodel Registry/Get Submodel Descriptor By Id - CarbonFootprint.bru b/example/apiCollection/Submodel Registry/Get Submodel Descriptor By Id - CarbonFootprint.bru new file mode 100644 index 0000000..77916ba --- /dev/null +++ b/example/apiCollection/Submodel Registry/Get Submodel Descriptor By Id - CarbonFootprint.bru @@ -0,0 +1,20 @@ +meta { + name: Get Submodel Descriptor By Id - CarbonFootprint + type: http + seq: 3 +} + +get { + url: {{DataEngineBaseUrl}}/submodel-descriptors/:submodelIdentifier + body: none + auth: inherit +} + +params:path { + submodelIdentifier: {{submodelIdentifierCarbonFootprint-1}} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/example/apiCollection/Submodel Registry/Get Submodel Descriptor By Id - Contact.bru b/example/apiCollection/Submodel Registry/Get Submodel Descriptor By Id - Contact.bru new file mode 100644 index 0000000..a9265fc --- /dev/null +++ b/example/apiCollection/Submodel Registry/Get Submodel Descriptor By Id - Contact.bru @@ -0,0 +1,20 @@ +meta { + name: Get Submodel Descriptor By Id - Contact + type: http + seq: 1 +} + +get { + url: {{DataEngineBaseUrl}}/submodel-descriptors/:submodelIdentifier + body: none + auth: inherit +} + +params:path { + submodelIdentifier: {{submodelIdentifierContact-1}} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/example/apiCollection/Submodel Registry/Get Submodel Descriptor By Id - HandoverDocumentation.bru b/example/apiCollection/Submodel Registry/Get Submodel Descriptor By Id - HandoverDocumentation.bru new file mode 100644 index 0000000..66fb37a --- /dev/null +++ b/example/apiCollection/Submodel Registry/Get Submodel Descriptor By Id - HandoverDocumentation.bru @@ -0,0 +1,20 @@ +meta { + name: Get Submodel Descriptor By Id - HandoverDocumentation + type: http + seq: 4 +} + +get { + url: {{DataEngineBaseUrl}}/submodel-descriptors/:submodelIdentifier + body: none + auth: inherit +} + +params:path { + submodelIdentifier: {{submodelIdentifierHandoverDocumentation-1}} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/example/apiCollection/Submodel Registry/Get Submodel Descriptor By Id - Nameplate.bru b/example/apiCollection/Submodel Registry/Get Submodel Descriptor By Id - Nameplate.bru new file mode 100644 index 0000000..08b73d4 --- /dev/null +++ b/example/apiCollection/Submodel Registry/Get Submodel Descriptor By Id - Nameplate.bru @@ -0,0 +1,20 @@ +meta { + name: Get Submodel Descriptor By Id - Nameplate + type: http + seq: 2 +} + +get { + url: {{DataEngineBaseUrl}}/submodel-descriptors/:submodelIdentifier + body: none + auth: inherit +} + +params:path { + submodelIdentifier: {{submodelIdentifierNameplate-1}} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/example/apiCollection/Submodel Registry/Get Submodel Descriptor By Id - TechnicalData.bru b/example/apiCollection/Submodel Registry/Get Submodel Descriptor By Id - TechnicalData.bru new file mode 100644 index 0000000..e2b1dfc --- /dev/null +++ b/example/apiCollection/Submodel Registry/Get Submodel Descriptor By Id - TechnicalData.bru @@ -0,0 +1,20 @@ +meta { + name: Get Submodel Descriptor By Id - TechnicalData + type: http + seq: 5 +} + +get { + url: {{DataEngineBaseUrl}}/submodel-descriptors/:submodelIdentifier + body: none + auth: inherit +} + +params:path { + submodelIdentifier: {{submodelIdentifierTechnicalData-1}} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/example/apiCollection/Submodel Registry/folder.bru b/example/apiCollection/Submodel Registry/folder.bru new file mode 100644 index 0000000..bd3aeb4 --- /dev/null +++ b/example/apiCollection/Submodel Registry/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Submodel Registry + seq: 4 +} + +auth { + mode: inherit +} diff --git a/example/apiCollection/Submodel Repository/Serialization/Get appropriate serialization - Product1.bru b/example/apiCollection/Submodel Repository/Serialization/Get appropriate serialization - Product1.bru new file mode 100644 index 0000000..da9bd6d --- /dev/null +++ b/example/apiCollection/Submodel Repository/Serialization/Get appropriate serialization - Product1.bru @@ -0,0 +1,26 @@ +meta { + name: Get appropriate serialization - Product1 + type: http + seq: 1 +} + +get { + url: {{DataEngineBaseUrl}}/serialization?aasIds={{aasIdentifier-1}}&submodelIds={{submodelIdentifierContact-1}}&submodelIds={{submodelIdentifierNameplate-1}}&submodelIds={{submodelIdentifierCarbonFootprint-1}}&submodelIds={{submodelIdentifierHandoverDocumentation-1}}&submodelIds={{submodelIdentifierTechnicalData-1}}&includeConceptDescriptions=false + body: none + auth: inherit +} + +params:query { + aasIds: {{aasIdentifier-1}} + submodelIds: {{submodelIdentifierContact-1}} + submodelIds: {{submodelIdentifierNameplate-1}} + submodelIds: {{submodelIdentifierCarbonFootprint-1}} + submodelIds: {{submodelIdentifierHandoverDocumentation-1}} + submodelIds: {{submodelIdentifierTechnicalData-1}} + includeConceptDescriptions: false +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/apiCollection/Submodel Repository/Serialization/folder.bru b/example/apiCollection/Submodel Repository/Serialization/folder.bru similarity index 100% rename from apiCollection/Submodel Repository/Serialization/folder.bru rename to example/apiCollection/Submodel Repository/Serialization/folder.bru diff --git a/example/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - CarbonFootprint.bru b/example/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - CarbonFootprint.bru new file mode 100644 index 0000000..9ee3129 --- /dev/null +++ b/example/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - CarbonFootprint.bru @@ -0,0 +1,21 @@ +meta { + name: Get Submodel Element - CarbonFootprint + type: http + seq: 3 +} + +get { + url: {{DataEngineBaseUrl}}/submodels/:submodelIdentifier/submodel-elements/:idShortPath + body: none + auth: inherit +} + +params:path { + submodelIdentifier: {{submodelIdentifierCarbonFootprint-1}} + idShortPath: ProductCarbonFootprints[0].LifeCyclePhases[0] +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/example/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - ContactInfo.bru b/example/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - ContactInfo.bru new file mode 100644 index 0000000..9471771 --- /dev/null +++ b/example/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - ContactInfo.bru @@ -0,0 +1,21 @@ +meta { + name: Get Submodel Element - ContactInfo + type: http + seq: 1 +} + +get { + url: {{DataEngineBaseUrl}}/submodels/:submodelIdentifier/submodel-elements/:idShortPath + body: none + auth: inherit +} + +params:path { + submodelIdentifier: {{submodelIdentifierContact-1}} + idShortPath: ContactInformation1 +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/example/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - HandoverDocumentation.bru b/example/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - HandoverDocumentation.bru new file mode 100644 index 0000000..be59fe6 --- /dev/null +++ b/example/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - HandoverDocumentation.bru @@ -0,0 +1,21 @@ +meta { + name: Get Submodel Element - HandoverDocumentation + type: http + seq: 5 +} + +get { + url: {{DataEngineBaseUrl}}/submodels/:submodelIdentifier/submodel-elements/:idShortPath + body: none + auth: inherit +} + +params:path { + submodelIdentifier: {{submodelIdentifierHandoverDocumentation-1}} + idShortPath: Documents[0].DocumentClassifications[1].ClassName +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/example/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - Nameplate.bru b/example/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - Nameplate.bru new file mode 100644 index 0000000..5203039 --- /dev/null +++ b/example/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - Nameplate.bru @@ -0,0 +1,21 @@ +meta { + name: Get Submodel Element - Nameplate + type: http + seq: 2 +} + +get { + url: {{DataEngineBaseUrl}}/submodels/:submodelIdentifier/submodel-elements/:idShortPath + body: none + auth: inherit +} + +params:path { + submodelIdentifier: {{submodelIdentifierNameplate-1}} + idShortPath: ManufacturerName +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/example/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - TechnicalData.bru b/example/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - TechnicalData.bru new file mode 100644 index 0000000..34ec067 --- /dev/null +++ b/example/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - TechnicalData.bru @@ -0,0 +1,21 @@ +meta { + name: Get Submodel Element - TechnicalData + type: http + seq: 4 +} + +get { + url: {{DataEngineBaseUrl}}/submodels/:submodelIdentifier/submodel-elements/:idShortPath + body: none + auth: inherit +} + +params:path { + submodelIdentifier: {{submodelIdentifierTechnicalData-1}} + idShortPath: GeneralInformation.ProductImage +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/example/apiCollection/Submodel Repository/Submodel Element/folder.bru b/example/apiCollection/Submodel Repository/Submodel Element/folder.bru new file mode 100644 index 0000000..04497c4 --- /dev/null +++ b/example/apiCollection/Submodel Repository/Submodel Element/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Submodel Element + seq: 2 +} + +auth { + mode: inherit +} diff --git a/example/apiCollection/Submodel Repository/Submodel/Get Submodel - ContactInfo.bru b/example/apiCollection/Submodel Repository/Submodel/Get Submodel - ContactInfo.bru new file mode 100644 index 0000000..e927e4b --- /dev/null +++ b/example/apiCollection/Submodel Repository/Submodel/Get Submodel - ContactInfo.bru @@ -0,0 +1,20 @@ +meta { + name: Get Submodel - ContactInfo + type: http + seq: 1 +} + +get { + url: {{DataEngineBaseUrl}}/submodels/:submodelIdentifier + body: none + auth: inherit +} + +params:path { + submodelIdentifier: {{submodelIdentifierContact-1}} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/example/apiCollection/Submodel Repository/Submodel/Get Submodel - HandoverDocumentation.bru b/example/apiCollection/Submodel Repository/Submodel/Get Submodel - HandoverDocumentation.bru new file mode 100644 index 0000000..07c7652 --- /dev/null +++ b/example/apiCollection/Submodel Repository/Submodel/Get Submodel - HandoverDocumentation.bru @@ -0,0 +1,20 @@ +meta { + name: Get Submodel - HandoverDocumentation + type: http + seq: 5 +} + +get { + url: {{DataEngineBaseUrl}}/submodels/:submodelIdentifier + body: none + auth: inherit +} + +params:path { + submodelIdentifier: {{submodelIdentifierHandoverDocumentation-1}} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/example/apiCollection/Submodel Repository/Submodel/Get Submodel - Nameplate.bru b/example/apiCollection/Submodel Repository/Submodel/Get Submodel - Nameplate.bru new file mode 100644 index 0000000..f69644b --- /dev/null +++ b/example/apiCollection/Submodel Repository/Submodel/Get Submodel - Nameplate.bru @@ -0,0 +1,20 @@ +meta { + name: Get Submodel - Nameplate + type: http + seq: 2 +} + +get { + url: {{DataEngineBaseUrl}}/submodels/:submodelIdentifier/ + body: none + auth: inherit +} + +params:path { + submodelIdentifier: {{submodelIdentifierNameplate-1}} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/example/apiCollection/Submodel Repository/Submodel/Get Submodel - TechnicalData.bru b/example/apiCollection/Submodel Repository/Submodel/Get Submodel - TechnicalData.bru new file mode 100644 index 0000000..2a645ba --- /dev/null +++ b/example/apiCollection/Submodel Repository/Submodel/Get Submodel - TechnicalData.bru @@ -0,0 +1,20 @@ +meta { + name: Get Submodel - TechnicalData + type: http + seq: 3 +} + +get { + url: {{DataEngineBaseUrl}}/submodels/:submodelIdentifier + body: none + auth: inherit +} + +params:path { + submodelIdentifier: {{submodelIdentifierTechnicalData-1}} +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/example/apiCollection/Submodel Repository/Submodel/Get Submodel -CarbonFootprint.bru b/example/apiCollection/Submodel Repository/Submodel/Get Submodel -CarbonFootprint.bru new file mode 100644 index 0000000..847d0e2 --- /dev/null +++ b/example/apiCollection/Submodel Repository/Submodel/Get Submodel -CarbonFootprint.bru @@ -0,0 +1,15 @@ +meta { + name: Get Submodel - CarbonFootprint + type: http + seq: 4 +} + +get { + url: {{DataEngineBaseUrl}}/submodels/:submodelIdentifier + body: none + auth: inherit +} + +params:path { + submodelIdentifier: {{submodelIdentifierCarbonFootprint-1}} +} diff --git a/apiCollection/Submodel Repository/Submodel/folder.bru b/example/apiCollection/Submodel Repository/Submodel/folder.bru similarity index 100% rename from apiCollection/Submodel Repository/Submodel/folder.bru rename to example/apiCollection/Submodel Repository/Submodel/folder.bru diff --git a/example/apiCollection/Submodel Repository/folder.bru b/example/apiCollection/Submodel Repository/folder.bru new file mode 100644 index 0000000..a5f1714 --- /dev/null +++ b/example/apiCollection/Submodel Repository/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Submodel Repository + seq: 5 +} + +auth { + mode: inherit +} diff --git a/example/apiCollection/bruno.json b/example/apiCollection/bruno.json new file mode 100644 index 0000000..1e3ad27 --- /dev/null +++ b/example/apiCollection/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "DataEngine-DPPPlugin", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} \ No newline at end of file diff --git a/example/apiCollection/collection.bru b/example/apiCollection/collection.bru new file mode 100644 index 0000000..00ee856 --- /dev/null +++ b/example/apiCollection/collection.bru @@ -0,0 +1,38 @@ +vars:pre-request { + aasIdentifier-1: https://mm-software.com/ids/aas/{{productId-1}} + submodelIdentifierContact-1: https://mm-software.com/submodel/{{productId-1}}/ContactInformation + submodelIdentifierNameplate-1: https://mm-software.com/submodel/{{productId-1}}/Nameplate + aasIdentifier-2: https://mm-software.com/ids/aas/{{productId-2}} + aasIdentifier-3: https://mm-software.com/ids/aas/{{productId-3}} + submodelIdentifierTechnicalData-1: https://mm-software.com/submodel/{{productId-1}}/TechnicalData + submodelIdentifierCarbonFootprint-1: https://mm-software.com/submodel/{{productId-1}}/CarbonFootprint + submodelIdentifierHandoverDocumentation-1: https://mm-software.com/submodel/{{productId-1}}/HandoverDocumentation + productId-1: 000-001 + productId-2: 000-002 + productId-3: 001-001 +} + +script:pre-request { + function b64EncodeUnicode(str){ + return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (m,p)=>String.fromCharCode('0x'+p))); + } + + const varsToEncode = [ + 'aasIdentifier-1', + 'aasIdentifier-2', + 'aasIdentifier-3', + 'submodelIdentifierContact-1', + 'submodelIdentifierNameplate-1', + 'submodelIdentifierHandoverDocumentation-1', + 'submodelIdentifierTechnicalData-1', + 'submodelIdentifierCarbonFootprint-1' + ]; + + varsToEncode.forEach(name => { + const plain = bru.getCollectionVar(name); + const encoded = b64EncodeUnicode(plain); + bru.setVar(name, encoded); + }); + + +} diff --git a/example/apiCollection/environments/local.bru b/example/apiCollection/environments/local.bru new file mode 100644 index 0000000..5987017 --- /dev/null +++ b/example/apiCollection/environments/local.bru @@ -0,0 +1,3 @@ +vars { + DataEngineBaseUrl: http://localhost:8080 +} diff --git a/example/basyx/aas-env.properties b/example/basyx/aas-env.properties new file mode 100644 index 0000000..70ef7e2 --- /dev/null +++ b/example/basyx/aas-env.properties @@ -0,0 +1,16 @@ +server.port=8081 +basyx.environment=file:aas +basyx.cors.allowed-origins=* +basyx.cors.allowed-methods=GET,POST,PATCH,DELETE,PUT,OPTIONS,HEAD +basyx.aasrepository.feature.registryintegration=http://aas-template-registry:8080 +basyx.submodelrepository.feature.registryintegration=http://sm-template-registry:8080 +spring.servlet.multipart.max-file-size=128MB +spring.servlet.multipart.max-request-size=128MB +basyx.backend=MongoDB +spring.data.mongodb.host=mongo +spring.data.mongodb.port=27017 +spring.data.mongodb.database=aasenvironment +spring.data.mongodb.authentication-database=admin +spring.data.mongodb.username=mongoAdmin +spring.data.mongodb.password=mongoPassword + diff --git a/example/data/checkmark.png b/example/data/checkmark.png new file mode 100644 index 0000000000000000000000000000000000000000..971aaff46500ad44ec5ad4a0cb300e521a8b7908 GIT binary patch literal 153823 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

g5!t0-C_rJk^v!uJ;(E<&7 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 z z`NRwNl~z^(=GI|M=v+j9whJcLY|*f2Wm8J0DJ-X~I&4+pLZkm}No$w4x2uyQ$B zWqug{KtC$>It4fBT;|`kBQq#3u2oYXx#-_@z;E8{chBXR5{7YbAc8t3tL{-A=Y7E> za`&$s+&72r&o+c}SFy?3)k_LCQA^&bmC~_eLpm9$ zLDJxM{Aft4NJSR`hJo|_RxgtGc^5c`tlt3XGg`?o9JG9|R9ecZp?QtHNjmXE!MKBRa-yD`HZFP?Whb^-mG2<3dFXB-rQhlT;8>3Mz$RQ8Biq0}xXtIH7 zpN*~0d#~Nx44P@Nb$I{(NHT$~O?wvq1E@(pYR)SZfH!%6)iL_9p-Txf`NFdD!$lR#)muiO^%0A{@Kl#+bMI z+u7OqSlQcSeLn?+Etm-)^YT|#m8%FTij8Y4zV}F%I(ReQsq){RXivAvAvJ=-cfpoM z!BW5RH{BYa_ytHYJ%dwmgui~)dih^L2gT7eFnI0`lqIzsDe(V1&{AS!`sQ(v{It51lq0^ zxv<&RfWB{G2d)%oUGC4{7pSl@o(xEI{Z3kPkrqAw_m;->m*<&0zP{VTDG>_biAhS< zU5Pz@t}xa~;BuBE>+Q{srOmq)@o5J=Jub;yO%8BEli~Fe^ZB0ksv;s7*67LvUFzDq z|F?th^6~rU_h3;})Cc?3g{^Iu+*92;-CO1I5599HE_0~d(2*DZ_W>1mubOwW9HWg# zVz`xbcv-hI{t~c_x(ZJR|L!Am<)q9h^_pEsKAM6MsjS?Kty74seyU!87e&+9pr>6D zzvnN;0X~DEe%9?0!HVE_E%>=N(R;Kz5qoyP>TkKaz#1<#o!J2HMRz=q7~%3N$xfQ( zszrXC?h=eCv!4(IJ2BX0syGZ(ee(`KTakeS*c3egCb6#%ug4sNWTx@ z6qFc6Y;r0PNF+51KpF;wp0(l4U7~4 zI8IUo!9l>(%7hYjOa6Q7H3Gy7!;&QZkQbsf7~^l-x2%DRU}E{(3s3ua;O-9j08ruA zoc~ZA&BhIJc+;t~ZstcXO3&Chg-r;O%8SP`a27PA=g2iFapNcubpP1rDf*hwK|z)- zxDwrNnEG7+AGHe+$idinv*QSGzt*XEEFgD3%z)Pv0LxhX_K@W0j}9aX<)xH(7Pvne z;dfM@$;`szleQx(s74CdxrakDgcen0CFg;ifBwu=9-o{DV`<~p99!-_HieK1(9P!7 zgMVA=$NxVrfK>p}3&T5QStM;|9G_NUk@s41fHW+P==jqt>h01npvgh5Y~*r*D7^5+ zx1DuQ$RM;;OxPaWq`0k3_d<<)pZHukv$4W^h~4dL)M2=bhfO)f!OcL@nF#j*hQN;V z-+dEAxDXyQ3{WguR;+C4-&^2n4Ocgvv*4umj~B*eA+NBvYhN?8KY7PNQLqL$XqU$^htgUDXx<=V;6 z&ee{Z)oSN-fDk8*}UwY!TmDTSGRLV z;_LS1IW`5eAnv|Q4HdUIF}kC-J)d%K<|C}RXS6ZvUQ`nSj(E9ZGZ03^*w$&E_>t2n z#5Yhl-jy=y6lkdb?)dGZGog}|eJ-9+89uR96Rh7bD|IL`elh(L`w%GL<>BR#Jc#A~ zfLgf(K=kw%pDT#`X{}}K0(2eLXG9@MJlw1q?>j^1FSLK~LL(T+Nr`NS+(JglN(DG) z&grDr^v61s!i6X9zWWxevCYPsE$>DayQucKv0A5?82+KIl%Bwcn7N3^rF`7y`9k1yldI3NFg`Y z&s7tO&$6&BV-MTNl{CS1ui!e>wRp6r`0krLjjgE#oN-l+ggu!E3iQX}wA(D^!EJ07&_78RXuZ1tBmut;(AM=M+l^bCbSt^`(3luGQ&*vq%@3N>atLEzwQ@1e}FP` zlJKMzk3FCNlNL@$r!3q!Es6B=;ROn{gB!!4*-fmhZkBXb;cJ6V_Se1JLZW{pyt3zk z!Bq?}xWdXVbds}%F%v^_pzeSarzn^g2icDap;#_mnOs{{W@XS=sC|>4xHZG^)jGw% z`-0Nx0k>e`R83EKy8eP)tX?VtbJ=rCWvA%h7=L8KQ;ZoAroVKLl#3{P&`cF9 zbS*YfkVyYE7D+nzX1I)g7mlxwfL%(R0ACrk*A&*Xhu5ZUd55i1By+hM-%PF~4o4dt zqj@C8zDh6B&IzmLd<(6Ns~1dCG!F^<^|*H5n}TM07)8bI4l0n|lAP=E#_ezE?|*oCoEbY=`g|pBHDvpHVvzK-G4@d&{(+9An+X_ zK$UMgPNb2!mAdNAAP4Eeqnqpj7Cf=_$!0HA(l-TTTMy2 zt>FGTZ#H4Bl_oeWXYo=ag^C)g!qiHrkg0=0qtf5K*~pA>lUe|z?xt3^@?N~XXql>g zh3)o3v_x*g*K_RX*bddC76CdE4+7+_WhhMT@%@deZyME0UFo>3L|HwQ2L;=UG3C;V|tk8jeo93tJC5BC(_ zWOmQXTCC#de8z~cS%mcBtS?KRg2C7z|2K5X3??E=cGu)pCUnWwsHt=E=~mM+h=nIQ zH5Lz4H5Oij@t0kulpshra9@pNqH9Db`44KIe5lkr2QekxHXTJ^ik{heW5x~k`gL%I1=J-93j^La9G)&XASB)d2pK8IkKeLJWS~jBd zOa|Rib6)y-iTN>bDU@9CSi-mA2m1#$;O@#PDJzRSRjoxVzeqPj@83b6iUltOTu{A; zWh`-dg9(~%{D9~{05gE1!PGS%Ff}%M7O*dSzi{-q%I(@_EnV?G0W-&Oe`7>RAShWl zly=?$(VZYsVb^Mp1$)jAA|@&nj{7JITvXGFD78h;(`Gk=l+}!gL*}aZXnDm6P65>o zYCl@5=F2*a(z+ZmVDQi5_4lf{Vav2TtVpbczMggxpIV|xy*d?Sc+mKfEGcHFusKc} z&ZCZ+SOoA1ngBk*Y)ngA9~QJ85Vis>E}UCjJL1KLZ5n$NYjdR&ys~rhk6h>P@B+K+o6B6Aj!f%Z+gDY_Xl!Y!J-nj3RhBPu!&#dRYI+Vz*f0kvF39vs6Z z`t_{%AC#~)ks->UG6}ZaXlv=t^T#`PuGgwPuKBu_q2y~e+76Soe6p~w#O`jb_(wSG zI06BWF3xEe!^=-=Jl4qg1W5jLC}TfnCM`c6Dy(nw-_}6HNBo=Dr_#> ziFFOi#<0s!uOxOVT%CQR07>?@Apgey=ELhazbQ@JJwVAfqMRo)QV{4*8S`SuXtEy*D6aQ3PXf7Zj z<&JlcyH>5X>k<){-DpKkfr&HP<%4A$#AK1hc60Xh+?I zHL8gmY72H9@=x_$%-S?8Na(`8$*-hPHH3s0;@R*fpFOI%aS=O-iateQ!EDg{bv{Tu z2e%X4g}-fw_C-3C;*|QAP*Ol9yE@+Pb6@w7fAAYCQt=xTnJ1jHpojT&dSK5j153Jr zf!;uyH!jDj!1iW#pmm<|TQE)0xR_E;Mt00$54w&vSc*e5o(JLiW8yxwAJqz zyeSB?A@;XwdXTP@+GPCHqKQJt0NxYv5ht14cb1xUzmQ;p@huu2D)cv~G5fU6TY0~E zJhmg2s|7yQ@Ce0=yKc?-Vp{xOW&mktBa~H6{wdJy?D+Wj4HhQmO5;_~I0#!)3_^b! zTWRS0fexEb4^l$*{AYb+4}w*JuSVZ}cbOLJ_$pE+iv-IP+;@*NgkmB9 zG5HdR$qaOjEmR2DUq$#ia9DBIZGTiPjCP4N7FpqmR@YgO6j_v&r*602TpP)>U24R$ z#N}>h-O$0>g#c#5iNZyYkmB1{&yDt@6dJR6q3VvenY-;vQgG<@~nooQV=&KenvoB0z8L* zEp!;5U1*;R7dmR)W26R_+G1zTE+G?oOs?yC{e@7N^1sPJAjK~atbl;AVKCjCq-yDj z4w3ijoDD~=_ihJR&HMi>GKwmtQQJPhO5mqlUY-sXzuJ*YVSRjE(1!9C9sq`VKp#Jk z|;%rLn$`1YtOJSnCf z9YNry@tWXS>E}x*V>dFiJ`JR^Srvdufeiv<&uK!o@-EmH3U5x(KP}g%QubbSaPp3$ z5p~IuU|J0OeaVCXBF%p#aRM*eI-fb-6t+2jSSC>tAmN0->2=PB33rASN{kp^Z-B3q8 z=88eFNM$vwvxI);vX2N_2#pv1Es6xZ3MC=!*-yVMxEeM1K;iv5d%nC41NNA;AKE{2 z<}4(FImgaA$|Q5dK9BqmP3aC4zFHJAqS$nT9|Z8D`4PT?ZX2b3=$LRTiXCY2Dt2@U zqb` z7!R$=MePu{jkiaLw4?ti92I#JU*2mnrmHr|nz4(#B{N)GpIvtnX4H=1Mz;Wi0B>^2 zs(onTd5jIKTgmLvaY#VJzILh_3Z-47_LrPx7$3!vtSPIDMT6h|(oWZ{PTO^Igu(5V z!U=jU7F7NJF!j|@QEl(v!!Wc+gCePPgLH!e(%mW2-5mpn2uMi@LyDA0NcUhM(hb7U zH6YTR^PchE`~AIZ&0+D+oZ0)?Pkf%-(ZDt<#>mQggo&4sl}{I8Tm7c{Tu0Y4X_!G> zlQgUKdjjWuCl=zVv-HO?$i~9QBZ~iS^%pj{*y%)U&}3&aVn6>q8|Yj4fZNeH6;qXL zcwtz;}(X!2P%k6B&s7L0aGLJ?gI}*=%FXFA`%m~gG#h9O-n-Z;EH#Oh)cmrN&Sc=~i z_}>|yFemq~%Pj8r?Mky=<2(kBd&xMw7(17@n`Ki4BZd@H#c*OkG&pg1A!sQ;i+CyS z1BD1Eae?a@og!lQCYRbpeLO67&i%6Av7tjBMwe2mA=5zj&It$n2r7%WbWyJ7DoC?i z*z?a$fxlC!{~KMcbroBj-$+xhur-|l3eneddFY+@FnQ$@UdcM;KkcrZ)2Td8gy3ye zV8bUn5)&aexGzl))H(8aGi9b71&uhXqr)FFEL}sz31;o@wwDw`2mLjXLRmy3s7Y!l za8->gTjrU?tivDF*yVG@gnub+wT)-0}{^uLa@O-1I$kq6bDgT zYbu#o5kn$<;#NGl$iUFrduy}=f4#1MR0bw6boPSPr1Ho19gKWuI+09Kr|p4g+!;5m z4OW^LK=wyZdfIJzFL2_khZL|lFJOB{i4Us45igjMe~-Qbwa*UGd%IhZOm(jedAczGh)&C0<7+^nHU@jeKKf-Kfn z$|g$qxpZ8XdI9oy1C)ClI6aN!`@SGL=|e%ZVd3ikn8_ehmdEg1tqHMlE?-+?MK(8x z{q$lNVMgkj0omZEm4G5iua(m|iVa1!)m^TdS^+@Nhy@I}f|x?V>X3-Xj9r+=e5uOw zzxIgSbK9}Ur9xb8gBss~HoDqh-(PxNLh@(~QTL-WwLS3}-d_a;=qL{p$j{3C@->6= ztMQXl8_x48(I?tR1PID*?YLJA8M5N(PK2+X%sG(9Z5%suix;lUF=;Z@(=F5o6GxXZ!; z0mIY)4pQGWuXi=ag&2T<;vV;f#YGh3tpp8?>j6k@F^mqX{#b^#!SL#ICJj%RyOS=h zM^JxXneYo)oKK6{Z3KXdmA1i@CxAzQKHms##;!K}gT|oHVMK32&s2cSELZsNCHljZ zmMCWe8SRHZCBMI+wZcD^bYH5(jkx`FkZUy- zU0_Z}pi>j>wx@ImkM1qoV0#|u*9zggX)o%QSFG5`%{I7gXw*jJzapeU3Pd)mD<_>~ z6w-?dzJ7TRNxjrXX5Pj96Q@MJE?+j3a!{a@g8ETkmU5^o@ojXcJejweNmdUZ z()b3@U#3&b4!cfdKi5@~cKu-CB=~az(A52%lqL0Vn=X9$SzCkew-pJG=RCamkT0tI zp%!>noS?*|ev=U1eDyv5AWRF}E&#^=qq>(|a2=?=ZkOScWp>0ax!2uIGdUD9?r3Vk z{1thtP_K0dyz%y@{MYY6HLYifims^OVQexLEDT~k!1I6xgCPcQWQ9|&N-coKLk~V= zM9$6mF^6RE#*L`qW#ZsQGz8k?8jzY zLPxegmxrEzPD0@KJ(Pp0XUxjW=1Zj6i|;hyL}|06D~r6k!7#nM0yyP057CmIk|--A zD@(#Qx_F}H(d76WX^3P6cb={{VO>17#};vZbkY6xYYR`v10esu z5*`X}k-x2*_e0X@2~^+Kk#!RdnK8a%@O<+@^I`q;<3mCNt zv=jpY9uZk9EI^T^BZXimhU{NVo$g~}e;}6mB1#`(78cZME3Q7a6Reid`5gnd>P*+s zU65uj<94I#30gz^^T{ENux`0LG$=aFFX_f2Kz#|X|GFTonH(5ZRqkrfn5(ftSn^($ z9J~?2c&;81wWtyimG7hSOzRFVR8}5vSC}BtHVhuK5C`~Nc=3&wm_y=Lzy0_=;-iSN zh&y5P>#cn5otz8$$pe;;tF9U_!oGhu9l^~aTk(6L^lLN-VdRR?Fj}p}|HBdCTqs*L ze&NjgO^tr}qVgc1UKK}_TN)5rPP8{n@%xS*7UT|C;nJMo<@wC?ouQ)e?f#yx%>)YJ%rAIL#(%}lFgRY}#06$b5)DU{;en@} z-yL>*@etggz4L!YQ`7?NU-+l0kazDpnbBhOOw6kHJsQz-dzYip%-Pq(;b{k!N>tgn zw}sYFE^xjQWkj{~Ldhc$Kq;=G9S+a2rxpwz@(`8iF;3Xd#*y(zNm&^C>G6Z420E*% zCEJi|UuQ0n$!OT!OAG}s*)%^iBo_4p=7?`_-pHXMQw|pyrkG1N|iaS9>)k%ZlfAgz;_mR>|7U(eF%GbKkC* z-MnQxT{XjtJY-9uryMYVR&xJ&D0m+-g$~dh}v$U{w$vg^0{vo6hpZy>OrYD}q6sf6c}tU}{}o_?VkW-qWn+#*Nf#^WCt< z!`xN>ccQ%)Ta?|BTfi0bnvR0h;a(qptXKmy)nx=|T5hGt$l$>QzRszgSuD`wkg5h| z*>JElvB^nmtkR35j7$%+JeqyrA)x%+8PsS{Mqj?(eDA*vrh#0G&wK>R!*$uNkDhI3 zHz{>*DXR=icMezOdGj(nh9uzx->x0aX!?qGRI8d0j{%IB!;JhJpd6W^cK4^RW_ahsqrkdr#NBV)D1xgex2gInOBsK+s?Vd0I+YLG< zbHW2>%f`~pDY9K`a|hN#d7@g)dXVS+ol%}-FGVxKLyzxVlG(V!7$COVf=fwSf=f6B zSN{VCf)`O9WFDhRk70#EwsATsH5j@4a*0ci`9S z9b$IQ_L4B@u%IhGNZpv1^4I3DF|9#GTexloZAedsA6YgbkQf03xnW0KF9d<7S1}0( zZfjpurg?90vKom!Lk`-o>B_Nrmx`Mk!*=I`*+R9bIlDogw)6QwhRHLE)JN6jB zysaCGz!H!zU$i#wa7XsBd}>R>@Rqw_BN?WeIMI!YRBr9nG*H=hr<)d|96gEGyXy%2 zra@HK!>?bQChj!=CUyj}t1ct0Y-!5JpuvjSe#%%+GLec6TP%nVs;^$M`O0u{vS}#& zi7bfZKOn-u1o&~C6^NZqSXkVO*$+ctQ{DT^G}cik>?0+$i{%q%Gb~V@x?VX4TH*;; z_r1BjojuDSYH=bU=pzN7k(#`lS0+(iX~5+R7n8t1D|)WfS09GYay0XGsGALN&Y4|Qoi3{sF{KMy5pYQU+li=htq@??eFM05hqK^(Wo4) zz+SL=SG}&m{DGRNgFV!LWC*kf`v86hAeg`6^*{xhz9q?H*L!sfHJJ65%p123W-%H; z7N7O*JL&#l)oJDZsg4xWCLWj|oON0n^4V1-*hp9dzWo1L=0sVm%&Lx9R#Hl4*}11* zxTexA5?Kkk95y6`oSB%rhEqtF^pzZVc>u)r7yA0L%BqCyVZS{TQ@op$LesBQedvAV z^E7-OxMK~tEq6n}YJ+!L`3?w=>r7dM*QovKC zz6q+w_?PCC%*nlWKp*Bwr+9J|>sOhiMLJ{1xeaJ3uIB~EsyYG2jp^`_>x;T_vk}`w zA1S{Iq>Q&#+j&ELBgqz;-OwyvG0j>=g3F~|V8jFOm~@a#6xbZkq`*9U)5e0)NCA1w zkUXJu`1**pJ{vXOEuFjMZF1n@e;K+nM))K8Uxu%F1vxA})X%kBUDi7xNl4oA2wBT; zfQNeL(NTR6al(KeO<_cQ`y4cs;4aOuZ__5no33#d6D5~YWzUI)jwwv#pzX>+M42@1 zOGwG10G8Cw;zjx#0-PWV?WH8TW%E7|Sqb}Q-IIqBEGpuNk5LxunO?SIUr>jEJTiQ~ z$GYWT97NkWZxSkBdH{Fbqj7`_76$&p6@t9|OzGQ?J|k!Rv#(#*!3D;S#TfJ=;V?(r z>%2i6yE;fq#`fb=|7#zP)F3VRB5L2j=PorK*i;tKJKUQ#f}U--HUe2!qp~;JPQH8r zoQ0;?zM011I$9<73tbICz5RBC45_J0H>ra*uR6j|r^l>4eE;(dgfqGp78cOuSw}-s zjR&K8pI}GF9yCO02cC}QuUjTVGp!lw2M8YC9xVd5LUl*+gn9!pwG@)V2(Wp%2Y{%Hmqob*VRY% z6M1BK9DGv|^N8;4BlY8AZf(13yI^(Z_MvY=(@iaeufbMN{ZJ=gYT!+WR@!oa!iYxy zGM|7Mkm!2?mB+KBN$Noca5E6EvT8?PWI9SaIP2xey0?;U>c^Gg#c~I?W zHMQJWGA|HeO{mwl58iD5rx&Af=N8UxoRx~Smm-y-==WYfqW3l{_T<~|Mjl<#Bq2gV z=R~QR!8lap&57 z2#Pba!cd61VNdOsG!q)-KS87X=cR>x48(F>;ww}t#WGGJFwMek8+%aTsVmFt5-s`@ zgxx89F{nmevYAI}L@cV_W>j<`PPpkTByLqDazx)}2z`yNXe!)r`)1_W_%Q0W5+GM4&8Zl?!+& zGrT9Q9f)YxWMz3$1!(`877NL2yB4v6`HN2ffQL+}j52RDh+Sm#IP@(hU8eHkLvz~x z|EToJM_L;8UY2ynZ3<3e1i$VpK*Ntj9TKBSDlW$P-`1rZ=+9l4y{N%GzofnhrfGfn z^So^}m14o6{af3?T)S^aG{l@ zo4z6#zxU-67D$j6@$<8$XaTt4$ZW-7-|`mSqQC^OqyXrU!1vj_=P?afU*ue6jJ8y=87?RQ-!7C!> zAq+f#Lg@W2t>M|+(4tyLhhB$n#t1Hz)JuguJrEXYWt$d9{Bj|?F8|pw8CK!NklPnx z%lD_Q|LfD4M6{=60C-wfoUQj0AQ4X>?LgrSi9iLMK6hCkl&Hf+4|oI8)rM^6&D^*t z+*0)M@mPEny`R#$guayG4LY#iKy!1u5-|Q|wE!3)>I_cz>4WrgvjMO22|kLnS0A}W7L;8PGb1=5xVma;_(IDI~WMS{R2 zLol<+=X1$Qqx}q-dcCE<7x=_w9A#&m!rQb+>H_t<7gA*Q+E)1^TEz~L?{65y^25Y) z!;Y=^pb}ji^Xg>;e~l$Q25+v$yyU$*#>pkW#Od&MU&I~`3l4Y+I{8|@^u91$0Xr18 z)2X@!Iu?d6;64}5OO*1ecwjGZHn-&`(-vy}iW0E8=x7!Q(g>5y$_^*i(6KrEioi?} z=xVv_`o0oLt8a6jo$aisjAz<@l6O|t!Z&?h{dNI+V~ zy$JaIBI_E#PrHHg($7fIVi-XL2$gRT0 z7|)rQ?vj7^B!ATEEAdy36=oTLt-Y{5s1~oBDoW}DO#>_cBm$@dlkIt(2_R$1@D>=i zb~ztZckxZ}y@w9zfP#(n7iAmU1z|`2{#erMb+fnkFyvZ}3CEoJq4JDpQ+{n%Og{Yo zD&`#W2%pydr7s2C2lg%zmkw$K@AteZTqr%PyX`K|`i&gOu6@;L9kT}(T~{mkc{G1N zar2%|M$FBB_0;?Ar&E^93-BpE8tz6*;OJvU=&~gDZ5+}6@f9bkO|J;K7u6fxUCLk8 zs(el3>NtRxo_Aj341$@L#C3nYGOav)vdR&0^dG)t!>CQWa;r^id)xDXc}-O~@w0R5 zlSCdK9z~6bi)IFBYfgEanK;bxZqQBNdsoFEKtl|oV=;E8oF+hmj&vVz9=09rp$Kj4 zM?rGuG$efIYEuNqOg)aL^hGq!R(Oc@!;M>(!hK(^%K@R-r`3x$CAJTe{hA9ANbjifacGB{ z+O>_01A9Oy18lkT$G@H$@*bCbbM53kJvV@ta?0^W~Z9XH%?c;p|0;q zYMG1hQIWX_*G$S-o zPo%z2ybpJ~X0pX{)qU3~)6I4MOIvK|FEP;Sa^slU1Pp}6WHSUrgFJIPudBl}2UvK< ze|{~5uey*tgGPPlKDN^zui_!Q- zw#6h=waDP=wpa8Sw%77Kvcz~v{2|F3dzbp*PO)LX%1!4l7FS5b#@5@IL0T)P(|ncE zcC*IwbAyT@H-f*R+^#eL^*7%K-e?AKQ##5Ghkd_=K^6$e4o?+HuU``>RNB3>OD?(S z%sYN2b&L5X+H*mpy~d*a zQ59mNG?1pOqK$aKg%#g92&HbBXH&D}t`YWzB4O+8vos=`obV4~TPbR;73?_%oEW0& z1t0c9svpxO7~}01D_}4H$uu<3H6VRPfqyaFh>-gp;{7M};1!CZxLMVa`$e%>m}ig_ zHjg;f^t!TzPG-Tkp}n1(AKf=MNEh3^!cl-}K0_!&x4zrGh5-M=6biK1LS)om-X?Ew zE`NB@+MU84p!%&SI@teZmuNBc{sU-#U1j7zmXb~+MhiqTlKlQ2;!~cS^FODRm?g^$ z>kVm{-lNj*RNmSkt>AGBxsTHF^ z+%+$lPM3R|Cs9l<@*b%#C9onlevI?o-P2Rnh}+$MRWv#;((-LY7|J|OLG&Jc=Lf%`E!G8As^IL{mZpPhY3Xf~AM<-tNrv7&Vb}f~xq~!! zfHRfZ1#-D~6-uX($-sPw)O6DMj`e6aE8P5IA6(%B-86))PQ z>koI`I3bBWZFS?Unr#Hf?oy52&www3z-af_gA)+7?<6&1i&qzBw@0~d$SReQjI&tv z0RJqB*+dv6hTYBkp>2vA*%6|2(2H8B{ck5>!YOIJ7MYPOQtu*JLb>jOhyJztkO+yA z$1~y7$H8qA)E1xZ8BLQc$DU>GyBeBlD)upPUVC+X5Heo{_n>MtHu_|WbN_r_4VE^( zNeWm;Z6s_`2!a88IRPLc52rG1Vq6!R2sP1zvX^9K3NHg;S>ZWbaBMa=klfnp{>zFG zDaO?L71my|%ID>JL3VXBA>%(zh^Zler3Ofy0HM6P#U0d%gFRE{ZIGH(G=I1aR-QkG zN9YRqs5Wr$pH+r?)we<84F#GMS3#Fyl=A@Y0D{d`Wo;5B7gV#EHbJFfSUJ3%DkzAYdSljOxYL!9E3K}x8`lj@}{Q^B-i`MROEAi=#p>nXD4GK z%Zy}DUSi?RRhbvOcNd;0aMdT@ApftV=s)r>7B+3Jns%lFvj-iLWX&2F8d^zfIOC(1 zlc{W8aMUG5VNNSp&%8#=c5MElN_^zKh5-x8)bpsZ=2Hbmfy{cjQ>(+$w^W=c`(V{VAL3tZfNpZpLkn^Z zDa1{+p*gYB_;Qv-wGl7bfb=IvyGyhUB2Jk_g=g6!m~kX{HyII;_65~v5MfX(S8jHx zRyVrVn>kD~joWftq@U zj$_URmWktj5{-B`n8Ne6E0=&(%FG<$6yMLgNLguFl_YS5^ct89n!bzqmMGJ9%6+Za z@A|+a=FF6?SSPFzxYRYEX!IOqkR$Q&ua9i10BtedE?AG(!C>bv-8bnLb2=!9mua{p*=9DYcYyUj&QVYXG=dgiKsZv*C^qb7V zb?NDCzdbsG7c}3`ll*?g#U0a6XXAEv9glr*e7Zr!EMM@cK(T~93NgNDbdj`8h<0U+ z(AHb^`8NgNJp{yTS7EB zp%>A+G4{xdsW}{oW}Jg*lK%+V7K;Z7ud1y?FEZ`f=af zV!h(H6GV!|;SAiH%6-SB;?`@xM<{iBz%wCyF|kvGI|dKk>jRaQ1PsT@eMT$*qmAMv zOUUvSvJZY@ifT!BE&lE|&}jVv=o#U3gpFj8@q5cV1oP4f-#_Wd_4<7L&(=XJd;FYI2F00SBF&237lk--%q5RYgu()hfvNDUSMOD--aI^*(hG zD6G4mFiSynx?(pCtHR1zsLoKOuMo=49AC_3-j4782j<-+tEs%)qf}YftcN93*S4Rz zf?KnZvp7+F^t#4>ekX|@-Ig6)=6okk4u7Dxl)I(s>HSZGPAW4XKfQ+vyTGQ7Ahlr} zzB>-0He^bdYOFS%sUO)Al?^P8Pm~RUe1ddUzrdOAJ+B!gM`XXH|6SBNzZ*QV{0}jD zG@*Q1?5a-49jqQ(CxC6-E0;fy&$X49*6EeH?@7h7;%Ov*6@@!XHG7XFQ3D zFDP$~@t{C>1yDnxz^Q1^@Z$qc4ZuW{V0b zXstCyzU7L5fm-O^H_XE1Ft(A3=d^sGaM&)`}p$L<};Z0SGv^r+}V+_7X1^pnD=r_OssQwn5!R?(%n~>W@kkwpI1N282&O z3}#ql+L{o(yU0S?95rXewz|@)GbVUlJzv}Hy3(iMh&^{wD^M(@#iFWj?OmS)yd?UBCXyZNvJ)ti>oSxM(uqYoia>EIN7`1fk5;tF|D} zdIYSgffev|^0cd`7oQPksda7+u8mrzqHyT>5z`y6m5Opq*(@#_fs%Rf$|uYZk~d** z+J4-TWIid_mV_IzOIGN+cRb&bz$g66A*qj+Qr9QwQ`RR^CxR{Es0(*mxWW&t4r+JP z3*`2|xxg5HXr*e)17Ar=s%KX$_jf3}kUHyLS>y`|RJk7>B%fJG|Fg) zS68&^uOFe#+|_xThcs9JtUFKWAPfRAfRvxg>G?fj%!fH$&L{~jp7KmpTzI)ow2Nqr zIzPS=Ngp#yoTN_3{BHZ|uV++) z1?g);`l0H>ww6YZ!R!9ERxFx~`af}9THm`p?$>;{&w8|#FE_RR+Bc$tU?mO9+KYHC zOQVz?2vtfqW8K8EpqU;6VEU^*`YtlFw+-ZR0O`$7r6k63kvW`gWw7dB+<8u0-w{{uz z9`Tn^^#MPihC?bM2$1msl4lp?&M`&Hxtj$RvHYwKv-A_fj*nSmSZon}Z#Bs5S9l%G zOG;e2yAb-ZLf>+_zb;sum|tB9Envy9ls5kj)LV_?&2eN7hJHe9FQia&upF}EXRZXU zY7<}CQev~n@&`82pB~q;V~Q5PxnHG!*?iU-MvvI&B|YjE>Vz2FVLESmwh!mL2^}x- zXPX5~PK^cnAggcXUFEDQaML~pySq)DqVj7-H;y&K`fY{?h~AalHlF3MpDR`JWvSro zgcYFjP6j`flwg7KM^oOv0;ZrT-&Sk>0Rp)5ABPlP0K0BXfLhMXhrYO3T2lJYz72MX0uZckLN)~|goYxx-W~WRmaHQqj{UkTl@VZFMztM3cc64e< z&%Zzb;CmK~xa?~#;JNg$W+v-yrl9s#2>Zx{dn8hw^mrTD|K1GCg_gAsBQ)0t^z(Zy zmBl7ie|J(nHTMElYi!%!fIrasfF*Y`)n;(?HCI!Bwj%~xvEjYR%a2z_&z(ieQ6rRqju=GiFuN006HM>} zWgwXf+=l9Ob78ZxnRGp&5hQr$Ay#XM(8-zxi;q(1B6F6{>^W=JPi#jW^4|=WJRR?i ztKlP~DKQCFBDFoEY{2`3Sh^w`jl@IMKO2RGS4=i@xe#@1|1#pl{p~fkx5n1bli;0F ztMCiCjWla^;avqvtxJGX>qpLW{s6{=c2N?G{_giDATEprXvHbNNs5O(;T=lH#K%-j`pJ1Z}<7Js97qY z`LGFEVenGwBjC&cjwJEVG>&v4VORpr%eK~i35^BVANStKcb zMutBB-KM5=qrjkWcAw2^$VeK)lq%rTng9b_IusZfx&IpX|10eVa;dVTM>m>@N=0oi z$y#DnyKKaJJ2NB;*Qy=wF9l6=w~{iE85-WkuK`xGQ<3FskXLJ}vTfB4(G^kQ(KIi? zoxin$U5=z>dK@xSE`nj+f^>BSmC^2j)7^toMK9f8>zQG^2=cDOs)V3$CH4nq_|}xy zV#Cr{*TD=}*H@+EYj0}Nn90f*?8&=qd(`a#P82bqM#NzFSP0@!F&01S;c{-Vn_ew8wU z$^+Wl7|E}VB{fD7YSs1Lc%Jwsp7b8Znk`OSvlNQ(q~efi3A3aJ)(uuB9M8fmIC49) zX8YUjKa0h|&Aqb}0oIl105}8k}B~kqp z>pS)Qxy+ll`RCjlCI|kLzup|uud^`hndD}wxE~$nV{;wjUtBNNOVHk(i~EbvK2e}G z?Z-APV<0dMBv^(mN4I^dOLB|4pXw4I8hcA!f%~sS(ia6tCT?$ebam@q96NQk#MwIC zQ>&X$ZaMNEo{4*h^zSP^q!EWaIw07g9OZ{sBrQ67|A8 zjWvi-8-Ou&nY0$>L;c!DF9~&;?ViZxFUHi4@fa9de`bQa+Fj31FEj>KFAkRoW#X=1 zhC0HATL(}VtxOW%20HqWT2=rQJ{7uU72ip^v`@ZO1gI^5A?W}mO+%!#29ze$fFbDQ zh!Bt*d8PlHF^koaR814-HR-Z=)fAxS8@E@v?z~OQst?mmo$%rCO&+qL(=^4o<)$b& zTTbL8LZiAyVCDg3arAHY^xq2$<7~C42W(5`Jd&F_Ax>|#!RMZ2nkE@lO;S*o0-n3K z<9M7|K#(dF@#V{x)TFjCpzR$H=AlTy(X^MNwFMAC&OMB{r=ga!;(OofJHK(R=^p7_ z!YVP&iwyJZI`ha1XL8!JQ=d66iROy!#Dij#WN+utM{LJ3L{41QRsF|Xq`A8e<&4oQB%i-ckqF!HXKUW1edgb{m(kl8N>=)wt)H2&_n`~x zXe+j(T0Gm(^l4QCZJn|IjJT%p9h$=o{KG+t`25XAlQRND{)fW{!Knq$0U?K>d*BAQ zb?b2pH3!0gw13|ddpb1qf12fd9zq-?uT4; zxrQ$H3}yrHTM-`(5wEKCFx%1bkLoG6^n{IV4#v^ZCOz*j3 zFi;Y#%<{46SHMR;OX_*Rkm&J_azhL%d@A-Nm8U!7zn$OqaWG`ryJ+f}$qf z9qc{C+Fd0CAPg&bK@cMZ-xX^vY4?vf}FJf>x5Ph5rD!L%8ZRV|QlGu`O(BcMn4(h9z zy%CjSS4x+ZnJE(7HMi3ZY32H0wr9ADZ8{<=UDb3}*P(P0ti|siy=t$mBLIqCD*4ktWi@OMpzdf|4sK-S?bq}t$TrUk-D$fDL&1UyRsHdQPLa66le zCvwURa}&G^aR}svsC1Ke@c-Z%N#%jw-8>s+tB2squZ{*H3`&I=E%s*@d}o8=#%IQ! zUK~5iEpjRK$i?bDyrY`lN5Ho6rP^(#1B!M&T=*lJ@ir-ovpnHXdBnj$qNa%H4So_QEUJLFYvPb3uqX4iO{~?c@Y=og0mQR| zbG>hGa{I(yn|cNn6y=T~NC*+m0JU)|PlUOx(GKOq|5O4)fUh$yzNt3I^!<_PZR1rg zoTdH}LZz7Q9t_X+CFL2|>_1*-Ls6yb;XD2XOuD!<&)>GNox)lX$3T^ai| z6u-~A?j=#`MpEvxYw0~RQ3q5_wS9|i7VBLsy8)i>rh{lR^DYORN=Iz#X0t-O_!FP#*WIC52lu)i&fjP?%mnBb0~(@tF_ z#6~uxZS1a!5@-@H5o5%?x{<0hK&qH9tn~6%Qy*OM&tX`#0{!%Nx#Yb`Y@yDR>V1!! z+Qv>sDlV0`O9$PMTy*Ud8l`4)H|{5w7(IBh$)7$vO}Pt6%!l^(ARDQ8+Ws_9!M*y) zepV=h^WzF+DzRoT0=<`S|C~G?+(5qH;w+Y*pcxzZreJNXAeZw{ZXs#BHPw2}72Uq= ze{CK~18}3PRR`LkwYZ6%1sIh^24jb}&6FeKwsXd5AtsK*a&$y6Onh&OrEcTHLju zTy!OCX-@9b87gLwrF(EvpKlOFesz<2Skd)~YpPeBhUcDV=6*GTfz0oA+Q)wcE>_*e z5ozWWlRn=^;GnS6aO_R}lo7+539xLU)DfonGU2!I-M#5B4?*gh{<|`nE)C!F(+>TS z1-Il2&!8haP{aZ3>;UrJCquVHwRo%zHn`#cGS$}j z0v~I|QLE$J42gx}%YWjA^)|^lTD5wjAF@!Ql^oP+Fz!N$TQ^yo1(yARlT}{_{qptI z{iij$Jee|cJAUDfK(me{-klG?hK%0H#?7VCDY)k@ItrWqLD1uOMl1~vGs{#T-e$AW z0hQ+A0js1kA;}7|8OL@CW*H#yT=t*a&h=ll;7%z1twcdlQ?uOz= zo^NWD9xd=;-nd!bljZ^T1By^b`_e2YEVICdJj@^^sS4RXGOr?+K5sb)=hz%U^jO+U zk}CVDyK4LB^@RG?^a-x*#5?eRT)ghB=27D0`tJ2bXhwwf_^nLobOp5iQl>;6+b0>d zp)Vx#{LFtSSK{o4YNlHt2CX}dp*;gV@h=^4m>=s^LyOnxNrK{$T)k*Mni= zwX6?2Y7RH1@W*Q{!)}mKa4(GklOy{-Q&D4 z6u@2Z?$$X8Z^~|Pze^;)zc?0yGYB|>9)RNk^2b5zo$q8n_0B%JSME&HJ;3ic++D-_ z6qdwdEEUhoE#7^Q?inz)$06p_J;Xwba=S=pCNf^X|B!A!(xLrlJ-J91Pw}TQ(U`cl<~B8MyGUh64M#qZ$7(VhbyB3 z*^D3Fym%X#jE%;||C^oS8|r34zWFg5+!;8O-ehKfxsJ{0nV0{tdTtcBB?7gmCw}u7aF%aWWqX*> z^P1u=bhg;PS)iD2!uB+tN7+t>P)@ij%9~=%gxr{e#gJB=FOHAgt0oYiGT(D$e|V+6 z4s*2LrWW)Yvo5E%94~U8Itr~eOBh~!^#IEi-E0g*ieJO{=!C+}{#MDt6uTA? z6}Qw_OP4bq4=Y&*EQs%0vCJY^W_jWxn$z2x+uwQ5tw~k!z9+}1a&8c-s#kwWHlrY` z7J7yJh`iM-S`2i9&)?$OvPJYWxwqs|;MNj6G0XL&gG{A^unww2P|Y#{&w(`TVa95b<&S5xy*Cu>L)v3U4S>jI<< zpfKQn2uCnQ^*K)Ct^b93;>68BHed97-n_V$##eRD!~RQsu6k^?2O{KBeOb~?fVPMvz0+TEaugEEL0dDgA=x8;4H z=y2~rOi}q=wZ^@*X6g^mj>pIpR@7ht6OofAjmX85PLB>wvppR1f3Q9Ob zomp7E8pWe9@itwa4NHx^L`z9U9-yck%_V3L%5-$lfeGLeWs-tiaNewof0)0$;2J~3 zbxPUXLx?I!dK%w+uCG&dze%J1Nh~7=PlR#aeV6`{cdxVmC}rWQ(kek1g|Z>+_Hy}# z;>@CkR+nwdiru&QE#eWRW0p%Uj7WI#WU3l){#jKAXd*)3I z7m0ie5X3@WWa+`*4c*EX&4(ehC2-J1p1`zupuJby{qci(TJ*V5GJ7a_5d`qIV9h-u z_fRM^_rc2T7cxAlx8erIeMR$Xu#J#;Ogq6VuRlb_7_j~NEkHH$hI&i;;SLYOI$xU{ zOMYkCFeR#Xal0cWBvj^}mwCMA4X}aBh*IGnTeoV^$uo^5 zVtxyg{&ChccXrqwyYI?DXzrU7Jmos=7*C)1;O)?~+6$9AS+>nwptqeB$5&B{6xR|B zZT`!@qi#%ZX>aBww-TSC3yRRAUVxH`V>-nvU?Up2Krd<-UpGA5)Su7B+cImQU!`fH z$1HYS9*PUr-rH<0FfS>uGGOzV3DO_6!y>lj__R&M`to(xqa9?>o39iaB+w7JG*41E zf0T^suquB;m8hoAhpPszT0UA_lqpzj$_YZgmnD8j(md#9d*&GR9MuD>oTA;VX~y`x zM16h?jJc_cj7Jr_@=GRV)Nd&YW4R1z!|7dA1djqU-(jTrQ3p-fz1sWDd(`6Fk@SJ* z^2R3E@*OZHHU2{SakW1Oj=;NK{`ZWNJUB-1d*0_ zsG$S|k#6Y_3#7ZHk-z(~-#%IAhvBTV&YHbv?|a{I zUDtj3xq^jHgm^9yGfFH#98v~5ij*?udOc?24#p4ii)W zXPAHdycwlZAN)eSs^mVD?Yl2mzHj7(TOmx`TwPuaDM*uz9ka;#e{Xh2 zu>raG3w!q(!cFI6@!ZAK<^wAx|FP&m4P6eWbYzL^nwG73^0mRz!;*U z0P8>~%R+PQP%C|v3k_^zR%n0s6k7+^kl{i_@iOr z6U}OWVUIhL8Z^pAG9f2hZk3`;;#@k8P3@_btOFFzwuFLvib>H|ZRuG}aGarpC@>*leb=>54(D zM{6j@Uv-0UN8#g#n6u3*sKWN@giQ|@_S%z!a zo@i@Fg&)&4RHK(@O?0?5`;_~uFy6aXlk$hk;=!oA%cD^INk5)D$bIvRfAiZ8VxXg*+im@ zQ77xLhx?}>$*hRbPq7W>+#0``C=hf+P?nqp&y z%FKM3@l`fmt$82(2w6Dk^Q~CT>7m}X8io11`UKC6ZoZV&<1h$_G$d$&RgHf@xq+NM z9Klu$iOD43)9$&E#rUc^*|xgjH|*FuM;ed{K|i1Nd+?iLt|L`8kk@`k=i6`NrMwU5 z)>>FS%&Cw2&_bBVA$qavG;+>j*2tKmfs+a`Yp`kX*s59Zyn)3vcX=GbfhLH9Zr=Z!hVBvFL5 zz$x}i6~|^)c<*kD2^twY-;oavakhPx`q02Hfv`E_MQWdXdlX@dic=&wPtY&1_JvtB zpHs5%P*F4JQ_?Ynkmd&!bcaw(+I2{ffm-HZQfhV?r+p>!j4|vizTpQ=^`djoDK4^Ha)uj~?HT7R?Lp7qE!+S_(R0tR$+gYt`D$#O^VLu# zH!gmXlPPiQPLVI8eraeX|MUdwsOF#7TV<0p#VbB6@q@; zG&9Oa;A&dq8-%YmdS#)SzT$vsb&%8SNy@PY86o~-9_z`!?okAoiQqpm~HGW zq6`9B#6a}9{+*u#5dUZ*d5fd(lV`prIgts8v6XkCkkw#qv@OQg>8RW1%&T=o zE?PlUc-L#5yg@`XTp|o|*W=RHkjEvzQ@mhe8ey%RfmhoCGip}_8z$C9__y*L6RNv< zK^iv>MowiYxu*~Lm1X$X^1z#RTl+_I7ss0me1MiCu*hX(Fww>74U7>kxf8Ty;=ap! zUVFKl%{AZ=v<_(K(78ycf109jorwGOpiq$jsSSHw?VI{FiJ#WXV(_2v{G zOs8<~d$DV=WGvRdSb+0&t`gqGOnu$w^YZ7zn!JLGPQYFuK);@yo>($=#uOvJUY$Ti7S*)kk zoVQd($Q!KJ-u~qB)*sa&BYhAzbXb~6m}ssZ(X{SjmF{M|W|+7ZJka6$Ol=2Jb#@N~ zYi7aIyQnlQJ+X2hvccPU1N&UafSD(|dq9?5xa8?~dcbIeGruU)^@N!eGf0FOcI6MT z{(5^AkxA>tHZd%ZlW4lD3EPEC_TVk@Nw#?!+%7zkU-#?w?i1dR+Iv>JRN;iWA4GrS zWrUc0+{kJwQf`SD1Pg#UiJqA?&0kR_=U1zAD4-x4P>Yu!$dOWTuG2AB-CnFfjk!{& z(BULbj@?8)f)mN#Sd9alpx#I=7}I-IjXWQJb5(ce!-~&wQ>nNJSS_M;jEZI!$BOkw zWSZ2G5-R=2aA_|`w1ZC@d0>5iLQ>|*8*kIM`Jata*`A!YFcAvh$ri<<) z^r35U*1JiqxU zLx$ESrJA(iVzp|^TSNwP9Nl|v8$-~7&&x79x2Gp^pf$r3!j(};29Nh(Jh>{Z3aBPk7!Ki~ z0e@_RAK#i}zlQZLboXTU5iJP-8`fyFP>KbERC-SN?Y3O@0v&>d#+9Z@9kDE%ax$h)F;B?DI$6jRCS>m>7>Vs6{ysmEhfS@_Nv; zynAahAR}bx$+r(wxvE*ZKax6wwDr|~K}1=eSzAy*VhA+SJ|YaIthIgR877dtS^;3N zYTn0^q*P^GGP@@&NiS6x0HQuYx1I4r+H;Wf(-^Acj@T13d1b6}l^uM_rg6st0h9EL zQo2pIEmsT2pZ8At!kv2&XZNdg_L|jaPd`%5MIrn@2Wwb)o;kwcu9I;hquNrF{sC?o z!p-8+zex^PzCTpiEUu9=&0n-PFbo+BS6n%ID`<`<|HCW9BX7~xGF5b+EVI{^z$;0a zXY>Ai+qz%W9+g_!Fy&OvAO%a(#IND45!*>u5B+y$-(Jr7ZF)P{+kdkCc=Sz|>l?4i z)5?G!r^cv{JMTW<$p(0N*vTmA)T5#$ zGgKL^BgTP4iKa_Y#uxsf4Ko_rdh@e8Ky%f5^3m3;XorF+T^%YVH-ayyZ|bD5v1bxm zTlD56S*>vbry^|{r)tM%(|TzHF8@8}?oDslF`IH){YkRdFWv7CNTjS&Dr~V$;+VBe zTQ!Y4j`B~-8_{{NW1`)$s+TM!rwcsW&Tckf@1vsKMb9g|1rWG*&P*m7I@6_Rq_)kcn^xeNT9)}N1HwR>!&PEyqJBkUKSz9I}2 zY6(XgZOG+S;TA-Pn#IZ959Z?4x2z}_-COLrHycOF5F;yw`qV*&=MdYqP(ZGRsMF1r zY&Gvr^&{V@AX$T~NxHQFyh{8gylwPMeIoPu0}k+Ez3e$P)Tp6 zOr0wC>987NS?_1*?~5?y8KEFf8c79i zXKDH9wSaKnS}%S#G8xO`Y0B~Jg-hWqkYyvjt3K0=P2qSzK#jOjS_k1lsn3t~PGF&yd(ZpWm() zi!#H<=H=OF<0!6YB;}K^(|mb zicAHU_7FZ)3Mm)u2pLl>rR0b1ywVGdO|VW0hn9ie@QmOuDHdXvDw1} zF6lhgjL^@JHGQ=sdB62@1D3a#^ggbLmmfWOOhN)aaSgrg(Xq7XFP>~vmaJBgsUUUL z7T_B%r;gbd01a6UnCt2t?6~Wh=TKSHT~l6LOV>px-t2mr5NeJc3fY+*sK!!%hov}5 zA?>sfXA&T8`5Zk%TR z0{m~^pMw|*>GhmrXck+?gm11XN*uDx8qPS3fNHF%^u1d}I#`N_>yKn`6DZ$voj&(> zCuDc#?sH~ea$ZrJ+d?RnmOLYTKg%vvbQLzmt4?Ts5yW?JPf0V zc+g^%E}tY4k6Ri1=)v=GYuQr{O>*9@g~FW4%isyogA8!3=X~nHu=?3sf3BLFF>n(B z!Z>QmU%M9g=jSk$0RMe96PbP>Jiu&QA%bqTe%+jeLIC8C$(d;qB>m`_f1Erl{`&nc zLJlvmM162!LAG1Kb;k8G5gTdMowdecAuD#pk^7s+nw_9&o9~Z=2SSnL^G?4LyFs=w zl=liAR_%uw3Pnk<_+(Eb%~_SqiZvDIa=cn%pDZ%>if z6LQx;`%{*nEBHolDJrGYB5ko3UK>SK$4bx9+-D#lPnet%c~Od|(mJ)q$FdijG?ePa zC#I|TDB>_WlA>zl>Y*do;Z?;Pzk1InPMPdky1EeyBQ#X^Xk|7RTHk;9a)Fnmln6hfjVG^vj-14s?voQuJZAZ<3A!GuD>DYD_ zpB1?*ColH&OQ4g};#F4rNbn6}ur~*`_FIqS{4gwAir%BjT&X=_7R3k!WQZbRQzNgl zT%uaxBzF&A*2I*QpDHmbzv4B;2V{~O=swSKh^g_1(Ex+I&;dOVV_GH4ut)MB5x6bs z(qh51?3{D!N^r*ZgY#*5S|s>;q_>YeNL-a2at>Q#&q6xONY4<(O`H}tHR%Dh;|$pV z)4Jqx;hO8LwhVIwkhpRVak|#<77h!Xm=U$JDr;^!ua~VmomDUJvsIZemdjM9ebC-b z_|5zM^s@yWx%!Z&dgTqcK=Nv6N-A}c!TKesr$|GHfw6Sr9&MsX=Cm9_0H;KyFaar_ zX5NvH6>4rP-~n=AGuD?bM*EXXIonWGe*L6{<^04slp1WDox!9@`nl)U^rXtGALnZ> zoALOGcl^WrneHX`wva5pUA(4q1-7SP@HQi2wrdzj6zRvlm2W zSG-{eVul|-((X@iRXR(qGy1tDRN&t5yJz4i=5FrEQ8 zGOHD10i95|p+NKACV^HoHy~!@kVV=l7el@(L7ottX%XYn{Jk3|&6C!QB@{1noZ5@4 zosA|ybxBO!>8E8jE?9d}TrJxf$0Ba8^;3LsvDy&9ML`coMEKA9T*h(#JWVrKfbTXc zvLfzPrj?aHK6lnC{|5EnW8Qz<1$AR2>ZOrVy|@Suwzk(owEt{-!D*rqxu6jBG+Ff}HiD)0bj@(?~I-3CKky0v_tBYuC?{!{c~lu$iO zrn|Kt@Q9?>x4zNGPHOV5hJB@66B(NxdgDLG^@cvXFy108h)NJux+(&%W&TV)8qGen$ceo};8-GVl#t@U5FM zqut8P%A&^WbF7_ZR6mB>+*4y$8%hpj@6CjC<$5lN8c9NGJ18q+*Bf4oIE>64Vc9#Ieb3XAjn1?kFeXckuq7yyzqQ#!x<1&S z>M6DXgz{GQ#mCkW*0a~A*u1Z2bS;rdCWJz%W?^(+`x@)9+{+NW+{3yQ7YfnO2os#$ zYvb>76!zAR4L{y2s$Zq!NNYN%-j6@;x+}q7R?4>)JxAhVvIEj4WHY4mL;v!?{#5+Y zstCar%3O&XWvv9+SwVOM{h4XU4zEl@+xCS~BhEtqkuMF(!+=Qz#f<*gZBj)*I6&fU zpGHgbyqzyM;-=hE`g=~Y+)n4$%MpN+`cwBF>IWsZp=6CJP>3#tJ|J~^JD)QV!M3qk-%E9_qAN* zp+={Uem$iqyYz&X9B>|%klDF^UTOREqo+3}QmJJ%1MYoGU}mo1@#fDk&LmjVW?jP% zrr$(gQP$B?vWC}77Jxzm6l*Mz>m*!Yy8-DhanGfy%#9k_vFv|F#b4mDJqskr@_t}I z9IvIPO&U=}>qH3A>`bGQ%Tsm5gFPq=UF3l0`TDik=NS!t54K1n{jEi?QDwiTm>${0 zWJiTt7Vl2hJ96l}2AyMaGn0w+i@A!d>a8%~(MPe2+scE2R6QU*K@%0|t0c(wxnk&h zqzgF;W@AJUB)!Ae{jWL5!-OrA!bX4i_I-6e!1wcj? zEt_h{kn;H8TraywNwP}0tlv>KWS`JWg9P{P7waoYCTn6mMpPg z`$7}ZpqiXA&+2eHY|1_BXlb6ciF0ET{gr>C*?M{%S8vpS*vTwx|E)J*#j+I%4H|mb zuU72XeWLi+UVF&Qurk7<7+Zmruo$;6TA@|q>;I6pnezxPEjoaY z?}aJL0+$RukZN2eSo-%kw;@Xcw~`R6tsNrq^@`T0f8N<5+4N|7MRw8I&3Y8ARI4a^ zv+sUPl%E*qAkOFqJ+nDU!1|MRefKb@^}c<#j)pJ2Be ziOOs9#rT&y2(k1##21M?;XxVz?BfsBJ+S>7spz1YKi6G(mOoJzAUwyM2%_569$e3O zHW=7fmoh?Z8Ievh5#}C9Fui=mZ?&2IaNKH3(`wa9$gX(Y&fX3ERSlvvdP zAuqpsM^i*U?t%PIqP9(4mRD?d$DgzitO@JkC%%_Yn)=4}-D~p-eqpI#8saWY#j<2d zM|37W%>#7Od?nvwGD4 zyu`>yH)rMy;6KZkVY_l8?OI1VlYdKm#cj2j!y;?V(LJ(XzhpfNm9g1zFiT0Y&GhFz zN>Rp6HD?*Mgt3Q+$6-_4IE{)O!jYZL)vK;OkXIS3@^*k|9FX1-CMG`A{Z!rQWFYAz zHCtm!ylTY1@Fo&H*>RB%Rz+oKbg+9qo3YyrdvCtZ07$g)=31fz^Bfr_T0-sqctn6~ z1*Ran)vw9?Cti9)@u}u>LDi_WK(dczSP#ch$dRbdWlqp@dc?idNQIW{nenD73sinnOZmC60u~%$ckEd@r}E@++gU^WzKxK4 zzenYxLy`BMsdyg-A_G#Du^aG1BIC+SWrhXgJgPdGGB)5u@A(sm-WLzVp-XFoV2Xul zspdPA^**~vj_#J%K|inx6fqI$_p_yIfE*wy%l3kuJLvo$uBJ$u+czVdU!j-hZLrgB z+zQ=uw2e=rKm>6fzMZg3p9p>N1{JGVYBnTl&xI>b-~pmrR^85tqfGORD-JeD+!Mj? z&fMZqRd`=~)%j$r8Psk2;9V(+ofjY;npu#3Uke{;cNLrLhT=U&bwD?NjU=c~km`23SDIW*_f5d z>xSmrks~cd#_Fz!Ms;^uXZuI*eJV{zUlBc~eD!5~8D>61_ES22bS{Q})p6KLK=0VQ zg_|zCS9)TJZVwk@u7Imu7$X-#=fhx+;V=QvzYr|$n-2eQQtlWH!GCWeyq7 z?|Pz}Cu#jZ-0_Q!h@z9ra9wA-9E-Sh*h;|yn4ZI%Fx}mk8wDsyO1VNI`16%&*MEy6=MQ^I+qI1sSiB? z)sdeEV9J}U!%e{#n+I-AGGHvJ$4MDK6^Q}Ajd}m}**)SD5AGW|J&ZJpx!TDlkf>d& zAIWsu$-_eLOb}zU+b8$~c4%*9e;HE<<$uL#Wflnl=#40jgwY>kkAo)c%cf*r*=T2A++ zkNkX34|hkeV3KIl)4sbzP4G(Z_7&CXxJ3QN{HP~bVv9NJ)8Tpv8B<6-SNST5EgFdvceL=0c;_;aVFz8V= z8zrnEMGLRIeEn+ep#r=^Ltg0tGwKr)T_a09jlWHJ5dQPL6OHK+nRWJ8Hii(6!{z`; z`k01;#xBgHU3$B2TQG9EJ|ln4Dy7cd;AG{?Inp~Vy-h4#ovF72b$*Dv@He_bJ3}J^ zB6g*}^z}J^xZ2$Zl+bti@edMJoskPOs9w`gYB;zAq|AFvI5>gQWY?d^$GfWIr@??(7*2mUpYf6d_kCo}lh^!+t`e@)+C)A!e_ k{u@;Mjj8_cL&|pQ5Y3DsF5ZkREZ|Q`L0!H~&Mf$U06qDuk^lez literal 0 HcmV?d00001 diff --git a/example/data/dummy_document.jpg b/example/data/dummy_document.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f6badb5bd6cffa995da025c6f00376f1e04a3d92 GIT binary patch literal 12688 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 literal 0 HcmV?d00001 diff --git a/example/data/product1.jpg b/example/data/product1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..45ecc8c8a4dcde6f7f90884b06e351b7e6815efb GIT binary patch literal 114423 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∾Z4&K0mZ}QBiT?+ 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 EA3gR4D*ylh literal 0 HcmV?d00001 diff --git a/example/data/product2.jpg b/example/data/product2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..184bb83cbe3090fa65b7dc8e0b0d437877612735 GIT binary patch literal 92622 zcmeGF1yq#V_dkx$&>a%e-7+vkgCN~VxAZWSbc3`K(yf3X9R`R}DvC&#G!hDe2q-8b zNXP#f1h3vJ_ulvC^L>BTZ>_(x)-X?;ea=4n?0sJQ?C0!>8>lcuM8FCP5rrW{tgT`E2slC%XbV&Rs5;JX)meJ{rBNPB4-bU9 zr@f1_2S9*9ctjw5NMPmS{v!bil1m;tOAk99D;Hk`k^n>j=ku^bphgM?KUY&OV#$!ar{KWMT=8p+IkNX7982-R(otYH=kHvDT@Vp(`9_}l%OKi zI*~A3tiO_`zATzG(qfILi0_S*UK1`Roo-XdvHnT!cjw|JUS{(^$DnWc8YNq z*&BA^PcrPp=u3X4Qz&$DR3{P=hFmI7-mk#+$IWqw(I58(2hC_Jez_s1xdN%P#xaRI zk#Gv_^1x!678!K!;1quF#KTl&912%S>IZG!Mb@W^Us9Y%=!PNgUYY+q27L?rp5r$E zI5#$N*WCl$F_tAZjA{Zb`x6OuZL&U=e3Y=nZc49f zL_(LmC+CMeWrm|LUomg?nqXXPCqt3H`I_Zyqr{mO}i^$)3U#S}ft*vP+IJ$)m=PyMN~ZxEZ;VxioF{alHG zz!M2AgY*u%@Z`((plQhvQu%^%)b(do`t)Rer!IP^KU@ELBH;)+8(jy_#HIMQcS>6a zFHlD&rf2EfaXHJbgYTr+ZW@TZ=+e$DJE z&54B4Sf5=c%}9c|=M^vnM@dW2vFZ?a7D?WXX-!tPOopGVtDE?>n#rk|qd{63D_OIV z1Zasb;>I&-J8l@&dW+%2{n?P^)#Z+6xzHTJUv~-czNO8Joc`ha2^+TTV7{`j|bev z8sWivRNmvTyeCke*VEn7-Wg#n?P%-bZtrR5WDh@&u(r3f`PRDQA%wx*5tg0^YbOsE z8&4lgcZ9Spz>k--zn43LLBkc{EUnC->||++P((O`KqziULSQ)*0w3J*M@&3KZXUR! zrM(k?hA8sLu9o}&d-m3FF&h_mCreLzdt{ZouFkgh9`?4*mYzUszG@x_M;m5j12z&! z)_DITi03`*&G*_I?43P4EuG;AWjQ#E2kLUjl{wHiguA7ui@Pt)d3(6K3$oMZ+6WI< z0P^jv91-Ty%Dg~n&p(wmw|BO23C*`gqO~vQcS?pIC>f6B#GvW!;tk+B(6p^L!rcS# zCXfs`{NY=K75pF*xR<-5uj&7Tp8rw*H%a{AnjGml1z0^R+=xIyMjO?waZ;=niW1|w zcnLH)I&kJ~OyecAzi?&zU*PIU?~hoD_DKp^g8Cye7r;eO$@eIIi`>CQ|cm2TjQTpn12CEwOp5N-#Uz-1#0PG}egD>MuZ zKNl4RjDkYK4-~#~3SGF;cp@YhI0nw3_!!t+5lRso_+T&|ItZ!{!-ZmFpz5Jvkf7-4 z!04bf$P*kAvOg3+*2Ue`1yCqpkYVglR^)wD61qR`>&VJ6$Rh2*(%I94L0d19fTSNP5=Sdqfx@Zp`h6e!SD(U=?0FMIA}2Xn?g6v zE0%NaJ=XaIMoYT=a3B9}y&=wKxx&QLf~&-y)15w7pT%8zq74p|zGs}B{SZodhLmw6 z$F$i{B~nghD~VLZor|SbE^bz`T=X2LZf0XdDb2k3&@F{)pRQo1=wjcw`f8~`U∨ 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

m1{qDS&rI&{N0> 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 IKmYgIKYrgv1^@s6 literal 0 HcmV?d00001 diff --git a/example/data/product3.jpg b/example/data/product3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9ff957c7ec339aa852bd29b82f168f2e55390efe GIT binary patch literal 121068 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 gIY9&e&K_z4{`vpcoA}?4hbj4g(Eq>Qqkew + sh -c ' + echo "Waiting for template-repository to be ready..."; + sleep 10; + curl -X POST http://template-repository:8081/shells \ + -H "accept: application/json" \ + -H "Content-Type: application/json" \ + -d "{\"id\":\"https://mm-software.com/aas/aasTemplate\",\"assetInformation\":{\"assetKind\":\"Instance\"},\"submodels\":[{\"type\":\"ModelReference\",\"keys\":[{\"type\":\"Submodel\",\"value\":\"Nameplate\"}]},{\"type\":\"ModelReference\",\"keys\":[{\"type\":\"Submodel\",\"value\":\"ContactInformation\"}]},{\"type\":\"ModelReference\",\"keys\":[{\"type\":\"Submodel\",\"value\":\"TechnicalData\"}]},{\"type\":\"ModelReference\",\"keys\":[{\"type\":\"Submodel\",\"value\":\"CarbonFootprint\"}]},{\"type\":\"ModelReference\",\"keys\":[{\"type\":\"Submodel\",\"value\":\"HandoverDocumentation\"}]}],\"modelType\":\"AssetAdministrationShell\"}"; + echo "Shell creation request sent."; + ' + networks: + - twinengine-network + + aas-web-ui: + image: eclipsebasyx/aas-gui:SNAPSHOT + container_name: aas-ui + volumes: + - ./logo:/usr/src/app/dist/Logo + environment: + AAS_REGISTRY_PATH: http://localhost:8080/shell-descriptors + SUBMODEL_REGISTRY_PATH: http://localhost:8080/submodel-descriptors + AAS_REPO_PATH: http://localhost:8080/shells + SUBMODEL_REPO_PATH: http://localhost:8080/submodels + CD_REPO_PATH: http://localhost:8080/concept-descriptions + BASE_PATH: "/aas-ui" + LOGO_PATH: "MM_Logo.svg" + PRIMARY_DARK_COLOR: "#00F2E5" + PRIMARY_LIGHT_COLOR: "#041b2b" + restart: always + depends_on: + template-repository: + condition: service_healthy + networks: + - twinengine-network + + mongo: + image: mongo:6.0 + container_name: mongo + environment: + MONGO_INITDB_ROOT_USERNAME: mongoAdmin + MONGO_INITDB_ROOT_PASSWORD: mongoPassword + networks: + - twinengine-network + + postgres: + image: postgres:16-alpine + container_name: postgres + environment: + POSTGRES_DB: twinengine + POSTGRES_USER: postgres + POSTGRES_PASSWORD: admin + volumes: + - ./postgres:/docker-entrypoint-initdb.d + restart: always + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d twinengine"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - twinengine-network + + pgadmin: + image: dpage/pgadmin4:snapshot + container_name: pgadmin + ports: + - "8081:80" + environment: + PGADMIN_DEFAULT_EMAIL: admin@example.com + PGADMIN_DEFAULT_PASSWORD: admin + depends_on: + postgres: + condition: service_healthy + restart: always + networks: + - twinengine-network diff --git a/example/logo/MM_Logo.svg b/example/logo/MM_Logo.svg new file mode 100644 index 0000000..35fe73f --- /dev/null +++ b/example/logo/MM_Logo.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/example/nginx/default.conf.template b/example/nginx/default.conf.template new file mode 100644 index 0000000..d3fadf5 --- /dev/null +++ b/example/nginx/default.conf.template @@ -0,0 +1,133 @@ +server { + listen 80; + server_name _; + + # Serve static files + location /fileprovider/ { + # Allow only GET and OPTIONS requests, deny all other methods + limit_except GET OPTIONS { + deny all; + } + root /usr/share/nginx/html; + } + + location /aas-ui/ { + proxy_pass http://aas-web-ui:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Authorization $http_authorization; + proxy_set_header Accept $http_accept; + proxy_set_header Content-Type $http_content_type; + } + + + location /shell-descriptors { + limit_except GET OPTIONS { + deny all; + } + proxy_pass http://twinengine-dataengine:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /shells { + # Allow only GET and OPTIONS requests, deny all other methods + limit_except GET OPTIONS { + deny all; + } + proxy_pass http://twinengine-dataengine:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /submodels { + # Allow only GET and OPTIONS requests, deny all other methods + limit_except GET OPTIONS { + deny all; + } + proxy_pass http://twinengine-dataengine:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /concept-descriptions/ { + # Allow only GET and OPTIONS requests, deny all other methods + limit_except GET OPTIONS { + deny all; + } + proxy_pass http://template-repository:8081; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /concept-descriptions { + # Allow only GET and OPTIONS requests, deny all other methods + limit_except GET OPTIONS { + deny all; + } + proxy_pass http://template-repository:8081; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /submodel-descriptors/ { + # Allow only GET and OPTIONS requests, deny all other methods + limit_except GET OPTIONS { + deny all; + } + proxy_pass http://twinengine-dataengine:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + } + + location /submodel-descriptors { + # Allow only GET and OPTIONS requests, deny all other methods + limit_except GET OPTIONS { + deny all; + } + proxy_pass http://twinengine-dataengine:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /serialization { + # Allow only GET and OPTIONS requests, deny all other methods + limit_except GET OPTIONS { + deny all; + } + proxy_pass http://twinengine-dataengine:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /description { + # Allow only GET and OPTIONS requests, deny all other methods + limit_except GET OPTIONS { + deny all; + } + proxy_pass http://sm-template-registry:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} \ No newline at end of file diff --git a/example/postgres/01_core_asset_tables.sql.inc b/example/postgres/01_core_asset_tables.sql.inc new file mode 100644 index 0000000..6b983ca --- /dev/null +++ b/example/postgres/01_core_asset_tables.sql.inc @@ -0,0 +1,118 @@ +-- ============================================================ +-- Core Asset Tables +-- ============================================================ + +CREATE TABLE "Asset" ( + "Id" INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + "ProductId" VARCHAR(50), + "IdShort" VARCHAR(100), + "GlobalAssetId" TEXT, + "AasId" TEXT, + "ThumbnailContentType" VARCHAR(50), + "ThumbnailPath" TEXT, + "UriOfTheProduct" TEXT, + "ManufacturerProductType" TEXT, + "OrderCodeOfManufacturer" TEXT, + "ProductArticleNumberOfManufacturer" TEXT, + "SerialNumber" TEXT, + "YearOfConstruction" INT, + "DateOfManufacture" DATE, + "HardwareVersion" TEXT, + "FirmwareVersion" TEXT, + "SoftwareVersion" TEXT, + "CountryOfOrigin" TEXT, + "UniqueFacilityIdentifier" TEXT, + "ManufacturerName" TEXT, + "ManufacturerProductDesignation_en" TEXT, + "ManufacturerProductDesignation_de" TEXT, + "ManufacturerProductRoot_en" TEXT, + "ManufacturerProductRoot_de" TEXT, + "ManufacturerProductFamily_en" TEXT, + "ManufacturerProductFamily_de" TEXT, + "CompanyLogo" TEXT, + "ManufacturerArticleNumber" TEXT, + "ManufacturerOrderCode" TEXT, + "ProductImage" TEXT, + "ManufacturerLogo" TEXT, + "TextStatement_en" TEXT, + "TextStatement_de" TEXT, + "ValidDate" DATE, + "PcfCalculationMethod" TEXT, + "LifeCyclePhase" TEXT, + "PcfCO2eq" NUMERIC, + "ReferenceImpactUnitForCalculation" TEXT, + "QuantityOfMeasureForCalculation" NUMERIC, + "PublicationDate" TIMESTAMP, + "ExpirationDate" TIMESTAMP, + "ExplanatoryStatement" TEXT +); + +CREATE TABLE "SpecificAssetIds" ( + "Id" INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + "AssetId" INT NOT NULL REFERENCES "Asset"("Id") ON DELETE CASCADE, + "Name" TEXT, + "Value" TEXT +); + +INSERT INTO "Asset" ( + "ProductId","IdShort","GlobalAssetId","AasId","ThumbnailContentType","ThumbnailPath", + "UriOfTheProduct","ManufacturerProductType","OrderCodeOfManufacturer","ProductArticleNumberOfManufacturer", + "SerialNumber","YearOfConstruction","DateOfManufacture","HardwareVersion","FirmwareVersion","SoftwareVersion", + "CountryOfOrigin","UniqueFacilityIdentifier","ManufacturerName","ManufacturerProductDesignation_en", + "ManufacturerProductDesignation_de","ManufacturerProductRoot_en","ManufacturerProductRoot_de", + "ManufacturerProductFamily_en","ManufacturerProductFamily_de","CompanyLogo","ManufacturerArticleNumber", + "ManufacturerOrderCode","ProductImage","ManufacturerLogo","TextStatement_en","TextStatement_de", + "ValidDate","PcfCalculationMethod","LifeCyclePhase","PcfCO2eq","ReferenceImpactUnitForCalculation", + "QuantityOfMeasureForCalculation","PublicationDate","ExpirationDate","ExplanatoryStatement" +) VALUES +('000-001','Product1','https://mm-software.com/ids/assets/000-001','https://mm-software.com/ids/aas/000-001','image/jpeg', + 'https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/product1.jpg', + 'https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/product1.jpg', + 'FM-ABC-1234','FMABC1234','FM11-ABC22-123456','9804820',2022,'2022-01-01', + '1.0.0','1.0.0','1.0.0','DE','987654321','M&M Germany', + 'FM-ABC-1234','ABC-123','Camera','Kamera','Electronics','Elektronik', + 'https://mmsoftwaregmbh.sharepoint.com/_api/siteiconmanager/getsitelogo?type=%271%27&hash=638518734598723853', + '123456','EEA-EX-200-S/47-Q3', + 'https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/product1.jpg', + 'https://mmsoftwaregmbh.sharepoint.com/_api/siteiconmanager/getsitelogo?type=%271%27&hash=638518734598723853', + 'Restricted use','Eingeschränkte Nutzung','2035-05-05', + 'ISO 14067','C4 - landfill',17.2,'ml',5, + '2025-12-24T14:30:00Z','2035-12-24T14:30:00Z', + 'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf'), +('000-002','Product2','https://mm-software.com/ids/assets/000-002','https://mm-software.com/ids/aas/000-002','image/jpeg', + 'https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/product2.jpg', + 'https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/product2.jpg', + 'FM-ABC-1235','FMABC1238','FM11-ABC22-123458','9804821',2023,'2023-01-01', + '1.0.1','1.0.1','1.0.1','IN','123567890','M&M India', + 'FM-ABC-123','ABC-123','Camera','Kamera','Electronics','Elektronik', + 'https://mmsoftwaregmbh.sharepoint.com/_api/siteiconmanager/getsitelogo?type=%271%27&hash=638518734598723853', + '123456','EEA-EX-200-S/47-Q4', + 'https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/product2.jpg', + 'https://mmsoftwaregmbh.sharepoint.com/_api/siteiconmanager/getsitelogo?type=%271%27&hash=638518734598723853', + 'Restricted use','Eingeschränkte Nutzung','2035-06-05', + 'EN 15804','A5 - Installation',6.2,'cbm',2.2999999999999998, + '2026-01-15T09:15:00+05:30','2036-01-15T09:15:00+05:31', + 'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf'), +('001-001','Product3','https://mm-software.com/ids/assets/001-001','https://mm-software.com/ids/aas/001-001','image/jpeg', + 'https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/product3.jpg', + 'https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/product3.jpg', + 'TM-ABC-1680','TMABC1680','TM11-ABC22-123456','9804760',2024,'2024-01-01', + '2.0.0','1.0.0','1.0.2','CN','874512451','M&M China', + 'TM-ABC-1234','ABC-1234','perfume','Parfüm','Cosmetics','Kosmetika', + 'https://mmsoftwaregmbh.sharepoint.com/_api/siteiconmanager/getsitelogo?type=%271%27&hash=638518734598723853', + '123456','EEA-EX-200-S/47-Q5', + 'https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/product3.jpg', + 'https://mmsoftwaregmbh.sharepoint.com/_api/siteiconmanager/getsitelogo?type=%271%27&hash=638518734598723853', + 'Restricted use','Eingeschränkte Nutzung','2035-07-05', + 'PACT v2.0.0','C3 - recycling, waste treatment',2.2999999999999998,'piece',7.8, + '2024-07-01T18:45:00-04:00','2034-07-01T18:45:00-04:01', + 'https://docs.google.com/viewer?url=https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/dummy_document.pdf'); + +-- ============================================================ +-- Specific Asset IDs +-- ============================================================ + +INSERT INTO "SpecificAssetIds" ("AssetId","Name","Value") VALUES +(1,'product1.0','M&M - 001'), +(1,'product1.1','M&M - 002'), +(2,'product2','M&M - 003'); diff --git a/example/postgres/02_nameplate_carbonfootprint_technicaldata.sql.inc b/example/postgres/02_nameplate_carbonfootprint_technicaldata.sql.inc new file mode 100644 index 0000000..a0806d5 --- /dev/null +++ b/example/postgres/02_nameplate_carbonfootprint_technicaldata.sql.inc @@ -0,0 +1,116 @@ +-- ============================================================ +-- Core Data Insertion +-- ============================================================ +-- This file contains CREATE and INSERT statements for core business entities for NamePlate Submodel: +-- - Markings +-- - Asset-Marking relationships +-- - Product Classification Items +-- - Carbon Footprint data +-- -Asset-ProductClassificationItem relationships +-- ============================================================ + +CREATE TABLE "Marking" ( + "Id" INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + "Index" INT, + "MarkingName" TEXT, + "DesignationOfCertificateOrApproval" TEXT, + "IssueDate" DATE, + "ExpiryDate" DATE, + "MarkingAdditionalText" TEXT, + "MarkingFile" TEXT +); + +CREATE TABLE "AssetMarking" ( + "Id" INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + "AssetId" INT NOT NULL REFERENCES "Asset"("Id") ON DELETE CASCADE, + "MarkingId" INT NOT NULL REFERENCES "Marking"("Id") ON DELETE CASCADE +); + +-- ============================================================ +-- Product Classification Tables +-- ============================================================ + +CREATE TABLE "ProductClassificationItem" ( + "Id" INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + "Index" INT, + "ProductClassificationSystem" TEXT, + "ClassificationSystemVersion" TEXT, + "ProductClassId" TEXT +); + +CREATE TABLE "AssetProductClassificationItem" ( + "Id" INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + "AssetId" INT REFERENCES "Asset"("Id") ON DELETE CASCADE, + "ProductClassificationItemId" INT REFERENCES "ProductClassificationItem"("Id") ON DELETE CASCADE +); + +CREATE TABLE "ProductOrSectorSpecificCarbonFootprint" ( + "Id" INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + "AssetId" INT REFERENCES "Asset"("Id") ON DELETE CASCADE, + "PcfCalculationMethod" TEXT, + "PcfRuleOperator" TEXT, + "PcfRuleName" TEXT, + "PcfRuleVersion" TEXT, + "PcfRuleOnlineReference" TEXT, + "PcfApiEndpoint" TEXT, + "PcfApiQuery" TEXT +); + +-- ============================================================ +-- Marking Data +-- ============================================================ + +INSERT INTO "Marking" ("Index","MarkingName","DesignationOfCertificateOrApproval","IssueDate","ExpiryDate","MarkingAdditionalText","MarkingFile") VALUES +(0,'0173-1#07-DAA603#004','KEMA99IECEX1105/128','2022-01-01','2030-01-01','additional information on the marking - 00', + 'https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/checkmark.png'), +(0,'0173-1#07-DAA603#005','KEMA99IECEX1105/129','2022-02-01','2030-02-01','additional information on the marking - 01', + 'https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/checkmark.png'), +(1,'0173-1#07-DAA603#006','KEMA99IECEX1105/130','2022-03-01','2030-03-01','additional information on the marking - 02', + 'https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/checkmark.png'), +(0,'0173-1#07-DAA603#007','KEMA99IECEX1105/131','2022-04-01','2030-04-01','additional information on the marking - 03', + 'https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/checkmark.png'), +(1,'0173-1#07-DAA603#008','KEMA99IECEX1105/132','2022-05-01','2030-05-01','additional information on the marking - 04', + 'https://raw.githubusercontent.com/AAS-TwinEngine/AAS.TwinEngine.DataEngine/refs/heads/main/example/data/checkmark.png'); + +-- ============================================================ +-- Asset-Marking Relationships +-- ============================================================ + +INSERT INTO "AssetMarking" ("AssetId","MarkingId") VALUES +(1,1), +(1,3), +(2,2), +(2,3), +(3,4), +(3,5); + +-- ============================================================ +-- Product Classification +-- ============================================================ + +INSERT INTO "ProductClassificationItem" ("Index","ProductClassificationSystem","ClassificationSystemVersion","ProductClassId") VALUES +(0,'ECLASS','14','19-01-01-01'), +(1,'IEC CDD','2024-09','IEC-CDD-AAA124'), +(2,'UNSPSC','23.0301','UNSPSC-43211503'), +(0,'ISO 13584','2023','ISO13584-XYZ789'), +(0,'ECLASS','5','27-02-03-05'), +(1,'IEC CDD','2024-09','IEC-CDD-AAA123'); + +INSERT INTO "AssetProductClassificationItem" ("AssetId","ProductClassificationItemId") VALUES +(1,1), +(1,2), +(1,3), +(2,4), +(3,5), +(3,6); + +-- ============================================================ +-- Carbon Footprint Data +-- ============================================================ + +INSERT INTO "ProductOrSectorSpecificCarbonFootprint" ( + "AssetId","PcfCalculationMethod","PcfRuleOperator","PcfRuleName","PcfRuleVersion","PcfRuleOnlineReference","PcfApiEndpoint","PcfApiQuery" +) VALUES +(1,'IEC TS 63058','GHG Protocol','GHG Protocol Product Standard','1.1','https://ghgprotocol.org/standards/product-standard','https://api.carbonfootprint.org/v1/calculate','?productId=12345&unit=kgCO2e&scope=cradle-to-gate'), +(2,'EN 15804','ISO 14067','ISO 14067','2.1','https://www.iso.org/standard/43278.html','https://api.iso14067.org/v2/emissions','?sector=electronics®ion=EU&year=2025'), +(3,'PACT v2.0.0','PAS 2050','PAS 2050','0.9','https://www.bsigroup.com/en-GB/PAS-2050-Carbon-Footprint/','https://api.pas2050.com/v1/footprint','?material=steel&quantity=1000kg&method=ISO14067'); diff --git a/example/postgres/03_contactinformations.sql.inc b/example/postgres/03_contactinformations.sql.inc new file mode 100644 index 0000000..9cb8af6 --- /dev/null +++ b/example/postgres/03_contactinformations.sql.inc @@ -0,0 +1,221 @@ +-- ============================================================ +-- Contact Information Data Insertion +-- ============================================================ +-- This file contains CREATE And INSERT statements for contact-related entities: +-- - ContactInformation +-- - Phone numbers +-- - Fax numbers +-- - Email addresses +-- - IP Communication channels +-- - Asset-ContactInformation relationships +-- ============================================================ +-- ============================================================ +-- Contact Information Tables +-- ============================================================ + +CREATE TABLE "ContactInformation" ( + "Id" INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + "Index" INT, + "RoleOfContactPerson" TEXT, + "Language" TEXT, + "TimeZone" TEXT, + "AddressOfAdditionalLink" TEXT, + "NationalCode_en" TEXT, + "NationalCode_de" TEXT, + "CityTown_en" TEXT, + "CityTown_de" TEXT, + "Company_en" TEXT, + "Company_de" TEXT, + "Department_en" TEXT, + "Department_de" TEXT, + "Street_en" TEXT, + "Street_de" TEXT, + "Zipcode_en" TEXT, + "Zipcode_de" TEXT, + "POBox_en" TEXT, + "POBox_de" TEXT, + "ZipCodeOfPOBox_en" TEXT, + "ZipCodeOfPOBox_de" TEXT, + "StateCounty_en" TEXT, + "StateCounty_de" TEXT, + "NameOfContact_en" TEXT, + "NameOfContact_de" TEXT, + "FirstName_en" TEXT, + "FirstName_de" TEXT, + "MiddleNames_en" TEXT, + "MiddleNames_de" TEXT, + "Title_en" TEXT, + "Title_de" TEXT, + "AcademicTitle_en" TEXT, + "AcademicTitle_de" TEXT, + "FurtherDetailsOfContact_en" TEXT, + "FurtherDetailsOfContact_de" TEXT +); + +CREATE TABLE "AssetContactInformation" ( + "Id" INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + "AssetId" INT NOT NULL REFERENCES "Asset"("Id") ON DELETE CASCADE, + "ContactInformationId" INT NOT NULL REFERENCES "ContactInformation"("Id") ON DELETE CASCADE +); + +CREATE TABLE "Phone" ( + "Id" INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + "ContactInformationId" INT REFERENCES "ContactInformation"("Id") ON DELETE CASCADE, + "TelephoneNumber_en" TEXT, + "TelephoneNumber_de" TEXT, + "AvailableTime_en" TEXT, + "AvailableTime_de" TEXT, + "TypeOfTelephone" TEXT +); + +CREATE TABLE "Fax" ( + "Id" INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + "ContactInformationId" INT REFERENCES "ContactInformation"("Id") ON DELETE CASCADE, + "FaxNumber_en" TEXT, + "FaxNumber_de" TEXT, + "TypeOfFaxNumber" TEXT +); + +CREATE TABLE "Email" ( + "Id" INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + "ContactInformationId" INT REFERENCES "ContactInformation"("Id") ON DELETE CASCADE, + "EmailAddress" TEXT, + "TypeOfEmailAddress" TEXT, + "PublicKey_en" TEXT, + "PublicKey_de" TEXT, + "TypeOfPublicKey_en" TEXT, + "TypeOfPublicKey_de" TEXT +); + +CREATE TABLE "IPCommunication" ( + "Id" INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + "Index" INT, + "AddressOfAdditionalLink" TEXT, + "TypeOfCommunication" TEXT, + "AvailableTime_en" TEXT, + "AvailableTime_de" TEXT +); + +CREATE TABLE "ContactInformationIPCommunication" ( + "Id" INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + "ContactInformationId" INT REFERENCES "ContactInformation"("Id") ON DELETE CASCADE, + "IPCommunicationId" INT REFERENCES "IPCommunication"("Id") ON DELETE CASCADE +); + + +-- ============================================================ +-- Contact Information +-- ============================================================ + +INSERT INTO "ContactInformation" ( + "Index","RoleOfContactPerson","Language","TimeZone","AddressOfAdditionalLink", + "NationalCode_en","NationalCode_de","CityTown_en","CityTown_de","Company_en","Company_de", + "Department_en","Department_de","Street_en","Street_de","Zipcode_en","Zipcode_de","POBox_en","POBox_de", + "ZipCodeOfPOBox_en","ZipCodeOfPOBox_de","StateCounty_en","StateCounty_de","NameOfContact_en","NameOfContact_de", + "FirstName_en","FirstName_de","MiddleNames_en","MiddleNames_de","Title_en","Title_de","AcademicTitle_en","AcademicTitle_de", + "FurtherDetailsOfContact_en","FurtherDetailsOfContact_de" +) VALUES +(0,'0173-1#07-AAS927#001','EN','Z+05:30','https://www.mm-software.com/','IN','IN','Mumbai','München', + 'M&M India','M&M India','Human Resources','Human Resources', + '221B Baker Street','221B Baker Street','110001','110001','PO Box 123','PO Box 124','110002','110002', + 'Delhi','Delhi','Aarav Sharma','Aarav Sharma','Aarav','Aarav','Sharma','Sharma','Mr.','Herr','Dr.','Dr.', + 'Responsible for B2B sales in region-1','Responsible for B2B sales in region-1'), +(1,'0173-1#07-AAS928#001','DE','Z+01:00','https://www.mm-software.com/we/','DE','DE','Berlin','Berlin', + 'M&M Germany','M&M Germany','Finance','Finance','Hauptstraße 15','Hauptstraße 16','10115','10115','Postfach 789','Postfach 790', + '10116','10116','Berlin','Berlin','Lukas Mueller','Lukas Müller','Lukas','Lukas','Müller','Mueller','Mr.','Herr','Dr.','Dr.', + 'Responsible for B2B sales in region-2','Responsible for B2B sales in region-2'), +(0,'0173-1#07-AAS931#001','EN','Z+05:30','https://www.mm-software.com/more-the-newsroom/detail/mm-software-auf-der-sps-2025/','IN','IN','Mumbai','München', + 'M&M India','M&M India','Operations','Operations', + '221B Baker Street','221B Baker Street','110001','110001','PO Box 123','PO Box 124','110002','110002','Delhi','Delhi', + 'Priya Mehta','Priya Mehta','Priya','Priya','Mehta','Mehta','Ms','Frau','Prof.','Prof.','Responsible for B2B sales in region-3','Responsible for B2B sales in region-3'), +(1,'0173-1#07-AAS930#001','DE','Z+01:00','https://www.mm-software.com/more-the-newsroom/detail/entscheide-dich-teil-2/','DE','DE','Berlin','Berlin', + 'M&M Germany','M&M Germany','Human Resources','Human Resources','Hauptstraße 15','Hauptstraße 16','10115','10115','Postfach 789','Postfach 790', + '10116','10116','Berlin','Berlin','Anna Schneider','Anna Schneider','Anna','Anna','Schneider','Schneider','Ms','Frau','Prof.','Prof.','Responsible for B2B sales in region-4','Responsible for B2B sales in region-4'), +(2,'0173-1#07-AAS929#001','FR','Z+08:00','https://www.mm-software.com/more-the-newsroom/detail/digitaler-produktpass-dpp-in-der-praxis-leitfaden-und-checkliste-fuer-unternehmen/','CN','CN','Beijing','Beijing', + 'M&M China','M&M China','Finance','Finance','88 Nanjing Road','89 Nanjing Road','200001','200001','P.O. Box 456','P.O. Box 457', + '200002','200002','hanghai Municipality','hanghai Municipality','Anna Müller','Wei Li','Li','Li','Wei','Wei','Mr.','Herr','Dr.','Dr.', + 'Responsible for B2B sales in region-5','Responsible for B2B sales in region-5'), +(3,'0173-1#07-AAS928#001','DE','Z+01:00','https://www.mm-software.com/more-the-newsroom/','DE','DE','Berlin','Berlin', + 'M&M Germany','M&M Germany','Operations','Operations','Hauptstraße 15','Hauptstraße 16','10115','10115','Postfach 789','Postfach 790', + '10116','10116','Berlin','Berlin','Johann Becker','Johann Becker','Johann','Johann','Becker','Becker','Ms','Frau','Prof.','Prof.', + 'Responsible for B2B sales in region-6','Responsible for B2B sales in region-6'); + +-- ============================================================ +-- Phone Numbers +-- ============================================================ + +INSERT INTO "Phone" ("ContactInformationId","TelephoneNumber_en","TelephoneNumber_de","AvailableTime_en","AvailableTime_de","TypeOfTelephone") VALUES +(1,'+49 151 23456789','+49 151 23456790','Monday – Friday 08:00 to 16:00','Montag – Freitag 08:00 bis 16:00','0173-1#07-AAS754#001'), +(2,'+91 98765 43210','+91 98765 43211','Monday – Thursday 09:00 to 17:00','Montag – Donnerstag 09:00 bis 17:00','0173-1#07-AAS755#001'), +(3,'+49 160 98765432','+49 160 98765433','Tuesday – Saturday 07:30 to 15:30','Dienstag – Samstag 07:30 bis 15:30','0173-1#07-AAS756#001'), +(4,'+91 91234 56789','+91 91234 56790','Monday – Friday 10:00 to 18:00','Montag – Freitag 10:00 bis 18:00','0173-1#07-AAS757#001'), +(5,'+86 138 1234 5678','+86 138 1234 5679','Wednesday – Sunday 08:00 to 14:00','Mittwoch – Sonntag 08:00 bis 14:00','0173-1#07-AAS758#001'), +(6,'+49 170 12345678','+49 170 12345679','Monday – Friday 06:00 to 12:00','Montag – Freitag 06:00 bis 12:00','0173-1#07-AAS759#001'); + +-- ============================================================ +-- Fax Numbers +-- ============================================================ + +INSERT INTO "Fax" ("ContactInformationId","FaxNumber_en","FaxNumber_de","TypeOfFaxNumber") VALUES +(1,'+49 151 23456789','+49 151 23456790','0173-1#07-AAS754#001'), +(2,'+91 98765 43210','+91 98765 43211','0173-1#07 AAS756#001'), +(3,'+49 160 98765432','+49 160 98765433','0173-1#07-AAS756#001'), +(4,'+91 91234 56789','+91 91234 56790','0173-1#07-AAS754#002'), +(5,'+86 138 1234 5678','+86 138 1234 5679','0173-1#07 AAS756#002'), +(6,'+49 170 12345678','+49 170 12345679','0173-1#07-AAS756#002'); + +-- ============================================================ +-- Email Addresses +-- ============================================================ + +INSERT INTO "Email" ("ContactInformationId","EmailAddress","TypeOfEmailAddress","PublicKey_en","PublicKey_de","TypeOfPublicKey_en","TypeOfPublicKey_de") VALUES +(1,'aarav.sharma@example.in','0173-1#07-AAS754#001','A1B2C3D4E5F67890ABCDEF1234567890ABCDEF12','A1B2C3D4E5F67890ABCDEF1234567890ABCDEF13','RSA Encryption','RSA-Verschlüsselung'), +(2,'lukas.mueller@example.de','0173-1#07-AAS756#001','B2C3D4E5F67890ABCDEF1234567890ABCDEF1234','B2C3D4E5F67890ABCDEF1234567890ABCDEF1235','ECC Encryption','ECC-Verschlüsselung'), +(3,'priya.mehta@example.in','0173-1#07-AAS757#001','C3D4E5F67890ABCDEF1234567890ABCDEF123456','C3D4E5F67890ABCDEF1234567890ABCDEF123457','DSA Signature','DSA-Signatur'), +(4,'anna.schneider@example.de','0173-1#07-AAS758#001','E5F67890ABCDEF1234567890ABCDEF1234567890','E5F67890ABCDEF1234567890ABCDEF1234567891','EdDSA Signature','EdDSA-Signatur'), +(5,'wei.li@example.cn','0173-1#07-AAS754#001','F67890ABCDEF1234567890ABCDEF1234567890AB','F67890ABCDEF1234567890ABCDEF1234567890AB','RSA Encryption','RSA-Verschlüsselung'), +(6,'johann.becker@example.de','0173-1#07-AAS756#001','D4E5F67890ABCDEF1234567890ABCDEF12345678','D4E5F67890ABCDEF1234567890ABCDEF12345679','ECC Encryption','ECC-Verschlüsselung'); + +-- ============================================================ +-- IP Communication Channels +-- ============================================================ + +INSERT INTO "IPCommunication" ("Index","AddressOfAdditionalLink","TypeOfCommunication","AvailableTime_en","AvailableTime_de") VALUES +(0,'https://www.mm-software.com/more-the-newsroom/','Chat','Monday – Friday 08:00 to 16:00','Montag – Freitag 08:00 bis 16:00'), +(1,'https://www.mm-software.com/more-the-newsroom/detail/digitaler-produktpass-dpp-in-der-praxis-leitfaden-und-checkliste-fuer-unternehmen/','Video call','Monday – Thursday 09:00 to 17:00','Montag – Donnerstag 09:00 bis 17:00'), +(0,'https://www.mm-software.com/more-the-newsroom/','Chat','Tuesday – Saturday 07:30 to 15:30','Dienstag – Samstag 07:30 bis 15:30'), +(0,'https://www.mm-software.com/we/','Video call','Monday – Friday 10:00 to 18:00','Montag – Freitag 10:00 bis 18:00'), +(0,'https://www.mm-software.com/we/code-of-conduct/','Chat','Wednesday – Sunday 08:00 to 14:00','Mittwoch – Sonntag 08:00 bis 14:00'), +(1,'https://www.mm-software.com/more-the-newsroom/anmeldung/','Video call','Monday – Friday 06:00 to 12:00','Montag – Freitag 06:00 bis 12:00'), +(2,'https://www.mm-software.com/','Chat','Monday – Friday 08:00 to 16:00','Montag – Freitag 08:00 bis 16:00'); + +-- ============================================================ +-- Contact-IPCommunication Relationships +-- ============================================================ + +INSERT INTO "ContactInformationIPCommunication" ("ContactInformationId","IPCommunicationId") VALUES +(1,1), +(1,2), +(2,3), +(3,4), +(4,5), +(5,4), +(6,5), +(6,6), +(6,7); + +-- ============================================================ +-- Asset-ContactInformation Relationships +-- ============================================================ + +INSERT INTO "AssetContactInformation" ( + "AssetId", + "ContactInformationId" +) +VALUES + (1, 1), + (1, 2), + (2, 3), + (3, 4), + (3, 5), + (3, 6); diff --git a/example/postgres/04_handoverdocumentation.sql.inc b/example/postgres/04_handoverdocumentation.sql.inc new file mode 100644 index 0000000..14d0e89 --- /dev/null +++ b/example/postgres/04_handoverdocumentation.sql.inc @@ -0,0 +1,224 @@ +-- ============================================================ +-- Classification and Document Data Insertion +-- ============================================================ +-- This file contains CREATE and INSERT statements for: +-- - Documents and Document metadata +-- - Document Classifications +-- - Document Versions +-- - All related relationships +-- ============================================================ + +-- ============================================================ +-- Document Management Tables +-- ============================================================ + +CREATE TABLE "Document" ( + "Id" INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + "Index" INT +); + +CREATE TABLE "DocumentId" ( + "Id" INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + "Index" INT, + "DocumentDomainId" UUID, + "DocumentIdentifier" TEXT, + "DocumentIsPrimary" BOOLEAN +); + +CREATE TABLE "AssetDocument" ( + "Id" INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + "AssetId" INT REFERENCES "Asset"("Id") ON DELETE CASCADE, + "DocumentId" INT REFERENCES "Document"("Id") ON DELETE CASCADE +); + +CREATE TABLE "DocumentDocumentId" ( + "Id" INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + "DocumentId" INT REFERENCES "Document"("Id") ON DELETE CASCADE, + "DocumentIdentifierId" INT REFERENCES "DocumentId"("Id") ON DELETE CASCADE +); + +CREATE TABLE "DocumentClassification" ( + "Id" INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + "Index" INT, + "ClassId" TEXT, + "ClassificationSystem" TEXT, + "ClassName_en" TEXT, + "ClassName_de" TEXT +); + +CREATE TABLE "DocumentDocumentClassification" ( + "Id" INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + "DocumentId" INT REFERENCES "Document"("Id") ON DELETE CASCADE, + "DocumentClassificationId" INT REFERENCES "DocumentClassification"("Id") ON DELETE CASCADE +); + +CREATE TABLE "DocumentVersion" ( + "Id" INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + "Index" INT, + "en" TEXT, + "DigitalFile" TEXT, + "Version" TEXT, + "StatusSetDate" DATE, + "StatusValue" TEXT, + "OrganizationShortName" TEXT, + "OrganizationOfficialName" TEXT, + "Title_en" TEXT, + "Title_de" TEXT, + "Subtitle_en" TEXT, + "Subtitle_de" TEXT, + "Description_en" TEXT, + "Description_de" TEXT, + "KeyWords_en" TEXT, + "KeyWords_de" TEXT, + "PreviewFile" TEXT +); + +CREATE TABLE "DocumentDocumentVersion" ( + "Id" INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + "DocumentId" INT REFERENCES "Document"("Id") ON DELETE CASCADE, + "DocumentVersionId" INT REFERENCES "DocumentVersion"("Id") ON DELETE CASCADE +); + +-- ============================================================ +-- Documents +-- ============================================================ + +INSERT INTO "Document" ("Index") VALUES +(0),(1),(2),(0),(1),(0),(1); + +-- ============================================================ +-- Document Identifiers +-- ============================================================ + +INSERT INTO "DocumentId" ("Index","DocumentDomainId","DocumentIdentifier","DocumentIsPrimary") VALUES +(0,'a3f1c2b4-9d81-4e5a-b6f2-01ac9e11d001','DOC2025A001',TRUE), +(1,'b7e92d10-3c45-4a8f-9f21-02bc9e11d002','CERTX94B21',FALSE), +(0,'c81a4f22-6d91-43bb-a812-03cd9e11d003','MANUAL8F2Q7',TRUE), +(0,'a3f1c2b4-9d81-4e5a-b6f2-01ac9e11d002','DOC2025A002',TRUE), +(0,'b7e92d10-3c45-4a8f-9f21-02bc9e11d003','CERTX94B22',TRUE), +(0,'c81a4f22-6d91-43bb-a812-03cd9e11d004','MANUAL8F2Q8',TRUE), +(1,'a3f1c2b4-9d81-4e5a-b6f2-01ac9e11d003','DOC2025A003',TRUE), +(2,'b7e92d10-3c45-4a8f-9f21-02bc9e11d004','CERTX94B23',FALSE), +(0,'c81a4f22-6d91-43bb-a812-03cd9e11d005','MANUAL8F2Q9',FALSE); + +-- ============================================================ +-- Asset-Document Relationships +-- ============================================================ + +INSERT INTO "AssetDocument" ("AssetId","DocumentId") VALUES +(1,1), +(1,2), +(1,3), +(2,4), +(2,5), +(3,6), +(3,7); + +-- ============================================================ +-- Document-DocumentId Relationships +-- ============================================================ + +INSERT INTO "DocumentDocumentId" ("DocumentId","DocumentIdentifierId") VALUES +(1,1), +(1,2), +(2,3), +(3,4), +(4,5), +(5,6), +(6,6), +(7,9), +(7,7), +(7,8); + +-- ============================================================ +-- Document Classifications +-- ============================================================ + +INSERT INTO "DocumentClassification" ("Index","ClassId","ClassificationSystem","ClassName_en","ClassName_de") VALUES +(0,'CLS-001','IEC-61360','Electrical Components','Elektrische Bauteile'), +(1,'CLS-002','ISO-13584','Hydraulic Pumps','Hydraulikpumpen'), +(0,'CLS-003','ECLASS-13.0','Industrial Sensors','Industrielle Sensoren'), +(0,'CLS-004','UNSPSC','Fasteners','Befestigungselemente'), +(0,'CLS-005','ISO-81346','Bearings','Lager'), +(0,'CLS-006','IEC-61131','PLC Controllers','PLC-Steuerungen'), +(1,'CLS-007','ECLASS-13.0','Safety Equipment','Sicherheitsausrüstung'), +(2,'CLS-008','GS1','Packaging Materials','Verpackungsmaterialien'), +(0,'CLS-009','ISO-13584','Cooling Systems','Kühlsysteme'); + +-- ============================================================ +-- Document-Classification Relationships +-- ============================================================ + +INSERT INTO "DocumentDocumentClassification" ("DocumentId","DocumentClassificationId") VALUES +(1,1), +(1,2), +(2,3), +(3,4), +(4,5), +(5,6), +(6,6), +(7,9), +(7,7), +(7,8); + +-- ============================================================ +-- Document Versions +-- ============================================================ + +INSERT INTO "DocumentVersion" ( + "Index","en","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', + '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', + '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', + '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', + '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', + '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', + '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', + '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', + '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', + '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'); + +-- ============================================================ +-- Document-Version Relationships +-- ============================================================ + +INSERT INTO "DocumentDocumentVersion" ("DocumentId","DocumentVersionId") VALUES +(1,1), +(1,2), +(2,3), +(3,4), +(4,5), +(5,6), +(6,6), +(7,9), +(7,7), +(7,8); diff --git a/example/postgres/init.sql b/example/postgres/init.sql new file mode 100644 index 0000000..e82ea3e --- /dev/null +++ b/example/postgres/init.sql @@ -0,0 +1,27 @@ +-- ============================================================ +-- DPP Plugin Database Initialization Script +-- ============================================================ +-- This is the main orchestration file that executes all database +-- initialization scripts in the correct hierarchical order. +-- +-- Execution Order: +-- 1. 01_core_asset_tables.sql.inc - Create core asset database schema +-- 2. 02_nameplate_carbonfootprint_technicaldata.sql.inc - Create Nameplate, Carbon Footprint, Technical Data tables and insert data +-- 3. 03_contactinformations.sql.inc - Create Contact information and relationships table and insert data +-- 4. 04_handoverdocumentation.sql.inc - Create Handover Documentation table and insert data +-- +-- ============================================================ + +\echo 'Executing: 01_core_asset_tables.sql.inc - Creating core asset database schema...' +\i /docker-entrypoint-initdb.d/01_core_asset_tables.sql.inc + +\echo 'Executing: 02_nameplate_carbonfootprint_technicaldata.sql.inc - Inserting data...' +\i /docker-entrypoint-initdb.d/02_nameplate_carbonfootprint_technicaldata.sql.inc + +\echo 'Executing: 03_contactinformations.sql.inc - Inserting contact information...' +\i /docker-entrypoint-initdb.d/03_contactinformations.sql.inc + +\echo 'Executing: 04_handoverdocumentation.sql.inc - Inserting document metadata...' +\i /docker-entrypoint-initdb.d/04_handoverdocumentation.sql.inc + +\echo 'Database initialization completed successfully!' diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/AasRegistry/ShellDescriptorServiceTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/AasRegistry/ShellDescriptorServiceTests.cs index 9848293..4de2a88 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/AasRegistry/ShellDescriptorServiceTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/AasRegistry/ShellDescriptorServiceTests.cs @@ -254,97 +254,92 @@ public async Task SyncShellDescriptorsAsync_ShouldHandle_EmptyListsGracefully() } [Fact] - public async Task SyncShellDescriptorsAsync_ShouldThrow_InternalDataProcessingException_WhenRegistryDescriptorHasNoId() + public async Task SyncShellDescriptorsAsync_WhenRegistryDescriptorHasNoId_ShouldLogAndReturn() { var metaData = new ShellDescriptorsMetaData { - PagingMetaData = new PagingMetaData { Cursor = "nextCursor" }, + PagingMetaData = new PagingMetaData(), ShellDescriptors = [new ShellDescriptorMetaData { Id = "valid" }] }; - _aasRegistryProvider.GetAllAsync(Arg.Any()).Returns([new ShellDescriptor { Id = "" }]); - var manifests = new List - { - new() - { - PluginName = "TestPlugin", - PluginUrl = new Uri("http://test-plugin"), - SupportedSemanticIds = new List(), - Capabilities = new Capabilities { HasShellDescriptor = true } - } - }; - _pluginManifestConflictHandler.Manifests.Returns(manifests); - _pluginDataHandler.GetDataForAllShellDescriptorsAsync(null, null, manifests, Arg.Any()).Returns(metaData); - await Assert.ThrowsAsync(() => _sut.SyncShellDescriptorsAsync(CancellationToken.None)); + _aasRegistryProvider + .GetAllAsync(Arg.Any()) + .Returns([new ShellDescriptor { Id = "" }]); + + _pluginDataHandler + .GetDataForAllShellDescriptorsAsync(null, null, Arg.Any>(), Arg.Any()) + .Returns(metaData); + + var exception = await Record.ExceptionAsync( + () => _sut.SyncShellDescriptorsAsync(CancellationToken.None)); + + Assert.Null(exception); + + await _aasRegistryProvider + .DidNotReceive() + .CreateAsync(Arg.Any(), Arg.Any()); } [Fact] - public async Task SyncShellDescriptorsAsync_WhenRegistryThrowsResourceNotFoundException_ThrowsShellDescriptorNotFoundException() + public async Task SyncShellDescriptorsAsync_WhenRegistryThrowsResourceNotFoundException_ShouldNotThrow() { - _aasRegistryProvider.GetAllAsync(Arg.Any()) - .Throws(new ResourceNotFoundException()); + _aasRegistryProvider + .GetAllAsync(Arg.Any()) + .Throws(new ResourceNotFoundException()); - var ex = await Assert.ThrowsAsync(() => _sut.SyncShellDescriptorsAsync(CancellationToken.None)); + var exception = await Record.ExceptionAsync( + () => _sut.SyncShellDescriptorsAsync(CancellationToken.None)); - Assert.NotNull(ex.InnerException); - Assert.IsType(ex.InnerException); + Assert.Null(exception); } [Fact] - public async Task SyncShellDescriptorsAsync_WhenRegistryThrowsResponseParsingException_ThrowsInternalDataProcessingException() + public async Task SyncShellDescriptorsAsync_WhenRegistryThrowsResponseParsingException_ShouldNotThrow() { - _aasRegistryProvider.GetAllAsync(Arg.Any()).Throws(new ResponseParsingException()); + _aasRegistryProvider + .GetAllAsync(Arg.Any()) + .Throws(new ResponseParsingException()); - var ex = await Assert.ThrowsAsync(() => _sut.SyncShellDescriptorsAsync(CancellationToken.None)); + var exception = await Record.ExceptionAsync( + () => _sut.SyncShellDescriptorsAsync(CancellationToken.None)); - Assert.NotNull(ex.InnerException); - Assert.IsType(ex.InnerException); + Assert.Null(exception); } [Fact] - public async Task SyncShellDescriptorsAsync_WhenRegistryThrowsRequestTimeoutException_ThrowsRegistryNotAvailableException() + public async Task SyncShellDescriptorsAsync_WhenRegistryThrowsRequestTimeoutException_ShouldNotThrow() { - _aasRegistryProvider.GetAllAsync(Arg.Any()).Throws(new RequestTimeoutException()); + _aasRegistryProvider + .GetAllAsync(Arg.Any()) + .Throws(new RequestTimeoutException()); - var ex = await Assert.ThrowsAsync(() => _sut.SyncShellDescriptorsAsync(CancellationToken.None)); + var exception = await Record.ExceptionAsync( + () => _sut.SyncShellDescriptorsAsync(CancellationToken.None)); - Assert.NotNull(ex.InnerException); - Assert.IsType(ex.InnerException); + Assert.Null(exception); } [Fact] - public async Task SyncShellDescriptorsAsync_ShouldThrow_InternalDataProcessingException_WhenPluginMetadataHasNoId() + public async Task SyncShellDescriptorsAsync_WhenPluginMetadataHasNoId_ShouldLogAndReturn() { var metaData = new ShellDescriptorsMetaData { - PagingMetaData = new PagingMetaData { Cursor = "nextCursor" }, + PagingMetaData = new PagingMetaData(), ShellDescriptors = [new ShellDescriptorMetaData { Id = "" }] }; - _aasRegistryProvider.GetAllAsync(Arg.Any()).Returns([new ShellDescriptor { Id = "1" }]); - var manifests = new List - { - new() - { - PluginName = "TestPlugin", - PluginUrl = new Uri("http://test-plugin"), - SupportedSemanticIds = new List(), - Capabilities = new Capabilities { HasShellDescriptor = true } - } - }; - _pluginManifestConflictHandler.Manifests.Returns(manifests); - _pluginDataHandler.GetDataForAllShellDescriptorsAsync(null, null, manifests, Arg.Any()).Returns(metaData); - await Assert.ThrowsAsync(() => _sut.SyncShellDescriptorsAsync(CancellationToken.None)); - } + _aasRegistryProvider + .GetAllAsync(Arg.Any()) + .Returns([new ShellDescriptor { Id = "1" }]); - [Fact] - public async Task SyncShellDescriptorsAsync_ShouldThrowException_WhenPluginFails() - { - _aasRegistryProvider.GetAllAsync(Arg.Any()).Returns([new ShellDescriptor { Id = "1" }]); + _pluginDataHandler + .GetDataForAllShellDescriptorsAsync(null, null, Arg.Any>(), Arg.Any()) + .Returns(metaData); - _pluginDataHandler.GetDataForAllShellDescriptorsAsync(null, null, Arg.Any>(), Arg.Any()).Throws(new InternalDataProcessingException()); + var exception = await Record.ExceptionAsync( + () => _sut.SyncShellDescriptorsAsync(CancellationToken.None)); - await Assert.ThrowsAsync(() => _sut.SyncShellDescriptorsAsync(CancellationToken.None)); + Assert.Null(exception); } [Fact] 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 457c508..fd96d9c 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandlerTests.cs @@ -557,6 +557,31 @@ public void FillOutTemplate_SubmodelOfComplexData_ReturnsSubmodelWithValues() AssertContactInfo(complexData1, 2, "Test1 John Doe"); } + [Fact] + public void FillOutTemplate_SubmodelOfComplexData_ReturnsSubmodelWithValue() + { + var complexData = TestData.ComplexData; + var submodel = TestData.CreateSubmodelWithComplexData(); + submodel.SubmodelElements?.Add(complexData); + var values = TestData.CreateSubmodelWithInValidComplexDataTreeNode(); + + var submodelWithValues = (Submodel)_sut.FillOutTemplate(submodel, values); + + Equal(2, submodelWithValues.SubmodelElements!.Count); + Equal("ComplexData0", submodelWithValues.SubmodelElements[0].IdShort); + Equal("ComplexData1", submodelWithValues.SubmodelElements[1].IdShort); + var complexData0 = GetSubmodelElementCollection(submodelWithValues, 0); + var complexData1 = GetSubmodelElementCollection(submodelWithValues, 1); + Equal(3, complexData1.Value!.Count); + AssertMultiLanguageProperty(complexData0, "Test Example Manufacturer", "Test Beispiel Hersteller"); + AssertMultiLanguageProperty(complexData1, "Test1 Example Manufacturer", "Test1 Beispiel Hersteller"); + AssertModelType(complexData0, 1, "22.47"); + AssertModelType(complexData1, 1, "22.47"); + AssertContactList(complexData0, 2, "Test John Doe", "Test Example Model"); + AssertContactInfo(complexData0, 3, "Test John Doe"); + AssertContactInfo(complexData1, 2, "Test1 John Doe"); + } + [Fact] public void FillOutTemplate_ShouldNotChangeAnyThing_WhenReferenceElementHasNullValue() { diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SubmodelTemplateServiceTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SubmodelTemplateServiceTests.cs index 3a38608..f41ebbc 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SubmodelTemplateServiceTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/SubmodelTemplateServiceTests.cs @@ -189,22 +189,64 @@ public async Task GetSubmodelTemplateAsync_WithListIndexPath_ReturnsSubmodelWith } [Fact] - public async Task GetSubmodelTemplateAsync_ThrowsNotFoundException_WhenListIndexIsOutOfRange() + public async Task GetSubmodelTemplateAsync_Supports_UrlEncoded_ListIndex() { var submodel = TestData.CreateSubmodelWithModel3DList(); - const string Path = "Model3D[5].ModelFile1"; + const string Path = "Model3D%5B0%5D.ModelDataFile"; + _mappingProvider.GetTemplateId(SubmodelId).Returns(TemplateId); _templateProvider.GetSubmodelTemplateAsync(TemplateId, Arg.Any()) .Returns(submodel); - await Assert.ThrowsAsync(() => _sut.GetSubmodelTemplateAsync(SubmodelId, Path, CancellationToken.None)); + var result = await _sut.GetSubmodelTemplateAsync(SubmodelId, Path, CancellationToken.None); + + Assert.NotNull(result); } [Fact] - public async Task GetSubmodelTemplateAsync_ThrowsNotFoundException_WhenListElementNotFound() + public async Task GetSubmodelTemplateAsync_Throws_When_ListIndex_IsNegative() { var submodel = TestData.CreateSubmodelWithModel3DList(); - const string Path = "NonExistentList[0].ModelFile1"; + const string Path = "Model3D[-1]"; + + _mappingProvider.GetTemplateId(SubmodelId).Returns(TemplateId); + _templateProvider.GetSubmodelTemplateAsync(TemplateId, Arg.Any()) + .Returns(submodel); + + await Assert.ThrowsAsync( + () => _sut.GetSubmodelTemplateAsync(SubmodelId, Path, CancellationToken.None)); + } + + [Fact] + public async Task GetSubmodelTemplateAsync_ReturnsSubmodel_WhenTypeValueListElementIsSubmodelCollection_AndListIndexExceedsAvailableElements() + { + var expectedSubmodel = TestData.CreateSubmodelWithModel3DList(); + var submodel = TestData.CreateSubmodelWithModel3DList(); + const string Path = "Model3D[5].ModelDataFile"; + _mappingProvider.GetTemplateId(SubmodelId).Returns(TemplateId); + _templateProvider.GetSubmodelTemplateAsync(TemplateId, Arg.Any()) + .Returns(submodel); + + var result = await _sut.GetSubmodelTemplateAsync(SubmodelId, Path, CancellationToken.None); + + Assert.Equal(GetSemanticId(expectedSubmodel), GetSemanticId(result)); + + var list = result.SubmodelElements?.FirstOrDefault() as SubmodelElementList; + Assert.NotNull(list); + Assert.Single(list.Value!); + var collection = list.Value![0] as SubmodelElementCollection; + Assert.Single(collection!.Value!); + var file = collection!.Value!.FirstOrDefault() as File; + Assert.NotNull(file); + Assert.Equal("ModelDataFile", file.IdShort); + Assert.Equal("https://localhost/ModelDataFile.glb", file.Value); + } + + [Fact] + public async Task GetSubmodelTemplateAsync_ThrowsInternalDataProcessingException_WhenTypeValueListElementIsSubmodelProperty_AndListIndexExceedsAvailableElements() + { + var submodel = TestData.CreateSubmodelWithPropertyInsideList(); + const string Path = "listProperty[2]"; _mappingProvider.GetTemplateId(SubmodelId).Returns(TemplateId); _templateProvider.GetSubmodelTemplateAsync(TemplateId, Arg.Any()) .Returns(submodel); @@ -280,5 +322,4 @@ public async Task GetSubmodelTemplateAsync_WithIdShortPath_ThrowsSubmodelElement await Assert.ThrowsAsync( () => _sut.GetSubmodelTemplateAsync(SubmodelId, "SomePath", CancellationToken.None)); } - } diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/TestData.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/TestData.cs index b3732d2..07c7fbf 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/TestData.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/SubmodelRepository/TestData.cs @@ -632,6 +632,41 @@ public static Submodel CreateSubmodelWithoutExtraElementsNested() ); } + public static SubmodelElementList CreateElementListWithProperty() + { + return new SubmodelElementList( + idShort: "listProperty", + semanticId: new Reference( + ReferenceTypes.ExternalReference, + [ + new Key(KeyTypes.SubmodelElementList, + "http://example.com/idta/digital-nameplate/list-property") + ] + ), + typeValueListElement: AasSubmodelElements.Property, + value: [ + CreateContactName() + ] + ); + } + + public static Submodel CreateSubmodelWithPropertyInsideList() + { + return new Submodel( + id: "http://example.com/idta/digital-nameplate", + idShort: "DigitalNameplate", + semanticId: new Reference( + ReferenceTypes.ExternalReference, + [ + new Key(KeyTypes.Submodel, "http://example.com/idta/digital-nameplate/semantic-id") + ] + ), + submodelElements: [ + CreateElementListWithProperty() + ] + ); + } + public static Submodel CreateSubmodelWithoutExtraElements() { return new Submodel( @@ -720,6 +755,45 @@ public static Submodel CreateSubmodelWithModel3DList() ] ); + public static readonly SubmodelElementCollection InValidComplexData = new( + idShort: "ComplexData", + semanticId: new Reference( + ReferenceTypes.ExternalReference, + [ + new Key(KeyTypes.SubmodelElementList, "http://example.com/idta/digital-nameplate/complex-data") + ] + ), + qualifiers: + [ + new Qualifier( + type: "ExternalReference", + valueType: DataTypeDefXsd.String, + value: "OneToMany") + ], + value: [ + CreateManufacturerName(), + new Property( + idShort: "ModelType", + valueType: DataTypeDefXsd.String, + value: "", // left intentionally empty for FillOut tests + semanticId: new Reference( + ReferenceTypes.ExternalReference, + [ + new Key(KeyTypes.Property, "http://example.com/idta/digital-nameplate/model-type") + ] + ), + qualifiers: + [ + new Qualifier( + type: "ExternalReference", + valueType: DataTypeDefXsd.String, + value: "ZeroToOne") + ]), + CreateContactList(), + CreateContactInformation(), + ] +); + public static readonly SemanticTreeNode SubmodelTreeNode = CreateSubmodelTreeNode(); public static SemanticTreeNode CreateSubmodelTreeNode() @@ -778,6 +852,41 @@ public static SemanticTreeNode CreateSubmodelWithComplexDataTreeNode() return semanticTreeNode; } + public static SemanticTreeNode CreateSubmodelWithInValidComplexDataTreeNode() + { + var semanticTreeNode = new SemanticBranchNode("http://example.com/idta/digital-nameplate/semantic-id", Cardinality.Unknown); + + var complexDataBranchNode1 = new SemanticBranchNode("http://example.com/idta/digital-nameplate/complex-data", Cardinality.ZeroToMany); + + var complexDataBranchNode2 = new SemanticBranchNode("http://example.com/idta/digital-nameplate/complex-data", Cardinality.ZeroToMany); + + semanticTreeNode.AddChild(complexDataBranchNode1); + + semanticTreeNode.AddChild(complexDataBranchNode2); + + complexDataBranchNode1.AddChild(CreateManufacturerNameTreeNode()); + + complexDataBranchNode1.AddChild(CreateModelTypeTreeNode()); + + complexDataBranchNode1.AddChild(CreateContactListTreeNode()); + + complexDataBranchNode1.AddChild(CreateContactInformationTreeNode()); + + complexDataBranchNode2.AddChild(CreateManufacturerNameTreeNode("1")); + + complexDataBranchNode2.AddChild(CreateModelTypeTreeNode()); + + complexDataBranchNode2.AddChild(CreateContactListTreeNode("1")); + + complexDataBranchNode2.AddChild(CreateContactListTreeNode("2")); + + complexDataBranchNode2.AddChild(new SemanticLeafNode("http://example.com/idta/digital-nameplate/contact-list", $"Test InValid Contact List", DataType.String, Cardinality.One)); + + complexDataBranchNode2.AddChild(CreateContactInformationTreeNode("1")); + + return semanticTreeNode; + } + public static SemanticTreeNode CreateManufacturerNameTreeNode(string testObject = "") { var manufacturerName = new SemanticBranchNode("http://example.com/idta/digital-nameplate/manufacturer-name", Cardinality.One); diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/ProviderTestData.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/ProviderTestData.cs index 67ac836..714ac2b 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/ProviderTestData.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/ProviderTestData.cs @@ -89,7 +89,7 @@ public static class ProviderTestData "contentType": "image/jpeg", "path": "http://localhost/fileprovider/k.jpg" }, - "globalAssetId": "https://sew-eurodrive.de/shell/1" + "globalAssetId": "https://mm-software.de/shell/1" } """; @@ -118,9 +118,9 @@ public static class ProviderTestData "specificAssetIds": [] }, { - "globalAssetId": "https://wago.com/ids/assets/2206-1631/1000-859", + "globalAssetId": "https://mm-software.com/ids/assets/2206-1631/1000-859", "idShort": "2206-1631/1000-859", - "id": "https://wago.com/ids/aas/2206-1631/1000-859", + "id": "https://mm-software.com/ids/aas/2206-1631/1000-859", "specificAssetIds": [] } ] diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/TemplateProviderTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/TemplateProviderTests.cs index 54bdea1..bc8e684 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/TemplateProviderTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/TemplateProviderTests.cs @@ -269,7 +269,7 @@ public async Task GetAssetInformationTemplateAsync_ReturnsAssetInformation_WhenV var result = await _sut.GetAssetInformationTemplateAsync(TemplateId, CancellationToken.None); Assert.NotNull(result); - Assert.Equal("https://sew-eurodrive.de/shell/1", result.GlobalAssetId); + Assert.Equal("https://mm-software.de/shell/1", result.GlobalAssetId); } [Fact] diff --git a/source/AAS.TwinEngine.DataEngine.sln b/source/AAS.TwinEngine.DataEngine.sln index 72313cb..b56b843 100644 --- a/source/AAS.TwinEngine.DataEngine.sln +++ b/source/AAS.TwinEngine.DataEngine.sln @@ -9,6 +9,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AAS.TwinEngine.DataEngine.U EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AAS.TwinEngine.DataEngine.ModuleTests", "AAS.TwinEngine.DataEngine.ModuleTests\AAS.TwinEngine.DataEngine.ModuleTests.csproj", "{D16C8CF8-6A29-4A40-971A-9A070A1817A9}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AAS.TwinEngine.Plugin.TestPlugin", "AAS.TwinEngine.Plugin.TestPlugin\AAS.TwinEngine.Plugin.TestPlugin.csproj", "{455B960D-57D0-4D9B-805C-E1DEA05B9311}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AAS.TwinEngine.Plugin.TestPlugin.UnitTests", "AAS.TwinEngine.Plugin.TestPlugin.UnitTests\AAS.TwinEngine.Plugin.TestPlugin.UnitTests.csproj", "{573F3A2B-8CC3-40D8-B543-74BF5DF7D4FF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +31,14 @@ Global {D16C8CF8-6A29-4A40-971A-9A070A1817A9}.Debug|Any CPU.Build.0 = Debug|Any CPU {D16C8CF8-6A29-4A40-971A-9A070A1817A9}.Release|Any CPU.ActiveCfg = Release|Any CPU {D16C8CF8-6A29-4A40-971A-9A070A1817A9}.Release|Any CPU.Build.0 = Release|Any CPU + {455B960D-57D0-4D9B-805C-E1DEA05B9311}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {455B960D-57D0-4D9B-805C-E1DEA05B9311}.Debug|Any CPU.Build.0 = Debug|Any CPU + {455B960D-57D0-4D9B-805C-E1DEA05B9311}.Release|Any CPU.ActiveCfg = Release|Any CPU + {455B960D-57D0-4D9B-805C-E1DEA05B9311}.Release|Any CPU.Build.0 = Release|Any CPU + {573F3A2B-8CC3-40D8-B543-74BF5DF7D4FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {573F3A2B-8CC3-40D8-B543-74BF5DF7D4FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {573F3A2B-8CC3-40D8-B543-74BF5DF7D4FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {573F3A2B-8CC3-40D8-B543-74BF5DF7D4FF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/AasRegistry/ShellDescriptorService.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/AasRegistry/ShellDescriptorService.cs index 63e2529..4a13fec 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/AasRegistry/ShellDescriptorService.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/AasRegistry/ShellDescriptorService.cs @@ -72,26 +72,37 @@ public class ShellDescriptorService( throw new InvalidUserInputException(ex); } } + public async Task SyncShellDescriptorsAsync(CancellationToken cancellationToken) { try { - var existingDescriptors = await aasRegistryProvider.GetAllAsync(cancellationToken).ConfigureAwait(false) ?? throw new RegistryNotAvailableException(); + var existingDescriptors = await aasRegistryProvider.GetAllAsync(cancellationToken).ConfigureAwait(false); + if (existingDescriptors == null) + { + logger.LogError("AAS Registry returned null. Sync skipped."); + return; + } var pluginManifests = pluginManifestConflictHandler.Manifests; - var pluginMetadata = await pluginDataHandler.GetDataForAllShellDescriptorsAsync(null, null, pluginManifests, cancellationToken).ConfigureAwait(false) ?? throw new PluginNotAvailableException(); + var pluginMetadata = await pluginDataHandler.GetDataForAllShellDescriptorsAsync(null, null, pluginManifests, cancellationToken).ConfigureAwait(false); + if (pluginMetadata == null) + { + logger.LogError("Plugin metadata unavailable. Sync skipped."); + return; + } if (existingDescriptors.Any(d => string.IsNullOrWhiteSpace(d.Id))) { logger.LogError("One or more registry descriptors have missing IDs: {@Descriptors}", existingDescriptors); - throw new InternalDataProcessingException(); + return; } if (pluginMetadata.ShellDescriptors.Any(m => string.IsNullOrWhiteSpace(m.Id))) { logger.LogError("One or more plugin metadata entries have missing IDs: {@Metadata}", pluginMetadata); - throw new InternalDataProcessingException(); + return; } var existingDescriptorsMap = existingDescriptors.ToDictionary(d => d.Id!); @@ -100,26 +111,9 @@ public async Task SyncShellDescriptorsAsync(CancellationToken cancellationToken) await CreateOrUpdateShellDescriptorsAsync(existingDescriptorsMap!, pluginMetadata.ShellDescriptors, cancellationToken).ConfigureAwait(false); await DeleteMissingShellDescriptorsAsync(existingDescriptors, pluginMetadataMap!, cancellationToken).ConfigureAwait(false); } - catch (ResourceNotFoundException ex) - { - throw new ShellDescriptorNotFoundException(ex); - } - catch (ResponseParsingException ex) - { - throw new InternalDataProcessingException(ex); - } - catch (RequestTimeoutException ex) - { - throw new RegistryNotAvailableException(ex); - } - catch (PluginMetaDataInvalidRequestException ex) - { - throw new InvalidUserInputException(ex); - } catch (Exception ex) { logger.LogError(ex, "Unexpected error during ShellDescriptor synchronization."); - throw new InternalDataProcessingException(); } } @@ -144,22 +138,10 @@ private async Task CreateOrUpdateShellDescriptorsAsync( await aasRegistryProvider.CreateAsync(newDescriptor, cancellationToken).ConfigureAwait(false); } } - catch (ResourceNotFoundException ex) - { - throw new ShellDescriptorNotFoundException(ex); - } - catch (ResponseParsingException ex) - { - throw new InternalDataProcessingException(ex); - } - catch (RequestTimeoutException ex) - { - throw new RegistryNotAvailableException(ex); - } catch (Exception ex) { logger.LogError(ex, "Unhandled error while processing descriptor with ID '{Id}'", metadata.Id); - throw new InternalDataProcessingException(ex); + continue; } } } @@ -184,14 +166,10 @@ private async Task DeleteMissingShellDescriptorsAsync( { await aasRegistryProvider.DeleteByIdAsync(descriptorId!, cancellationToken).ConfigureAwait(false); } - catch (RequestTimeoutException ex) - { - throw new ShellDescriptorNotFoundException(ex); - } catch (Exception ex) { logger.LogError(ex, "Unexpected error while deleting descriptor with ID '{Id}'", descriptorId); - throw new InternalDataProcessingException(ex); + continue; } } } diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandler.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandler.cs index 646b923..cca443c 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandler.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SemanticIdHandler.cs @@ -526,6 +526,15 @@ private void FillOutSubmodelElementValue(List elements, Semant 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); @@ -548,6 +557,25 @@ private void FillOutSubmodelElementValue(List elements, Semant } } + 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) @@ -800,7 +828,7 @@ private static IEnumerable FindNodeBySemanticId(SemanticTreeNo /// e.g. "element[3]" -> matches Group1= "element", Group2 = "3" /// Pattern: ^(.+?)\[(\d+)\]$ /// - [GeneratedRegex(@"^(.+?)\[(\d+)\]$")] + [GeneratedRegex(@"^(.+?)(?:\[(\d+)\]|%5B(\d+)%5D)$")] private static partial Regex SubmodelElementListIndex(); ///

@@ -821,19 +849,27 @@ private static IEnumerable FindNodeBySemanticId(SemanticTreeNo return submodelElements?.FirstOrDefault(e => e.IdShort == idShort); } - private static bool TryParseIdShortWithBracketIndex(string segment, out string idShortWithoutIndex, out int index) + private static bool TryParseIdShortWithBracketIndex(string idShort, out string idShortWithoutIndex, out int index) { - var match = SubmodelElementListIndex().Match(segment); - if (match.Success) + var match = SubmodelElementListIndex().Match(idShort); + if (!match.Success) { - idShortWithoutIndex = match.Groups[1].Value; - index = int.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture); - return true; + 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; } - 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) diff --git a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelTemplateService.cs b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelTemplateService.cs index ab72948..385d5ca 100644 --- a/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelTemplateService.cs +++ b/source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/SubmodelRepository/SubmodelTemplateService.cs @@ -5,6 +5,7 @@ using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Base; using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure; using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.AasEnvironment.Providers; +using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; using AasCore.Aas3_0; @@ -131,16 +132,24 @@ private static ISubmodelElement FindMatchingElement(IEnumerable elements, string idShort) @@ -157,7 +166,20 @@ private static SubmodelElementList GetListElementByIdShort(List= list.Value?.Count) + if (index < 0) + { + throw new InternalDataProcessingException(); + } + + if (list.TypeValueListElement is AasSubmodelElements.SubmodelElementCollection or AasSubmodelElements.SubmodelElementList && list.Value?.Count > 0) + { + if (GetCardinality(list.Value.FirstOrDefault()!) is Cardinality.OneToMany or Cardinality.ZeroToMany) + { + return list.Value.FirstOrDefault()!; + } + } + + if (index >= list.Value?.Count) { throw new InternalDataProcessingException(); } @@ -183,7 +205,7 @@ private static SubmodelElementList GetListElementByIdShort(List matches Group1= "element", Group2 = "3" /// Pattern: ^(.+?)\[(\d+)\]$ /// - [GeneratedRegex(@"^(.+?)\[(\d+)\]$")] + [GeneratedRegex(@"^(.+?)(?:\[(\d+)\]|%5B(\d+)%5D)$")] private static partial Regex SubmodelElementListIndex(); /// @@ -201,4 +223,17 @@ private static void ValidateSubmodelId(string submodelId) throw new InternalDataProcessingException(); } } + + 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; + } } diff --git a/source/AAS.TwinEngine.DataEngine/Dockerfile b/source/AAS.TwinEngine.DataEngine/Dockerfile index 5ff23cd..63b409d 100644 --- a/source/AAS.TwinEngine.DataEngine/Dockerfile +++ b/source/AAS.TwinEngine.DataEngine/Dockerfile @@ -1,31 +1,23 @@ -#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. - -FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base -USER app -WORKDIR /app -EXPOSE 8080 - -FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:8.0@sha256:aa05b91be697b83229cb000b90120f0783604ad74ed92a0b45cdf3d1a9c873de AS build +# Install CycloneDX SBOM Generator Package +RUN dotnet tool install --global CycloneDX --version 5.5.0 +ENV PATH="$PATH:/root/.dotnet/tools" +# Build application ARG BUILD_CONFIGURATION=Release -WORKDIR /src -COPY ["AAS.TwinEngine.DataEngine/AAS.TwinEngine.DataEngine.csproj", "AAS.TwinEngine.DataEngine/"] +WORKDIR /App +COPY ["AAS.TwinEngine.DataEngine/", "AAS.TwinEngine.DataEngine/"] RUN dotnet restore "AAS.TwinEngine.DataEngine/AAS.TwinEngine.DataEngine.csproj" -COPY AAS.TwinEngine.DataEngine/ AAS.TwinEngine.DataEngine/ -WORKDIR "/src/AAS.TwinEngine.DataEngine" - -RUN dotnet build "AAS.TwinEngine.DataEngine.csproj" -c "$BUILD_CONFIGURATION" -o /app/build +RUN dotnet publish "AAS.TwinEngine.DataEngine/AAS.TwinEngine.DataEngine.csproj" -c "$BUILD_CONFIGURATION" -o out +# Generate Application SBOM at sbom/bom.xml (omitting dev/test dependencies as they do not appear in final build) +RUN dotnet-CycloneDX "AAS.TwinEngine.DataEngine/AAS.TwinEngine.DataEngine.csproj" -o "sbom/" --exclude-dev --exclude-test-projects --set-nuget-purl --spec-version 1.6 --disable-package-restore -FROM build AS publish -ARG BUILD_CONFIGURATION=Release -RUN dotnet publish "AAS.TwinEngine.DataEngine.csproj" -c "$BUILD_CONFIGURATION" -o /app/publish /p:UseAppHost=false - -FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final +# Create an "app-sbom-artifact" image which can be use to extract the pure APP SBOM after completing the docker build (we want to keep the SBOM for the application separate from the pure linux image SBOM as this provides more details and simplifies vuln management) +FROM scratch AS app-sbom-artifact +COPY --from=build /App/sbom/bom.xml /sbom_application.cyclonedx.xml -RUN useradd -m appuser \ - && mkdir /app \ - && chown appuser:appuser /app - -USER appuser -WORKDIR /app -COPY --from=publish /app/publish . +# Create final image containing application +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine@sha256:a0ce42fe86548363a9602c47fc3bd4cf9c35a2705c68cd98d7ce18ae8735b83c +USER app +WORKDIR /App +COPY --from=build /App/out . ENTRYPOINT ["dotnet", "AAS.TwinEngine.DataEngine.dll"] diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/MultiPluginDataHandler.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/MultiPluginDataHandler.cs index 516f5bd..05ce5f5 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/MultiPluginDataHandler.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/MultiPluginDataHandler.cs @@ -131,7 +131,7 @@ public SemanticTreeNode Merge(SemanticTreeNode globalTree, IList valueTrees) + private SemanticTreeNode MergeBranch(SemanticBranchNode branch, IList valueTrees) { var mergedBranch = new SemanticBranchNode(branch.SemanticId, branch.Cardinality); @@ -241,8 +241,14 @@ private static List MergeBranchNodes(SemanticBranchNode templa } } - private static List FindMatchingNodes(IEnumerable valueTrees, string semanticId) + private List FindMatchingNodes(IEnumerable valueTrees, string semanticId) { + if (semanticId.Contains(_submodelElementIndexContextPrefix, StringComparison.Ordinal)) + { + var indexPrefixIndex = semanticId.IndexOf(_submodelElementIndexContextPrefix, StringComparison.OrdinalIgnoreCase); + semanticId = semanticId[..indexPrefixIndex]; + } + var matches = new List(); foreach (var vt in valueTrees.OfType()) diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandler.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandler.cs index 98623a3..e7d6306 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandler.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Services/PluginDataHandler.cs @@ -11,6 +11,7 @@ using AAS.TwinEngine.DataEngine.DomainModel.Plugin; using AAS.TwinEngine.DataEngine.DomainModel.SubmodelRepository; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Helper; +using AAS.TwinEngine.DataEngine.Infrastructure.Shared; using Json.Schema; @@ -82,7 +83,7 @@ public async Task GetDataForAllShellDescriptorsAsync(i try { - var shellDescriptorData = JsonSerializer.Deserialize(responseContent); + var shellDescriptorData = JsonSerializer.Deserialize(responseContent, JsonSerializationOptions.DeserializationOption); if (shellDescriptorData == null) { logger.LogError("Failed to deserialize All ShellDescriptorData. Response content: {Content}", responseContent); diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProvider.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProvider.cs index 1d59b88..30b7577 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProvider.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/TemplateProvider/Services/ShellTemplateMappingProvider.cs @@ -43,7 +43,7 @@ public string GetProductIdFromRule(string aasIdentifier) }) .Where(x => x.Parts is { Length: >= 1 } && x.Rule.Index > 0 && x.Parts.Length >= x.Rule.Index) .Select(x => x.Parts![x.Rule.Index - 1]) - .FirstOrDefault(); + .FirstOrDefault(extractedId => !string.Equals(extractedId, aasIdentifier, StringComparison.Ordinal)); if (!string.IsNullOrEmpty(productId)) { diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Shared/JsonSerializationOptions.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Shared/JsonSerializationOptions.cs index b3e4a39..5b38e80 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Shared/JsonSerializationOptions.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Shared/JsonSerializationOptions.cs @@ -25,4 +25,9 @@ public static class JsonSerializationOptions DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) } }; + + public static readonly JsonSerializerOptions DeserializationOption = new() + { + PropertyNameCaseInsensitive = true + }; } diff --git a/source/AAS.TwinEngine.DataEngine/Program.cs b/source/AAS.TwinEngine.DataEngine/Program.cs index b603dc6..ff0b43a 100644 --- a/source/AAS.TwinEngine.DataEngine/Program.cs +++ b/source/AAS.TwinEngine.DataEngine/Program.cs @@ -1,6 +1,4 @@ -using System.Globalization; - -using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure; +using AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure; using AAS.TwinEngine.DataEngine.Infrastructure.Monitoring; using AAS.TwinEngine.DataEngine.Infrastructure.Providers.PluginDataProvider.Services; using AAS.TwinEngine.DataEngine.ServiceConfiguration; diff --git a/source/AAS.TwinEngine.DataEngine/appsettings.development.json b/source/AAS.TwinEngine.DataEngine/appsettings.development.json index bbce44b..071ae0f 100644 --- a/source/AAS.TwinEngine.DataEngine/appsettings.development.json +++ b/source/AAS.TwinEngine.DataEngine/appsettings.development.json @@ -13,10 +13,6 @@ { "PluginName": "Plugin1", "PluginUrl": "http://localhost:8086" - }, - { - "PluginName": "Plugin2", - "PluginUrl": "http://localhost:8087" } ] }, @@ -56,6 +52,18 @@ { "templateId": "https://admin-shell.io/idta/SubmodelTemplate/ContactInformation/1/0", "pattern": [ "ContactInformation" ] + }, + { + "templateId": "https://admin-shell.io/ZVEI/TechnicalData/Submodel/1/2", + "pattern": [ "TechnicalData" ] + }, + { + "templateId": "https://admin-shell.io/idta/SubmodelTemplate/CarbonFootprint/1/0", + "pattern": [ "CarbonFootprint" ] + }, + { + "templateId": "https://admin-shell.io/idta/SubmodelTemplate/HandoverDocumentation/2/0", + "pattern": [ "HandoverDocumentation" ] } ], "ShellTemplateMappings": [ diff --git a/source/AAS.TwinEngine.DataEngine/appsettings.json b/source/AAS.TwinEngine.DataEngine/appsettings.json index bbce44b..3c8b7ac 100644 --- a/source/AAS.TwinEngine.DataEngine/appsettings.json +++ b/source/AAS.TwinEngine.DataEngine/appsettings.json @@ -11,12 +11,8 @@ "PluginConfig": { "Plugins": [ { - "PluginName": "Plugin1", - "PluginUrl": "http://localhost:8086" - }, - { - "PluginName": "Plugin2", - "PluginUrl": "http://localhost:8087" + "PluginName": "", + "PluginUrl": "" } ] }, @@ -41,35 +37,22 @@ }, "AasRegistryPreComputed": { "ShellDescriptorCron": "0 */3 * * * *", - "IsPreComputed": true + "IsPreComputed": false }, "TemplateMappingRules": { "SubmodelTemplateMappings": [ { - "templateId": "https://admin-shell.io/idta/SubmodelTemplate/Reliability/1/0", - "pattern": [ "Reliability" ] - }, - { - "templateId": "https://admin-shell.io/idta/SubmodelTemplate/DigitalNameplate/3/0", - "pattern": [ "Nameplate" ] - }, - { - "templateId": "https://admin-shell.io/idta/SubmodelTemplate/ContactInformation/1/0", - "pattern": [ "ContactInformation" ] + "templateId": "", + "pattern": [ "" ] } ], "ShellTemplateMappings": [ { - "templateId": "https://mm-software.com/aas/aasTemplate", + "templateId": "", "pattern": [ "" ] } ], "AasIdExtractionRules": [ - { - "Pattern": "Regex", - "Index": 3, - "Separator": ":" - }, { "Pattern": "Regex", "Index": 6, diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/AAS.TwinEngine.Plugin.TestPlugin.UnitTests.csproj b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/AAS.TwinEngine.Plugin.TestPlugin.UnitTests.csproj new file mode 100644 index 0000000..617925c --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/AAS.TwinEngine.Plugin.TestPlugin.UnitTests.csproj @@ -0,0 +1,34 @@ + + + + net8.0 + enable + enable + + false + true + true + IDE1006, IDE0058 + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Manifest/Handler/ManifestHandlerTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Manifest/Handler/ManifestHandlerTests.cs new file mode 100644 index 0000000..520ea07 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Manifest/Handler/ManifestHandlerTests.cs @@ -0,0 +1,42 @@ +using AAS.TwinEngine.Plugin.TestPlugin.Api.Manifest.Handler; +using AAS.TwinEngine.Plugin.TestPlugin.Api.Manifest.Responses; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.Manifest; +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.Manifest; + +using Microsoft.Extensions.Logging; + +using NSubstitute; +using NSubstitute.ExceptionExtensions; + +namespace AAS.TwinEngine.Plugin.TestPlugin.UnitTests.Api.Manifest.Handler; + +public class ManifestHandlerTests +{ + private readonly ILogger _logger = Substitute.For>(); + private readonly IManifestService _manifestService = Substitute.For(); + private readonly ManifestHandler _sut; + + public ManifestHandlerTests() => _sut = new ManifestHandler(_logger, _manifestService); + + [Fact] + public async Task GetManifestData_ShouldReturnDto_WhenManifestIsAvailable() + { + var manifest = new ManifestData { Capabilities = new CapabilitiesData { HasAssetInformation = true, HasShellDescriptor = true }, SupportedSemanticIds = ["test"] }; + var expectedDto = new ManifestDto { Capabilities = new CapabilitiesDto { HasAssetInformation = true, HasShellDescriptor = true }, SupportedSemanticIds = ["test"] }; + _manifestService.GetManifestData(Arg.Any()) + .Returns(Task.FromResult(manifest)); + + var result = await _sut.GetManifestData(CancellationToken.None); + + Assert.Equal(expectedDto.ToString(), result.ToString()); + } + + [Fact] + public async Task GetManifestData_ShouldThrowException_WhenServiceThrows() + { + _manifestService.GetManifestData(Arg.Any()) + .Throws(new Exception("Service failure")); + + await Assert.ThrowsAsync(() => _sut.GetManifestData(CancellationToken.None)); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Manifest/ManifestControllerTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Manifest/ManifestControllerTests.cs new file mode 100644 index 0000000..c21027b --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Manifest/ManifestControllerTests.cs @@ -0,0 +1,30 @@ +using AAS.TwinEngine.Plugin.TestPlugin.Api.Manifest; +using AAS.TwinEngine.Plugin.TestPlugin.Api.Manifest.Handler; +using AAS.TwinEngine.Plugin.TestPlugin.Api.Manifest.Responses; + +using Microsoft.AspNetCore.Mvc; + +using NSubstitute; + +namespace AAS.TwinEngine.Plugin.TestPlugin.UnitTests.Api.Manifest; + +public class ManifestControllerTests +{ + private readonly IManifestHandler _manifestHandler = Substitute.For(); + private readonly ManifestController _sut; + + public ManifestControllerTests() => _sut = new ManifestController(_manifestHandler); + + [Fact] + public async Task RetrieveManifestDataAsync_ShouldReturnOk_WhenDataIsAvailable() + { + var expectedManifest = new ManifestDto { Capabilities = new CapabilitiesDto(), SupportedSemanticIds = ["abc"] }; + _manifestHandler.GetManifestData(Arg.Any()) + .Returns(Task.FromResult(expectedManifest)); + + var result = await _sut.RetrieveManifestDataAsync(CancellationToken.None); + + var okResult = Assert.IsType(result.Result); + Assert.Equal(expectedManifest, okResult.Value); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Manifest/MappingProfiles/ManifestMappingProfileTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Manifest/MappingProfiles/ManifestMappingProfileTests.cs new file mode 100644 index 0000000..81bcc8d --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Manifest/MappingProfiles/ManifestMappingProfileTests.cs @@ -0,0 +1,37 @@ +using AAS.TwinEngine.Plugin.TestPlugin.Api.Manifest.MappingProfiles; +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.Manifest; + +namespace AAS.TwinEngine.Plugin.TestPlugin.UnitTests.Api.Manifest.MappingProfiles; + +public class ManifestMappingProfileTests +{ + [Fact] + public void ToDto_ShouldMapManifestDataToManifestDtoCorrectly() + { + var manifestData = new ManifestData + { + Capabilities = new CapabilitiesData + { + HasAssetInformation = true, + HasShellDescriptor = false + }, + SupportedSemanticIds = ["semantic1", "semantic2"] + }; + + var result = manifestData.ToDto(); + + Assert.NotNull(result); + Assert.NotNull(result.Capabilities); + Assert.Equal(manifestData.Capabilities.HasAssetInformation, result.Capabilities.HasAssetInformation); + Assert.Equal(manifestData.Capabilities.HasShellDescriptor, result.Capabilities.HasShellDescriptor); + Assert.Equal(manifestData.SupportedSemanticIds, result.SupportedSemanticIds); + } + + [Fact] + public void ToDto_ShouldThrowException_WhenManifestDataIsNull() + { + ManifestData? manifestData = null; + + Assert.Throws(() => manifestData!.ToDto()); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/MetaData/Handler/MetaDataHandlerTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/MetaData/Handler/MetaDataHandlerTests.cs new file mode 100644 index 0000000..da6ea4f --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/MetaData/Handler/MetaDataHandlerTests.cs @@ -0,0 +1,101 @@ +using AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData.Handler; +using AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData.Requests; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Constants; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Exceptions; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.MetaData; +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.MetaData; + +using Microsoft.Extensions.Logging; + +using NSubstitute; + +namespace AAS.TwinEngine.Plugin.TestPlugin.UnitTests.Api.MetaData.Handler; + +public class MetaDataHandlerTests +{ + private readonly ILogger _logger = Substitute.For>(); + private readonly IMetaDataService _shellDescriptorService = Substitute.For(); + private readonly MetaDataHandler _sut; + + public MetaDataHandlerTests() => _sut = new MetaDataHandler(_logger, _shellDescriptorService); + + [Fact] + public async Task GetShellDescriptors_ReturnsShellDescriptorsDto_WhenDescriptorsExist() + { + var request = new GetShellDescriptorsRequest(10, "cursor123"); + var shellDescriptorsData = new ShellDescriptorsData + { + PagingMetaData = new PagingMetaData() { Cursor = "nextCursor" }, + Result = new List() + { + new() { Id = "desc1" }, + new() { Id = "desc2" } + } + }; + _shellDescriptorService.GetShellDescriptorsAsync(request.Limit, request.Cursor, Arg.Any()) + .Returns(shellDescriptorsData); + + var result = await _sut.GetShellDescriptors(request, CancellationToken.None); + + Assert.NotNull(result); + Assert.Equal(2, result.Result!.Count); + Assert.Equal("desc1", result.Result![0].Id); + Assert.Equal("desc2", result.Result![1].Id); + Assert.Equal("nextCursor", result.PagingMetaData!.Cursor); + } + + [Fact] + public async Task GetShellDescriptors_ThrowBadRequest_WhenLimitIsZero() + { + var request = new GetShellDescriptorsRequest(0, "cursor123"); + + var record = await Assert.ThrowsAsync(() => + _sut.GetShellDescriptors(request, CancellationToken.None)); + Assert.Equal(ExceptionMessages.InvalidRequestedLimit, record.Message); + } + + [Fact] + public async Task GetShellDescriptor_ReturnsShellDescriptor_WhenExists() + { + var request = new GetShellDescriptorRequest("test"); + var shellMetaData = new ShellDescriptorData { Id = "test" }; + _shellDescriptorService.GetShellDescriptorAsync("test", Arg.Any()).Returns(shellMetaData); + + var result = await _sut.GetShellDescriptor(request, CancellationToken.None); + + Assert.Equal(result.Id, shellMetaData.Id); + } + + [Fact] + public async Task GetShellDescriptor_ThrowsNotFoundException_WhenShellDoesNotExist() + { + var request = new GetShellDescriptorRequest("test"); + _shellDescriptorService.GetShellDescriptorAsync("test", Arg.Any())!.Returns((ShellDescriptorData)null!); + + await Assert.ThrowsAsync(() => _sut.GetShellDescriptor(request, CancellationToken.None)); + } + + [Fact] + public async Task GetAsset_ReturnsAsset_WhenExists() + { + var request = new GetAssetRequest("test-shell"); + var assetMetaData = new AssetData { GlobalAssetId = "test-shell" }; + _shellDescriptorService.GetAssetAsync("test-shell", Arg.Any()) + .Returns(assetMetaData); + + var result = await _sut.GetAsset(request, CancellationToken.None); + + Assert.Equal(assetMetaData.GlobalAssetId, result.GlobalAssetId); + } + + [Fact] + public async Task GetAsset_ThrowsNotFoundException_WhenAssetDoesNotExist() + { + var request = new GetAssetRequest("test-shell"); + _shellDescriptorService.GetAssetAsync("test-shell", Arg.Any()) + .Returns((AssetData)null!); + + await Assert.ThrowsAsync(() => + _sut.GetAsset(request, CancellationToken.None)); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/MetaData/MappingProfiles/AssetMappingProfileTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/MetaData/MappingProfiles/AssetMappingProfileTests.cs new file mode 100644 index 0000000..030b175 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/MetaData/MappingProfiles/AssetMappingProfileTests.cs @@ -0,0 +1,69 @@ +using AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData.MappingProfiles; +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.MetaData; + +namespace AAS.TwinEngine.Plugin.TestPlugin.UnitTests.Api.MetaData.MappingProfiles; + +public class AssetMappingProfileTests +{ + [Fact] + public void ToDto_MapsAllFieldsCorrectly_WhenThumbnailIsPresent() + { + var assetData = new AssetData + { + GlobalAssetId = "asset-001", + SpecificAssetIds = [new SpecificAssetIdsData { Name = "SerialNumber", Value = "SN001" }], + DefaultThumbnail = new DefaultThumbnailData + { + ContentType = "image/png", + Path = "/images/thumb.png" + } + }; + + var result = assetData.ToDto(); + + Assert.NotNull(result); + Assert.Equal("asset-001", result.GlobalAssetId); + Assert.Single(result.SpecificAssetIds!); + Assert.Equal("SerialNumber", result.SpecificAssetIds![0].Name); + Assert.Equal("SN001", result.SpecificAssetIds[0].Value); + Assert.NotNull(result.DefaultThumbnail); + Assert.Equal("image/png", result.DefaultThumbnail.ContentType); + Assert.Equal("/images/thumb.png", result.DefaultThumbnail.Path); + } + + [Fact] + public void ToDto_SetsDefaultThumbnailToNull_WhenThumbnailIsMissing() + { + var assetData = new AssetData + { + GlobalAssetId = "asset-002", + SpecificAssetIds = [], + DefaultThumbnail = null + }; + + var result = assetData.ToDto(); + + Assert.NotNull(result); + Assert.Equal("asset-002", result.GlobalAssetId); + Assert.Empty(result.SpecificAssetIds!); + Assert.Null(result.DefaultThumbnail); + } + + [Fact] + public void ToDto_HandlesNullSpecificAssetIds() + { + var assetData = new AssetData + { + GlobalAssetId = "asset-003", + SpecificAssetIds = null, + DefaultThumbnail = null + }; + + var result = assetData.ToDto(); + + Assert.NotNull(result); + Assert.Equal("asset-003", result.GlobalAssetId); + Assert.Null(result.DefaultThumbnail); + Assert.Null(result.SpecificAssetIds); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/MetaData/MappingProfiles/ShellDescriptorProfileTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/MetaData/MappingProfiles/ShellDescriptorProfileTests.cs new file mode 100644 index 0000000..700a250 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/MetaData/MappingProfiles/ShellDescriptorProfileTests.cs @@ -0,0 +1,73 @@ +using AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData.MappingProfiles; +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.MetaData; + +namespace AAS.TwinEngine.Plugin.TestPlugin.UnitTests.Api.MetaData.MappingProfiles; + +public class ShellDescriptorProfileTests +{ + [Fact] + public void ToDto_MapsAllFieldsCorrectly() + { + var shellDescriptorData = new ShellDescriptorData + { + Id = "shell-001", + GlobalAssetId = "asset-001", + IdShort = "Shell001", + SpecificAssetIds = + [ + new SpecificAssetIdsData { Name = "SerialNumber", Value = "SN001" }, + new SpecificAssetIdsData { Name = "PartNumber", Value = "PN001" } + ] + }; + + var result = shellDescriptorData.ToDto(); + + Assert.NotNull(result); + Assert.Equal("shell-001", result.Id); + Assert.Equal("asset-001", result.GlobalAssetId); + Assert.Equal("Shell001", result.IdShort); + Assert.Equal(2, result.SpecificAssetIds!.Count); + Assert.Equal("SerialNumber", result.SpecificAssetIds[0].Name); + Assert.Equal("SN001", result.SpecificAssetIds[0].Value); + } + + [Fact] + public void ToDto_HandlesNullSpecificAssetIds() + { + var shellDescriptorData = new ShellDescriptorData + { + Id = "shell-002", + GlobalAssetId = "asset-002", + IdShort = "Shell002", + SpecificAssetIds = null + }; + + var result = shellDescriptorData.ToDto(); + + Assert.NotNull(result); + Assert.Equal("shell-002", result.Id); + Assert.Equal("asset-002", result.GlobalAssetId); + Assert.Equal("Shell002", result.IdShort); + Assert.Null(result.SpecificAssetIds); + } + + [Fact] + public void ToDto_HandlesEmptySpecificAssetIds() + { + var shellDescriptorData = new ShellDescriptorData + { + Id = "shell-003", + GlobalAssetId = "asset-003", + IdShort = "Shell003", + SpecificAssetIds = [] + }; + + var result = shellDescriptorData.ToDto(); + + Assert.NotNull(result); + Assert.Equal("shell-003", result.Id); + Assert.Equal("asset-003", result.GlobalAssetId); + Assert.Equal("Shell003", result.IdShort); + Assert.Empty(result.SpecificAssetIds!); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/MetaData/MappingProfiles/ShellDescriptorsProfileTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/MetaData/MappingProfiles/ShellDescriptorsProfileTests.cs new file mode 100644 index 0000000..b6db442 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/MetaData/MappingProfiles/ShellDescriptorsProfileTests.cs @@ -0,0 +1,56 @@ +using AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData.MappingProfiles; +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.MetaData; + +namespace AAS.TwinEngine.Plugin.TestPlugin.UnitTests.Api.MetaData.MappingProfiles; + +public class ShellDescriptorsProfileTests +{ + [Fact] + public void ToDto_MapsAllFieldsCorrectly() + { + var shellDescriptorsData = new ShellDescriptorsData + { + PagingMetaData = new PagingMetaData { Cursor = "cursor-123" }, + Result = new List + { + new() + { + Id = "shell-001", + GlobalAssetId = "asset-001", + IdShort = "Shell001", + SpecificAssetIds = [new SpecificAssetIdsData { Name = "SerialNumber", Value = "SN001" }] + } + } + }; + + var result = shellDescriptorsData.ToDto(); + + Assert.NotNull(result); + Assert.NotNull(result.PagingMetaData); + Assert.Equal("cursor-123", result.PagingMetaData.Cursor); + Assert.Single(result.Result!); + Assert.Equal("shell-001", result.Result![0].Id); + Assert.Equal("asset-001", result.Result[0].GlobalAssetId); + Assert.Equal("Shell001", result.Result[0].IdShort); + Assert.Single(result.Result[0]!.SpecificAssetIds!); + Assert.Equal("SerialNumber", result.Result[0].SpecificAssetIds?[0]!.Name); + Assert.Equal("SN001", result.Result[0].SpecificAssetIds?[0].Value); + } + + [Fact] + public void ToDto_HandlesNullResultList() + { + var shellDescriptorsData = new ShellDescriptorsData + { + PagingMetaData = new PagingMetaData { Cursor = "cursor-456" }, + Result = null + }; + + var result = shellDescriptorsData.ToDto(); + + Assert.NotNull(result); + Assert.NotNull(result.PagingMetaData); + Assert.Equal("cursor-456", result.PagingMetaData.Cursor); + Assert.Null(result.Result); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/MetaData/MetaDataControllerTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/MetaData/MetaDataControllerTests.cs new file mode 100644 index 0000000..08e1ee9 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/MetaData/MetaDataControllerTests.cs @@ -0,0 +1,148 @@ +using AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData; +using AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData.Handler; +using AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData.Requests; +using AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData.Responses; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Exceptions; + +using Microsoft.AspNetCore.Mvc; + +using NSubstitute; +using NSubstitute.ExceptionExtensions; + +namespace AAS.TwinEngine.Plugin.TestPlugin.UnitTests.Api.MetaData; + +public class MetaDataControllerTests +{ + private readonly IMetaDataHandler _handler = Substitute.For(); + private readonly MetaDataController _sut; + private const string AasIdentifier = "dGVzdA=="; + + public MetaDataControllerTests() => _sut = new MetaDataController(_handler); + + [Fact] + public async Task GetShellDescriptorsAsync_ReturnsOk_WithShellList() + { + var request = new GetShellDescriptorsRequest(null, null); + var expectedShells = new ShellDescriptorsDto(); + _handler.GetShellDescriptors(request, Arg.Any()).Returns(expectedShells); + + var result = await _sut.GetShellDescriptorsAsync(null, null, CancellationToken.None); + + var okResult = Assert.IsType(result.Result); + var actualShells = Assert.IsType(okResult.Value); + Assert.Equal(expectedShells, actualShells); + } + + [Fact] + public async Task GetShellDescriptorsAsync_ThrowsNotFoundException_Returns404() + { + var request = new GetShellDescriptorsRequest(null, null); + _handler.GetShellDescriptors(request, Arg.Any()) + .Throws(new NotFoundException("Shell not found")); + + await Assert.ThrowsAsync(() => _sut.GetShellDescriptorsAsync(null, null, CancellationToken.None)); + } + + [Fact] + public async Task GetShellDescriptorsAsync_ThrowsBadRequestException_Returns400() + { + var request = new GetShellDescriptorsRequest(null, null); + _handler.GetShellDescriptors(request, Arg.Any()) + .Throws(new BadRequestException("Invalid request")); + + await Assert.ThrowsAsync(() => _sut.GetShellDescriptorsAsync(null, null, CancellationToken.None)); + } + + [Fact] + public async Task GetShellDescriptorsAsync_ThrowsException_Returns500() + { + var request = new GetShellDescriptorsRequest(null, null); + _handler.GetShellDescriptors(request, Arg.Any()) + .Throws(new Exception("Unexpected error")); + + await Assert.ThrowsAsync(() => _sut.GetShellDescriptorsAsync(null, null, CancellationToken.None)); + } + + [Fact] + public async Task GetShellDescriptorAsync_ReturnsOk_WithShell() + { + var expectedShell = new ShellDescriptorDto(); + _handler.GetShellDescriptor(Arg.Any(), Arg.Any()) + .Returns(expectedShell); + + var result = await _sut.GetShellDescriptorAsync(AasIdentifier, CancellationToken.None); + + var okResult = Assert.IsType(result.Result); + var actualShell = Assert.IsType(okResult.Value); + Assert.Equal(expectedShell, actualShell); + } + + [Fact] + public async Task GetShellDescriptorAsync_ThrowsNotFoundException_Returns404() + { + _handler.GetShellDescriptor(Arg.Any(), Arg.Any()) + .Throws(new NotFoundException("Shell not found")); + + await Assert.ThrowsAsync(() => _sut.GetShellDescriptorAsync(AasIdentifier, CancellationToken.None)); + } + + [Fact] + public async Task GetShellDescriptorAsync_ThrowsBadRequestException_Returns400() + { + _handler.GetShellDescriptor(Arg.Any(), Arg.Any()) + .Throws(new BadRequestException("Invalid AAS identifier")); + + await Assert.ThrowsAsync(() => _sut.GetShellDescriptorAsync(AasIdentifier, CancellationToken.None)); + } + + [Fact] + public async Task GetShellDescriptorAsync_ThrowsException_Returns500() + { + _handler.GetShellDescriptor(Arg.Any(), Arg.Any()) + .Throws(new Exception("Unexpected error")); + + await Assert.ThrowsAsync(() => _sut.GetShellDescriptorAsync(AasIdentifier, CancellationToken.None)); + } + + [Fact] + public async Task GetAssetAsync_ReturnsOk_WithAssetInformation() + { + var expectedAssetInfo = new AssetDto(); + _handler.GetAsset(Arg.Any(), Arg.Any()) + .Returns(expectedAssetInfo); + + var result = await _sut.GetAssetAsync(AasIdentifier, CancellationToken.None); + + var okResult = Assert.IsType(result.Result); + var actualAssetInfo = Assert.IsType(okResult.Value); + Assert.Equal(expectedAssetInfo, actualAssetInfo); + } + + [Fact] + public async Task GetAssetAsync_ThrowsNotFoundException_Returns404() + { + _handler.GetAsset(Arg.Any(), Arg.Any()) + .Throws(new NotFoundException("Asset not found")); + + await Assert.ThrowsAsync(() => _sut.GetAssetAsync(AasIdentifier, CancellationToken.None)); + } + + [Fact] + public async Task GetAssetAsync_ThrowsBadRequestException_Returns400() + { + _handler.GetAsset(Arg.Any(), Arg.Any()) + .Throws(new BadRequestException("Invalid request")); + + await Assert.ThrowsAsync(() => _sut.GetAssetAsync(AasIdentifier, CancellationToken.None)); + } + + [Fact] + public async Task GetAssetAsync_ThrowsException_Returns500() + { + _handler.GetAsset(Arg.Any(), Arg.Any()) + .Throws(new Exception("Unexpected error")); + + await Assert.ThrowsAsync(() => + _sut.GetAssetAsync(AasIdentifier, CancellationToken.None)); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Handler/SubmodelHandlerTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Handler/SubmodelHandlerTests.cs new file mode 100644 index 0000000..cabd0f7 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Handler/SubmodelHandlerTests.cs @@ -0,0 +1,143 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +using AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel.Handler; +using AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel.Requests; +using AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel.Services; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.Submodel; +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.Submodel; + +using Json.Schema; + +using Microsoft.Extensions.Logging; + +using NSubstitute; + +namespace AAS.TwinEngine.Plugin.TestPlugin.UnitTests.Api.Submodel.Handler; + +public class SubmodelHandlerTests +{ + private const string JsonSchemaString = @"{ + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""https://admin-shell.io/zvei/nameplate/1/0/ContactInformations"": { + ""type"": ""object"", + ""properties"": { + ""https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation"": { + ""anyOf"": [ + { ""$ref"": ""#/definitions/ContactInformation"" }, + { + ""type"": ""array"", + ""items"": { ""$ref"": ""#/definitions/ContactInformation"" } + } + ] + } + }, + ""required"": [ + ""https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation"" + ] + } + }, + ""definitions"": { + ""ContactInformation"": { + ""type"": ""object"", + ""properties"": { + ""0173-1#02-AAO204#003"": { ""type"": ""string"" }, + ""https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Language"": { ""type"": ""string"" } + }, + ""required"": [ + ""0173-1#02-AAO204#003"", + ""https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Language"" + ] + } + }}"; + + private const string JsonResponse = @"{ + ""https://admin-shell.io/zvei/nameplate/1/0/ContactInformations"": { + ""https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation"": { + ""0173-1#02-AAO204#003"": ""John Doe"", + ""https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Language"": ""en"" + } + } + }"; + + private readonly JsonSerializerOptions _options = new() + { + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + ReadCommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true, + Converters = + { + new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) + } + }; + + private readonly JsonSchema JsonSchemaRequest; + private readonly JsonObject _expectedResponse; + private readonly ILogger _logger = Substitute.For>(); + private readonly ISubmodelService _pluginService = Substitute.For(); + private readonly IJsonSchemaParser _jsonSchemaParser = Substitute.For(); + private readonly SubmodelHandler _sut; + private readonly GetSubmodelDataRequest _request; + private readonly SemanticBranchNode _semanticTree; + private readonly SemanticBranchNode _sematicTreeWithData; + private readonly ISemanticTreeHandler _semanticTreeHandler = Substitute.For(); + + public SubmodelHandlerTests() + { + _semanticTree = new SemanticBranchNode("https://admin-shell.io/zvei/nameplate/1/0/ContactInformations", DataType.Object); + var contactNode = new SemanticBranchNode("https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation", DataType.Object); + contactNode.AddChild(new SemanticLeafNode("0173-1#02-AAO204#003", DataType.String, "")); + contactNode.AddChild(new SemanticLeafNode( + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Language", DataType.String, "")); + _semanticTree.AddChild(contactNode); + + _sematicTreeWithData = new SemanticBranchNode("https://admin-shell.io/zvei/nameplate/1/0/ContactInformations", DataType.Object); + var contactNodeWithData = new SemanticBranchNode("https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation", DataType.Object); + contactNodeWithData.AddChild(new SemanticLeafNode("0173-1#02-AAO204#003", DataType.String, "John Doe")); + contactNodeWithData.AddChild(new SemanticLeafNode( + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Language", DataType.String, "en")); + _sematicTreeWithData.AddChild(contactNodeWithData); + + _expectedResponse = JsonNode.Parse(JsonResponse)!.AsObject(); + JsonSchemaRequest = JsonSerializer.Deserialize(JsonSchemaString, _options); + _sut = new SubmodelHandler(_logger, _pluginService, _jsonSchemaParser, _semanticTreeHandler); + _request = new GetSubmodelDataRequest("ContactInformation", JsonSchemaRequest); + } + + [Fact] + public async Task Handle_ReturnsJsonObject_WhenParserAndServiceSucceed() + { + _jsonSchemaParser.ParseJsonSchema(JsonSchemaRequest).Returns(_semanticTree); + _pluginService.GetValuesBySemanticIds(_semanticTree, "ContactInformation").Returns(_sematicTreeWithData); + _semanticTreeHandler.GetJson(_sematicTreeWithData, JsonSchemaRequest).Returns(_expectedResponse); + + var actual = await _sut.GetSubmodelData(_request, CancellationToken.None); + + Assert.Same(_expectedResponse, actual); + _jsonSchemaParser.Received(1).ParseJsonSchema(JsonSchemaRequest); + _pluginService.Received(1).GetValuesBySemanticIds(_semanticTree, "ContactInformation"); + _semanticTreeHandler.Received(1).GetJson(_sematicTreeWithData, JsonSchemaRequest); + } + + [Fact] + public async Task Handle_CallsServiceEvenWhenParserReturnsEmptyList() + { + var emptyBranch = new SemanticBranchNode("emptyRoot", DataType.Object); + _jsonSchemaParser.ParseJsonSchema(JsonSchemaRequest).Returns(emptyBranch); + var emptyResponse = new JsonObject(); + _pluginService.GetValuesBySemanticIds(emptyBranch, "ContactInformation").Returns(emptyBranch); + _semanticTreeHandler.GetJson(emptyBranch, JsonSchemaRequest).Returns(emptyResponse); + + var actual = await _sut.GetSubmodelData(_request, CancellationToken.None); + + Assert.Same(emptyResponse, actual); + _jsonSchemaParser.Received(1).ParseJsonSchema(JsonSchemaRequest); + _pluginService.Received(1).GetValuesBySemanticIds(emptyBranch, "ContactInformation"); + _semanticTreeHandler.Received(1).GetJson(emptyBranch, JsonSchemaRequest); + } +} + diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaParserTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaParserTests.cs new file mode 100644 index 0000000..a0513cd --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaParserTests.cs @@ -0,0 +1,389 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +using AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel.Services; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Exceptions; +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.Submodel; + +using Json.Schema; + +using Microsoft.Extensions.Logging; + +using NSubstitute; + +namespace AAS.TwinEngine.Plugin.TestPlugin.UnitTests.Api.Submodel.Services; + +public class JsonSchemaParserTests +{ + private readonly JsonSerializerOptions _options = new() + { + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + ReadCommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true, + Converters = + { + new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) + } + }; + + private readonly string _invalidJson = @"{""Invalid json"": {}}"; + + private const string ValidationFailSchemaString = @"{ ""type"" : ""null"" }"; + + private const string NoPropertiesSchemaString = @"{ + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"" + }"; + + private const string SimpleSchemaString = @"{ + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""foo"": { ""type"": ""string"" } + }}"; + + private const string NestedSchemaString = @"{ + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""parent"": { + ""type"": ""object"", + ""properties"": { + ""child"": { ""type"": ""number"" } + }}}}"; + + private const string ArraySchemaString = @"{ + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""list"": { + ""type"": ""array"", + ""properties"": { ""id"": { ""type"": ""integer"" } } + }}}"; + + private const string ArrayWithRefSchemaString = @"{ + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""items"": { + ""type"": ""array"", + ""$ref"": ""#/definitions/ItemDef"" + } + }, + ""definitions"": { + ""ItemDef"": { + ""type"": ""object"", + ""properties"": { ""val"": { ""type"": ""integer"" } } + } + } + }"; + + private const string AllDataTypesSchemaWithRefString = @"{ + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""root"" :{ + ""type"" : ""array"", + ""properties"" : { + ""stringField"": { ""type"": ""string"" }, + ""numberField"": { ""type"": ""number"" }, + ""integerField"": { ""type"": ""integer"" }, + ""booleanField"": { ""type"": ""boolean"" }, + ""arrayField"": { + ""$ref"" : ""#/definitions/itemField"" + }, + ""objectField"": { + ""type"": ""object"", + ""properties"": { + ""nestedProp"": { ""type"": ""string"" } + } + }, + ""nullField"": { ""type"": ""null"" } + } + } + }, + ""definitions"" : { + ""itemField"":{ + ""type"":""array"", + ""properties"": { + ""items"": { ""type"": ""string"" } + } + } + } + }"; + + private readonly ILogger _logger; + private readonly JsonSchemaParser _sut; + + public JsonSchemaParserTests() + { + _logger = Substitute.For>(); + _sut = new JsonSchemaParser(_logger); + } + + [Fact] + public void ParseJsonSchema_InvalidJson_ThrowsBadRequestException() + { + var InvalidJsonSchema = JsonSerializer.Deserialize(_invalidJson, _options); + + Assert.Throws(() => _sut.ParseJsonSchema(InvalidJsonSchema)); + } + + [Fact] + public void ParseJsonSchema_SchemaValidationFails_ThrowsBadRequestException() + { + var ValidationFailSchema = JsonSerializer.Deserialize(ValidationFailSchemaString, _options); + + Assert.Throws(() => _sut.ParseJsonSchema(ValidationFailSchema)); + } + + [Fact] + public void ParseJsonSchema_NoRootProperties_ThrowsBadRequestException() + { + var NoPropertiesSchema = JsonSerializer.Deserialize(NoPropertiesSchemaString, _options); + + Assert.Throws(() => _sut.ParseJsonSchema(NoPropertiesSchema)); + } + + [Fact] + public void ParseJsonSchema_SimpleSchema_ReturnsLeafNode() + { + var SimpleSchema = JsonSerializer.Deserialize(SimpleSchemaString, _options); + + var node = _sut.ParseJsonSchema(SimpleSchema); + + Assert.NotNull(node); + Assert.IsType(node); + var leaf = (SemanticLeafNode)node; + Assert.Equal("foo", leaf.SemanticId); + Assert.Equal(string.Empty, leaf.Value); + } + + [Fact] + public void ParseJsonSchema_NestedObject_ReturnsBranchNodeWithChild() + { + var NestedSchema = JsonSerializer.Deserialize(NestedSchemaString, _options); + + var node = _sut.ParseJsonSchema(NestedSchema); + + Assert.NotNull(node); + Assert.IsType(node); + var branch = (SemanticBranchNode)node; + Assert.Equal("parent", branch.SemanticId); + Assert.Single(branch.Children); + var child = branch.Children[0] as SemanticLeafNode; + Assert.NotNull(child); + Assert.Equal("child", child.SemanticId); + } + + [Fact] + public void ParseJsonSchema_ArrayOfObjects_ReturnsBranchNodeWithLeafChild() + { + var arraySchema = JsonSerializer.Deserialize(ArraySchemaString, _options); + + var node = _sut.ParseJsonSchema(arraySchema!); + + Assert.NotNull(node); + Assert.IsType(node); + var branch = (SemanticBranchNode)node; + Assert.Equal("list", branch.SemanticId); + Assert.Single(branch.Children); + var child = branch.Children[0] as SemanticLeafNode; + Assert.NotNull(child); + Assert.Equal("id", child.SemanticId); + } + + [Fact] + public void ParseJsonSchema_ArrayWithRef_ReturnsBranchNodeWithLeafChild() + { + var arrayWithRefSchema = JsonSerializer.Deserialize(ArrayWithRefSchemaString, _options); + + var node = _sut.ParseJsonSchema(arrayWithRefSchema!); + + Assert.NotNull(node); + Assert.IsType(node); + var branch = (SemanticBranchNode)node; + Assert.Equal("items", branch.SemanticId); + Assert.Single(branch.Children); + var child = branch.Children[0] as SemanticLeafNode; + Assert.NotNull(child); + Assert.Equal("val", child.SemanticId); + } + + [Fact] + public void ParseJsonSchema_AllDataTypeSchemaWithRef_ReturnsBranchNodeWithLeafChild() + { + var allDataTypesSchemaWithRef = JsonSerializer.Deserialize(AllDataTypesSchemaWithRefString, _options); + + var node = _sut.ParseJsonSchema(allDataTypesSchemaWithRef!); + + Assert.NotNull(node); + Assert.IsType(node); + var branch = (SemanticBranchNode)node; + Assert.Equal("root", branch.SemanticId); + Assert.Equal(DataType.Array, branch.DataType); + var child1 = branch.Children[0] as SemanticLeafNode; + Assert.Equal("stringField", child1!.SemanticId); + Assert.Equal(DataType.String, child1.DataType); + var child2 = branch.Children[1] as SemanticLeafNode; + Assert.Equal("numberField", child2!.SemanticId); + Assert.Equal(DataType.Number, child2.DataType); + var child3 = branch.Children[2] as SemanticLeafNode; + Assert.Equal("integerField", child3!.SemanticId); + Assert.Equal(DataType.Integer, child3.DataType); + var child4 = branch.Children[3] as SemanticLeafNode; + Assert.Equal("booleanField", child4!.SemanticId); + Assert.Equal(DataType.Boolean, child4.DataType); + var branch1 = branch.Children[4] as SemanticBranchNode; + Assert.Equal("arrayField", branch1?.SemanticId); + Assert.Equal(DataType.Array, branch1?.DataType); + var leaf1 = branch1!.Children[0] as SemanticLeafNode; + Assert.Equal("items", leaf1!.SemanticId); + Assert.Equal(DataType.String, leaf1.DataType); + } + + [Fact] + public void ParseJsonSchema_ReferenceNotFound_ReturnsLeafNode() + { + const string SchemaString = @"{ + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""mystery"": { ""$ref"": ""#/definitions/DoesNotExist"" } + } + }"; + var schema = JsonSerializer.Deserialize(SchemaString, _options); + + var node = _sut.ParseJsonSchema(schema!); + + Assert.NotNull(node); + Assert.IsType(node); + var leaf = (SemanticLeafNode)node; + Assert.Equal("mystery", leaf.SemanticId); + Assert.Equal(DataType.Unknown, leaf.DataType); + } + + [Fact] + public void ParseJsonSchema_ReferenceToObjectDefinition_ReturnsBranchNode() + { + const string SchemaString = @"{ + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""person"": { ""$ref"": ""#/definitions/Person"" } + }, + ""definitions"": { + ""Person"": { + ""type"": ""object"", + ""properties"": { + ""name"": { ""type"": ""string"" }, + ""age"": { ""type"": ""integer"" } + } + } + } + }"; + var schema = JsonSerializer.Deserialize(SchemaString, _options); + + var node = _sut.ParseJsonSchema(schema); + + Assert.NotNull(node); + Assert.IsType(node); + var branch = (SemanticBranchNode)node; + Assert.Equal("person", branch.SemanticId); + Assert.Equal(DataType.Object, branch.DataType); + Assert.Collection(branch.Children, + child => + { + var leaf = Assert.IsType(child); + Assert.Equal("name", leaf.SemanticId); + Assert.Equal(DataType.String, leaf.DataType); + }, + child => + { + var leaf = Assert.IsType(child); + Assert.Equal("age", leaf.SemanticId); + Assert.Equal(DataType.Integer, leaf.DataType); + }); + } + + [Fact] + public void ParseJsonSchema_ReferenceToArrayDefinition_ReturnsBranchNode() + { + const string SchemaString = @"{ + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""primes"": { ""$ref"": ""#/definitions/PrimeList"" } + }, + ""definitions"": { + ""PrimeList"": { + ""type"": ""array"" + } + } + }"; + var schema = JsonSerializer.Deserialize(SchemaString, _options); + + var node = _sut.ParseJsonSchema(schema); + + Assert.NotNull(node); + Assert.IsType(node); + var branch = (SemanticBranchNode)node; + Assert.Equal("primes", branch.SemanticId); + Assert.Equal(DataType.Array, branch.DataType); + Assert.Empty(branch.Children); + } + + [Fact] + public void ParseJsonSchema_ReferenceToLeafNodeDefinition_ReturnsLeafNode() + { + const string SchemaString = @"{ + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""person"": { ""$ref"": ""#/definitions/Person"" } + }, + ""definitions"": { + ""Person"": { + ""type"": ""string"" + } + } + }"; + var schema = JsonSerializer.Deserialize(SchemaString, _options); + + var node = _sut.ParseJsonSchema(schema); + + Assert.NotNull(node); + Assert.IsType(node); + var branch = (SemanticLeafNode)node; + Assert.Equal("person", branch.SemanticId); + Assert.Equal(DataType.String, branch.DataType); + } + + [Fact] + public void ParseJsonSchema_InlineObjectWithNoProperties_CoversProcessObjectFallback() + { + const string SchemaString = @"{ + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""outer"": { + ""type"": ""object"", + ""properties"": { + ""inner"": { ""type"": ""object"" } + } + } + } + }"; + var schema = JsonSerializer.Deserialize(SchemaString, _options); + + var root = _sut.ParseJsonSchema(schema); + + var outerBranch = Assert.IsType(root); + Assert.Equal("outer", outerBranch.SemanticId); + Assert.Single(outerBranch.Children); + var innerBranch = Assert.IsType(outerBranch.Children[0]); + Assert.Equal("inner", innerBranch.SemanticId); + Assert.Empty(innerBranch.Children); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaValidatorTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaValidatorTests.cs new file mode 100644 index 0000000..808a410 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/JsonSchemaValidatorTests.cs @@ -0,0 +1,180 @@ +using AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel.Services; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Exceptions; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.Submodel.Config; + +using Json.Schema; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using NSubstitute; + +namespace AAS.TwinEngine.Plugin.TestPlugin.UnitTests.Api.Submodel.Services; + +public class JsonSchemaValidatorTests +{ + private readonly JsonSchemaValidator _sut; + + public static IEnumerable InvalidPrimitives => [ + [SchemaValueType.String, "name", 123], + [SchemaValueType.Integer, "count", 12.34], + [SchemaValueType.Number, "price", "19.99a"], + [SchemaValueType.Boolean, "flag", "flase"], + [SchemaValueType.Number, "age", "8o5"], + [SchemaValueType.Number, "age", "-10n5"], + [SchemaValueType.Integer, "name", "10o"], + [SchemaValueType.Boolean, "flag", "\"true\""] + ]; + + public JsonSchemaValidatorTests() + { + var semantics = Substitute.For>(); + semantics.Value.Returns(new Semantics + { + IndexContextPrefix = "_aastwinengine_" + }); + var logger = Substitute.For>(); + _sut = new JsonSchemaValidator(semantics, logger); + } + + [Fact] + public void ValidateResponseContent_EmptyResponse_ThrowsBadRequest() + { + var schema = new JsonSchemaBuilder().Type(SchemaValueType.Object).Build(); + + Assert.Throws(() => _sut.ValidateResponseContent("", schema)); + } + + [Fact] + public void ValidateResponseContent_ValidateJsonSchemaRemovePrefix_DoesNotThrow() + { + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["ContactInformation_aastwinengine_00"] = new JsonSchemaBuilder().Type(SchemaValueType.Object).Build() + }) + .Required("ContactInformation_aastwinengine_00") + .Build(); + + const string Json = "{\"ContactInformation\": {}}"; + + _sut.ValidateResponseContent(Json, schema); + } + + [Fact] + public void ValidateResponseContent_ValidJsonAndSchema_DoesNotThrow() + { + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String).Build() + }) + .Required("name") + .Build(); + + const string Json = "{\"name\": \"Test\"}"; + + _sut.ValidateResponseContent(Json, schema); + } + + [Theory] + [MemberData(nameof(InvalidPrimitives))] + public void ValidateResponseContent_InvalidValueType_ThrowsBadRequest( + SchemaValueType expectedType, + string property, + string rawValue) + { + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + [property] = new JsonSchemaBuilder().Type(expectedType).Build() + }) + .Required(property) + .Build(); + var json = $"{{\"{property}\": {rawValue} }}"; + + Assert.Throws(() => _sut.ValidateResponseContent(json, schema)); + } + + [Fact] + public void ValidateResponseContent_PropertyTypeStringOrArray_WithString_DoesNotThrow() + { + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["value"] = new JsonSchemaBuilder().Type(SchemaValueType.String, SchemaValueType.Array).Build() + }) + .Required("value") + .Build(); + + const string Json = "{\"value\": \"hello\"}"; + + _sut.ValidateResponseContent(Json, schema); + } + + [Fact] + public void ValidateResponseContent_PropertyTypeStringOrArray_WithArray_DoesNotThrow() + { + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["value"] = new JsonSchemaBuilder().Type(SchemaValueType.String, SchemaValueType.Array).Build() + }) + .Required("value") + .Build(); + + const string Json = "{\"value\": [\"one\", \"two\"]}"; + + _sut.ValidateResponseContent(Json, schema); + } + + [Fact] + public void ValidateResponseContent_PropertyTypeStringOrArray_WithNumber_ThrowsBadRequest() + { + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["value"] = new JsonSchemaBuilder().Type(SchemaValueType.String, SchemaValueType.Array).Build() + }) + .Required("value") + .Build(); + + const string Json = "{\"value\": 123}"; + + Assert.Throws(() => _sut.ValidateResponseContent(Json, schema)); + } + + [Fact] + public void ValidateResponseContent_SchemaMismatch_ThrowsBadRequest() + { + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Properties(new Dictionary + { + ["name"] = new JsonSchemaBuilder().Type(SchemaValueType.String).Build() + }) + .Required("name") + .Build(); + + const string Json = "{}"; + + Assert.Throws(() => _sut.ValidateResponseContent(Json, schema)); + } + + [Fact] + public void ValidateResponseContent_InvalidJson_ThrowsBadRequest() + { + var schema = new JsonSchemaBuilder() + .Type(SchemaValueType.Object) + .Build(); + const string BadJson = "{ not valid json }"; + + Assert.Throws(() => _sut.ValidateResponseContent(BadJson, schema)); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/SemanticTreeHandlerTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/SemanticTreeHandlerTests.cs new file mode 100644 index 0000000..8530135 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/Services/SemanticTreeHandlerTests.cs @@ -0,0 +1,304 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +using AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel.Services; +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.Submodel; + +using Json.More; +using Json.Schema; + +using NSubstitute; + +namespace AAS.TwinEngine.Plugin.TestPlugin.UnitTests.Api.Submodel.Services; + +public class SemanticTreeHandlerTests +{ + private readonly SemanticTreeHandler _sut; + + private readonly JsonSerializerOptions _options = new() + { + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + ReadCommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true, + Converters = + { + new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) + } + }; + + public SemanticTreeHandlerTests() + { + var jsonSchemaValidator = Substitute.For(); + _sut = new SemanticTreeHandler(jsonSchemaValidator); + } + + [Fact] + public void GetJson_WithLeafNodeWithStringDataType_ReturnsJsonWithValue() + { + var dataQueryString = JsonNode.Parse(@" + { + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""Name"": { + ""type"": ""string"" + }}, + ""required"": [""Name""], + ""definitions"" : {} + }").AsJsonString(); + var dataQuery = JsonSerializer.Deserialize(dataQueryString, _options); + var leafNodeWithStringDataType = new SemanticLeafNode("Name", DataType.String, "John"); + var expectedLeafWithStringDataTypeJson = JsonNode.Parse(@"{""Name"" : ""John""}")?.AsObject(); + + var result = _sut.GetJson(leafNodeWithStringDataType, dataQuery); + + Assert.Equal(JsonSerializer.Serialize(expectedLeafWithStringDataTypeJson), JsonSerializer.Serialize(result)); + } + + [Fact] + public void GetJson_WithLeafNodeWithBooleanDataType_ReturnsJsonWithValue() + { + var dataQueryString = JsonNode.Parse(@" + { + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""HaveTitle"": { + ""type"": ""boolean"" + }}, + ""required"": [""HaveTitle""], + ""definitions"" : {} + }").AsJsonString(); + var dataQuery = JsonSerializer.Deserialize(dataQueryString, _options); + var leafNodeWithBooleanDataType = new SemanticLeafNode("HaveTitle", DataType.Boolean, "true"); + var expectedLeafWithBooleanDataTypeJson = JsonNode.Parse(@"{""HaveTitle"" : true }")?.AsObject(); + + var result = _sut.GetJson(leafNodeWithBooleanDataType, dataQuery); + + Assert.Equal(JsonSerializer.Serialize(expectedLeafWithBooleanDataTypeJson), JsonSerializer.Serialize(result)); + } + + [Fact] + public void GetJson_WithLeafNodeWithIntegerDataType_ReturnsJsonWithValue() + { + var dataQueryString = JsonNode.Parse(@" + { + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""Weight"": { + ""type"": ""integer"" + }}, + ""required"": [""Weight""], + ""definitions"" : {} + }").AsJsonString(); + var dataQuery = JsonSerializer.Deserialize(dataQueryString, _options); + var leafNodeWithIntergerDataType = new SemanticLeafNode("Weight", DataType.Integer, "22"); + var expectedLeafWithIntergerDataTypeJson = JsonNode.Parse(@"{""Weight"" : 22 }").AsObject(); + + var result = _sut.GetJson(leafNodeWithIntergerDataType, dataQuery); + + Assert.Equal(JsonSerializer.Serialize(expectedLeafWithIntergerDataTypeJson), JsonSerializer.Serialize(result)); + } + + [Fact] + public void GetJson_WithLeafNodeWithNumberDataType_ReturnsJsonWithValue() + { + var dataQueryString = JsonNode.Parse(@" + { + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""Weight"": { + ""type"": ""number"" + }}, + ""required"": [""Weight""], + ""definitions"" : {} + }").AsJsonString(); + var dataQuery = JsonSerializer.Deserialize(dataQueryString, _options); + var leafNodeWithNumberDataType = new SemanticLeafNode("Weight", DataType.Number, "35.485"); + var expectedLeafWithNumberDataTypeJson = JsonNode.Parse(@"{""Weight"" : 35.485 }").AsObject(); + + var result = _sut.GetJson(leafNodeWithNumberDataType, dataQuery); + + Assert.Equal(JsonSerializer.Serialize(expectedLeafWithNumberDataTypeJson), JsonSerializer.Serialize(result)); + } + + [Fact] + public void GetJson_WithLeafNodeWithNoDataType_ReturnsJsonWithValueAsString() + { + var dataQueryString = JsonNode.Parse(@" + { + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""ImageLink"": { + }}, + ""required"": [""ImageLink""], + ""definitions"" : {} + }").AsJsonString(); + var dataQuery = JsonSerializer.Deserialize(dataQueryString, _options); + var leafNodeWithNoDataType = new SemanticLeafNode("ImageLink", DataType.Unknown, "https://www.mm-software.com/fake"); + var expectedLeafWithNoDataTypeJson = JsonNode.Parse(@"{""ImageLink"" : ""https://www.mm-software.com/fake"" }").AsObject(); + + var result = _sut.GetJson(leafNodeWithNoDataType, dataQuery); + + Assert.Equal(JsonSerializer.Serialize(expectedLeafWithNoDataTypeJson), JsonSerializer.Serialize(result)); + } + + [Fact] + public void GetJson_WithSingleChildBranchWithObjectDataType_ReturnsValue() + { + var dataQueryString = JsonNode.Parse(@"{ + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""ContactInformation"": { + ""type"": ""object"", + ""properties"": { + ""Name"": { + ""type"": ""string"" + } + }, + ""required"": [""Name""], + ""definitions"" : {} + }}} + ").AsJsonString(); + var dataQuery = JsonSerializer.Deserialize(dataQueryString, _options); + var singleChildBranchWithObjectDataType = new SemanticBranchNode("ContactInformation", DataType.Object); + singleChildBranchWithObjectDataType.AddChild(new SemanticLeafNode("Name", DataType.String, "John")); + var singleChildBranchWithObjectDataTypeExpectedJson = JsonNode.Parse(@"{ ""ContactInformation"" : { ""Name"" : ""John""} }")!.AsObject(); + + var result = _sut.GetJson(singleChildBranchWithObjectDataType, dataQuery); + + var resultObj = JsonNode.Parse(JsonSerializer.Serialize(result))?.AsObject(); + Assert.Equal(JsonSerializer.Serialize(singleChildBranchWithObjectDataTypeExpectedJson), JsonSerializer.Serialize(result)); + Assert.Single(resultObj?["ContactInformation"]?.AsObject()!); + Assert.Equal("John", resultObj?["ContactInformation"]!["Name"]!.GetValue()); + } + + [Fact] + public void GetJson_WithSingleChildBranchWithArrayDataType_ReturnsValue() + { + var dataQueryString = JsonNode.Parse(@"{ + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""ContactInformation"": { + ""type"": ""array"", + ""properties"": { + ""Name"": { + ""type"": ""string"" + } + }, + ""required"": [""Name""], + ""definitions"" : {} + }}} + ").AsJsonString(); + var dataQuery = JsonSerializer.Deserialize(dataQueryString, _options); + var singleChildBranchWithArrayDataType = new SemanticBranchNode("ContactInformation", DataType.Array); + singleChildBranchWithArrayDataType.AddChild(new SemanticLeafNode("Name", DataType.String, "John")); + var singleChildBranchWithArrayDataTypeExpectedJson = JsonNode.Parse(@"{ ""ContactInformation"" :[ { ""Name"" : ""John""} ] } ")!.AsObject(); + + var result = _sut.GetJson(singleChildBranchWithArrayDataType, dataQuery); + + var resultObj = JsonNode.Parse(JsonSerializer.Serialize(result))!.AsObject(); + Assert.Equal(JsonSerializer.Serialize(singleChildBranchWithArrayDataTypeExpectedJson), JsonSerializer.Serialize(result)); + Assert.Single(resultObj["ContactInformation"]!.AsArray()); + Assert.Equal("John", resultObj["ContactInformation"]![0]!["Name"]!.GetValue()); + } + + [Fact] + public void GetJson_WithNestedArraytypeWithDifferntDataType_ReturnsValue() + { + var dataQueryString = JsonNode.Parse(@"{ + ""$schema"": ""http://json-schema.org/draft-07/schema#"", + ""type"": ""object"", + ""properties"": { + ""ContactInformations"": { + ""type"" : ""object"", + ""properties"":{ + ""ContactInformation"" :{ + ""$ref"": ""#/definitions/ContactInformation"" + } + }, + ""required"": [""ContactInformation""] + }}, + ""definitions"": { + ""ContactInformation"": { + ""type"": ""array"", + ""properties"": { + ""name"": { + ""type"": ""string"" + }, + ""description"": { + ""type"": ""string"" + }, + ""ipCommunication"": { + ""$ref"": ""#/definitions/IPCommunication"" + }}, + ""required"": [""name"", ""description"", ""ipCommunication""] + }, + ""IPCommunication"": { + ""type"": ""array"", + ""properties"": { + ""AvailableTime"": { + ""type"": ""number"" + }}, + ""required"": [""AvailableTime""], + ""additionalProperties"": false + }}}").AsJsonString(); + var dataQuery = JsonSerializer.Deserialize(dataQueryString, _options); + var contactRoot = new SemanticBranchNode("ContactInformations", DataType.Object); + var contactInformation = new SemanticBranchNode("ContactInformation", DataType.Array); + var nameLeaf = new SemanticLeafNode("name", DataType.String, "jone doh"); + var descriptionLeaf = new SemanticLeafNode("description", DataType.String, "this is contact infomation"); + var ipCommunication = new SemanticBranchNode("ipCommunication", DataType.Array); + var availableTimeLeaf = new SemanticLeafNode("AvailableTime", DataType.Number, "9"); + ipCommunication.AddChild(availableTimeLeaf); + contactInformation.AddChild(nameLeaf); + contactInformation.AddChild(descriptionLeaf); + contactInformation.AddChild(ipCommunication); + var contactInformation1 = new SemanticBranchNode("ContactInformation", DataType.Array); + var nameLeaf1 = new SemanticLeafNode("name", DataType.String, "jane"); + var descriptionLeaf1 = new SemanticLeafNode("description", DataType.String, "this is contact infomation"); + var ipCommunication1 = new SemanticBranchNode("ipCommunication", DataType.Array); + var availableTimeLeaf1_1 = new SemanticLeafNode("AvailableTime", DataType.Number, "8"); + var availableTimeLeaf1_2 = new SemanticLeafNode("AvailableTime", DataType.Number, "56"); + ipCommunication1.AddChild(availableTimeLeaf1_1); + ipCommunication1.AddChild(availableTimeLeaf1_2); + contactInformation1.AddChild(nameLeaf1); + contactInformation1.AddChild(descriptionLeaf1); + contactInformation1.AddChild(ipCommunication1); + contactRoot.AddChild(contactInformation); + contactRoot.AddChild(contactInformation1); + var expectedContactJson = JsonNode.Parse(@" + { + ""ContactInformations"":{ + ""ContactInformation"": [ + {""name"": ""jone doh"", + ""description"": ""this is contact infomation"", + ""ipCommunication"":[ { + ""AvailableTime"": 9 + }]}, + {""name"": ""jane"", + ""description"": ""this is contact infomation"", + ""ipCommunication"": [{ + ""AvailableTime"": 8 + },{ + ""AvailableTime"": 56 + }]} + ]}}") + ?.AsObject(); + + var result = _sut.GetJson(contactRoot, dataQuery); + + var resultObj = JsonNode.Parse(JsonSerializer.Serialize(result))!.AsObject(); + Assert.Equal(JsonSerializer.Serialize(expectedContactJson), JsonSerializer.Serialize(result)); + } + + [Fact] + public void GetJson_WithNullNode_ThrowsArgumentException() => Assert.Throws(() => _sut.GetJson(null, null)); +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/SubmodelControllerTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/SubmodelControllerTests.cs new file mode 100644 index 0000000..e58872a --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Api/Submodel/SubmodelControllerTests.cs @@ -0,0 +1,77 @@ +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +using AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel; +using AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel.Handler; +using AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel.Requests; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Constants; + +using Json.Schema; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.WebUtilities; + +using NSubstitute; + +namespace AAS.TwinEngine.Plugin.TestPlugin.UnitTests.Api.Submodel; + +public class SubmodelControllerTests +{ + private readonly ISubmodelHandler _handler = Substitute.For(); + private readonly SubmodelController _sut; + private readonly string _submodelId = "ContactInformation"; + private readonly string _encodedSubmodelId; + private readonly JsonSchema _dataQuery; + private readonly JsonObject _response; + private readonly string _dataQueryString = "{\r\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\r\n \"type\": \"object\",\r\n \"properties\": {\r\n \"https://admin-shell.io/zvei/nameplate/1/0/ContactInformations\": {\r\n \"type\": \"object\",\r\n \"properties\": {\r\n \"https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation\": {\r\n \"anyOf\": [\r\n { \"$ref\": \"#/definitions/ContactInformation\" },\r\n {\r\n \"type\": \"array\",\r\n \"items\": { \"$ref\": \"#/definitions/ContactInformation\" }\r\n }\r\n ]\r\n }\r\n },\r\n \"required\": [\r\n \"https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation\"\r\n ]\r\n }\r\n },\r\n \"definitions\": {\r\n \"ContactInformation\": {\r\n \"type\": \"object\",\r\n \"properties\": {\r\n \"0173-1#02-AAO204#003\": { \"type\": \"string\" },\r\n \"https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Language\": { \"type\": \"string\" }\r\n },\r\n \"required\": [\r\n \"0173-1#02-AAO204#003\",\r\n \"https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Language\"\r\n ]\r\n }\r\n }\r\n}\r\n"; + + private readonly JsonSerializerOptions _options = new() + { + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + ReadCommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true, + Converters = + { + new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) + } + }; + + public SubmodelControllerTests() + { + _sut = new SubmodelController(_handler); + _encodedSubmodelId = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(_submodelId)); + _response = JsonNode.Parse(@"{""https://admin-shell.io/zvei/nameplate/1/0/ContactInformations"": { + ""https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation"": { + ""0173-1#02-AAO204#003"": ""John Doe"", + ""https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Language"": ""en"" + }}}").AsObject(); + _dataQuery = JsonSerializer.Deserialize(_dataQueryString, _options); + } + + [Fact] + public async Task RetrieveDataAsync_ReturnsBadRequest_WhenComplexDataQueryIsNull() + { + var result = await _sut.RetrieveDataAsync(null, _encodedSubmodelId, CancellationToken.None); + + var badResult = Assert.IsType(result.Result); + + Assert.Equal(ExceptionMessages.InvalidRequestPayload, badResult.Value); + } + + [Fact] + public async Task RetrieveDataAsync_ReturnsOk_WithJsonObject() + { + _handler.GetSubmodelData(Arg.Any(), Arg.Any()) + .Returns(_response); + + var result = await _sut.RetrieveDataAsync(_dataQuery, _encodedSubmodelId, CancellationToken.None); + + var okResult = Assert.IsType(result.Result); + var json = Assert.IsType(okResult.Value); + Assert.Equal(_response.ToJsonString(), json.ToJsonString()); + } +} + diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/ApplicationLogic/Services/Manifest/ManifestServiceTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/ApplicationLogic/Services/Manifest/ManifestServiceTests.cs new file mode 100644 index 0000000..9f4b250 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/ApplicationLogic/Services/Manifest/ManifestServiceTests.cs @@ -0,0 +1,36 @@ +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.Manifest; +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.Manifest; + +using NSubstitute; + +namespace AAS.TwinEngine.Plugin.TestPlugin.UnitTests.ApplicationLogic.Services.Manifest; + +public class ManifestServiceTests +{ + private readonly IManifestProvider _manifestProvider = Substitute.For(); + private readonly ManifestService _sut; + + public ManifestServiceTests() => _sut = new ManifestService(_manifestProvider); + + [Fact] + public async Task GetManifestData_ShouldReturnManifestData_FromProvider() + { + var expectedManifest = new ManifestData + { + Capabilities = new CapabilitiesData + { + HasAssetInformation = true, + HasShellDescriptor = false + }, + SupportedSemanticIds = ["semantic1"] + }; + _manifestProvider.GetManifestData().Returns(expectedManifest); + + var result = await _sut.GetManifestData(CancellationToken.None); + + Assert.Equal(expectedManifest, result); + Assert.Equal(expectedManifest.Capabilities.HasAssetInformation, result.Capabilities.HasAssetInformation); + Assert.Equal(expectedManifest.Capabilities.HasShellDescriptor, result.Capabilities.HasShellDescriptor); + Assert.Equal(expectedManifest.SupportedSemanticIds, result.SupportedSemanticIds); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/ApplicationLogic/Services/MetaData/MetaDataServiceTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/ApplicationLogic/Services/MetaData/MetaDataServiceTests.cs new file mode 100644 index 0000000..b7a10ef --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/ApplicationLogic/Services/MetaData/MetaDataServiceTests.cs @@ -0,0 +1,77 @@ +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.MetaData; +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.MetaData; + +using Microsoft.Extensions.Logging; + +using NSubstitute; + +namespace AAS.TwinEngine.Plugin.TestPlugin.UnitTests.ApplicationLogic.Services.MetaData; + +public class MetaDataServiceTests +{ + private readonly ILogger _logger = Substitute.For>(); + private readonly IMetaDataProvider _repository = Substitute.For(); + private readonly MetaDataService _sut; + private const string AasIdentifier = "ContactInformation"; + + public MetaDataServiceTests() => _sut = new MetaDataService(_logger, _repository); + + [Fact] + public async Task GetShellsAsync_ReturnsShells() + { + var expectedShells = new ShellDescriptorsData(); + _repository.GetShellDescriptorsAsync(null, null, Arg.Any()).Returns(expectedShells); + + var result = await _sut.GetShellDescriptorsAsync(null, null, CancellationToken.None); + + Assert.NotNull(result); + } + + [Fact] + public async Task GetShellElementAsync_ReturnsShellElement() + { + var expectedShell = new ShellDescriptorData(); + _repository.GetShellDescriptorAsync(AasIdentifier, Arg.Any()) + .Returns(expectedShell); + + var result = await _sut.GetShellDescriptorAsync(AasIdentifier, CancellationToken.None); + + Assert.NotNull(result); + Assert.Equal(expectedShell, result); + } + + [Fact] + public async Task GetShellElementAsync_ReturnsNull_WhenNotFound() + { + _repository.GetShellDescriptorAsync(AasIdentifier, Arg.Any())! + .Returns((ShellDescriptorData)null!); + + var result = await _sut.GetShellDescriptorAsync(AasIdentifier, CancellationToken.None); + + Assert.Null(result); + } + + [Fact] + public async Task GetAssetElementAsync_ReturnsAssetElement() + { + var expectedAsset = new AssetData(); + _repository.GetAssetAsync(AasIdentifier, Arg.Any()) + .Returns(expectedAsset); + + var result = await _sut.GetAssetAsync(AasIdentifier, CancellationToken.None); + + Assert.NotNull(result); + Assert.Equal(expectedAsset, result); + } + + [Fact] + public async Task GetAssetElementAsync_ReturnsNull_WhenNotFound() + { + _repository.GetAssetAsync(AasIdentifier, Arg.Any()) + .Returns((AssetData)null!); + + var result = await _sut.GetAssetAsync(AasIdentifier, CancellationToken.None); + + Assert.Null(result); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/ApplicationLogic/Services/Submodel/SubmodelServiceTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/ApplicationLogic/Services/Submodel/SubmodelServiceTests.cs new file mode 100644 index 0000000..d2590b6 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/ApplicationLogic/Services/Submodel/SubmodelServiceTests.cs @@ -0,0 +1,51 @@ +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.Submodel; +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.Submodel; + +using NSubstitute; + +namespace AAS.TwinEngine.Plugin.TestPlugin.UnitTests.ApplicationLogic.Services.Submodel; + +public class SubmodelServiceTests +{ + private readonly SemanticBranchNode _sampleInputTree; + private readonly SemanticBranchNode _enrichedTree; + private readonly ISubmodelProvider _repository; + private readonly SubmodelService _sut; + private const string SubmodelId = "ContactInformation"; + + public SubmodelServiceTests() + { + _repository = Substitute.For(); + _sut = new SubmodelService(_repository); + _sampleInputTree = new SemanticBranchNode("ContactInformation", DataType.Object); + _sampleInputTree.AddChild(new SemanticLeafNode("Name", DataType.String, "")); + _enrichedTree = new SemanticBranchNode("ContactInformation", DataType.Object); + _enrichedTree.AddChild(new SemanticLeafNode("Name", DataType.String, "John")); + } + + [Fact] + public async Task GetProductDataAsync_ReturnsEnricSematicTree_OnHappyPath() + { + _repository + .EnrichWithData(_sampleInputTree, SubmodelId) + .Returns(_enrichedTree); + + var actual = await _sut.GetValuesBySemanticIds(_sampleInputTree, SubmodelId); + + Assert.Equal(actual, _enrichedTree); + _repository.Received(1).EnrichWithData(_sampleInputTree, SubmodelId); + } + + [Fact] + public async Task GetProductDataAsync_ReturnsSameTreeNode_WhenValueNotFound() + { + _repository + .EnrichWithData(_sampleInputTree, SubmodelId) + .Returns(_sampleInputTree); + + var actual = await _sut.GetValuesBySemanticIds(_sampleInputTree, SubmodelId); + + Assert.Equal(_sampleInputTree, actual); + _repository.Received(1).EnrichWithData(_sampleInputTree, SubmodelId); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/CleanArchitectureTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/CleanArchitectureTests.cs new file mode 100644 index 0000000..c2f60b8 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/CleanArchitectureTests.cs @@ -0,0 +1,129 @@ +using ArchUnitNET.Domain; +using ArchUnitNET.Loader; +using ArchUnitNET.xUnit; + +using static ArchUnitNET.Fluent.ArchRuleDefinition; + +namespace AAS.TwinEngine.Plugin.TestPlugin.UnitTests; + +/// +/// This validates that the Onion Architecture as described in the SAD is not broken. +/// https://dev.azure.com/mm-products/AAS.TwinEngine/_wiki/wikis/Wiki/163/5-Solution-Strategy +/// +public class CleanArchitectureTests +{ + private const string BaseNamespace = "AAS.TwinEngine.Plugin.TestPlugin"; + + private readonly Architecture _architecture; + private readonly IObjectProvider _apiLayer; + private readonly IObjectProvider _applicationLogicLayer; + private readonly IObjectProvider _domainModelLayer; + private readonly IObjectProvider _infrastructureLayer; + + public CleanArchitectureTests() + { + _architecture = new ArchLoader().LoadAssemblies(System.Reflection.Assembly.Load(BaseNamespace)).Build(); + + _apiLayer = Types().That().ResideInNamespace($"{BaseNamespace}.Api.*", true).As("Api"); + _applicationLogicLayer = Types().That().ResideInNamespace($"{BaseNamespace}.ApplicationLogic.*", true).As("ApplicationLogic"); + _domainModelLayer = Types().That().ResideInNamespace($"{BaseNamespace}.DomainModel*", true).As("DomainModel"); + _infrastructureLayer = Types().That().ResideInNamespace($"{BaseNamespace}.Infrastructure.*", true).As("Infrastructure"); + } + + [Fact] + public void DomainModelShallNotHaveExternalDependencies() + { + var forbiddenTypes = new List(); + forbiddenTypes.AddRange(_infrastructureLayer.GetObjects(_architecture)); + forbiddenTypes.AddRange(_apiLayer.GetObjects(_architecture)); + forbiddenTypes.AddRange(_applicationLogicLayer.GetObjects(_architecture)); + + Types().That().Are(_domainModelLayer) + .Should() + .NotDependOnAny(Types().That().Are(forbiddenTypes)) + .Check(_architecture); + } + + [Fact] + public void ApplicationLogicShallNotHaveDependenciesToInfrastructure() + { + Types().That().Are(_applicationLogicLayer) + .Should() + .NotDependOnAny(Types().That().Are(_infrastructureLayer)) + .Check(_architecture); + } + + [Fact] + public void ApplicationLogicShallNotHaveDependenciesToApi() + { + Types().That().Are(_applicationLogicLayer) + .Should() + .NotDependOnAny(Types().That().Are(_apiLayer)) + .Check(_architecture); + } + + [Fact] + public void InfrastructureShallNotHaveDependenciesToApi() + { + Types().That().Are(_infrastructureLayer) + .Should() + .NotDependOnAny(Types().That().Are(_apiLayer)) + .Check(_architecture); + } + + [Fact] + public void ApiShallNotHaveDependenciesToInfrastructure() + { + Types().That().Are(_apiLayer) + .Should() + .NotDependOnAny(Types().That().Are(_infrastructureLayer)) + .Check(_architecture); + } + + [Fact] + public void RepositoryClassesShallBeInCorrectNamespace() + { + Classes().That().HaveNameEndingWith("Repository").Should() + .ResideInNamespace($"{BaseNamespace}.Infrastructure.DataAccess*", true) + .WithoutRequiringPositiveResults() + .Check(_architecture); + } + + [Fact] + public void RepositoryInterfacesShallBeInCorrectNamespace() + { + Interfaces().That() + .HaveNameEndingWith("Repository") + .And() + .DoNotHaveFullName($"{BaseNamespace}.Infrastructure.DataAccess.GenericRepository.IMongoDbRepository") + .Should() + .ResideInNamespace($"{BaseNamespace}.ApplicationLogic.*", true) + .WithoutRequiringPositiveResults() + .Check(_architecture); + } + + [Fact] + public void ServicesShallBeInCorrectNamespace() + { + Classes().That().HaveNameEndingWith("Service").Should() + .ResideInNamespace($"{BaseNamespace}.ApplicationLogic.Service.*", true) + .Check(_architecture); + } + + [Fact] + public void ServiceInterfacesShallBeInCorrectNamespace() + { + Interfaces().That().HaveNameEndingWith("Service") + .Should() + .ResideInNamespace($"{BaseNamespace}.ApplicationLogic.Service.*", true) + .Check(_architecture); + } + + [Fact] + public void ControllerShallBeInCorrectNamespace() + { + Classes().That().HaveNameEndingWith("Controller").Should() + .ResideInNamespace($"{BaseNamespace}.Api.*", true) + .Check(_architecture); + } +} \ No newline at end of file diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/DataAccess/MapperProfiles/AssetMappingProfileTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/DataAccess/MapperProfiles/AssetMappingProfileTests.cs new file mode 100644 index 0000000..fcd3ac8 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/DataAccess/MapperProfiles/AssetMappingProfileTests.cs @@ -0,0 +1,46 @@ +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.MetaData; +using AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.DataAccess.Entity; +using AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.DataAccess.MapperProfiles; + +namespace AAS.TwinEngine.Plugin.TestPlugin.UnitTests.Infrastructure.DataAccess.MapperProfiles; + +public class AssetMappingProfileTests +{ + [Fact] + public void ToDomainModel_MapsAllFields_WhenThumbnailIsPresent() + { + const string GlobalAssetId = "asset-123"; + var entity = new AssetInformationDataEntity { DefaultThumbnail = new DefaultThumbnailDataEntity { ContentType = "image/png", Path = "/images/thumb.png" }, GlobalAssetId = GlobalAssetId }; + var specificAssetIds = new List + { + new() { Name = "SerialNumber", Value = "SN123" } + }; + + var result = entity.ToDomainModel(GlobalAssetId, specificAssetIds); + + Assert.NotNull(result); + Assert.Equal(GlobalAssetId, result.GlobalAssetId); + Assert.Equal(specificAssetIds, result.SpecificAssetIds); + Assert.NotNull(result.DefaultThumbnail); + Assert.Equal("image/png", result.DefaultThumbnail.ContentType); + Assert.Equal("/images/thumb.png", result.DefaultThumbnail.Path); + } + + [Fact] + public void ToDomainModel_SetsDefaultThumbnailToNull_WhenThumbnailIsMissing() + { + var entity = new AssetInformationDataEntity + { + DefaultThumbnail = null + }; + const string GlobalAssetId = "asset-456"; + var specificAssetIds = new List(); + + var result = entity.ToDomainModel(GlobalAssetId, specificAssetIds); + + Assert.NotNull(result); + Assert.Equal(GlobalAssetId, result.GlobalAssetId); + Assert.Equal(specificAssetIds, result.SpecificAssetIds); + Assert.Null(result.DefaultThumbnail); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/DataAccess/MapperProfiles/ShellDescriptorMappingProfileTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/DataAccess/MapperProfiles/ShellDescriptorMappingProfileTests.cs new file mode 100644 index 0000000..87c4d75 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/DataAccess/MapperProfiles/ShellDescriptorMappingProfileTests.cs @@ -0,0 +1,62 @@ +using AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.DataAccess.Entity; +using AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.DataAccess.MapperProfiles; + +namespace AAS.TwinEngine.Plugin.TestPlugin.UnitTests.Infrastructure.DataAccess.MapperProfiles; + +public class ShellDescriptorMappingProfileTests +{ + [Fact] + public void MapToDomainModel_MapsAllFieldsCorrectly() + { + var entity = new MetaDataEntity + { + Id = "shell-001", + GlobalAssetId = "asset-001", + IdShort = "Shell001", + SpecificAssetIds = + [ + new SpecificAssetIdEntity { Name = "SerialNumber", Value = "SN001" }, + new SpecificAssetIdEntity { Name = "PartNumber", Value = "PN001" } + ] + }; + + var result = entity.MapToDomainModel(); + + Assert.NotNull(result); + Assert.Equal(entity.Id, result.Id); + Assert.Equal(entity.GlobalAssetId, result.GlobalAssetId); + Assert.Equal(entity.IdShort, result.IdShort); + Assert.Equal(2, result.SpecificAssetIds!.Count); + Assert.Equal("SerialNumber", result.SpecificAssetIds[0].Name); + Assert.Equal("SN001", result.SpecificAssetIds[0].Value); + } + + [Fact] + public void MapToDomainModel_HandlesNullSpecificAssetIds() + { + var entity = new MetaDataEntity + { + Id = "shell-002", + GlobalAssetId = "asset-002", + IdShort = "Shell002", + SpecificAssetIds = null + }; + + var result = entity.MapToDomainModel(); + + Assert.NotNull(result); + Assert.Equal(entity.Id, result.Id); + Assert.Empty(result.SpecificAssetIds!); + } + + [Fact] + public void ToDomainModelList_ReturnsEmptyList_WhenInputIsNull() + { + List? entities = null; + + var result = entities!.ToDomainModelList(); + + Assert.NotNull(result); + Assert.Empty(result); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/Providers/ManifestProvider/Helper/JsonConverterTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/Providers/ManifestProvider/Helper/JsonConverterTests.cs new file mode 100644 index 0000000..317cdeb --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/Providers/ManifestProvider/Helper/JsonConverterTests.cs @@ -0,0 +1,150 @@ +using System.Text.Json; + +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.Submodel; +using AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.Providers.ManifestProvider.Helper; + +namespace AAS.TwinEngine.Plugin.TestPlugin.UnitTests.Infrastructure.Providers.ManifestProvider.Helper; + +public class JsonConverterTests +{ + [Fact] + public void ParseJson_ShouldReturnBranchNode_WhenJsonIsObject() + { + const string Json = """ + { + "name": "value" + } + """; + using var doc = JsonDocument.Parse(Json); + + var result = JsonConverter.ParseJson(doc); + + Assert.IsType(result); + var branch = (SemanticBranchNode)result; + Assert.Single(branch.Children); + Assert.Equal("name", branch.Children[0].SemanticId); + } + + [Fact] + public void ParseJson_ShouldParseNestedJsonString_WhenRootIsString() + { + const string Json = """ + { + "items": { + "item":{ + "key" : "value" + } + } + } + """; + using var doc = JsonDocument.Parse(Json); + + var result = JsonConverter.ParseJson(doc); + + Assert.IsType(result); + var branch = (SemanticBranchNode)result; + Assert.Single(branch.Children); + Assert.Equal("item", branch.Children[0].SemanticId); + } + + [Fact] + public void ParseJson_ShouldReturnArrayBranch_WhenJsonIsArray() + { + const string Json = "[{\"item\": 1}, {\"item\": 2}]"; + using var doc = JsonDocument.Parse(Json); + + var result = JsonConverter.ParseJson(doc); + + Assert.IsType(result); + var branch = (SemanticBranchNode)result; + Assert.Equal(2, branch.Children.Count); + } + + [Fact] + public void ParseJson_ShouldReturnArrayBranch_WhenJsonIsMultipleArray() + { + const string Json = "[{\"item\": 1}, {\"item\": 2}, {\"item\": 3}]"; + using var doc = JsonDocument.Parse(Json); + + var result = JsonConverter.ParseJson(doc); + + Assert.IsType(result); + var branch = (SemanticBranchNode)result; + Assert.Equal(3, branch.Children.Count); + } + + [Fact] + public void ParseJson_ShouldHandleEmptyObject() + { + const string Json = "{}"; + using var doc = JsonDocument.Parse(Json); + + var result = JsonConverter.ParseJson(doc); + + Assert.IsType(result); + var branch = (SemanticBranchNode)result; + Assert.Empty(branch.Children); + } + + [Fact] + public void ParseJson_ShouldProcessArrayInsideObjectProperty() + { + const string Json = """ + { + "items": [ + { "id": 1 }, + { "id": 2 } + ] + } + """; + using var doc = JsonDocument.Parse(Json); + + var result = JsonConverter.ParseJson(doc); + + Assert.IsType(result); + var branch = (SemanticBranchNode)result; + Assert.Equal(2, branch.Children.Count); + + var itemsBranch = branch.Children[0] as SemanticBranchNode; + Assert.NotNull(itemsBranch); + Assert.Equal("items", itemsBranch.SemanticId); + Assert.Single(itemsBranch.Children); + } + + [Fact] + public void ParseJson_ShouldProcessArrayAsSinglePropertyValue() + { + const string Json = """ + { + "data": [ + { "name": "A" }, + { "name": "B" } + ] + } + """; + using var doc = JsonDocument.Parse(Json); + + var result = JsonConverter.ParseJson(doc); + + Assert.IsType(result); + var branch = (SemanticBranchNode)result; + Assert.Equal("data", branch.SemanticId); + Assert.Equal(2, branch.Children.Count); + + var firstItem = branch.Children[0] as SemanticBranchNode; + Assert.NotNull(firstItem); + Assert.Single(firstItem.Children); + Assert.Equal("name", firstItem.Children[0].SemanticId); + } + + [Fact] + public void ParseJson_ShouldHandleNullString() + { + const string Json = "null"; + using var doc = JsonDocument.Parse(Json); + + var result = JsonConverter.ParseJson(doc); + + Assert.IsType(result); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/Providers/ManifestProvider/ManifestProviderTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/Providers/ManifestProvider/ManifestProviderTests.cs new file mode 100644 index 0000000..4ad12d2 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/Providers/ManifestProvider/ManifestProviderTests.cs @@ -0,0 +1,82 @@ +using System.Text.Json; + +using AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.Providers; +using AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.Providers.Config; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using NSubstitute; + +using Provider = AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.Providers.ManifestProvider.ManifestProvider; + +namespace AAS.TwinEngine.Plugin.TestPlugin.UnitTests.Infrastructure.Providers.ManifestProvider; + +public class ManifestProviderTests +{ + private readonly ILogger _logger; + private readonly Provider _sut; + + public ManifestProviderTests() + { + _logger = Substitute.For>(); + var capabilities = Substitute.For>(); + capabilities.Value.Returns(new Capabilities { HasAssetInformation = true, HasShellDescriptor = true }); + _sut = new Provider(_logger, capabilities); + } + + private static void SetSubmodelData(string jsonContent) => MockData.SubmodelData = JsonDocument.Parse(jsonContent); + + [Fact] + public void GetManifestData_ValidResource_ReturnsExpectedSemanticIdsAndCapabilities() + { + SetSubmodelData(TestData.TestSubmodelData); + + var manifest = _sut.GetManifestData(); + + Assert.NotNull(manifest); + Assert.NotNull(manifest.SupportedSemanticIds); + Assert.NotEmpty(manifest.SupportedSemanticIds); + Assert.Contains("Email", manifest.SupportedSemanticIds); + Assert.Contains("TelephoneNumber", manifest.SupportedSemanticIds); + Assert.Contains("ClassId", manifest.SupportedSemanticIds); + Assert.Contains("StatusValue", manifest.SupportedSemanticIds); + Assert.NotNull(manifest.Capabilities); + Assert.True(manifest.Capabilities.HasAssetInformation); + Assert.True(manifest.Capabilities.HasShellDescriptor); + } + + [Fact] + public void GetManifestData_EmptyArrayResource_ReturnsNoSupportedSemanticIds() + { + SetSubmodelData("{}"); + + var manifest = _sut.GetManifestData(); + + Assert.NotNull(manifest); + Assert.NotNull(manifest.SupportedSemanticIds); + Assert.Empty(manifest.SupportedSemanticIds); + } + + [Fact] + public void GetManifestData_WithCapabilitiesFalse_ReturnsCorrectCapabilities() + { + const string ValidSubmodelData = @"{ + ""test-submodelId"": { + ""Email"": ""test@example.com"" + } + }"; + + SetSubmodelData(ValidSubmodelData); + var capabilities = Substitute.For>(); + capabilities.Value.Returns(new Capabilities { HasAssetInformation = false, HasShellDescriptor = false }); + var sut = new Provider(_logger, capabilities); + + var manifest = sut.GetManifestData(); + + Assert.NotNull(manifest); + Assert.NotNull(manifest.Capabilities); + Assert.False(manifest.Capabilities.HasAssetInformation); + Assert.False(manifest.Capabilities.HasShellDescriptor); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/Providers/MetaDataProvider/Helper/PaginatorTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/Providers/MetaDataProvider/Helper/PaginatorTests.cs new file mode 100644 index 0000000..018d7de --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/Providers/MetaDataProvider/Helper/PaginatorTests.cs @@ -0,0 +1,121 @@ +using AAS.TwinEngine.Plugin.TestPlugin.Common.Extensions; +using AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.Providers.MetaDataProvider.Helper; + +namespace AAS.TwinEngine.Plugin.TestPlugin.UnitTests.Infrastructure.Providers.MetaDataProvider.Helper; + +public class PaginatorTests +{ + private readonly Func _idSelector; + private readonly List _items; + + public PaginatorTests() + { + _idSelector = i => i.Id; + _items = Enumerable.Range(1, 20) + .Select(i => new TestItem { Id = $"id{i}", Value = $"value{i}" }) + .ToList(); + } + + [Fact] + public void GetPagedResult_ShouldReturnFirstPage_WhenNoCursorProvided() + { + var (pagedItems, meta) = Paginator.GetPagedResult(_items, _idSelector, 5, null); + + Assert.Equal(5, pagedItems.Count); + Assert.Equal("id1", pagedItems.First().Id); + Assert.NotNull(meta.Cursor); + } + + [Fact] + public void GetPagedResult_ShouldReturnNextPage_WhenValidCursorProvided() + { + var cursor = "id5".EncodeToBase64(); + var (pagedItems, meta) = Paginator.GetPagedResult(_items, _idSelector, 5, cursor); + + Assert.Equal("id6", pagedItems.First().Id); + Assert.Equal(5, pagedItems.Count); + Assert.NotNull(meta.Cursor); + } + + [Fact] + public void GetPagedResult_ShouldReturnFromStart_WhenInvalidCursorProvided() + { + var cursor = "nonExistingId".EncodeToBase64(); + var (pagedItems, meta) = Paginator.GetPagedResult(_items, _idSelector, 5, cursor); + + Assert.Equal("id1", pagedItems.First().Id); + Assert.NotNull(meta.Cursor); + } + + [Fact] + public void GetPagedResult_ShouldReturnDefaultPageSize_WhenPageSizeIsNull() + { + var (pagedItems, meta) = Paginator.GetPagedResult(_items, _idSelector, null, null); + + Assert.Equal(20, pagedItems.Count); + } + + [Fact] + public void GetPagedResult_ShouldReturnDefaultPageSize_WhenTotalItemIsGreaterThenPageSize_WithCursorAsLastItem() + { + var items = Enumerable.Range(1, 200) + .Select(i => new TestItem { Id = $"id{i}", Value = $"value{i}" }) + .ToList(); + var expectedCursor = "id100".EncodeToBase64(); + + var (pagedItems, meta) = Paginator.GetPagedResult(items, _idSelector, null, null); + + Assert.Equal(100, pagedItems.Count); + Assert.Equal(expectedCursor, meta.Cursor); + } + + [Fact] + public void GetPagedResult_ShouldCapPageSizeAtMax_WhenPageSizeExceedsLimit() + { + var expectedCursor = "id20".EncodeToBase64(); + + var (pagedItems, meta) = Paginator.GetPagedResult(_items, _idSelector, 2000, null); + + Assert.Equal(20, pagedItems.Count); + Assert.Equal(expectedCursor, meta.Cursor); + } + + [Fact] + public void GetPagedResult_ShouldUseZeroPageSize_WhenPageSizeIsZeroOrNegative() + { + var (pagedItems1, _) = Paginator.GetPagedResult(_items, _idSelector, 0, null); + var (pagedItems2, _) = Paginator.GetPagedResult(_items, _idSelector, -10, null); + + Assert.Equal(0, pagedItems1.Count); + Assert.Equal(0, pagedItems2.Count); + } + + [Fact] + public void GetPagedResult_ShouldReturnLastIdAsCursor_WhenAtEndOfList() + { + var cursor = "id16".EncodeToBase64(); + var expectedCursor = "id20".EncodeToBase64(); + var (pagedItems, meta) = Paginator.GetPagedResult(_items, _idSelector, 10, cursor); + + Assert.Equal(4, pagedItems.Count); + Assert.Equal(expectedCursor, meta.Cursor); + } + + [Fact] + public void GetPagedResult_ShouldReturnLastIdAsCursor_WhenNotFullPage() + { + var cursor = "id18".EncodeToBase64(); + var expectedCursor = "id20".EncodeToBase64(); + var (pagedItems, meta) = Paginator.GetPagedResult(_items, _idSelector, 5, cursor); + + Assert.Equal(2, pagedItems.Count); + Assert.Equal(expectedCursor, meta.Cursor); + } + + private class TestItem + { + public string Id { get; set; } = null!; + public string Value { get; set; } = null!; + } +} + diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/Providers/MetaDataProvider/MetaDataProviderTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/Providers/MetaDataProvider/MetaDataProviderTests.cs new file mode 100644 index 0000000..f8556a0 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/Providers/MetaDataProvider/MetaDataProviderTests.cs @@ -0,0 +1,149 @@ +using System.Text.Json; + +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Constants; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Exceptions; +using AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.Providers; + +using Microsoft.Extensions.Logging; + +using NSubstitute; + +using Provider = AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.Providers.MetaDataProvider.MetaDataProvider; + +namespace AAS.TwinEngine.Plugin.TestPlugin.UnitTests.Infrastructure.Providers.MetaDataProvider; + +public class MetaDataProviderTests +{ + private readonly ILogger _logger = Substitute.For>(); + private readonly Provider _sut; + + public MetaDataProviderTests() + { + SetMetaData(TestData.TestMetaData); + _sut = new Provider(_logger); + } + + private static void SetMetaData(string jsonContent) => MockData.MetaData = JsonDocument.Parse(jsonContent); + + [Fact] + public async Task GetShellDescriptorsAsync_ReturnsPagedShells_WithPagingMetadata() + { + const int Limit = 2; + string? cursor = null; + + var result = await _sut.GetShellDescriptorsAsync(Limit, cursor, CancellationToken.None); + + Assert.NotNull(result); + Assert.NotNull(result.Result); + Assert.True(result.Result.Count <= Limit); + Assert.NotNull(result.PagingMetaData); + Assert.False(string.IsNullOrWhiteSpace(result.PagingMetaData.Cursor)); + } + + [Fact] + public async Task GetShellDescriptorsAsync_ReturnsCorrectPage_WhenCursorIsProvided() + { + var firstPage = await _sut.GetShellDescriptorsAsync(2, null, CancellationToken.None); + var nextCursor = firstPage.PagingMetaData?.Cursor; + + var secondPage = await _sut.GetShellDescriptorsAsync(2, nextCursor, CancellationToken.None); + + Assert.NotNull(secondPage); + Assert.NotNull(secondPage.Result); + Assert.True(secondPage.Result.Count <= 2); + Assert.NotEqual(firstPage.Result?.FirstOrDefault()?.Id, secondPage.Result?.FirstOrDefault()?.Id); + } + + [Fact] + public async Task GetShellDescriptorsAsync_ThrowsNotFound_WhenCursorIsInvalid() + { + var record = await Assert.ThrowsAsync(() => _sut.GetShellDescriptorsAsync(2, "bW0=", CancellationToken.None)); + + Assert.Equal(ExceptionMessages.ShellDescriptorDataNotFound, record.Message); + } + + [Fact] + public async Task GetShellDescriptorsAsync_NeverReturnsShell_WithEmptyIds() + { + var result = await _sut.GetShellDescriptorsAsync(null, null, CancellationToken.None); + + Assert.NotNull(result); + Assert.All(result.Result!, shell => Assert.False(string.IsNullOrWhiteSpace(shell.Id), "Shell with empty or null Id found.")); + } + + [Fact] + public async Task GetShellDescriptorsAsync_ReturnsEmptyList_WhenNoShellsExist() + { + SetMetaData("[]"); + var sut = new Provider(_logger); + + var result = await sut.GetShellDescriptorsAsync(null, null, CancellationToken.None); + + Assert.NotNull(result); + Assert.Empty(result.Result ?? []); + Assert.NotNull(result.PagingMetaData); + Assert.Null(result.PagingMetaData.Cursor); + } + + [Fact] + public async Task GetShellDescriptorAsync_ReturnsCorrectShell() + { + const string Id = "ContactInformation"; + + var result = await _sut.GetShellDescriptorAsync(Id, CancellationToken.None); + + Assert.NotNull(result); + Assert.Equal(Id, result.Id); + } + + [Fact] + public async Task GetShellDescriptorAsync_ReturnsCorrectShell_HasEmptyIdShort() + { + const string Id = "1000-859"; + + var result = await _sut.GetShellDescriptorAsync(Id, CancellationToken.None); + + Assert.NotNull(result); + Assert.Equal(Id, result.Id); + Assert.Empty(result.IdShort); + } + + [Fact] + public async Task GetShellDescriptorAsync_ThrowsNotFoundException_WhenIdNotFound() + { + const string InvalidId = "nonexistent-id"; + + await Assert.ThrowsAsync(() => _sut.GetShellDescriptorAsync(InvalidId, CancellationToken.None)); + } + + [Fact] + public async Task GetAssetAsync_ReturnsAsset_WhenExists() + { + const string AssetId = "ContactInformation"; + + var result = await _sut.GetAssetAsync(AssetId, CancellationToken.None); + + Assert.NotNull(result); + Assert.Equal(AssetId, result.GlobalAssetId); + } + + [Fact] + public async Task GetAssetAsync_ThrowsNotFoundException_WhenShellFound_ButDoesNotHaveAssetInformation() + { + const string InvalidAssetId = "SoftwareNameplate"; + + var exception = await Assert.ThrowsAsync(() => + _sut.GetAssetAsync(InvalidAssetId, CancellationToken.None)); + Assert.Contains(ExceptionMessages.AssetNotFound, exception.Message, StringComparison.CurrentCulture); + } + + [Fact] + public async Task GetAssetAsync_ThrowsNotFoundException_WhenAssetNotFound() + { + const string InvalidAssetId = "nonexistent-asset"; + + var exception = await Assert.ThrowsAsync(() => + _sut.GetAssetAsync(InvalidAssetId, CancellationToken.None)); + Assert.Contains(ExceptionMessages.AssetNotFound, exception.Message, StringComparison.CurrentCulture); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/Providers/MockDataInitializerTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/Providers/MockDataInitializerTests.cs new file mode 100644 index 0000000..6f7fe15 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/Providers/MockDataInitializerTests.cs @@ -0,0 +1,87 @@ +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Constants; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Exceptions; +using AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.Providers; + +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +using NSubstitute; + +namespace AAS.TwinEngine.Plugin.TestPlugin.UnitTests.Infrastructure.Providers; + +public class MockDataInitializerTests +{ + private readonly ILogger _logger; + private readonly IHostEnvironment _hostEnvironment; + private readonly string _testDataDirectory; + + public MockDataInitializerTests() + { + _logger = Substitute.For>(); + _hostEnvironment = Substitute.For(); + _testDataDirectory = CreateTestDataDirectory(); + _hostEnvironment.ContentRootPath.Returns(_testDataDirectory); + } + + private static string CreateTestDataDirectory() + { + var tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + var dataDir = Path.Combine(tempDir, "Data"); + Directory.CreateDirectory(dataDir); + return tempDir; + } + + private void CreateTestFile(string fileName, string content) + { + var filePath = Path.Combine(_testDataDirectory, "Data", fileName); + File.WriteAllText(filePath, content); + } + + [Fact] + public void Initialize_ValidFiles_LoadsDataIntoRegistry() + { + CreateTestFile("mock-metadata.json", "{ \"meta\": \"data\" }"); + CreateTestFile("mock-submodel-data.json", "{ \"submodel\": \"data\" }"); + + var initializer = new MockDataInitializer(_hostEnvironment, _logger); + initializer.Initialize(CancellationToken.None); + + Assert.NotNull(MockData.MetaData); + Assert.NotNull(MockData.SubmodelData); + } + + [Fact] + public void Initialize_MissingMetadataFile_ThrowsFileNotFoundException() + { + CreateTestFile("mock-submodel-data.json", "{ \"submodel\": \"data\" }"); + + var initializer = new MockDataInitializer(_hostEnvironment, _logger); + + var ex = Assert.Throws(() => initializer.Initialize(CancellationToken.None)); + Assert.Contains("file not found", ex.Message, StringComparison.Ordinal); + } + + [Fact] + public void Initialize_InvalidJson_ThrowsInternalServerException() + { + CreateTestFile("mock-metadata.json", "{ invalid json"); + CreateTestFile("mock-submodel-data.json", "{ \"submodel\": \"data\" }"); + + var initializer = new MockDataInitializer(_hostEnvironment, _logger); + + var ex = Assert.Throws(() => initializer.Initialize(CancellationToken.None)); + Assert.Contains(ExceptionMessages.ResourceNotValid, ex.Message, StringComparison.Ordinal); + } + + [Fact] + public void Initialize_EmptyFile_ThrowsInternalServerException() + { + CreateTestFile("mock-metadata.json", ""); + CreateTestFile("mock-submodel-data.json", "{ \"submodel\": \"data\" }"); + + var initializer = new MockDataInitializer(_hostEnvironment, _logger); + + var ex = Assert.Throws(() => initializer.Initialize(CancellationToken.None)); + Assert.Contains(ExceptionMessages.ResourceNotValid, ex.Message, StringComparison.Ordinal); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/Providers/SubmodelProviders/SubmodelProviderTests.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/Providers/SubmodelProviders/SubmodelProviderTests.cs new file mode 100644 index 0000000..daab9e5 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/Providers/SubmodelProviders/SubmodelProviderTests.cs @@ -0,0 +1,253 @@ +using System.Text.Json; + +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.Submodel.Config; +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.Submodel; +using AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.Providers; +using AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.Providers.SubmodelProviders; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using NSubstitute; + +namespace AAS.TwinEngine.Plugin.TestPlugin.UnitTests.Infrastructure.Providers.SubmodelProviders; + +public class SubmodelProviderTests +{ + private readonly ILogger _logger; + private readonly SubmodelProvider _sut; + private readonly IOptions _semantics; + private const string ProductId = "test-submodelId"; + + public SubmodelProviderTests() + { + _logger = Substitute.For>(); + _semantics = Substitute.For>(); + _semantics.Value.Returns(new Semantics { IndexContextPrefix = "_aastwinengine_" }); + SetSubmodelData(TestData.TestSubmodelData); + _sut = new SubmodelProvider(_logger, _semantics); + } + + private static void SetSubmodelData(string jsonContent) => MockData.SubmodelData = JsonDocument.Parse(jsonContent); + + [Fact] + public void EnrichWithData_LeafNode_SetsValueFromJson() + { + var leaf = new SemanticLeafNode("Email", DataType.String, null); + var root = new SemanticBranchNode("root", DataType.Object); + root.AddChild(leaf); + + _sut.EnrichWithData(root, ProductId); + + Assert.Equal("test@example.com", leaf.Value); + } + + [Fact] + public void EnrichWithData_LeafNode_WhenNoDetailsAvailable_SetsEmptyValue() + { + var name = new SemanticLeafNode("name", DataType.String, null!); + + _sut.EnrichWithData(name, ProductId); + + Assert.Equal(string.Empty, name.Value); + } + + [Fact] + public void EnrichWithData_BranchNodeWithoutLeaves_ReturnsUnmodifiedBranch() + { + var branch = new SemanticBranchNode("contactInformation", DataType.Object); + + var result = _sut.EnrichWithData(branch, ProductId); + + Assert.Empty(branch.Children); + Assert.Same(branch, result); + } + + [Fact] + public void EnricWithData_ForLeafWithoutComplexData_SetsValueFromData() + { + var root = new SemanticBranchNode("ManufacturerName", DataType.Object); + var leaf = new SemanticLeafNode("ManufacturerName_en", DataType.String, null!); + root.AddChild(leaf); + + _sut.EnrichWithData(root, ProductId); + + Assert.Equal("M&M", leaf.Value); + } + + [Fact] + public void EnrichWithData_ForBranchNodeWithoutComplexData_SetsValueFormData() + { + var contact = new SemanticBranchNode("ContactInformation", DataType.Object); + contact.AddChild(new SemanticLeafNode("Email", DataType.String, null!)); + contact.AddChild(new SemanticLeafNode("Phone", DataType.String, null!)); + + _sut.EnrichWithData(contact, ProductId); + + var email = (SemanticLeafNode)contact.Children + .First(c => c.SemanticId == "Email"); + var phone = (SemanticLeafNode)contact.Children + .First(c => c.SemanticId == "Phone"); + Assert.Equal("contact@test.com", email.Value); + Assert.Equal("555-1234", phone.Value); + } + + [Fact] + public void EnrichWithData_ForBranchNodeWithComplexData_ClonesChildrenForEachElement_SetsValueFormData() + { + var contactInformationBranch = new SemanticBranchNode("ContactInformations", DataType.Object); + var contactBranch = new SemanticBranchNode("ContactInformation", DataType.Array); + contactBranch.AddChild(new SemanticLeafNode("Email", DataType.String, null!)); + var phoneBranch = new SemanticBranchNode("Phone", DataType.Object); + contactBranch.AddChild(phoneBranch); + phoneBranch.AddChild(new SemanticLeafNode("TelephoneNumber", DataType.String, "")); + contactInformationBranch.AddChild(contactBranch); + + _sut.EnrichWithData(contactInformationBranch, ProductId); + + var contacts = contactInformationBranch.Children.OfType().ToList(); + Assert.Equal(2, contacts.Count); + var firstContact = contacts[0]; + var firstEmail = firstContact.Children.First(c => c.SemanticId == "Email") as SemanticLeafNode; + var firstPhone = firstContact.Children.First(c => c.SemanticId == "Phone") as SemanticBranchNode; + var phoneTelephoneNumber1 = firstPhone.Children.First(c => c.SemanticId == "TelephoneNumber") as SemanticLeafNode; + Assert.Equal("first@test.com", firstEmail!.Value); + Assert.Equal("111-1111", phoneTelephoneNumber1.Value); + var secondContact = contacts[1]; + var secondEmail = secondContact.Children.First(c => c.SemanticId == "Email") as SemanticLeafNode; + var secondPhone = secondContact.Children.First(c => c.SemanticId == "Phone") as SemanticBranchNode; + var phoneTelephoneNumber2 = secondPhone.Children.First(c => c.SemanticId == "TelephoneNumber") as SemanticLeafNode; + Assert.Equal("second@test.com", secondEmail!.Value); + Assert.Equal("222-2222", phoneTelephoneNumber2!.Value); + } + + [Fact] + public void EnrichWithData_ForBranchNodeWithSpecificIndex_SetsValueFormData() + { + var contactInformationBranch = new SemanticBranchNode("ContactInformations", DataType.Object); + var contactBranch = new SemanticBranchNode("ContactInformation_aastwinengine_01", DataType.Object); + contactBranch.AddChild(new SemanticLeafNode("Email", DataType.String, null!)); + var phoneBranch = new SemanticBranchNode("Phone", DataType.Object); + contactBranch.AddChild(phoneBranch); + phoneBranch.AddChild(new SemanticLeafNode("TelephoneNumber", DataType.String, "")); + var availableBranch = new SemanticBranchNode("AvailableTime", DataType.Object); + phoneBranch.AddChild(availableBranch); + availableBranch.AddChild(new SemanticLeafNode("AvailableTime_de", DataType.String, "")); + contactInformationBranch.AddChild(contactBranch); + + _sut.EnrichWithData(contactInformationBranch, ProductId); + + var email = contactBranch.Children.First(c => c.SemanticId == "Email") as SemanticLeafNode; + var phone = contactBranch.Children.First(c => c.SemanticId == "Phone") as SemanticBranchNode; + var phoneTelephoneNumber = phone?.Children.First(c => c.SemanticId == "TelephoneNumber") as SemanticLeafNode; + Assert.Equal("second@test.com", email!.Value); + Assert.Equal("222-2222", phoneTelephoneNumber!.Value); + var available = phone?.Children.First(c => c.SemanticId == "AvailableTime") as SemanticBranchNode; + var availableTime = available?.Children.First(c => c.SemanticId == "AvailableTime_de") as SemanticLeafNode; + Assert.Equal("Montag – Freitag 08:00 bis 16:00", availableTime?.Value); + } + + [Fact] + public void EnrichWithData_WhenBranchHasChildArray_ClonesNodesAndSetsLeafValues() + { + var nameplateNode = new SemanticBranchNode("Nameplate", DataType.Object); + var contactInfoNode = new SemanticBranchNode("ContactInformation", DataType.Array); + var phoneNode = new SemanticBranchNode("Phone", DataType.Object); + var phoneNumberLeaf = new SemanticLeafNode("TelephoneNumber", DataType.String, ""); + phoneNode.AddChild(phoneNumberLeaf); + contactInfoNode.AddChild(phoneNode); + nameplateNode.AddChild(contactInfoNode); + + _sut.EnrichWithData(nameplateNode, ProductId); + + var contactInfo = nameplateNode.Children[0] as SemanticBranchNode; + Assert.Equal(2, contactInfo?.Children.Count); + var phone1 = contactInfo?.Children[0] as SemanticBranchNode; + Assert.Single(phone1?.Children!); + var firstNumber = phone1.Children[0] as SemanticLeafNode; + Assert.Equal("+49571 8870", firstNumber.Value); + var phone2 = contactInfo?.Children[1] as SemanticBranchNode; + Assert.Single(phone1.Children); + var secondNumber = phone2?.Children[0] as SemanticLeafNode; + Assert.Equal("+91 7845129532", secondNumber.Value); + } + + [Fact] + public void EnrichWithData_WhenBranchHasDeepChildArray_ExpandsArrayAndSetsLeafValues() + { + var mcadNode = new SemanticBranchNode("MCAD", DataType.Object); + var documentStepNode = new SemanticBranchNode("Document_STEP", DataType.Array); + var documentIdNode = new SemanticBranchNode("DocumentId", DataType.Object); + var documentVersionNode = new SemanticBranchNode("DocumentVersion", DataType.Object); + var statusValueLeaf = new SemanticLeafNode("StatusValue", DataType.String, ""); + documentVersionNode.AddChild(statusValueLeaf); + documentIdNode.AddChild(documentVersionNode); + documentStepNode.AddChild(documentIdNode); + mcadNode.AddChild(documentStepNode); + + _sut.EnrichWithData(mcadNode, ProductId); + + var documentStep = mcadNode.Children[0] as SemanticBranchNode; + var documentId = documentStep.Children[0] as SemanticBranchNode; + Assert.Equal(2, documentId.Children.Count); + var firstVersion = documentId.Children[0] as SemanticBranchNode; + var firstStatus = firstVersion.Children[0] as SemanticLeafNode; + Assert.Equal("StatusValue", firstStatus.SemanticId); + Assert.Equal("Released", firstStatus.Value); + var secondVersion = documentId.Children[1] as SemanticBranchNode; + var secondStatus = secondVersion.Children[0] as SemanticLeafNode; + Assert.Equal("StatusValue", secondStatus.SemanticId); + Assert.Equal("Inprogress", secondStatus.Value); + } + + [Fact] + public void EnrichWithData_ForBranchNodeWithSpectificIndex_WithNestedComplexdata_SetsValueFromData() + { + var rootBranch = new SemanticBranchNode("Document_aastwinengine_00", DataType.Object); + rootBranch.AddChild(new SemanticLeafNode("IsPrimary", DataType.String, "")); + var branch = new SemanticBranchNode("DocumentClassification", DataType.Array); + rootBranch.AddChild(branch); + branch.AddChild(new SemanticLeafNode("ClassId", DataType.String, "")); + branch.AddChild(new SemanticLeafNode("ClassificationSystem", DataType.String, "")); + + _sut.EnrichWithData(rootBranch, ProductId); + + var documents = rootBranch.Children.OfType().ToList(); + Assert.Equal(2, documents.Count); + var primary = rootBranch.Children.First(c => c.SemanticId == "IsPrimary") as SemanticLeafNode; + Assert.Equal("true", primary.Value); + var firstDocumentClassification = documents[0]; + var firstClassId = firstDocumentClassification.Children.First(c => c.SemanticId == "ClassId") as SemanticLeafNode; + var firstClassificationSystem = firstDocumentClassification.Children.First(c => c.SemanticId == "ClassificationSystem") as SemanticLeafNode; + Assert.Equal("02-02", firstClassId.Value); + Assert.Equal("VDI2770:2020", firstClassificationSystem.Value); + var secondDocumentClassification = documents[1]; + var secondClassId = secondDocumentClassification.Children.First(c => c.SemanticId == "ClassId") as SemanticLeafNode; + var secondClassificationSystem = secondDocumentClassification.Children.First(c => c.SemanticId == "ClassificationSystem") as SemanticLeafNode; + Assert.Equal("STEP", secondClassId.Value); + Assert.Equal("IDTA-MCAD:2022", secondClassificationSystem.Value); + } + + [Fact] + public void EnrichWithData_ForBranchNodeWithSpectificIndex_WithoutNestedComplexdata_SetsValueFromData() + { + var rootBranch = new SemanticBranchNode("Document_aastwinengine_01", DataType.Object); + rootBranch.AddChild(new SemanticLeafNode("IsPrimary", DataType.String, "")); + var branch = new SemanticBranchNode("DocumentClassification", DataType.Object); + rootBranch.AddChild(branch); + branch.AddChild(new SemanticLeafNode("ClassId", DataType.String, "")); + branch.AddChild(new SemanticLeafNode("ClassificationSystem", DataType.String, "")); + + _sut.EnrichWithData(rootBranch, ProductId); + + var documents = rootBranch.Children.OfType().ToList(); + Assert.Equal(1, documents.Count); + var primary = rootBranch.Children.First(c => c.SemanticId == "IsPrimary") as SemanticLeafNode; + Assert.Equal("false", primary.Value); + var firstDocumentClassification = documents[0]; + var firstClassId = firstDocumentClassification.Children.First(c => c.SemanticId == "ClassId") as SemanticLeafNode; + var firstClassificationSystem = firstDocumentClassification.Children.First(c => c.SemanticId == "ClassificationSystem") as SemanticLeafNode; + Assert.Equal("01-01", firstClassId.Value); + Assert.Equal("VDI2770:2025", firstClassificationSystem.Value); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/Providers/TestData.cs b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/Providers/TestData.cs new file mode 100644 index 0000000..8f16093 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin.UnitTests/Infrastructure/Providers/TestData.cs @@ -0,0 +1,157 @@ +namespace AAS.TwinEngine.Plugin.TestPlugin.UnitTests.Infrastructure.Providers; + +public static class TestData +{ + public const string TestMetaData = @"[ + { + ""globalAssetId"": ""WeatherStation"", + ""idShort"": ""SensorWeatherStationExample"", + ""id"": ""WeatherStation"", + ""specificAssetIds"": [], + ""assetInformationData"": { + ""defaultThumbnail"": { + ""contentType"": ""image/svg+xml"", + ""path"": ""AAS_Logo.svg"" + } + } + }, + { + ""globalAssetId"": ""mm-2206-1631"", + ""idShort"": ""2206-1631/1000-859"", + ""id"": ""mm-2206-1631/1000-859"", + ""specificAssetIds"": [], + ""assetInformationData"": { + ""defaultThumbnail"": { + ""contentType"": ""image/svg+xml"", + ""path"": ""AAS_Logo.svg"" + } + } + }, + { + ""globalAssetId"": ""100-859"", + ""idShort"": """", + ""id"": ""1000-859"", + ""specificAssetIds"": [], + ""assetInformationData"": { + ""defaultThumbnail"": { + ""contentType"": ""image/svg+xml"", + ""path"": ""AAS_Logo.svg"" + } + } + }, + { + ""globalAssetId"": ""m&m-259"", + ""idShort"": """", + ""id"": """", + ""specificAssetIds"": [] + }, + { + ""globalAssetId"": ""SoftwareNameplate"", + ""idShort"": ""SoftwareNameplateAAS"", + ""id"": ""SoftwareNameplate/1/0"", + ""specificAssetIds"": [] + }, + { + ""globalAssetId"": ""ContactInformation"", + ""idShort"": ""ContactInformationAAS"", + ""id"": ""ContactInformation"", + ""specificAssetIds"": [ + { + ""name"": ""serialNumber"", + ""value"": ""SN-859-001"" + } + ], + ""assetInformationData"": { + ""defaultThumbnail"": { + ""contentType"": ""image/svg+xml"", + ""path"": ""AAS_Logo.svg"" + } + } + }, + { + ""globalAssetId"": ""DigitalNameplate"", + ""idShort"": ""DigitalNameplateAAS"", + ""id"": ""DigitalNameplate/3/0"", + ""specificAssetIds"": null + } + ]"; + + public const string TestSubmodelData = @" + { + ""test-submodelId"": { + ""root"": { + ""Email"": ""test@example.com"" + }, + ""Email"": ""test@example.com"", + ""ContactInformation"": { + ""Email"": ""contact@test.com"", + ""Phone"": ""555-1234"" + }, + ""ContactInformations"": { + ""ContactInformation"": [ + { + ""Email"": ""first@test.com"", + ""Phone"": { + ""TelephoneNumber"": ""111-1111"" + } + }, + { + ""Email"": ""second@test.com"", + ""Phone"": { + ""TelephoneNumber"": ""222-2222"", + ""AvailableTime"": { + ""AvailableTime_de"": ""Montag – Freitag 08:00 bis 16:00"" + } + } + } + ] + }, + ""ManufacturerName"": { + ""ManufacturerName_en"": ""M&M"" + }, + ""Document"": [ + { + ""IsPrimary"": ""true"", + ""DocumentClassification"": [ + { + ""ClassId"": ""02-02"", + ""ClassificationSystem"": ""VDI2770:2020"" + }, + { + ""ClassId"": ""STEP"", + ""ClassificationSystem"": ""IDTA-MCAD:2022"" + } + ] + }, + { + ""IsPrimary"": ""false"", + ""DocumentClassification"": { + ""ClassId"": ""01-01"", + ""ClassificationSystem"": ""VDI2770:2025"" + } + } + ], + ""Nameplate"": { + ""ContactInformation"": { + ""Phone"": [ + { + ""TelephoneNumber"": ""+49571 8870"" + }, + { + ""TelephoneNumber"": ""+91 7845129532"" + } + ] + } + }, + ""MCAD"": { + ""Document_STEP"": { + ""DocumentId"": { + ""DocumentVersion"": [ + { + ""StatusValue"": ""Released"" + }, + { + ""StatusValue"": ""Inprogress"" + } + ]}}}}}"; +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/AAS.TwinEngine.Plugin.TestPlugin.csproj b/source/AAS.TwinEngine.Plugin.TestPlugin/AAS.TwinEngine.Plugin.TestPlugin.csproj new file mode 100644 index 0000000..4f16565 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/AAS.TwinEngine.Plugin.TestPlugin.csproj @@ -0,0 +1,38 @@ + + + + net8.0 + enable + enable + Linux + . + 2ba5fa1c-2588-4655-bd1f-bc7cd274eba9 + true + + + + False + + + + False + + + + + + + + + + + + + + + + + + + + diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Manifest/Handler/IManifestHandler.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Manifest/Handler/IManifestHandler.cs new file mode 100644 index 0000000..85f3453 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Manifest/Handler/IManifestHandler.cs @@ -0,0 +1,8 @@ +using AAS.TwinEngine.Plugin.TestPlugin.Api.Manifest.Responses; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Api.Manifest.Handler; + +public interface IManifestHandler +{ + Task GetManifestData(CancellationToken cancellationToken); +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Manifest/Handler/ManifestHandler.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Manifest/Handler/ManifestHandler.cs new file mode 100644 index 0000000..c9c3496 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Manifest/Handler/ManifestHandler.cs @@ -0,0 +1,18 @@ +using AAS.TwinEngine.Plugin.TestPlugin.Api.Manifest.MappingProfiles; +using AAS.TwinEngine.Plugin.TestPlugin.Api.Manifest.Responses; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.Manifest; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Api.Manifest.Handler; + +public class ManifestHandler(ILogger logger, + IManifestService manifestService) : IManifestHandler +{ + public async Task GetManifestData(CancellationToken cancellationToken) + { + logger.LogInformation("Start executing request for manifest data"); + + var manifest = await manifestService.GetManifestData(cancellationToken); + + return manifest.ToDto(); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Manifest/ManifestController.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Manifest/ManifestController.cs new file mode 100644 index 0000000..30ea6ea --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Manifest/ManifestController.cs @@ -0,0 +1,28 @@ +using System.Text.Json.Nodes; + +using AAS.TwinEngine.Plugin.TestPlugin.Api.Manifest.Handler; +using AAS.TwinEngine.Plugin.TestPlugin.Api.Manifest.Responses; + +using Asp.Versioning; + +using Microsoft.AspNetCore.Mvc; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Api.Manifest; + +[ApiController] +[Route("")] +[ApiVersion(1)] +public class ManifestController(IManifestHandler manifestHandler) :ControllerBase +{ + [HttpGet("manifest")] + [ProducesResponseType(typeof(JsonObject), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ActionResult), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ActionResult), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ActionResult), StatusCodes.Status500InternalServerError)] + public async Task> RetrieveManifestDataAsync(CancellationToken cancellationToken) + { + var manifestData = await manifestHandler.GetManifestData(cancellationToken); + + return Ok(manifestData); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Manifest/MappingProfiles/ManifestMappingProfile.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Manifest/MappingProfiles/ManifestMappingProfile.cs new file mode 100644 index 0000000..4074303 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Manifest/MappingProfiles/ManifestMappingProfile.cs @@ -0,0 +1,20 @@ +using AAS.TwinEngine.Plugin.TestPlugin.Api.Manifest.Responses; +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.Manifest; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Api.Manifest.MappingProfiles; + +public static class ManifestMappingProfile +{ + public static ManifestDto ToDto(this ManifestData? data) + { + return new ManifestDto() + { + Capabilities = new CapabilitiesDto() + { + HasAssetInformation = data.Capabilities.HasAssetInformation, + HasShellDescriptor = data.Capabilities.HasShellDescriptor + }, + SupportedSemanticIds = data.SupportedSemanticIds + }; + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Manifest/Responses/ManifestDto.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Manifest/Responses/ManifestDto.cs new file mode 100644 index 0000000..fda6ff2 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Manifest/Responses/ManifestDto.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Api.Manifest.Responses; + +public class ManifestDto +{ + [JsonPropertyName("supportedSemanticIds")] + public required IList SupportedSemanticIds { get; init; } + + [JsonPropertyName("capabilities")] + public required CapabilitiesDto Capabilities { get; set; } +} + +public class CapabilitiesDto +{ + [JsonPropertyName("hasShellDescriptor")] + public bool HasShellDescriptor { get; set; } + + [JsonPropertyName("hasAssetInformation")] + public bool HasAssetInformation { get; set; } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/Handler/IMetaDataHandler.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/Handler/IMetaDataHandler.cs new file mode 100644 index 0000000..a9aedf6 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/Handler/IMetaDataHandler.cs @@ -0,0 +1,13 @@ +using AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData.Requests; +using AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData.Responses; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData.Handler; + +public interface IMetaDataHandler +{ + Task GetShellDescriptors(GetShellDescriptorsRequest request, CancellationToken cancellationToken); + + Task GetShellDescriptor(GetShellDescriptorRequest request, CancellationToken cancellationToken); + + Task GetAsset(GetAssetRequest request, CancellationToken cancellationToken); +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/Handler/MetaDataHandler.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/Handler/MetaDataHandler.cs new file mode 100644 index 0000000..349acc3 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/Handler/MetaDataHandler.cs @@ -0,0 +1,57 @@ +using AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData.MappingProfiles; +using AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData.Requests; +using AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData.Responses; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Constants; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Exceptions; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.MetaData; +using AAS.TwinEngine.Plugin.TestPlugin.Common.Extensions; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData.Handler; + +public class MetaDataHandler( + ILogger logger, + IMetaDataService metaDataService) : IMetaDataHandler +{ + public async Task GetShellDescriptors(GetShellDescriptorsRequest request, CancellationToken cancellationToken) + { + request?.Limit.ValidateLimit(logger); + + logger.LogDebug("Start executing get request for shell-descriptors metadata"); + + var shellDescriptors = await metaDataService.GetShellDescriptorsAsync(request?.Limit, request?.Cursor, cancellationToken); + + return shellDescriptors.ToDto(); + } + + public async Task GetShellDescriptor(GetShellDescriptorRequest request, CancellationToken cancellationToken) + { + logger.LogDebug($"Start executing get request for shell-descriptor metadata for {request.aasIdentifier}"); + + var shellDescriptorMetaData = await metaDataService.GetShellDescriptorAsync(request.aasIdentifier, cancellationToken); + + if (shellDescriptorMetaData != null) + { + var response = shellDescriptorMetaData.ToDto(); + return response; + } + + logger.LogWarning($"Shell-descriptor metadata not found for {request.aasIdentifier}."); + throw new NotFoundException(ExceptionMessages.ShellDescriptorDataNotFound); + } + + public async Task GetAsset(GetAssetRequest request, CancellationToken cancellationToken) + { + logger.LogDebug("Start executing get request for asset metadata element"); + + var assetMetaData = await metaDataService.GetAssetAsync(request.shellIdentifier, cancellationToken); + + if (assetMetaData != null) + { + var response = assetMetaData.ToDto(); + return response; + } + + logger.LogWarning($"Asset metadata not found for {request.shellIdentifier}."); + throw new NotFoundException(ExceptionMessages.AssetNotFound); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/MappingProfiles/AssetMappingProfile.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/MappingProfiles/AssetMappingProfile.cs new file mode 100644 index 0000000..24a79a1 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/MappingProfiles/AssetMappingProfile.cs @@ -0,0 +1,27 @@ +using AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData.Responses; +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.MetaData; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData.MappingProfiles; + +public static class AssetMappingProfile +{ + public static AssetDto ToDto(this AssetData? data) + { + return new AssetDto + { + GlobalAssetId = data.GlobalAssetId, + SpecificAssetIds = data.SpecificAssetIds?.Select(id => new SpecificAssetIdsDto + { + Name = id.Name, + Value = id.Value + }).ToList(), + DefaultThumbnail = data.DefaultThumbnail == null + ? null + : new DefaultThumbnailDto + { + ContentType = data.DefaultThumbnail.ContentType, + Path = data.DefaultThumbnail.Path + } + }; + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/MappingProfiles/ShellDescriptorProfile.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/MappingProfiles/ShellDescriptorProfile.cs new file mode 100644 index 0000000..8e81ef6 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/MappingProfiles/ShellDescriptorProfile.cs @@ -0,0 +1,22 @@ +using AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData.Responses; +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.MetaData; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData.MappingProfiles; + +public static class ShellDescriptorProfile +{ + public static ShellDescriptorDto ToDto(this ShellDescriptorData data) + => new() + { + GlobalAssetId = data.GlobalAssetId, + IdShort = data.IdShort, + Id = data.Id, + SpecificAssetIds = data.SpecificAssetIds? + .Select(x => new SpecificAssetIdsDto + { + Name = x.Name, + Value = x.Value + }) + .ToList() + }; +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/MappingProfiles/ShellDescriptorsProfile.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/MappingProfiles/ShellDescriptorsProfile.cs new file mode 100644 index 0000000..34b98a1 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/MappingProfiles/ShellDescriptorsProfile.cs @@ -0,0 +1,21 @@ +using AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData.Responses; +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.MetaData; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData.MappingProfiles; + +public static class ShellDescriptorsProfile +{ + public static ShellDescriptorsDto ToDto(this ShellDescriptorsData descriptors) + { + ArgumentNullException.ThrowIfNull(descriptors); + + return new ShellDescriptorsDto + { + PagingMetaData = new PagingMetaDataDto + { + Cursor = descriptors.PagingMetaData!.Cursor + }, + Result = descriptors.Result?.Select(s => s.ToDto()).ToList() + }; + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/MetaDataController.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/MetaDataController.cs new file mode 100644 index 0000000..6edd2c3 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/MetaDataController.cs @@ -0,0 +1,63 @@ +using AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData.Handler; +using AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData.Requests; +using AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData.Responses; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Exceptions.Responses; +using AAS.TwinEngine.Plugin.TestPlugin.Common.Extensions; + +using Asp.Versioning; + +using Microsoft.AspNetCore.Mvc; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData; + +[ApiController] +[Route("metadata")] +[ApiVersion(1)] +public class MetaDataController(IMetaDataHandler metaDataHandler) : ControllerBase +{ + [HttpGet("shells")] + [ProducesResponseType(typeof(ShellDescriptorsDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ServiceErrorResponse), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ServiceErrorResponse), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ServiceErrorResponse), StatusCodes.Status500InternalServerError)] + public async Task> GetShellDescriptorsAsync([FromQuery] int? limit, [FromQuery] string? cursor, CancellationToken cancellationToken) + { + var request = new GetShellDescriptorsRequest(limit, cursor); + + var response = await metaDataHandler.GetShellDescriptors(request, cancellationToken); + + return Ok(response); + } + + [HttpGet("shells/{AasIdentifier}")] + [ProducesResponseType(typeof(ShellDescriptorDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ServiceErrorResponse), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ServiceErrorResponse), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ServiceErrorResponse), StatusCodes.Status500InternalServerError)] + public async Task> GetShellDescriptorAsync([FromRoute] string aasIdentifier, CancellationToken cancellationToken) + { + var decodedAasIdentifier = aasIdentifier.DecodeBase64(); + + var request = new GetShellDescriptorRequest(decodedAasIdentifier); + + var response = await metaDataHandler.GetShellDescriptor(request, cancellationToken); + + return Ok(response); + } + + [HttpGet("assets/{shellIdentifier}")] + [ProducesResponseType(typeof(AssetDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ServiceErrorResponse), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ServiceErrorResponse), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ServiceErrorResponse), StatusCodes.Status500InternalServerError)] + public async Task> GetAssetAsync([FromRoute] string shellIdentifier, CancellationToken cancellationToken) + { + var decodedAasIdentifier = shellIdentifier.DecodeBase64(); + + var request = new GetAssetRequest(decodedAasIdentifier); + + var response = await metaDataHandler.GetAsset(request, cancellationToken); + + return Ok(response); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/Requests/GetAssetRequest.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/Requests/GetAssetRequest.cs new file mode 100644 index 0000000..74e1ea7 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/Requests/GetAssetRequest.cs @@ -0,0 +1,3 @@ +namespace AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData.Requests; + +public record GetAssetRequest(string shellIdentifier); diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/Requests/GetShellDescriptorRequest.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/Requests/GetShellDescriptorRequest.cs new file mode 100644 index 0000000..8f5dc75 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/Requests/GetShellDescriptorRequest.cs @@ -0,0 +1,3 @@ +namespace AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData.Requests; + +public record GetShellDescriptorRequest(string aasIdentifier); diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/Requests/GetShellDescriptorsRequest.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/Requests/GetShellDescriptorsRequest.cs new file mode 100644 index 0000000..2e2a976 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/Requests/GetShellDescriptorsRequest.cs @@ -0,0 +1,3 @@ +namespace AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData.Requests; + +public record GetShellDescriptorsRequest(int? Limit, string? Cursor); diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/Responses/AssetDto.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/Responses/AssetDto.cs new file mode 100644 index 0000000..1053530 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/Responses/AssetDto.cs @@ -0,0 +1,24 @@ +using System.Text.Json.Serialization; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData.Responses; + +public class AssetDto +{ + [JsonPropertyName("globalAssetId")] + public string? GlobalAssetId { get; set; } + + [JsonPropertyName("specificAssetIds")] + public List? SpecificAssetIds { get; set; } + + [JsonPropertyName("defaultThumbnail")] + public DefaultThumbnailDto? DefaultThumbnail { get; set; } +} + +public class DefaultThumbnailDto +{ + [JsonPropertyName("path")] + public string? Path { get; set; } + + [JsonPropertyName("contentType")] + public string? ContentType { get; set; } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/Responses/ShellDescriptorDto.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/Responses/ShellDescriptorDto.cs new file mode 100644 index 0000000..153bc55 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/Responses/ShellDescriptorDto.cs @@ -0,0 +1,28 @@ +using System.Text.Json.Serialization; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData.Responses; + +public class ShellDescriptorDto +{ + [JsonPropertyName("globalAssetId")] + public string? GlobalAssetId { get; set; } + + [JsonPropertyName("idShort")] + public string? IdShort { get; set; } + + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonPropertyName("specificAssetIds")] + public List? SpecificAssetIds { get; set; } +} + +public class SpecificAssetIdsDto +{ + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("value")] + public string? Value { get; set; } +} + diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/Responses/ShellDescriptorsDto.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/Responses/ShellDescriptorsDto.cs new file mode 100644 index 0000000..404c88f --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/MetaData/Responses/ShellDescriptorsDto.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData.Responses; + +public class ShellDescriptorsDto +{ + [JsonPropertyName("paging_metadata")] + public PagingMetaDataDto? PagingMetaData { get; set; } + + [JsonPropertyName("result")] + public IList? Result { get; init; } +} + +public class PagingMetaDataDto +{ + [JsonPropertyName("cursor")] + public string? Cursor { get; set; } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Handler/ISubmodelHandler.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Handler/ISubmodelHandler.cs new file mode 100644 index 0000000..fe0492d --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Handler/ISubmodelHandler.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Nodes; + +using AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel.Requests; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel.Handler; + +public interface ISubmodelHandler +{ + Task GetSubmodelData(GetSubmodelDataRequest request, CancellationToken cancellationToken); +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Handler/SubmodelHandler.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Handler/SubmodelHandler.cs new file mode 100644 index 0000000..5f4b562 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Handler/SubmodelHandler.cs @@ -0,0 +1,29 @@ +using System.Text.Json.Nodes; + +using AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel.Requests; +using AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel.Services; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.Submodel; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel.Handler; + +public class SubmodelHandler( + ILogger logger, + ISubmodelService submodelService, + IJsonSchemaParser jsonSchemaParser, + ISemanticTreeHandler semanticTreeHandler) : ISubmodelHandler +{ + public async Task GetSubmodelData(GetSubmodelDataRequest request, CancellationToken cancellationToken) + { + logger.LogDebug("Start executing get request for product data"); + + logger.LogInformation("Processing request for submodel ID: {submodelId}", request.submodelId); + + var semanticIds = jsonSchemaParser.ParseJsonSchema(request.dataQuery); + + var filledSemanticIds = await submodelService.GetValuesBySemanticIds(semanticIds, request.submodelId); + + var result = semanticTreeHandler.GetJson(filledSemanticIds, request.dataQuery); + + return result; + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Requests/GetSubmodelDataRequest.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Requests/GetSubmodelDataRequest.cs new file mode 100644 index 0000000..d0c8f94 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Requests/GetSubmodelDataRequest.cs @@ -0,0 +1,5 @@ +using Json.Schema; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel.Requests; + +public record GetSubmodelDataRequest(string submodelId, JsonSchema dataQuery); diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/IJsonSchemaParser.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/IJsonSchemaParser.cs new file mode 100644 index 0000000..ed6a21e --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/IJsonSchemaParser.cs @@ -0,0 +1,13 @@ +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.Submodel; + +using Json.Schema; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel.Services; + +/// +/// Parses a complex JSON schema and converts it into a semantic tree structure. +/// +public interface IJsonSchemaParser +{ + SemanticTreeNode ParseJsonSchema(JsonSchema jsonSchema); +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/IJsonSchemaValidator.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/IJsonSchemaValidator.cs new file mode 100644 index 0000000..da57e05 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/IJsonSchemaValidator.cs @@ -0,0 +1,8 @@ +using Json.Schema; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel.Services; + +public interface IJsonSchemaValidator +{ + void ValidateResponseContent(string responseJson, JsonSchema requestSchema); +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/ISemanticTreeHandler.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/ISemanticTreeHandler.cs new file mode 100644 index 0000000..515f813 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/ISemanticTreeHandler.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Nodes; + +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.Submodel; + +using Json.Schema; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel.Services; + +/// +/// Converts a semantic tree node with values into a structured JSON format. +/// +public interface ISemanticTreeHandler +{ + JsonObject GetJson(SemanticTreeNode semanticTreeNodeWithValues, JsonSchema dataQuery); +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaParser.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaParser.cs new file mode 100644 index 0000000..f346f22 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaParser.cs @@ -0,0 +1,166 @@ +using System.Text.Json; + +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Constants; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Exceptions; +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.Submodel; + +using Json.Schema; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel.Services; + +public class JsonSchemaParser(ILogger logger) : IJsonSchemaParser +{ + public SemanticTreeNode ParseJsonSchema(JsonSchema jsonSchema) + { + ValidateRequest(jsonSchema); + return CreateSemanticTree(jsonSchema); + } + + private void ValidateRequest(JsonSchema jsonSchema) + { + try + { + var node = JsonSerializer.SerializeToNode(jsonSchema); + var result = MetaSchemas.Draft7.Evaluate(node, new EvaluationOptions { OutputFormat = OutputFormat.List }); + if (!result.IsValid) + { + logger.LogError("Requested schema is not validate"); + throw new BadRequestException(ExceptionMessages.RequestBodyInvalid); + } + } + catch (JsonException) + { + logger.LogError("Requested schema is not validate"); + throw new BadRequestException(ExceptionMessages.FailedParsingJsonSchema); + } + } + + private SemanticTreeNode CreateSemanticTree(JsonSchema jsonSchema) + { + var propertiesKeyword = jsonSchema.GetKeyword(); + if (propertiesKeyword == null || !propertiesKeyword.Properties.Any()) + { + throw new BadRequestException(ExceptionMessages.InvalidJsonSchemaRootElement); + } + + var rootProperty = propertiesKeyword.Properties.First(); + return ProcessProperty(rootProperty.Key, rootProperty.Value, jsonSchema.GetKeyword()); + } + + private SemanticTreeNode ProcessProperty(string schemaPropertyName, JsonSchema property, DefinitionsKeyword definitions) + { + var refKeyword = property.GetKeyword(); + if (refKeyword != null) + { + return HandleReference(schemaPropertyName, property, definitions); + } + + var typeKeyword = property.GetKeyword(); + if (typeKeyword == null) + { + return new SemanticLeafNode(schemaPropertyName, DataType.String, ""); + } + + var schemaType = GetSchemaType(typeKeyword); + if (schemaType is DataType.Object or DataType.Array) + { + return BuildObjectNode(schemaPropertyName, schemaType, property, definitions); + } + + return new SemanticLeafNode(schemaPropertyName, schemaType, ""); + } + + private SemanticTreeNode HandleReference(string schemaPropertyName, JsonSchema property, DefinitionsKeyword definitions) + { + var refKeyword = property.GetKeyword(); + if (refKeyword == null) + { + return new SemanticLeafNode(schemaPropertyName, DataType.String, ""); + } + + var definitionKey = refKeyword.Reference.ToString().Replace("#/definitions/", ""); + if (definitions == null || !definitions.Definitions.TryGetValue(definitionKey, out var def)) + { + return new SemanticLeafNode(schemaPropertyName, DataType.Unknown, ""); + } + + var defTypeKeyword = def.GetKeyword(); + if (defTypeKeyword == null) + { + return new SemanticLeafNode(schemaPropertyName, DataType.String, ""); + } + + var schemaType = GetSchemaType(defTypeKeyword); + if (schemaType is DataType.Object or DataType.Array) + { + return BuildObjectNode(schemaPropertyName, schemaType, def, definitions); + } + + return new SemanticLeafNode(schemaPropertyName, schemaType, ""); + } + + private SemanticBranchNode BuildObjectNode(string schemaPropertyName, DataType dataType, JsonSchema schema, DefinitionsKeyword definitions) + { + var branchNode = new SemanticBranchNode(schemaPropertyName, dataType); + + switch (dataType) + { + case DataType.Object: + { + var propertiesKeyword = schema.GetKeyword(); + if (propertiesKeyword != null) + { + foreach (var prop in propertiesKeyword.Properties) + { + branchNode.AddChild(ProcessProperty(prop.Key, prop.Value, definitions)); + } + } + + break; + } + case DataType.Array: + { + var itemsKeyword = schema.GetKeyword(); + if (itemsKeyword == null) + { + var propertiesKeyword = schema.GetKeyword(); + if (propertiesKeyword != null) + { + foreach (var prop in propertiesKeyword.Properties) + { + branchNode.AddChild(ProcessProperty(prop.Key, prop.Value, definitions)); + } + } + + break; + } + + if (itemsKeyword is { SingleSchema: not null }) + { + branchNode.AddChild(ProcessProperty("item", itemsKeyword.SingleSchema, definitions)); + } + + break; + } + } + + return branchNode; + } + + private static DataType GetSchemaType(TypeKeyword typeKeyword) + { + var t = typeKeyword.Type; + + return t switch + { + _ when t.HasFlag(SchemaValueType.Object) => DataType.Object, + _ when t.HasFlag(SchemaValueType.Array) => DataType.Array, + _ when t.HasFlag(SchemaValueType.String) => DataType.String, + _ when t.HasFlag(SchemaValueType.Integer) => DataType.Integer, + _ when t.HasFlag(SchemaValueType.Number) => DataType.Number, + _ when t.HasFlag(SchemaValueType.Boolean) => DataType.Boolean, + _ => DataType.String + }; + } + +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaValidator.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaValidator.cs new file mode 100644 index 0000000..6081bef --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/JsonSchemaValidator.cs @@ -0,0 +1,232 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Constants; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Exceptions; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.Submodel.Config; + +using Json.Schema; + +using Microsoft.Extensions.Options; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel.Services; + +public class JsonSchemaValidator(IOptions semantics, ILogger logger) : IJsonSchemaValidator +{ + private readonly string _contextPrefix = semantics.Value.IndexContextPrefix; + private const string DefinitionsPrefix = "#/definitions/"; + + private static readonly JsonSerializerOptions Serialization = new() + { + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) } + }; + + public void ValidateResponseContent(string responseJson, JsonSchema requestSchema) + { + if (string.IsNullOrWhiteSpace(responseJson)) + { + LogAndThrowException("Response JSON is empty."); + } + + if (!TryParseJson(responseJson, out var responseDoc, out var parseError)) + { + LogAndThrowException($"Failed to parse response JSON: {parseError}"); + } + + if (!TryNormalizeSchema(requestSchema, out var normalizedSchema, out var normalizeError)) + { + LogAndThrowException($"Failed to normalize request schema: {normalizeError}"); + } + + if (!TryRegisterJsonSchema(normalizedSchema, out var registerError)) + { + LogAndThrowException($"Failed to register schema: {registerError}"); + } + + try + { + var schema = JsonSchema.FromText(normalizedSchema.ToJsonString()); + var result = schema.Evaluate(responseDoc!.RootElement, new EvaluationOptions { OutputFormat = OutputFormat.List }); + if (!result.IsValid) + { + LogAndThrowException("Response did not validate against schema."); + } + } + catch (Exception ex) + { + LogAndThrowException("Exception occurred during response validation.", ex); + } + } + + private void LogAndThrowException(string logMessage, Exception? ex = null) + { + if (ex != null) + { + logger.LogError(ex, logMessage); + } + else + { + logger.LogError(logMessage); + } + + throw new NotFoundException(ExceptionMessages.ResourceNotValid); + } + + private static bool TryParseJson(string json, out JsonDocument? document, out string? error) + { + error = null; + document = null; + + try + { + document = JsonDocument.Parse(json); + return true; + } + catch (Exception ex) + { + error = $"JSON parsing failed: {ex.Message}"; + return false; + } + } + + private bool TryNormalizeSchema(JsonSchema schema, out JsonObject normalized, out string? error) + { + error = null; + normalized = []; + + try + { + var json = JsonSerializer.Serialize(schema, Serialization); + + normalized = JsonNode.Parse(json)?.AsObject() + ?? throw new ArgumentException("Failed to parse schema JSON."); + + EscapeJsonReferencePointers(normalized); + normalized["$id"] = normalized["$id"]?.GetValue() ?? $"urn:uuid:{Guid.NewGuid():D}"; + + return true; + } + catch (Exception ex) + { + error = $"Schema normalization failed: {ex.Message}"; + return false; + } + } + + private static bool TryRegisterJsonSchema(JsonObject schemaJsonObject, out string? registrationErrorMessage) + { + registrationErrorMessage = null; + + try + { + var jsonSchema = JsonSchema.FromText(schemaJsonObject.ToJsonString()); + var schemaIdentifierUri = new Uri(schemaJsonObject["$id"]!.GetValue()!); + SchemaRegistry.Global.Register(schemaIdentifierUri, jsonSchema); + return true; + } + catch (Exception exception) + { + registrationErrorMessage = $"Schema registration failed: {exception.Message}"; + return false; + } + } + + private void EscapeJsonReferencePointers(JsonNode? currentNode) + { + switch (currentNode) + { + case JsonObject jsonObjectNode: + ProcessJsonObjectForEscaping(jsonObjectNode); + break; + + case JsonArray jsonArrayNode: + foreach (var arrayElement in jsonArrayNode) + { + EscapeJsonReferencePointers(arrayElement); + } + + break; + } + } + + private void ProcessJsonObjectForEscaping(JsonObject jsonObject) + { + var propertiesToRename = jsonObject + .Select(property => property.Key) + .Select(propertyName => (originalName: propertyName, strippedName: RemoveContextSuffix(propertyName))) + .Where(namePair => namePair.strippedName != namePair.originalName) + .ToList(); + + foreach (var (originalName, strippedName) in propertiesToRename) + { + RenameJsonProperty(jsonObject, originalName, strippedName); + } + + if (jsonObject.TryGetPropertyValue("required", out var requiredPropertiesNode) && + requiredPropertiesNode is JsonArray requiredPropertiesArray) + { + RemoveContextSuffixFromRequiredProperties(requiredPropertiesArray); + } + + foreach (var property in jsonObject.ToList()) + { + var propertyName = property.Key; + var propertyValue = property.Value; + + if (propertyName == "$ref" && + propertyValue is JsonValue referenceValue && + referenceValue.TryGetValue(out var referenceString) && + referenceString.StartsWith(DefinitionsPrefix, StringComparison.OrdinalIgnoreCase)) + { + jsonObject["$ref"] = BuildEscapedReferencePath(referenceString); + } + else + { + EscapeJsonReferencePointers(propertyValue); + } + } + } + + private void RemoveContextSuffixFromRequiredProperties(JsonArray requiredProperties) + { + for (var index = 0; index < requiredProperties.Count; index++) + { + if (requiredProperties[index]?.GetValue() is { } propertyName) + { + requiredProperties[index] = RemoveContextSuffix(propertyName); + } + } + } + + private string BuildEscapedReferencePath(string originalReferencePath) + { + var referenceWithoutPrefix = originalReferencePath[DefinitionsPrefix.Length..]; + + var strippedReference = RemoveContextSuffix(referenceWithoutPrefix); + + var escapedReference = strippedReference.Replace("~", "~0", StringComparison.OrdinalIgnoreCase).Replace("/", "~1", StringComparison.OrdinalIgnoreCase); + + return DefinitionsPrefix + escapedReference; + } + + private string RemoveContextSuffix(string propertyName) + { + var suffixIndex = propertyName.IndexOf(_contextPrefix, StringComparison.Ordinal); + return suffixIndex >= 0 ? propertyName[..suffixIndex] : propertyName; + } + + private static void RenameJsonProperty(JsonObject jsonObject, string oldPropertyName, string newPropertyName) + { + if (oldPropertyName == newPropertyName) + { + return; + } + + var propertyValue = jsonObject[oldPropertyName]; + jsonObject.Remove(oldPropertyName); + jsonObject[newPropertyName] = propertyValue!; + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/SemanticTreeHandler.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/SemanticTreeHandler.cs new file mode 100644 index 0000000..eff9d9a --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/Services/SemanticTreeHandler.cs @@ -0,0 +1,165 @@ +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Nodes; + +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Constants; +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.Submodel; + +using Json.Schema; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel.Services; + +public class SemanticTreeHandler(IJsonSchemaValidator jsonSchemaValidator) : ISemanticTreeHandler +{ + public JsonObject GetJson(SemanticTreeNode semanticTreeNodeWithValues, JsonSchema dataQuery) + { + var nodeObject = ConvertNode(semanticTreeNodeWithValues); + + var convertedJsonObject = new JsonObject { [semanticTreeNodeWithValues.SemanticId] = nodeObject }; + + var convertedJsonString = JsonSerializer.Serialize(convertedJsonObject); + + jsonSchemaValidator.ValidateResponseContent(convertedJsonString, dataQuery); + + return convertedJsonObject; + } + + private static JsonNode ConvertNode(SemanticTreeNode treeNode) + { + return treeNode switch + { + SemanticLeafNode leaf => ConvertLeafValue(leaf), + SemanticBranchNode branch => ConvertBranchNode(branch), + _ => throw new ArgumentException(ExceptionMessages.UnknownTypeError), + }; + } + + private static JsonNode ConvertLeafValue(SemanticLeafNode leafNode) + { + return leafNode.DataType switch + { + DataType.Boolean => TryParseBoolean(leafNode.Value), + DataType.Integer => TryParseInteger(leafNode.Value), + DataType.Number => TryParseNumber(leafNode.Value), + DataType.String => JsonValue.Create(leafNode.Value), + _ => JsonValue.Create(leafNode.Value) + }; + } + + private static JsonNode TryParseBoolean(string text) + { + return bool.TryParse(text, out var result) + ? JsonValue.Create(result) + : JsonValue.Create(text); + } + + private static JsonNode TryParseInteger(string text) + { + return int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) + ? JsonValue.Create(result) + : JsonValue.Create(text); + } + + private static JsonNode TryParseNumber(string text) + { + return double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var result) + ? JsonValue.Create(result) + : JsonValue.Create(text); + } + + private static JsonNode ConvertBranchNode(SemanticBranchNode branchNode) + { + var isArray = branchNode.DataType == DataType.Array; + var allBranchNodes = branchNode.Children.All(child => child is SemanticBranchNode); + var allLeafNodes = branchNode.Children.All(child => child is SemanticLeafNode); + var sameSemanticId = branchNode.Children.Select(child => child.SemanticId).Distinct().Count() == 1; + var sameSematicIdAsBranch = branchNode.Children.All(child => child is SemanticLeafNode) && branchNode.Children.All(child => child.SemanticId == branchNode.SemanticId); + var singleNode = branchNode.Children.Count() == 1; + + if (isArray) + { + var elementArray = new JsonArray(); + + if (allBranchNodes && sameSemanticId && !singleNode) + { + foreach (var childBranch in branchNode.Children.Cast()) + { + elementArray.Add(ConvertNode(childBranch)); + } + + return elementArray; + } + + if (allLeafNodes && sameSemanticId && !singleNode) + { + foreach (var leaf in branchNode.Children.Cast()) + { + elementArray.Add(BuildObjectFromLeafNode(leaf)); + } + + return elementArray; + } + + var singleObject = BuildObjectFromChildren(branchNode.Children); + elementArray.Add(singleObject); + return elementArray; + } + + return BuildObjectFromChildren(branchNode.Children); + } + + private static JsonObject BuildObjectFromLeafNode(SemanticLeafNode leafNode) + { + var jsonObject = new JsonObject + { + [leafNode.SemanticId] = ConvertLeafValue(leafNode) + }; + return jsonObject; + } + + private static JsonObject BuildObjectFromChildren(IEnumerable childNode) + { + var jsonObject = new JsonObject(); + + var groups = childNode.GroupBy(child => child.SemanticId); + + foreach (var group in groups) + { + var convertedValues = group.Select(ConvertNode).ToList(); + var count = convertedValues.Count; + var allArrays = convertedValues.All(v => v is JsonArray); + var singleValue = count == 1; + + if (singleValue) + { + jsonObject[group.Key] = convertedValues[0]; + } + else if (allArrays) + { + var mergedArray = new JsonArray(); + foreach (var array in convertedValues.Cast()) + { + foreach (var element in array) + { + mergedArray.Add(element.DeepClone()); + } + } + + jsonObject[group.Key] = mergedArray; + } + else + { + var wrapperArray = new JsonArray(); + foreach (var valueNode in convertedValues) + { + wrapperArray.Add(valueNode.DeepClone()); + } + + jsonObject[group.Key] = wrapperArray; + } + } + + return jsonObject; + } +} + diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/SubmodelController.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/SubmodelController.cs new file mode 100644 index 0000000..57bc418 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Api/Submodel/SubmodelController.cs @@ -0,0 +1,40 @@ +using System.Text.Json.Nodes; + +using AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel.Handler; +using AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel.Requests; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Constants; +using AAS.TwinEngine.Plugin.TestPlugin.Common.Extensions; + +using Asp.Versioning; + +using Json.Schema; + +using Microsoft.AspNetCore.Mvc; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel; + +[ApiController] +[Route("")] +[ApiVersion(1)] +public class SubmodelController(ISubmodelHandler submodelHandler) : ControllerBase +{ + [HttpPost("data/{submodelId}")] + [ProducesResponseType(typeof(JsonObject), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ActionResult), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ActionResult), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ActionResult), StatusCodes.Status500InternalServerError)] + public async Task> RetrieveDataAsync([FromBody] JsonSchema? dataQuery, [FromRoute] string submodelId, CancellationToken cancellationToken) + { + var decodedSubmodelId = submodelId.DecodeBase64(); + if (dataQuery is null) + { + return BadRequest(ExceptionMessages.InvalidRequestPayload); + } + + var request = new GetSubmodelDataRequest(decodedSubmodelId, dataQuery); + + var aasData = await submodelHandler.GetSubmodelData(request, cancellationToken); + + return Ok(aasData); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Constants/ExceptionMessages.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Constants/ExceptionMessages.cs new file mode 100644 index 0000000..23acbdf --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Constants/ExceptionMessages.cs @@ -0,0 +1,16 @@ +namespace AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Constants; + +public static class ExceptionMessages +{ + public const string InvalidRequestedLimit = "Limit must be greater than or equal to 1."; + public const string InvalidRequestPayload = "The request payload is invalid."; + public const string RequestBodyInvalid = "The request body could not be processed. Verify the structure and try again."; + public const string FailedParsingJsonSchema = "Failed to parse JSON schema."; + public const string InvalidJsonSchemaRootElement = "The JSON schema must contain a root element."; + public const string UnknownTypeError = "Unknown node type."; + public const string ResourceNotFound = "Required resource not found"; + public const string ResourceNotValid = "Required resource is not valid"; + public const string ResponseIsNotValidate = "Response body is not valid"; + public const string ShellDescriptorDataNotFound = "Required shell information not found"; + public const string AssetNotFound = "Required asset information not found"; +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Exceptions/BadRequestException.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Exceptions/BadRequestException.cs new file mode 100644 index 0000000..fd4e505 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Exceptions/BadRequestException.cs @@ -0,0 +1,33 @@ +namespace AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Exceptions; + +public class BadRequestException : Exception +{ + public BadRequestException() + { + } + + public BadRequestException(string message) + : base(message) + { + } + + public BadRequestException(int? errorCode, string message) + : base(message) + { + ErrorCode = errorCode; + } + + public BadRequestException(string message, Exception innerException) + : base(message, innerException) + { + } + + public BadRequestException(string message, string title) + : base(message) + { + Title = title; + } + + public string? Title { get; } + public int? ErrorCode { get; } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Exceptions/CustomException.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Exceptions/CustomException.cs new file mode 100644 index 0000000..6038d0c --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Exceptions/CustomException.cs @@ -0,0 +1,33 @@ +namespace AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Exceptions; + +public class CustomException : Exception +{ + public CustomException() + { + } + + public CustomException(string message) + : base(message) + { + } + + public CustomException(int? errorCode, string message) + : base(message) + { + ErrorCode = errorCode; + } + + public CustomException(string message, Exception innerException) + : base(message, innerException) + { + } + + public CustomException(string message, string title) + : base(message) + { + Title = title; + } + + public string? Title { get; } + public int? ErrorCode { get; } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Exceptions/ForbiddenException.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Exceptions/ForbiddenException.cs new file mode 100644 index 0000000..cccdc8a --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Exceptions/ForbiddenException.cs @@ -0,0 +1,26 @@ +namespace AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Exceptions; + +public class ForbiddenException : Exception +{ + public ForbiddenException() + { + } + + public ForbiddenException(string message) + : base(message) + { + } + + public ForbiddenException(string message, Exception innerException) + : base(message, innerException) + { + } + + public ForbiddenException(string message, string title) + : base(message) + { + Title = title; + } + + public string? Title { get; } +} \ No newline at end of file diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Exceptions/GlobalExceptionHandler.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Exceptions/GlobalExceptionHandler.cs new file mode 100644 index 0000000..6366921 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Exceptions/GlobalExceptionHandler.cs @@ -0,0 +1,39 @@ +using System.Net; + +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Exceptions.Responses; + +using Microsoft.AspNetCore.Diagnostics; + +namespace AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Exceptions; + +public class GlobalExceptionHandler(ILogger logger) : IExceptionHandler +{ + public async ValueTask TryHandleAsync(HttpContext httpContext, + Exception exception, + CancellationToken cancellationToken) + { + logger.LogError(exception, "An unhandled exception occurred."); + + var statusCode = exception switch + { + BadRequestException => StatusCodes.Status400BadRequest, + ForbiddenException => StatusCodes.Status403Forbidden, + NotFoundException => StatusCodes.Status404NotFound, + UnauthorizedAccessException => StatusCodes.Status401Unauthorized, + TimeoutException => StatusCodes.Status408RequestTimeout, + _ => StatusCodes.Status500InternalServerError + }; + + var traceId = httpContext.TraceIdentifier; + + var response = new ServiceErrorResponse().Create((HttpStatusCode)statusCode, + title: exception.Message, + traceId: traceId); + + httpContext.Response.ContentType = "application/json"; + httpContext.Response.StatusCode = statusCode; + await httpContext.Response.WriteAsJsonAsync(response, cancellationToken).ConfigureAwait(false); + + return true; + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Exceptions/InternalServerException.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Exceptions/InternalServerException.cs new file mode 100644 index 0000000..eeb8447 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Exceptions/InternalServerException.cs @@ -0,0 +1,26 @@ +namespace AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Exceptions; + +public class InternalServerException : Exception +{ + public InternalServerException(string message) + : base(message) + { + } + + public InternalServerException(string message, Exception innerException) + : base(message, innerException) + { + } + + public InternalServerException() + { + } + + public InternalServerException(string message, string title) + : base(message) + { + Title = title; + } + + public string? Title { get; } +} \ No newline at end of file diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Exceptions/NotFoundException.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Exceptions/NotFoundException.cs new file mode 100644 index 0000000..0cc4837 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Exceptions/NotFoundException.cs @@ -0,0 +1,26 @@ +namespace AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Exceptions; + +public class NotFoundException : Exception +{ + public NotFoundException() + { + } + + public NotFoundException(string message) + : base(message) + { + } + + public NotFoundException(string message, Exception innerException) + : base(message, innerException) + { + } + + public NotFoundException(string message, string title) + : base(message) + { + Title = title; + } + + public string? Title { get; } +} \ No newline at end of file diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Exceptions/Responses/ServiceErrorResponse.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Exceptions/Responses/ServiceErrorResponse.cs new file mode 100644 index 0000000..a33edba --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Exceptions/Responses/ServiceErrorResponse.cs @@ -0,0 +1,40 @@ +using System.Collections.ObjectModel; +using System.Net; +using System.Text.Json.Serialization; + +using Microsoft.AspNetCore.Mvc; + +namespace AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Exceptions.Responses; + +public class ServiceErrorResponse : ProblemDetails +{ + [JsonPropertyName("errors")] + public ReadOnlyCollection? Errors { get; set; } + + [JsonPropertyName("traceId")] + public string? TraceId { get; set; } + + public ServiceErrorResponse Create(HttpStatusCode status, string title, string? traceId = null) + { + var problem = new ServiceErrorResponse + { + Type = "https://tools.ietf.org/html/rfc9110#section-15.5.1", + Title = "An error occurred.", + Status = (int)status, + TraceId = traceId, + Errors = new ReadOnlyCollection(new List + { + new() { Description = title } + }) + }; + + return problem; + } +} + +public class ApiError +{ + [JsonPropertyName("description")] + public string Description { get; set; } = null!; +} + diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Exceptions/UnauthorizedAccessException.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Exceptions/UnauthorizedAccessException.cs new file mode 100644 index 0000000..92f8996 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Exceptions/UnauthorizedAccessException.cs @@ -0,0 +1,23 @@ +namespace AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Exceptions; + +public class UnauthorizedAccessException : Exception +{ + public UnauthorizedAccessException(string message) + : base(message) + { + } + + public UnauthorizedAccessException(string message, Exception innerException) + : base(message, innerException) + { + } + + public UnauthorizedAccessException(string name, string identifier) + : this($"You are not Authorize to access {name} with this identifier {identifier}") + { + } + + public UnauthorizedAccessException() + { + } +} \ No newline at end of file diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/Manifest/IManifestProvider.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/Manifest/IManifestProvider.cs new file mode 100644 index 0000000..991ca1c --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/Manifest/IManifestProvider.cs @@ -0,0 +1,8 @@ +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.Manifest; + +namespace AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.Manifest; + +public interface IManifestProvider +{ + public ManifestData GetManifestData(); +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/Manifest/IManifestService.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/Manifest/IManifestService.cs new file mode 100644 index 0000000..e8d4d8a --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/Manifest/IManifestService.cs @@ -0,0 +1,8 @@ +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.Manifest; + +namespace AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.Manifest; + +public interface IManifestService +{ + public Task GetManifestData(CancellationToken cancellationToken); +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/Manifest/ManifestService.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/Manifest/ManifestService.cs new file mode 100644 index 0000000..f283e7c --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/Manifest/ManifestService.cs @@ -0,0 +1,8 @@ +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.Manifest; + +namespace AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.Manifest; + +public class ManifestService(IManifestProvider manifestProvider) : IManifestService +{ + public Task GetManifestData(CancellationToken cancellationToken) => Task.FromResult(manifestProvider.GetManifestData()); +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/MetaData/IMetaDataProvider.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/MetaData/IMetaDataProvider.cs new file mode 100644 index 0000000..66b9d24 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/MetaData/IMetaDataProvider.cs @@ -0,0 +1,12 @@ +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.MetaData; + +namespace AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.MetaData; + +public interface IMetaDataProvider +{ + Task GetShellDescriptorsAsync(int? limit, string? cursor, CancellationToken cancellationToken); + + Task GetShellDescriptorAsync(string aasIdentifier, CancellationToken cancellationToken); + + Task GetAssetAsync(string assetIdentifier, CancellationToken cancellationToken); +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/MetaData/IMetaDataService.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/MetaData/IMetaDataService.cs new file mode 100644 index 0000000..c49d12e --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/MetaData/IMetaDataService.cs @@ -0,0 +1,12 @@ +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.MetaData; + +namespace AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.MetaData; + +public interface IMetaDataService +{ + Task GetShellDescriptorsAsync(int? limit, string? cursor, CancellationToken cancellationToken); + + Task GetShellDescriptorAsync(string aasIdentifier, CancellationToken cancellationToken); + + Task GetAssetAsync(string assetIdentifier, CancellationToken cancellationToken); +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/MetaData/MetaDataService.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/MetaData/MetaDataService.cs new file mode 100644 index 0000000..956de7e --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/MetaData/MetaDataService.cs @@ -0,0 +1,14 @@ +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.MetaData; + +namespace AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.MetaData; + +public class MetaDataService( + ILogger logger, + IMetaDataProvider metaDataProvider) : IMetaDataService +{ + public async Task GetShellDescriptorsAsync(int? limit, string? cursor, CancellationToken cancellationToken) => await metaDataProvider.GetShellDescriptorsAsync(limit, cursor, cancellationToken); + + public async Task GetShellDescriptorAsync(string aasIdentifier, CancellationToken cancellationToken) => await metaDataProvider.GetShellDescriptorAsync(aasIdentifier, cancellationToken); + + public async Task GetAssetAsync(string assetIdentifier, CancellationToken cancellationToken) => await metaDataProvider.GetAssetAsync(assetIdentifier, cancellationToken); +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/Submodel/Config/Semantics.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/Submodel/Config/Semantics.cs new file mode 100644 index 0000000..0cc721b --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/Submodel/Config/Semantics.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.Submodel.Config; + +public class Semantics +{ + public const string Section = "Semantics"; + + [Required] + public string IndexContextPrefix { get; set; } +} + diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/Submodel/ISubmodelProvider.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/Submodel/ISubmodelProvider.cs new file mode 100644 index 0000000..a4f4d27 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/Submodel/ISubmodelProvider.cs @@ -0,0 +1,11 @@ +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.Submodel; + +namespace AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.Submodel; + +/// +/// Provides functionality to enrich a semantic tree node with data. +/// +public interface ISubmodelProvider +{ + public SemanticTreeNode EnrichWithData(SemanticTreeNode semanticTreeNode, string submodelId); +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/Submodel/ISubmodelService.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/Submodel/ISubmodelService.cs new file mode 100644 index 0000000..6e3bc7f --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/Submodel/ISubmodelService.cs @@ -0,0 +1,11 @@ +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.Submodel; + +namespace AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.Submodel; + +/// +/// Retrieves product data based on a complex data query and returns it in a structured format. +/// +public interface ISubmodelService +{ + public Task GetValuesBySemanticIds(SemanticTreeNode semanticIds, string submodelId); +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/Submodel/SubmodelService.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/Submodel/SubmodelService.cs new file mode 100644 index 0000000..4fd3b29 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/ApplicationLogic/Services/Submodel/SubmodelService.cs @@ -0,0 +1,10 @@ +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.Submodel; + +namespace AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.Submodel; + +public class SubmodelService( + ISubmodelProvider submodelProvider + ) : ISubmodelService +{ + public Task GetValuesBySemanticIds(SemanticTreeNode semanticIds, string submodelId) => Task.FromResult(submodelProvider.EnrichWithData(semanticIds, submodelId)); +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Authorization/.gitkeep b/source/AAS.TwinEngine.Plugin.TestPlugin/Authorization/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Common/Extensions/PaginationValidationExtensions.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Common/Extensions/PaginationValidationExtensions.cs new file mode 100644 index 0000000..6438532 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Common/Extensions/PaginationValidationExtensions.cs @@ -0,0 +1,18 @@ +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Constants; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Exceptions; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Common.Extensions; + +public static class PaginationValidationExtensions +{ + public static void ValidateLimit(this int? limit, ILogger? logger = null) + { + if (limit is null or > 0) + { + return; + } + + logger?.LogError("Invalid pagination limit provided: {Limit}", limit); + throw new BadRequestException(ExceptionMessages.InvalidRequestedLimit); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Common/Extensions/StringExtensions.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Common/Extensions/StringExtensions.cs new file mode 100644 index 0000000..c76b3c0 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Common/Extensions/StringExtensions.cs @@ -0,0 +1,32 @@ +using System.Text; + +using Microsoft.AspNetCore.WebUtilities; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Common.Extensions; + +public static class StringExtensions +{ + public static string DecodeBase64(this string encodedValue) + { + if (string.IsNullOrEmpty(encodedValue)) + throw new ArgumentException("Input string cannot be null or empty.", nameof(encodedValue)); + + try + { + return Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(encodedValue)); + } + catch (FormatException) + { + throw new ArgumentException("The provided string is not a valid Base64 encoded string.", nameof(encodedValue)); + } + } + + public static string EncodeToBase64(this string plainValue) + { + if (string.IsNullOrEmpty(plainValue)) + throw new ArgumentException("Input string cannot be null or empty.", nameof(plainValue)); + + var bytes = System.Text.Encoding.UTF8.GetBytes(plainValue); + return Convert.ToBase64String(bytes); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Data/mock-metadata.json b/source/AAS.TwinEngine.Plugin.TestPlugin/Data/mock-metadata.json new file mode 100644 index 0000000..4d15e7b --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Data/mock-metadata.json @@ -0,0 +1,38 @@ +[ + { + "globalAssetId": "https://mm-software.com/ids/assets/000-001", + "idShort": "M&M01", + "id": "https://mm-software.com/ids/aas/000-001", + "specificAssetIds": [], + "assetInformationData": { + "defaultThumbnail": { + "contentType": "image/jpeg", + "path": "https://cdn.pixabay.com/photo/2023/06/06/16/10/computer-8045026_1280.jpg" + } + } + }, + { + "globalAssetId": "https://mm-software.com/ids/assets/000-002", + "idShort": "M&M02", + "id": "https://mm-software.com/ids/aas/000-002", + "specificAssetIds": [], + "assetInformationData": { + "defaultThumbnail": { + "contentType": "image/jpeg", + "path": "https://cdn.pixabay.com/photo/2023/07/08/07/25/ai-generated-8113887_1280.jpg" + } + } + }, + { + "globalAssetId": "https://mm-software.com/ids/assets/000-003", + "idShort": "M&M03", + "id": "https://mm-software.com/ids/aas/000-003", + "specificAssetIds": [], + "assetInformationData": { + "defaultThumbnail": { + "contentType": "image/jpeg", + "path": "https://images.pexels.com/photos/27934787/pexels-photo-27934787.jpeg" + } + } + } +] diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Data/mock-submodel-data.json b/source/AAS.TwinEngine.Plugin.TestPlugin/Data/mock-submodel-data.json new file mode 100644 index 0000000..b0cb97b --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Data/mock-submodel-data.json @@ -0,0 +1,648 @@ +{ + "https://mm-software.com/submodel/000-001/ContactInformation": { + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations": { + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation": [ + { + "0173-1#02-AAO204#003": "Manager001", + "0173-1#02-AAW001#001": { + "0173-1#02-AAW001#001_en": "Acme Corporation" + }, + "0173-1#02-AAO127#003": { + "0173-1#02-AAO127#003_de": "Marketing" + }, + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ + { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "234-567-8901" + }, + "0173-1#02-AAO137#003": "Office", + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" + } + }, + { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "345-678-9012" + }, + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Montags, 10-16 Uhr" + }, + "0173-1#02-AAO137#003": "Mobile" + } + ], + "0173-1#02-AAQ836#005": [ + { + "0173-1#02-AAO198#002": "john.doe@example.com", + "0173-1#02-AAO199#003": "Work" + } + ], + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/": [ + { + "https://mm-software.com/ipCommunication/AddressOfAdditionalLink": "http://example.com/more-info", + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication": "Chat", + "https://mm-software.com/IPCommunication/AvailableTime": { + "https://mm-software.com/IPCommunication/AvailableTime_de": "8:00 AM to 6:00 PM" + } + } + ], + "0173-1#02-AAO128#002": { + "0173-1#02-AAO128#002_de": "Bahnhofstraße 25" + }, + "0173-1#02-AAO129#002": { + "0173-1#02-AAO129#002_de": "80335" + }, + "0173-1#02-AAO133#002": { + "0173-1#02-AAO133#002_de": "Bayern" + }, + "0173-1#02-AAO205#002": { + "0173-1#02-AAO205#002_en": "John Doe" + }, + "https://mm-software.com/AddressOfAdditionalLink": "http://example.com/more-info" + }, + { + "0173-1#02-AAO204#003": "Marketing Manager", + "0173-1#02-AAW001#001": { + "0173-1#02-AAW001#001_en": "Acme Corporation" + }, + "0173-1#02-AAO127#003": { + "0173-1#02-AAO127#003_de": "Marketing" + }, + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ + { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "234-567-8901" + }, + "0173-1#02-AAO137#003": "Office", + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" + } + }, + { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "345-678-9012" + }, + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Montags, 10-16 Uhr" + }, + "0173-1#02-AAO137#003": "Mobile" + } + ], + "0173-1#02-AAQ836#005": [ + { + "0173-1#02-AAO198#002": "john.doe@example.com", + "0173-1#02-AAO199#003": "Work" + } + ], + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/": [ + { + "https://mm-software.com/ipCommunication/AddressOfAdditionalLink": "http://example.com/more-info", + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication": "Chat", + "https://mm-software.com/IPCommunication/AvailableTime": { + "https://mm-software.com/IPCommunication/AvailableTime_de": "8:00 AM to 6:00 PM" + } + } + ], + "0173-1#02-AAO128#002": { + "0173-1#02-AAO128#002_de": "Bahnhofstraße 25" + }, + "0173-1#02-AAO129#002": { + "0173-1#02-AAO129#002_de": "80335" + }, + "0173-1#02-AAO133#002": { + "0173-1#02-AAO133#002_de": "Bayern" + }, + "0173-1#02-AAO205#002": { + "0173-1#02-AAO205#002_en": "John Doe" + }, + "https://mm-software.com/AddressOfAdditionalLink": "http://example.com/more-info" + } + ] + } + }, + "https://mm-software.com/submodel/000-001/Nameplate": { + "https://admin-shell.io/idta/nameplate/3/0/Nameplate": { + "0112/2///61987#ABN590#002": "https://mm-software.com/Model-000/Serial-Nr-001", + "0112/2///61987#ABA565#009": { + "0112/2///61987#ABA565#009_de": "M&M" + }, + "0112/2///61987#ABP464#002": { + "0112/2///61987#ABP464#002_en": "M&MType ABC" + }, + "0112/2///61987#ABA581#007": "MM11-ABC22-001", + "0112/2///61987#ABB757#007": "2025-01-01", + "0112/2///61987#ABA567#009": { + "0112/2///61987#ABA567#009_en": "\u0022ABC-123\u0022" + }, + "0112/2///61987#ABA950#008": "FMABC1234", + "0112/2///61987#ABA951#009": "12345678", + "0112/2///61987#ABP000#002": "2022", + "0112/2///61360_7#AAS006#001": [ + { + "0112/2///61360_7#AAS009#001": [ + { + "0112/2///61987#ABA231#009": "0173-1#07-DAA603#004", + "0112/2///61987#ABH783#003": "KEMA99IECEX1105/128", + "0112/2///61987#ABO097#001": "2022-01-01", + "0112/2///61987#ABH830#002": "2022-01-01", + "0112/2///61987#ABO100#002": "file", + "0112/2///61987#ABB146#007": "0044" + } + ] + } + ], + "0112/2///61987#ABP462#001": "DE", + "0112/2///61987#ABP463#001": "mm-logo", + "0173-1#02-ABI218#003/0173-1#01-AGZ672#004": [ + { + "https://mm-software.com/AssetSpecificProperties/ArbitraryMLP": { + "https://mm-software.com/AssetSpecificProperties/ArbitraryMLP_en": "M&MTestsample" + }, + "https://mm-software.com/AssetSpecificProperties/ArbitraryFile": "sampleFile", + "0173-1#02-ABI219#003/0173-1#01-AHD205#004": [ + { + "0173-1#01-AHD205#004": [ + { + "0173-1#02-AAO856#002": "", + "https://admin-shell.io/SMT/General/ArbitraryProp": "", + "https://mm-software.com/GuidelineSpecificProperties/ArbitraryFile": "TestFile", + "https://mm-software.com/GuidelineSpecificProperties/ArbitraryMLP": { + "https://mm-software.com/GuidelineSpecificProperties/ArbitraryMLP_en": "\u0022sample\u0022" + } + } + ] + } + ] + } + ] + } + }, + "https://mm-software.com/submodel/000-001/Reliability": { + "https://admin-shell.io/idta/iec62683/1/0/Reliability": { + "0112/2///62683#ACE006#001": "4", + "0112/2///62683#ACG071#001": [ + { + "0112/2///61987#ABA969#007": "DC", + "0112/2///61987#ABA588#004": "14.8", + "0112/2///61987#ABD461#004": "3.8", + "0112/2///61987#ABD462#004": "6.7", + "https://admin-shell.io/idta/FunctionalSafety/RatedOperationalCurrent/1/0": "78.9", + "0112/2///62683#ACE053#001": "TestType001", + "0112/2///62683#ACE070#001": "true", + "0112/2///62683#ACE055#001": "150", + "0112/2///62683#ACE054#001": "45" + } + ], + "0112/2///62683#ACG080#001": [ + { + "0112/2///62683#ACE061#001": "2", + "0112/2///62683#ACE062#001": "7", + "https://admin-shell.io/idta/Reliability/B10/1/0": "1" + } + ] + } + }, + "https://mm-software.com/submodel/000-002/ContactInformation": { + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations": { + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation": [ + { + "0173-1#02-AAO204#003": "Manager001", + "0173-1#02-AAW001#001": { + "0173-1#02-AAW001#001_en": "Acme Corporation" + }, + "0173-1#02-AAO127#003": { + "0173-1#02-AAO127#003_de": "Marketing" + }, + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ + { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "234-567-8901" + }, + "0173-1#02-AAO137#003": "Office", + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" + } + }, + { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "345-678-9012" + }, + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Montags, 10-16 Uhr" + }, + "0173-1#02-AAO137#003": "Mobile" + } + ], + "0173-1#02-AAQ836#005": [ + { + "0173-1#02-AAO198#002": "john.doe@example.com", + "0173-1#02-AAO199#003": "Work" + } + ], + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/": [ + { + "https://mm-software.com/ipCommunication/AddressOfAdditionalLink": "http://example.com/more-info", + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication": "Chat", + "https://mm-software.com/IPCommunication/AvailableTime": { + "https://mm-software.com/IPCommunication/AvailableTime_de": "8:00 AM to 6:00 PM" + } + } + ], + "0173-1#02-AAO128#002": { + "0173-1#02-AAO128#002_de": "Bahnhofstraße 25" + }, + "0173-1#02-AAO129#002": { + "0173-1#02-AAO129#002_de": "80335" + }, + "0173-1#02-AAO133#002": { + "0173-1#02-AAO133#002_de": "Bayern" + }, + "0173-1#02-AAO205#002": { + "0173-1#02-AAO205#002_en": "John Doe" + }, + "https://mm-software.com/AddressOfAdditionalLink": "http://example.com/more-info" + }, + { + "0173-1#02-AAO204#003": "Assistant Manager", + "0173-1#02-AAW001#001": { + "0173-1#02-AAW001#001_en": "Beta Corporation" + }, + "0173-1#02-AAO127#003": { + "0173-1#02-AAO127#003_de": "Vertrieb" + }, + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ + { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "123-456-7890" + }, + "0173-1#02-AAO137#003": "assistant", + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" + } + } + ], + "0173-1#02-AAQ836#005": [ + { + "0173-1#02-AAO198#002": "jane.doe@betacorp.com", + "0173-1#02-AAO199#003": "Work" + } + ], + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/": [ + { + "https://mm-software.com/ipCommunication/AddressOfAdditionalLink": "http://example.com/more-info2", + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication": "Email", + "https://mm-software.com/IPCommunication/AvailableTime": { + "https://mm-software.com/IPCommunication/AvailableTime_de": "9:00 AM to 5:00 PM" + } + } + ], + "0173-1#02-AAO128#002": { + "0173-1#02-AAO128#002_de": "Ringstraße 2" + }, + "0173-1#02-AAO129#002": { + "0173-1#02-AAO129#002_de": "50667" + }, + "0173-1#02-AAO133#002": { + "0173-1#02-AAO133#002_de": "Nordrhein-Westfalen" + }, + "0173-1#02-AAO205#002": { + "0173-1#02-AAO205#002_en": "Jane Doe" + }, + "https://mm-software.com/AddressOfAdditionalLink": "http://example.com/info2" + } + ] + } + }, + "https://mm-software.com/submodel/000-002/Nameplate": { + "https://admin-shell.io/idta/nameplate/3/0/Nameplate": { + "0112/2///61987#ABA567#009": { + "0112/2///61987#ABA567#009_en": "\u0022ABC-123\u0022" + }, + "0112/2///61987#ABN590#002": "https://mm-software.com/Model-000/Serial-Nr-002", + "0112/2///61987#ABA565#009": { + "0112/2///61987#ABA565#009_de": "M&M" + }, + "0112/2///61987#ABP464#002": { + "0112/2///61987#ABP464#002_en": "M&MType ABC002" + }, + "0112/2///61987#ABA581#007": "MM11-ABC22-002", + "0112/2///61987#ABB757#007": "2024-01-01", + "0112/2///61987#ABA950#008": "FMABC1234", + "0112/2///61987#ABA951#009": "12345678", + "0112/2///61987#ABP000#002": "2022", + "0112/2///61360_7#AAS006#001": [ + { + "0112/2///61360_7#AAS009#001": [ + { + "0112/2///61987#ABA231#009": "0173-1#07-DAA603#004", + "0112/2///61987#ABH783#003": "KEMA99IECEX1105/128", + "0112/2///61987#ABO097#001": "2022-01-01", + "0112/2///61987#ABH830#002": "2022-01-01", + "0112/2///61987#ABO100#002": "file", + "0112/2///61987#ABB146#007": "0044" + } + ] + } + ], + "0112/2///61987#ABP462#001": "DE", + "0112/2///61987#ABP463#001": "mm-logo", + "0173-1#02-ABI218#003/0173-1#01-AGZ672#004": [ + { + "https://mm-software.com/AssetSpecificProperties/ArbitraryMLP": { + "https://mm-software.com/AssetSpecificProperties/ArbitraryMLP_en": "M&MTestsample" + }, + "https://mm-software.com/AssetSpecificProperties/ArbitraryFile": "sampleFile", + "0173-1#02-ABI219#003/0173-1#01-AHD205#004": [ + { + "0173-1#01-AHD205#004": [ + { + "0173-1#02-AAO856#002": "", + "https://admin-shell.io/SMT/General/ArbitraryProp": "", + "https://mm-software.com/GuidelineSpecificProperties/ArbitraryFile": "TestFile", + "https://mm-software.com/GuidelineSpecificProperties/ArbitraryMLP": { + "https://mm-software.com/GuidelineSpecificProperties/ArbitraryMLP_en": "\u0022sample\u0022" + } + } + ] + } + ] + } + ] + } + }, + "https://mm-software.com/submodel/000-002/Reliability": { + "https://admin-shell.io/idta/iec62683/1/0/Reliability": { + "0112/2///62683#ACE006#001": "4", + "0112/2///62683#ACG071#001": [ + { + "0112/2///61987#ABA969#007": "DC", + "0112/2///61987#ABA588#004": "14.8", + "0112/2///61987#ABD461#004": "3.8", + "0112/2///61987#ABD462#004": "6.7", + "https://admin-shell.io/idta/FunctionalSafety/RatedOperationalCurrent/1/0": "78.9", + "0112/2///62683#ACE053#001": "TestType002", + "0112/2///62683#ACE070#001": "true", + "0112/2///62683#ACE055#001": "150", + "0112/2///62683#ACE054#001": "45" + } + ], + "0112/2///62683#ACG080#001": [ + { + "0112/2///62683#ACE061#001": "2", + "0112/2///62683#ACE062#001": "7", + "https://admin-shell.io/idta/Reliability/B10/1/0": "1" + } + ] + } + }, + "https://mm-software.com/submodel/000-003/ContactInformation": { + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations": { + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation": [ + { + "0173-1#02-AAO204#003": "Product Manager0003", + "0173-1#02-AAW001#001": { + "0173-1#02-AAW001#001_en": "Acme Corporation" + }, + "0173-1#02-AAO127#003": { + "0173-1#02-AAO127#003_de": "Marketing" + }, + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ + { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "234-567-8901" + }, + "0173-1#02-AAO137#003": "Office", + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" + } + }, + { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "345-678-9012" + }, + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Montags, 10-16 Uhr" + }, + "0173-1#02-AAO137#003": "Mobile" + } + ], + "0173-1#02-AAQ836#005": [ + { + "0173-1#02-AAO198#002": "john.doe@example.com", + "0173-1#02-AAO199#003": "Work" + } + ], + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/": [ + { + "https://mm-software.com/ipCommunication/AddressOfAdditionalLink": "http://example.com/more-info", + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication": "Chat", + "https://mm-software.com/IPCommunication/AvailableTime": { + "https://mm-software.com/IPCommunication/AvailableTime_de": "8:00 AM to 6:00 PM" + } + } + ], + "0173-1#02-AAO128#002": { + "0173-1#02-AAO128#002_de": "Bahnhofstraße 25" + }, + "0173-1#02-AAO129#002": { + "0173-1#02-AAO129#002_de": "80335" + }, + "0173-1#02-AAO133#002": { + "0173-1#02-AAO133#002_de": "Bayern" + }, + "0173-1#02-AAO205#002": { + "0173-1#02-AAO205#002_en": "John Doe" + }, + "https://mm-software.com/AddressOfAdditionalLink": "http://example.com/more-info" + }, + { + "0173-1#02-AAO204#003": "Manager", + "0173-1#02-AAW001#001": { + "0173-1#02-AAW001#001_en": "Acme Corporation" + }, + "0173-1#02-AAO127#003": { + "0173-1#02-AAO127#003_de": "Marketing" + }, + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ + { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "234-567-8901" + }, + "0173-1#02-AAO137#003": "Office", + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" + } + }, + { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "345-678-9012" + }, + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Montags, 10-16 Uhr" + }, + "0173-1#02-AAO137#003": "Mobile" + } + ], + "0173-1#02-AAQ836#005": [ + { + "0173-1#02-AAO198#002": "john.doe@example.com", + "0173-1#02-AAO199#003": "Work" + } + ], + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/": [ + { + "https://mm-software.com/ipCommunication/AddressOfAdditionalLink": "http://example.com/more-info", + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication": "Chat", + "https://mm-software.com/IPCommunication/AvailableTime": { + "https://mm-software.com/IPCommunication/AvailableTime_de": "8:00 AM to 6:00 PM" + } + } + ], + "0173-1#02-AAO128#002": { + "0173-1#02-AAO128#002_de": "Bahnhofstraße 25" + }, + "0173-1#02-AAO129#002": { + "0173-1#02-AAO129#002_de": "80335" + }, + "0173-1#02-AAO133#002": { + "0173-1#02-AAO133#002_de": "Bayern" + }, + "0173-1#02-AAO205#002": { + "0173-1#02-AAO205#002_en": "John Doe" + }, + "https://mm-software.com/AddressOfAdditionalLink": "http://example.com/more-info" + }, + { + "0173-1#02-AAO204#003": "Assistant Manager", + "0173-1#02-AAW001#001": { + "0173-1#02-AAW001#001_en": "Beta Corporation" + }, + "0173-1#02-AAO127#003": { + "0173-1#02-AAO127#003_de": "Vertrieb" + }, + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ + { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "123-456-7890" + }, + "0173-1#02-AAO137#003": "assistant", + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" + } + } + ], + "0173-1#02-AAQ836#005": [ + { + "0173-1#02-AAO198#002": "jane.doe@betacorp.com", + "0173-1#02-AAO199#003": "Work" + } + ], + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/": [ + { + "https://mm-software.com/ipCommunication/AddressOfAdditionalLink": "http://example.com/more-info2", + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication": "Email", + "https://mm-software.com/IPCommunication/AvailableTime": { + "https://mm-software.com/IPCommunication/AvailableTime_de": "9:00 AM to 5:00 PM" + } + } + ], + "0173-1#02-AAO128#002": { + "0173-1#02-AAO128#002_de": "Ringstraße 2" + }, + "0173-1#02-AAO129#002": { + "0173-1#02-AAO129#002_de": "50667" + }, + "0173-1#02-AAO133#002": { + "0173-1#02-AAO133#002_de": "Nordrhein-Westfalen" + }, + "0173-1#02-AAO205#002": { + "0173-1#02-AAO205#002_en": "Jane Doe" + }, + "https://mm-software.com/AddressOfAdditionalLink": "http://example.com/info2" + } + ] + } + }, + "https://mm-software.com/submodel/000-003/Nameplate": { + "https://admin-shell.io/idta/nameplate/3/0/Nameplate": { + "0112/2///61987#ABA567#009": { + "0112/2///61987#ABA567#009_en": "\u0022ABC-123\u0022" + }, + "0112/2///61987#ABN590#002": "https://mm-software.com/Model-000/Serial-Nr-003", + "0112/2///61987#ABA565#009": { + "0112/2///61987#ABA565#009_de": "M&M" + }, + "0112/2///61987#ABP464#002": { + "0112/2///61987#ABP464#002_en": "M&MType ABC003" + }, + "0112/2///61987#ABA581#007": "MM11-ABC22-003", + "0112/2///61987#ABB757#007": "2025-01-01", + "0112/2///61360_7#AAS006#001": [ + { + "0112/2///61360_7#AAS009#001": [ + { + "0112/2///61987#ABA231#009": "0173-1#07-DAA603#004", + "0112/2///61987#ABH783#003": "KEMA99IECEX1105/128", + "0112/2///61987#ABO097#001": "2022-01-01", + "0112/2///61987#ABH830#002": "2022-01-01", + "0112/2///61987#ABO100#002": "file", + "0112/2///61987#ABB146#007": "0044" + } + ] + } + ], + "0112/2///61987#ABA950#008": "FMABC1234", + "0112/2///61987#ABA951#009": "12345678", + "0112/2///61987#ABP000#002": "2022", + "0112/2///61987#ABB757#007": "2022-01-01", + "0112/2///61987#ABP462#001": "DE", + "0112/2///61987#ABP463#001": "mm-logo", + "0173-1#02-ABI218#003/0173-1#01-AGZ672#004": [ + { + "https://mm-software.com/AssetSpecificProperties/ArbitraryMLP": { + "https://mm-software.com/AssetSpecificProperties/ArbitraryMLP_en": "\u0022sample\u0022" + }, + "https://mm-software.com/AssetSpecificProperties/ArbitraryFile": "sampleFile", + "0173-1#02-ABI219#003/0173-1#01-AHD205#004": [ + { + "0173-1#01-AHD205#004": [ + { + "0173-1#02-AAO856#002": "", + "https://admin-shell.io/SMT/General/ArbitraryProp": "", + "https://mm-software.com/GuidelineSpecificProperties/ArbitraryFile": "TestFile", + "https://mm-software.com/GuidelineSpecificProperties/ArbitraryMLP": { + "https://mm-software.com/GuidelineSpecificProperties/ArbitraryMLP_en": "\u0022sample\u0022" + } + } + ] + } + ] + } + ] + } + }, + "https://mm-software.com/submodel/000-003/Reliability": { + "https://admin-shell.io/idta/iec62683/1/0/Reliability": { + "0112/2///62683#ACE006#001": "4", + "0112/2///62683#ACG071#001": [ + { + "0112/2///61987#ABA969#007": "DC", + "0112/2///61987#ABA588#004": "14.8", + "0112/2///61987#ABD461#004": "3.8", + "0112/2///61987#ABD462#004": "6.7", + "https://admin-shell.io/idta/FunctionalSafety/RatedOperationalCurrent/1/0": "78.9", + "0112/2///62683#ACE053#001": "TestType003", + "0112/2///62683#ACE070#001": "true", + "0112/2///62683#ACE055#001": "150", + "0112/2///62683#ACE054#001": "45" + } + ], + "0112/2///62683#ACG080#001": [ + { + "0112/2///62683#ACE061#001": "2", + "0112/2///62683#ACE062#001": "7", + "https://admin-shell.io/idta/Reliability/B10/1/0": "1" + } + ] + } + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Dockerfile b/source/AAS.TwinEngine.Plugin.TestPlugin/Dockerfile new file mode 100644 index 0000000..f711db5 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Dockerfile @@ -0,0 +1,15 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0@sha256:aa05b91be697b83229cb000b90120f0783604ad74ed92a0b45cdf3d1a9c873de AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /App +COPY ["AAS.TwinEngine.Plugin.TestPlugin/", "AAS.TwinEngine.Plugin.TestPlugin/"] +RUN dotnet restore "AAS.TwinEngine.Plugin.TestPlugin/AAS.TwinEngine.Plugin.TestPlugin.csproj" +RUN dotnet publish "AAS.TwinEngine.Plugin.TestPlugin/AAS.TwinEngine.Plugin.TestPlugin.csproj" -c "$BUILD_CONFIGURATION" -o out + +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine@sha256:a0ce42fe86548363a9602c47fc3bd4cf9c35a2705c68cd98d7ce18ae8735b83c +WORKDIR /App + +ENV DATA_DIR=/App/Data +RUN mkdir -p $DATA_DIR +USER app +COPY --from=build /App/out . +ENTRYPOINT ["dotnet", "AAS.TwinEngine.Plugin.TestPlugin.dll"] diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/DomainModel/Manifest/ManifestData.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/DomainModel/Manifest/ManifestData.cs new file mode 100644 index 0000000..3a330d7 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/DomainModel/Manifest/ManifestData.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace AAS.TwinEngine.Plugin.TestPlugin.DomainModel.Manifest; + +public class ManifestData +{ + [JsonPropertyName("supportedSemanticIds")] + public required IList SupportedSemanticIds { get; set; } + + [JsonPropertyName("capabilities")] + public required CapabilitiesData Capabilities { get; set; } +} + +public class CapabilitiesData +{ + [JsonPropertyName("hasShellDescriptor")] + public bool HasShellDescriptor { get; set; } + + [JsonPropertyName("hasAssetInformation")] + public bool HasAssetInformation { get; set; } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/DomainModel/MetaData/AssetData.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/DomainModel/MetaData/AssetData.cs new file mode 100644 index 0000000..7eded81 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/DomainModel/MetaData/AssetData.cs @@ -0,0 +1,17 @@ +namespace AAS.TwinEngine.Plugin.TestPlugin.DomainModel.MetaData; + +public class AssetData +{ + public string? GlobalAssetId { get; set; } + + public List? SpecificAssetIds { get; set; } = []; + + public DefaultThumbnailData? DefaultThumbnail { get; set; } +} + +public class DefaultThumbnailData +{ + public string? Path { get; set; } + + public string? ContentType { get; set; } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/DomainModel/MetaData/ShellDescriptorData.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/DomainModel/MetaData/ShellDescriptorData.cs new file mode 100644 index 0000000..33aa027 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/DomainModel/MetaData/ShellDescriptorData.cs @@ -0,0 +1,15 @@ +namespace AAS.TwinEngine.Plugin.TestPlugin.DomainModel.MetaData; + +public class ShellDescriptorData +{ + public string GlobalAssetId { get; set; } = null!; + public string IdShort { get; set; } = null!; + public string Id { get; set; } = null!; + public List? SpecificAssetIds { get; set; } = []; +} + +public class SpecificAssetIdsData +{ + public string? Name { get; set; } + public string? Value { get; set; } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/DomainModel/MetaData/ShellDescriptorsData.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/DomainModel/MetaData/ShellDescriptorsData.cs new file mode 100644 index 0000000..af9e2a7 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/DomainModel/MetaData/ShellDescriptorsData.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace AAS.TwinEngine.Plugin.TestPlugin.DomainModel.MetaData; + +public class ShellDescriptorsData +{ + [JsonPropertyName("paging_metadata")] + public PagingMetaData? PagingMetaData { get; set; } + + [JsonPropertyName("result")] + public IList? Result { get; init; } +} + +public class PagingMetaData +{ + [JsonPropertyName("cursor")] + public string? Cursor { get; set; } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/DomainModel/Submodel/DataType.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/DomainModel/Submodel/DataType.cs new file mode 100644 index 0000000..e7c526d --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/DomainModel/Submodel/DataType.cs @@ -0,0 +1,13 @@ +namespace AAS.TwinEngine.Plugin.TestPlugin.DomainModel.Submodel; + +public enum DataType +{ + String, + Number, + Integer, + Object, + Array, + Boolean, + Unknown, + Null +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/DomainModel/Submodel/SemanticTreeNode.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/DomainModel/Submodel/SemanticTreeNode.cs new file mode 100644 index 0000000..73d74f6 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/DomainModel/Submodel/SemanticTreeNode.cs @@ -0,0 +1,31 @@ +using System.Collections.ObjectModel; + +namespace AAS.TwinEngine.Plugin.TestPlugin.DomainModel.Submodel; + +public abstract class SemanticTreeNode(string semanticId, DataType dataType) +{ + public string SemanticId { get; set; } = semanticId; + + public DataType DataType { get; set; } = dataType; +} + +public class SemanticBranchNode(string semanticId, DataType dataType) : SemanticTreeNode(semanticId, dataType) +{ + private readonly List _children = []; + + public ReadOnlyCollection Children => _children.AsReadOnly(); + + public void AddChild(SemanticTreeNode child) => _children.Add(child); + + public void ReplaceChildren(List newChildren) + { + _children.Clear(); + _children.AddRange(newChildren); + } +} + +public class SemanticLeafNode(string semanticId, DataType dataType, string value) : SemanticTreeNode(semanticId, dataType) +{ + public string Value { get; set; } = value; +} + diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Example/README.md b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/README.md new file mode 100644 index 0000000..eac771f --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/README.md @@ -0,0 +1,54 @@ +# TwinEngine Demonstrator Setup + +## Overview +This project provides a basic setup to demonstrate how **TwinEngine** can be integrated and run locally. It provides a complete environment for managing Asset Administration Shells (AAS) and related components. + +This example includes three submodels: + +- Nameplate +- ContactInformation +- Reliability +--- + +## Default configuration + +- `example/aas/` — contains default submodel templates (Nameplate, ContactInformation, Reliability). +- `plugin/`— contain JSON files mounted into the plugin containers: + Changes to these JSON files on the host not visible to the running containers, you must restart the container. + +- Two services are built from local sources in the repo: + +- twinengine-dataengine (built from local DataEngine source) +- twinengine-plugin (plugin build) + +--- +## Rebuild images (when you change code) + +Rebuild all images then restart: + +``` bash +# rebuild images +docker-compose build --no-cache + +# restart the stack +docker-compose up -d +``` +--- + +### Troubleshooting + +- If http://localhost:8080/aas-ui/ doesn't load: + + Check docker-compose logs nginx for errors + + Make sure port **8080, 8081, 8082, 8083, 8085, 8086** is not used by another service. + +- If a container fails to start because of bind port, stop whatever uses that port or change the mapping in `docker-compose.yml`. + +- If you edit plugin JSON files of twinengine-testplugin, make sure you restart that cotainerApp. + ```bash + docker-compose restart + ``` + - Verify the host path for the mounted volume is correct relative to the example folder. + +--- \ No newline at end of file diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Example/aas/ContactInformationAAS.aas.xml b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/aas/ContactInformationAAS.aas.xml new file mode 100644 index 0000000..5f1c575 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/aas/ContactInformationAAS.aas.xml @@ -0,0 +1,1791 @@ + + + + CustomContactInformationAAS + https://admin-shell.io/idta/aas/ContactInformation/1/0 + + Type + https://admin-shell.io/idta/asset/ContactInformation/1/0 + Type + + + + ModelReference + + + Submodel + https://admin-shell.io/idta/SubmodelTemplate/ContactInformation/1/0 + + + + + + + + + ContactInformations + https://admin-shell.io/idta/SubmodelTemplate/ContactInformation/1/0 + Template + + ModelReference + + + Submodel + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations + + + + + + ContactInformation + + + en + The SMC “ContactInformation” contains information on how to contact the manufacturer or an authorised service provider, e.g. when a maintenance service is required + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation + + + + + + ConceptQualifier + Multiplicity + xs:string + OneToMany + + + + + RoleOfContactPerson + + + en + enumeration: 0173-1#07-AAS927#001 (administrativ contact), 0173-1#07-AAS928#001 (commercial contact), 0173-1#07-AAS929#001 (other contact), 0173-1#07-AAS930#001 (hazardous goods contact), 0173-1#07-AAS931#001 (technical contact). Note: the above mentioned ECLASS enumeration should be declared as “open” for further addition. ECLASS enumeration IRDI is preferable. If no IRDI available, custom input as String may also be accepted. + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAO204#003 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + xs:string + 0173-1#07-AAS931#001 + + + Company + + ExternalReference + + + GlobalReference + 0173-1#02-AAW001#001 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + + en + ABC Company + + + + + Department + + ExternalReference + + + GlobalReference + 0173-1#02-AAO127#003 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + + de + Vertrieb + + + + + Phone + + ExternalReference + + + GlobalReference + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + + TelephoneNumber + + + en + Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice. + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAO136#002 + + + + + + ConceptQualifier + Multiplicity + xs:string + One + + + + + en + +491234567890 + + + + + TypeOfTelephone + + + en + enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS755#001 (office mobile), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home), 0173-1#07-AAS759#001 (private mobile) + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAO137#003 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + xs:string + 0173-1#07-AAS754#001 + + + AvailableTime + + ExternalReference + + + GlobalReference + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/ + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + ExternalReference + + + GlobalReference + https://mm-software.com/twinengine/qualifier + + + + TemplateQualifier + InternalSemanticId + xs:string + https://mm-software.com/Phone/AvailableTime + + + + + de + Montag – Freitag 08:00 bis 16:00 + + + + + + + Email + + ExternalReference + + + GlobalReference + 0173-1#02-AAQ836#005 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + + EmailAddress + + ExternalReference + + + GlobalReference + 0173-1#02-AAO198#002 + + + + + + ConceptQualifier + Multiplicity + xs:string + One + + + xs:string + email@muster-ag.de + + + TypeOfEmailAddress + + + en + enumeration: 0173-1#07-AAS754#001 (office), 0173-1#07-AAS756#001 (secretary), 0173-1#07-AAS757#001 (substitute), 0173-1#07-AAS758#001 (home) + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAO199#003 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + xs:string + 0173-1#07-AAS754#001 + + + + + IPCommunication__00__ + + ExternalReference + + + GlobalReference + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToMany + + + + + AddressOfAdditionalLink + + ExternalReference + + + GlobalReference + 0173-1#02-AAQ326#002 + + + + + + ConceptQualifier + Multiplicity + xs:string + One + + + + ExternalReference + + + GlobalReference + https://mm-software.com/twinengine/qualifier + + + + TemplateQualifier + InternalSemanticId + xs:string + https://mm-software.com/ipCommunication/AddressOfAdditionalLink + + + xs:string + + + TypeOfCommunication + + ExternalReference + + + GlobalReference + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + xs:string + Chat + + + AvailableTime + + ExternalReference + + + GlobalReference + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/ + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + ExternalReference + + + GlobalReference + https://mm-software.com/twinengine/qualifier + + + + TemplateQualifier + InternalSemanticId + xs:string + https://mm-software.com/IPCommunication/AvailableTime + + + + + de + Montag – Freitag 08:00 bis 16:00 + + + + + + + Street + + ExternalReference + + + GlobalReference + 0173-1#02-AAO128#002 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + + de + Musterstraße 1 + + + + + Zipcode + + + en + Recommendation: property declaration as MLP is required by its semantic definition. As the property value is language independent, users are recommended to provide maximal 1 string in any language of the user’s choice. + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAO129#002 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + + de + 12345 + + + + + StateCounty + + ExternalReference + + + GlobalReference + 0173-1#02-AAO133#002 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + + de + Muster-Bundesland + + + + + NameOfContact + + ExternalReference + + + GlobalReference + 0173-1#02-AAO205#002 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + + en + test + + + + + AddressOfAdditionalLink + + ExternalReference + + + GlobalReference + 0173-1#02-AAQ326#002 + + + + + + ConceptQualifier + Multiplicity + xs:string + ZeroToOne + + + + ExternalReference + + + GlobalReference + https://mm-software.com/twinengine/qualifier + + + + TemplateQualifier + InternalSemanticId + xs:string + https://mm-software.com/AddressOfAdditionalLink + + + xs:string + + + + + + + + + RoleOfContactPerson + 0173-1#02-AAO204#003 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + RoleOfContactPerson + + + + + en + function of a contact person in a process + + + + + + + + + NationalCode + 0173-1#02-AAO134#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + NationalCode + + + + + en + code of a country + + + + + + + + + Language + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Language + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + Language + + + + + en + Available language + + + + + + + + + ModelReference + + + Submodel + 0173-1#02-AAO895#003 + + + + + + + CityTown + 0173-1#02-AAO132#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + CityTown + + + + + en + town or city + + + + + + + + + Company + 0173-1#02-AAW001#001 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + Company + + + + + en + name of the company + + + + + + + + + Department + 0173-1#02-AAO127#003 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + Department + + + + + en + administrative section within an organisation where a business partner is located + + + + + + + + + Phone + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + Phone + + + + + en + Phone number including type + + + + + + + + + Fax + 0173-1#02-AAQ834#005 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + Fax + + + + + en + Fax number including type + + + + + + + + + Email + 0173-1#02-AAQ836#005 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + Email + + + + + en + E-mail address and encryption method + + + + + + + + + IPCommunication__00__ + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + IPCommunication_00 + + + + + en + ContactInformation/IPCommunication IP-based communication channels, e.g. chat or video call + + + + + + + + + Street + 0173-1#02-AAO128#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + Street + + + + + en + street name and house number + + + + + + + + + Zipcode + 0173-1#02-AAO129#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + Zipcode + + + + + en + ZIP code of address + + + + + + + + + POBox + 0173-1#02-AAO130#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + POBox + + + + + en + P.O. box number + + + + + + + + + ZipCodeOfPOBox + 0173-1#02-AAO131#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + ZipCodeOfPOBox + + + + + en + ZIP code of P.O. box address + + + + + + + + + StateCounty + 0173-1#02-AAO133#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + StateCounty + + + + + en + federal state a part of a state + + + + + + + + + NameOfContact + 0173-1#02-AAO205#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + NameOfContact + + + + + en + surname of a contact person + + + + + + + + + FirstName + 0173-1#02-AAO206#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + FirstName + + + + + en + first name of a contact person + + + + + + + + + MiddleNames + 0173-1#02-AAO207#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + MiddleNames + + + + + en + middle names of contact person + + + + + + + + + Title + 0173-1#02-AAO208#003 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + Title + + + + + en + common, formal, religious, or other title preceding a contact person's name + + + + + + + + + AcademicTitle + 0173-1#02-AAO209#003 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + AcademicTitle + + + + + en + academic title preceding a contact person's name + + + + + + + + + FurtherDetailsOfContact + 0173-1#02-AAO210#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + FurtherDetailsOfContact + + + + + en + additional information of the contact person + + + + + + + + + AddressOfAdditionalLink + 0173-1#02-AAQ326#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + AddressOfAdditionalLink + + + + + en + web site address where information about the product or contact is given + + + + + + + + + TelephoneNumber + 0173-1#02-AAO136#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + TelephoneNumber + + + + + en + complete telephone number to be called to reach a business partner + + + + + + + + + TypeOfTelephone + 0173-1#02-AAO137#003 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + TypeOfTelephone + + + + + en + characterization of a telephone according to its location or usage + + + + + + + + + AvailableTime + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/AvailableTime/ + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + AvailableTime + + + + + en + Specification of the available time window + + + + + + + + + FaxNumber + 0173-1#02-AAO195#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + FaxNumber + + + + + en + complete telephone number to be called to reach a business partner's fax machine + + + + + + + + + TypeOfFaxNumber + 0173-1#02-AAO196#003 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + TypeOfFaxNumber + + + + + en + characterization of the fax according its location or usage + + + + + + + + + EmailAddress + 0173-1#02-AAO198#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + EmailAddress + + + + + en + electronic mail address of a business partner + + + + + + + + + PublicKey + 0173-1#02-AAO200#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + PublicKey + + + + + en + public part of an unsymmetrical key pair to sign or encrypt text or messages + + + + + + + + + TypeOfEmailAddress + 0173-1#02-AAO199#003 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + TypeOfEmailAddress + + + + + en + characterization of an e-mail address according to its location or usage + + + + + + + + + TypeOfPublicKey + 0173-1#02-AAO201#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + TypeOfPublicKey + + + + + en + characterization of a public key according to its encryption process + + + + + + + + + TypeOfCommunication + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + TypeOfCommunication + + + + + en + characterization of an IP-based communication channel + + + + + + + + + ContactInformations + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + ContactInformations + + + + + en + The Submodel “ContactInformations” is the collection for various contact information. + + + + + + + + + ContactInformation + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + ContactInformation + + + + + en + The SMC “ContactInformation” contains information on how to contact the manufacturer or an authorised service provider, e.g. when a maintenance service is required + + + + + + + + + ModelReference + + + Submodel + 0173-1#02-AAQ837#005 + + + + + + + TimeZone + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/TimeZone + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + TimeZone + + + + + en + offsets from Coordinated Universal Time (UTC) + + + + + + + + + \ No newline at end of file diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Example/aas/DigitalNameplateAAS.aas.xml b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/aas/DigitalNameplateAAS.aas.xml new file mode 100644 index 0000000..2a8f5c5 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/aas/DigitalNameplateAAS.aas.xml @@ -0,0 +1,2395 @@ + + + + DigitalNameplateAAS + https://admin-shell.io/idta/aas/DigitalNameplate/3/0 + + Type + https://admin-shell.io/idta/asset/DigitalNameplate/3/0 + Type + + + + ModelReference + + + Submodel + https://admin-shell.io/idta/SubmodelTemplate/DigitalNameplate/3/0 + + + + + + + + + Nameplate + + + en + Contains the nameplate information attached to the product + + + + 3 + 0 + + https://admin-shell.io/idta/SubmodelTemplate/DigitalNameplate/3/0 + Template + + ExternalReference + + + GlobalReference + https://admin-shell.io/idta/nameplate/3/0/Nameplate + + + + + + URIOfTheProduct + + ExternalReference + + + GlobalReference + 0112/2///61987#ABN590#002 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABH173#003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + One + + + xs:anyURI + https://www.domain-abc.com/Model-Nr-1234/Serial-Nr-5678 + + + ManufacturerName + + ExternalReference + + + GlobalReference + 0112/2///61987#ABA565#009 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAO677#004 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + One + + + + + de + "Muster AG" + + + + + ManufacturerProductDesignation + + ExternalReference + + + GlobalReference + 0112/2///61987#ABA567#009 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAW338#003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + One + + + + + en + "ABC-123" + + + + + ManufacturerProductFamily + + ExternalReference + + + GlobalReference + 0112/2///61987#ABP464#002 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAU731#003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + + + en + "Type ABC" + + + + + OrderCodeOfManufacturer + + ExternalReference + + + GlobalReference + 0112/2///61987#ABA950#008 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAO227#004 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + One + + + xs:string + FMABC1234 + + + ProductArticleNumberOfManufacturer + + ExternalReference + + + GlobalReference + 0112/2///61987#ABA581#007 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAO676#005 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + xs:string + FM11-ABC22-123456 + + + SerialNumber + + ExternalReference + + + GlobalReference + 0112/2///61987#ABA951#009 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAM556#004 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + xs:string + 12345678 + + + YearOfConstruction + + ExternalReference + + + GlobalReference + 0112/2///61987#ABP000#002 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAP906#003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + xs:string + 2022 + + + DateOfManufacture + + ExternalReference + + + GlobalReference + 0112/2///61987#ABB757#007 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAR972#004 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + xs:date + 2022-01-01 + + + CountryOfOrigin + + + en + Note: Country codes defined accord. to DIN EN ISO 3166-1 alpha-2 codes + + + + ExternalReference + + + GlobalReference + 0112/2///61987#ABP462#001 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAO259#007 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + xs:string + DE + + + CompanyLogo + + ExternalReference + + + GlobalReference + 0112/2///61987#ABP463#001 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABI776#002 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + image/png + + + Markings + + + en + Note: CE marking is declared as mandatory according to EU Blue Guide + + + + ExternalReference + + + GlobalReference + 0112/2///61360_7#AAS006#001 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABI563#003/0173-1#01-AHF849#003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + SubmodelElementCollection + + + + + en + Note: CE marking is declared as mandatory according to the Blue Guide of the EU-Commission + + + + ExternalReference + + + GlobalReference + 0112/2///61360_7#AAS009#001 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABI564#003/0173-1#01-AHF850#003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + OneToMany + + + + + MarkingName + + ExternalReference + + + GlobalReference + 0112/2///61987#ABA231#009 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABI190#003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + One + + + xs:string + 0173-1#07-DAA603#004 + + + DesignationOfCertificateOrApproval + + + en + Note: Approval identifier, reference to the certificate number, to be entered without spaces + + + + ExternalReference + + + GlobalReference + 0112/2///61987#ABH783#003 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABI975#002 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + xs:string + KEMA99IECEX1105/128 + + + IssueDate + + + en + Note: format by lexical representation: CCYY-MM-DD Note: to be specified to the day + + + + ExternalReference + + + GlobalReference + 0112/2///61987#ABO097#001 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABL774#001 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + xs:date + 2022-01-01 + + + ExpiryDate + + + en + Note: format by lexical representation: CCYY-MM-DD Note: to be specified to the day + + + + ExternalReference + + + GlobalReference + 0112/2///61987#ABH830#002 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABL775#001 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + xs:date + 2022-01-01 + + + MarkingFile + + ExternalReference + + + GlobalReference + 0112/2///61987#ABO100#002 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABI191#003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + One + + + image/png + + + MarkingAdditionalText + + ExternalReference + + + GlobalReference + 0112/2///61987#ABB146#007 + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-ABI192#003 + + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToMany + + + xs:string + 0044 + + + + + + + AssetSpecificProperties + + ExternalReference + + + GlobalReference + 0173-1#02-ABI218#003/0173-1#01-AGZ672#004 + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + + + ArbitraryProperty + + + en + Note: Every property can be used. + + + en + Note: The idShort is arbitrary + + + en + Note: The use of a displayName is recommended. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SMT/General/ArbitraryProp + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToMany + + + xs:string + + + ArbitraryMLP + + + en + Note: Every multilanguage property can be used. + + + en + Note: The idShort is arbitrary + + + en + Note: The use of a displayName is recommended. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SMT/General/ArbitraryMLP + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToMany + + + + ExternalReference + + + GlobalReference + https://mm-software.com/twinengine/qualifier + + + + TemplateQualifier + InternalSemanticId + xs:string + https://mm-software.com/AssetSpecificProperties/ArbitraryMLP + + + + + en + "sample" + + + + + ArbitraryFile + + + en + Note: Every file can be used. + + + en + The idShort is arbitrary + + + en + Note: The use of a displayName is recommended. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SMT/General/ArbitraryFile + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToMany + + + + ExternalReference + + + GlobalReference + https://mm-software.com/twinengine/qualifier + + + + InternalSemanticId + xs:string + https://mm-software.com/AssetSpecificProperties/ArbitraryFile + + + application/pdf + + + GuidelineSpecificProperties + + ExternalReference + + + GlobalReference + 0173-1#02-ABI219#003/0173-1#01-AHD205#004 + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + SubmodelElementCollection + + + + ExternalReference + + + GlobalReference + 0173-1#01-AHD205#004 + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + OneToMany + + + + + GuidelineForConformityDeclaration + + ExternalReference + + + GlobalReference + 0173-1#02-AAO856#002 + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + One + + + xs:string + + + ArbitraryProperty + + + en + Note: Every property can be used. + + + en + Note: The idShort is arbitrary + + + en + Note: The use of a displayName is recommended. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SMT/General/ArbitraryProp + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToMany + + + xs:string + + + ArbitraryFile + + + en + Note: Every file can be used. + + + en + The idShort is arbitrary + + + en + Note: The use of a displayName is recommended. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SMT/General/ArbitraryFile + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToMany + + + + ExternalReference + + + GlobalReference + https://mm-software.com/twinengine/qualifier + + + + InternalSemanticId + xs:string + https://mm-software.com/GuidelineSpecificProperties/ArbitraryFile + + + image/png + + + ArbitraryMLP + + + en + Note: Every multilanguage property can be used. + + + en + Note: The idShort is arbitrary + + + en + Note: The use of a displayName is recommended. + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SMT/General/ArbitraryMLP + + + + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/SubmodelTemplates/Cardinality/1/0 + + + + TemplateQualifier + SMT/Cardinality + xs:string + ZeroToMany + + + + ExternalReference + + + GlobalReference + https://mm-software.com/twinengine/qualifier + + + + TemplateQualifier + InternalSemanticId + xs:string + https://mm-software.com/GuidelineSpecificProperties/ArbitraryMLP + + + + + en + "sample" + + + + + + + + + + + + + + + URIOfTheProduct + 0112/2///61987#ABN590#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + URIOfTheProduct + + + + + en + URIOfTheProduct + + + STRING + + + en + unique global identification of the product instance using an universal resource identifier (URI) + + + + + + + + + ManufacturerName + 0112/2///61987#ABA565#009 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + ManufacturerName + + + STRING_TRANSLATABLE + + + en + legally valid designation of the natural or judicial person which is directly responsible for the design, production, packaging and labeling of a product in respect to its being brought into circulation + + + + + + + + + ManufacturerProductDesignation + 0112/2///61987#ABA567#009 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + ManufacturerProductDesignation + + + STRING_TRANSLATABLE + + + en + short description of the product (short text), third or lowest level of a 3 level manufacturer specific product hierarchy + + + + + + + + + ManufacturerProductRoot + 0112/2///61360_7#AAS011#001 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + ManufacturerProductRoot + + + STRING_TRANSLATABLE + + + en + top level of a 3 level manufacturer specific product hierarchy + + + + + + + + + ManufacturerProductFamily + 0112/2///61987#ABP464#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + ManufacturerProductFamily + + + STRING_TRANSLATABLE + + + en + second level of a 3 level manufacturer specific product hierarchy + + + + + + + + + ManufacturerProductType + 0112/2///61987#ABA300#008 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + ManufacturerProductType + + + STRING + + + en + characteristic to differentiate between different products of a product family or special variants + + + + + + + + + OrderCodeOfManufacturer + 0112/2///61987#ABA950#008 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + OrderCodeOfManufacturer + + + STRING + + + en + unique combination of numbers and letters issued by the manufacturer that is used to identify the device for ordering + + + + + + + + + ProductArticleNumberOfManufacturer + 0112/2///61987#ABA581#007 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + ProductArticleNumberOfManufacturer + + + STRING + + + en + unique product identifier of the manufacturer + + + + + + + + + SerialNumber + 0112/2///61987#ABA951#009 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + SerialNumber + + + STRING + + + en + unique combination of numbers and letters used to identify the device once it has been manufactured + + + + + + + + + YearOfConstruction + 0112/2///61987#ABP000#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + YearOfConstruction + + + STRING + + + en + year in which the manufacturing process is completed + + + + + + + + + DateOfManufacture + 0112/2///61987#ABB757#007 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + DateOfManufacture + + + DATE + + + en + date when an item was manufactured + + + + + + + + + HardwareVersion + 0112/2///61987#ABA926#008 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + HardwareVersion + + + STRING + + + en + version of the hardware supplied with the device + + + + + + + + + FirmwareVersion + 0112/2///61987#ABA302#006 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + FirmwareVersion + + + STRING + + + en + version of the firmware supplied with the device + + + + + + + + + SoftwareVersion + 0112/2///61987#ABA601#008 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + SoftwareVersion + + + STRING + + + en + version of the software used by the device + + + + + + + + + CountryOfOrigin + 0112/2///61987#ABP462#001 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + CountryOfOrigin + + + STRING + + + en + country where the product was manufactured + + + + + + + + + CompanyLogo + 0112/2///61987#ABP463#001 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + CompanyLogo + + + STRING + + + en + a graphic mark used to represent a company, an organisation or a product + + + + + + + + + AssetSpecificProperties + 0173-1#01-AGZ672#001 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + AssetSpecificProperties + + + + + en + Group of properties that are listed on the asset's nameplate and are grouped based on guidelines + + + + + + + + + MarkingName + 0112/2///61987#ABA231#009 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + MarkingName + + + STRING + + + en + common name of the marking + + + + + + + + + DesignationOfCertificateOrApproval + 0112/2///61987#ABH783#003 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + DesignationOfCertificateOrApproval + + + STRING + + + en + alphanumeric character sequence identifying a certificate or approval + + + + + + + + + IssueDate + 0112/2///61987#ABO097#001 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + IssueDate + + + DATE + + + en + date, at which the specified certificate is issued + + + + + + + + + ExpiryDate + 0112/2///61987#ABH830#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + ExpiryDate + + + DATE + + + en + date, at which the specified certificate expires + + + + + + + + + MarkingFile + 0112/2///61987#ABO100#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + MarkingFile + + + STRING + + + en + conformity symbol of the marking + + + + + + + + + MarkingAdditionalText__00__ + 0112/2///61987#ABB146#007 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + MarkingAdditionalText__00__ + + + STRING + + + en + where applicable, additional information on the marking in plain text, e.g. the ID-number of the notified body involved in the conformity process + + + + + + + + + GuidelineSpecificProperties + 0173-1#01-AHD205#001 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + GuidelineSpecificProperties + + + + + en + Asset specific nameplate information required by guideline, stipulation or legislation. + + + + + + + + + GuidelineForConformityDeclaration + 0173-1#02-AAO856#002 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + GuidelineForConformityDeclaration + + + + + en + guideline, stipulation or legislation used for determining conformity + + + + + + + + + ContactInformation + https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + ContactInformation + + + + + en + The SMC “ContactInformation” contains information on how to contact the manufacturer or an authorised service provider, e.g. when a maintenance service is required + + + + + + + + + ExternalReference + + + GlobalReference + 0173-1#02-AAQ837#005 + + + + + + + Markings + + + en + Note: CE marking is declared as mandatory according to EU Machine Directive 2006/42/EC. + + + https://admin-shell.io/zvei/nameplate/3/0/Nameplate/Markings + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + Markings + + + + + en + Collection of product markings + + + + + + + + + Marking + + + en + Note: CE marking is declared as mandatory according to the Blue Guide of the EU-Commission + + + 0112/2///61360_7#AAS009#001 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + Marking + + + + + en + Single marking information + + + + + + + + + GuidelineSpecificProperties + http://admin-shell.io/IDTA/DigitalNameplate/GuidelineSpecificProperties/List/1/0 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + GuidelineSpecificProperties + + + + + en + Information elements guided by a specific standard or guideline + + + + + + + + + UniqueFacilityIdentifier + https://admin-shell.io/idta/nameplate/3/0/UniqueFacilityIdentifier + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0 + + + + + + + + en + UniqueFacilityIdentifier + + + STRING + + + en + unique string of characters for the identification of locations or buildings involved in a product’s value chain or used by actors involved in a product’s value chain + + + + + + + + + \ No newline at end of file diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Example/aas/Reliability.xml b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/aas/Reliability.xml new file mode 100644 index 0000000..25e8158 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/aas/Reliability.xml @@ -0,0 +1,1471 @@ + + + + CustomReliabilityAAS + https://admin-shell.io/idta/aas/Reliability/1/0 + + Type + https://admin-shell.io/idta/asset/Reliability/1/0 + Type + + + + ModelReference + + + Submodel + https://admin-shell.io/idta/SubmodelTemplate/Reliability/1/0 + + + + + + + + + Reliability + + 1 + 0 + + https://admin-shell.io/idta/SubmodelTemplate/Reliability/1/0 + Template + + ModelReference + + + Submodel + https://admin-shell.io/idta/iec62683/1/0/Reliability + + + + + + NumberOfReliabilitySets + + + en + number of reliability sets of characteristics + + + fr + nombre d'ensembles de caractéristiques de fiabilité + + + + ExternalReference + + + GlobalReference + 0112/2///62683#ACE006#001 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + One + + + xs:int + + + OperatingConditionsOfReliabilityCharacteristics + + + en + operating conditions of reliability characteristics + + + fr + conditions de fonctionnement des caractéristiques de fiabilité et de sécurité fonctionnelle + + + + ExternalReference + + + GlobalReference + 0112/2///62683#ACG071#001 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + ZeroToMany + + + + + TypeOfVoltage + + + en + type of voltage + + + fr + type de tension + + + de + Spannungsart + + + + ExternalReference + + + GlobalReference + 0112/2///61987#ABA969#007 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + ZeroToMany + + + xs:string + + ExternalReference + + + GlobalReference + 0112/2///61987#ABL837#001 + + + GlobalReference + 0112/2///61987#ABL838#001 + + + GlobalReference + 0112/2///61987#ABI407#004 + + + + + + RatedVoltage + + + en + rated voltage + + + fr + tension assignée + + + de + Bemessungsspannung + + + + ExternalReference + + + GlobalReference + 0112/2///61987#ABA588#004 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + ZeroToMany + + + xs:double + + + MinimumRatedVoltage + + + en + minimum rated voltage + + + fr + tension assignée minimale + + + de + minimale Bemessungsspannung + + + + ExternalReference + + + GlobalReference + 0112/2///61987#ABD461#004 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + ZeroToMany + + + xs:double + + + MaximumRatedVoltage + + + en + maximum rated voltage + + + fr + tension assignée maximale + + + de + maximale Bemessungsspannung + + + + ExternalReference + + + GlobalReference + 0112/2///61987#ABD462#004 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + ZeroToMany + + + xs:double + + + RatedOperationalCurrent + + + en + rated operational current + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/idta/FunctionalSafety/RatedOperationalCurrent/1/0 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + ZeroToMany + + + xs:double + + + TypeOfInterlockingDevice + + + en + type of interlocking device + + + fr + type de dispositif de verrouillage + + + + ExternalReference + + + GlobalReference + 0112/2///62683#ACE053#001 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + ZeroToMany + + + xs:string + + ExternalReference + + + GlobalReference + 0112/2///62683#ACH673#001 + + + GlobalReference + 0112/2///62683#ACH674#001 + + + GlobalReference + 0112/2///62683#ACH675#001 + + + GlobalReference + 0112/2///62683#ACH676#001 + + + + + + OtherOperatingConditions + + + en + other operating conditions + + + fr + autres conditions de fonctionnement + + + + ExternalReference + + + GlobalReference + 0112/2///62683#ACE070#001 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + ZeroToMany + + + xs:string + + + UsefulLifeInNumberOfOperations + + + en + useful life in number of operations + + + fr + durée de vie utile en cycle de fonctionnement + + + + ExternalReference + + + GlobalReference + 0112/2///62683#ACE055#001 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + ZeroToMany + + + xs:double + + + UsefulLifeInTimeInterval + + + en + useful life in time interval + + + fr + durée de vie utile en intervalle de temps + + + + ExternalReference + + + GlobalReference + 0112/2///62683#ACE054#001 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + ZeroToMany + + + xs:double + + + + + ReliabilityCharacteristics + + + en + Reliability characteristics + + + fr + Caractéristiques de fiabilité + + + + ExternalReference + + + GlobalReference + 0112/2///62683#ACG080#001 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + ZeroToOne + + + + + MTTF + + + en + mean operating time to failure + + + fr + durée moyenne de fonctionnement avant défaillance + + + de + mittlere Betriebszeit bis zum Ausfall + + + jp + 平均故障間動作時間 + + + cn + 平均失效前工作时间 + + + + ExternalReference + + + GlobalReference + 0112/2///62683#ACE061#001 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + ZeroToMany + + + xs:int + + + MTBF + + + en + mean operating time between failure + + + fr + moyenne des temps de bon fonctionnement + + + de + mittlere Betriebszeit zwischen Ausfällen + + + cn + 平均失效间隔工作时间 + + + + ExternalReference + + + GlobalReference + 0112/2///62683#ACE062#001 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + ZeroToMany + + + xs:int + + + B10cycle + + + en + B10 + + + + ExternalReference + + + GlobalReference + https://admin-shell.io/idta/Reliability/B10/1/0 + + + + + + ConceptQualifier + SMT/Cardinality + xs:string + ZeroToMany + + + xs:int + + + + + + + + + NumberOfReliabilitySetsOfCharacteristics + 0112/2///62683#ACE006#001 + + + + ExternalReference + + + GlobalReference + 0112/2///62683#ACE006#001 + + + + + + + + en + number of reliability sets of characteristics + + + fr + nombre d'ensembles de caractéristiques de fiabilité + + + + + en + NoOfReliSets + + + NR1..2 + + + + + + + ExternalReference + + + GlobalReference + 0112/2///62683#ACE006#001 + + + + + + + ReliabilityCharacteristics + 0112/2///62683#ACG080#001 + + + + ExternalReference + + + GlobalReference + 0112/2///62683#ACG080#001 + + + + + + + + en + Reliability characteristics + + + fr + Caractéristiques de fiabilité + + + + + en + ReliabilityChar + + + + + en + characteristics of a subsystem or a subsystem element intended for evaluating its ability to perform as required, without failure, for a given time interval, under given conditions + + + + + + + + + ExternalReference + + + GlobalReference + 0112/2///62683#ACG080#001 + + + + + + + MeanOperatingTimeToFailure + 0112/2///62683#ACE061#001 + + + + ExternalReference + + + GlobalReference + 0112/2///62683#ACE061#001 + + + + + + + + en + mean operating time to failure + + + fr + durée moyenne de fonctionnement avant défaillance + + + de + mittlere Betriebszeit bis zum Ausfall + + + jp + 平均故障間動作時間 + + + cn + 平均失效前工作时间 + + + + + en + MTTF + + + y + + ExternalReference + + + GlobalReference + 0112/2///62720#UAB026 + + + + MTTF + + + en + expectation of the operating time to failure + + + NR1..7 + + + + + + + ExternalReference + + + GlobalReference + 0112/2///62683#ACE061#001 + + + + + + + MeanOperatingTimeBetweenFailure + 0112/2///62683#ACE062#001 + + + + ExternalReference + + + GlobalReference + 0112/2///62683#ACE062#001 + + + + + + + + en + mean operating time between failure + + + fr + moyenne des temps de bon fonctionnement + + + de + mittlere Betriebszeit zwischen Ausfällen + + + cn + 平均失效间隔工作时间 + + + + + en + MTBF + + + y + + ExternalReference + + + GlobalReference + 0112/2///62720#UAB026 + + + + MTBF + + + en + expectation of the duration of the operating time between failures + + + NR1..7 + + + + + + + ExternalReference + + + GlobalReference + 0112/2///62683#ACE062#001 + + + + + + + B10 + https://admin-shell.io/idta/Reliability/B10/1/0 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + B10 + + + + + en + B10 + + + + + en + mean number of cycles until 10% of the components fail + + + + + + + + + OperatingConditionsOfReliabilityCharacteristics + 0112/2///62683#ACG071#001 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + operating conditions of reliability characteristics + + + fr + conditions de fonctionnement des caractéristiques de fiabilité et de sécurité fonctionnelle + + + + + + + + + TypeOfVoltage + 0112/2///61987#ABA969#007 + + + + ExternalReference + + + GlobalReference + 0112/2///61987#ABA969#007 + + + + + + + + en + type of voltage + + + fr + type de tension + + + de + Spannungsart + + + + + en + type of voltage + + + + + en + classification of a power supply according to the time behaviour of the voltage + + + + + + + + + ExternalReference + + + GlobalReference + 0112/2///61987#ABA969#007 + + + + + + + RatedVoltage + 0112/2///61987#ABA588#004 + + + + ExternalReference + + + GlobalReference + 0112/2///61987#ABA588#004 + + + + + + + + en + rated voltage + + + fr + tension assignée + + + de + Bemessungsspannung + + + + + en + RatedVoltage + + + V + + ExternalReference + + + GlobalReference + 0112/2///62720#UAA296 + + + + + + en + operating voltage of the device as defined by the manufacturer and to which certain device properties are referenced + + + + + + + + + ExternalReference + + + GlobalReference + 0112/2///61987#ABA588#004 + + + + + + + OperatingConditionsOfFunctionalSafetyCharacteristics + 0112/2///62683#ACG057#001 + + + + ExternalReference + + + GlobalReference + 0112/2///62683#ACG057#001 + + + + + + + + en + Operating conditions of functional safety characteristics + + + fr + Conditions de fonctionnement des caractéristiques de sécurité fonctionnelle + + + + + + + + + ExternalReference + + + GlobalReference + 0112/2///62683#ACG057#001 + + + + + + + MinimumRatedVoltage + 0112/2///61987#ABD461#004 + + + + ExternalReference + + + GlobalReference + 0112/2///61987#ABD461#004 + + + + + + + + en + minimum rated voltage + + + fr + tension assignée minimale + + + de + minimale Bemessungsspannung + + + + + en + MinRatedVol + + + V + + ExternalReference + + + GlobalReference + 0112/2///62720#UAA296 + + + + + + en + lowest operating voltage of the device as defined by the manufacturer + + + + + + + + + ExternalReference + + + GlobalReference + 0112/2///61987#ABD461#004 + + + + + + + MaximumRatedVoltage + 0112/2///61987#ABD462#004 + + + + ExternalReference + + + GlobalReference + 0112/2///61987#ABD462#004 + + + + + + + + en + maximum rated voltage + + + fr + tension assignée maximale + + + de + maximale Bemessungsspannung + + + + + en + MaxRatVol + + + V + + ExternalReference + + + GlobalReference + 0112/2///62720#UAA296 + + + + + + en + highest operating voltage of the device as defined by the manufacturer + + + + + + + + + ExternalReference + + + GlobalReference + 0112/2///61987#ABD462#004 + + + + + + + RatedOperationalCurrent + https://admin-shell.io/idta/FunctionalSafety/RatedOperationalCurrent/1/0 + + + + ExternalReference + + + GlobalReference + http://admin-shell.io/DataSpecificationTemplates/DataSpecificationIEC61360/3/0 + + + + + + + + en + rated operational current + + + + + en + RatOperCurrent + + + mA + + ExternalReference + + + GlobalReference + 0112/2///62720#UAA775 + + + + + + en + current combined with a rated operational voltage intended to be switched by the device under specified conditions + + + + + + + + + TypeOfInterlockingDevice + 0112/2///62683#ACE053#001 + + + + ExternalReference + + + GlobalReference + 0112/2///62683#ACE053#001 + + + + + + + + en + type of interlocking device + + + fr + type de dispositif de verrouillage + + + + + en + classification of device which prevent the hazardous operation of machine, depending on the technology of their actuating means and their output system + + + X.6 + + + + + + + ExternalReference + + + GlobalReference + 0112/2///62683#ACE053#001 + + + + + + + OtherOperatingConditions + 0112/2///62683#ACE070#001 + + + + ExternalReference + + + GlobalReference + 0112/2///62683#ACE070#001 + + + + + + + + en + other operating conditions + + + fr + autres conditions de fonctionnement + + + + + en + other limits of operation related to functional safety characteristics + + + X..256 + + + + + + + ExternalReference + + + GlobalReference + 0112/2///62683#ACE070#001 + + + + + + + UsefulLifeInNumberOfOperations + 0112/2///62683#ACE055#001 + + + + ExternalReference + + + GlobalReference + 0112/2///62683#ACE055#001 + + + + + + + + en + useful life in number of operations + + + fr + durée de vie utile en cycle de fonctionnement + + + + + en + under given conditions, the number of operations for which the failure rate becomes unacceptable + + + NR3..1.2E1 + + + + + + + ExternalReference + + + GlobalReference + 0112/2///62683#ACE055#001 + + + + + + + UsefulLifeInTimeInterval + 0112/2///62683#ACE054#001 + + + + ExternalReference + + + GlobalReference + 0112/2///62683#ACE054#001 + + + + + + + + en + useful life in time interval + + + fr + durée de vie utile en intervalle de temps + + + + + en + LifeInTime + + + y + + ExternalReference + + + GlobalReference + 0112/2///62720#UAB026 + + + + + + en + under given conditions, the time interval beginning at a given instant of time, and ending when the failure rate becomes unacceptable + + + NR2..2.1 + + + + + + + ExternalReference + + + GlobalReference + 0112/2///62683#ACE054#001 + + + + + + + \ No newline at end of file diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Aas Registry/Get All ShellDescriptors.bru b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Aas Registry/Get All ShellDescriptors.bru new file mode 100644 index 0000000..1fc5791 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Aas Registry/Get All ShellDescriptors.bru @@ -0,0 +1,21 @@ +meta { + name: Get All ShellDescriptors + type: http + seq: 1 +} + +get { + url: {{DataEngineBaseUrl}}/shell-descriptors?limit&cursor + body: none + auth: inherit +} + +params:query { + limit: + cursor: +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/apiCollection/Aas Registry/Get Shell Descriptor By Id.bru b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Aas Registry/Get Shell Descriptor By Id.bru similarity index 100% rename from apiCollection/Aas Registry/Get Shell Descriptor By Id.bru rename to source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Aas Registry/Get Shell Descriptor By Id.bru diff --git a/apiCollection/Aas Registry/folder.bru b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Aas Registry/folder.bru similarity index 100% rename from apiCollection/Aas Registry/folder.bru rename to source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Aas Registry/folder.bru diff --git a/apiCollection/Aas Repository/Get Asset Information By Id.bru b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Aas Repository/Get Asset Information By Id.bru similarity index 100% rename from apiCollection/Aas Repository/Get Asset Information By Id.bru rename to source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Aas Repository/Get Asset Information By Id.bru diff --git a/apiCollection/Aas Repository/Get Shell By Id.bru b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Aas Repository/Get Shell By Id.bru similarity index 100% rename from apiCollection/Aas Repository/Get Shell By Id.bru rename to source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Aas Repository/Get Shell By Id.bru diff --git a/apiCollection/Aas Repository/Get Submodel Ref By Id.bru b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Aas Repository/Get Submodel Ref By Id.bru similarity index 100% rename from apiCollection/Aas Repository/Get Submodel Ref By Id.bru rename to source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Aas Repository/Get Submodel Ref By Id.bru diff --git a/apiCollection/Aas Repository/folder.bru b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Aas Repository/folder.bru similarity index 100% rename from apiCollection/Aas Repository/folder.bru rename to source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Aas Repository/folder.bru diff --git a/apiCollection/README.md b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/README.md similarity index 85% rename from apiCollection/README.md rename to source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/README.md index 15d4f1a..56a2e83 100644 --- a/apiCollection/README.md +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/README.md @@ -6,20 +6,15 @@ This directory contains the Bruno collection and instructions to test the **AAS. --- -## 📚 Contents - -[[_TOC_]] - - -## 🔍 Quick Summary +## Quick Summary | Item | Description | |--------------------------|-----------------------------------------------------| | **API** | `AAS.TwinEngine.DataEngine` (.NET) | | **Testing Tool** | [Bruno](https://www.usebruno.com/downloads) | -| **Default API URL** | `https://localhost:5059` | +| **Default API URL** | `local` `http://localhost:8085`
- started with Docker compose
`local_net` `https://localhost:5059`
- started with devleopment environment (dotnet run) | | **SDK Required** | .NET 8 (recommended) | -| **Run docker compose file** | Run `docker-compose-up` form AasTwin.DataEngine | +| **Run docker compose file** | Run `docker-compose up -d` from AAS.TwinEngine.Plugin.TestPlugin Example folder | --- @@ -43,11 +38,13 @@ This directory contains the Bruno collection and instructions to test the **AAS. ### 1. Run docker compose file -Before starting , run twinengine environmnet with multi-plugin. +Before starting , run twinengine environmnet with plugin. [click here for getting starated with docker-compose](../README.md) ### 2. Start the DataEngine .NET API +**Annotation:** To avoid complications you should deactivate the running DataEngine started with Docker compose. + Run the API: ```bash @@ -63,7 +60,7 @@ By default the API listens at `https://localhost:5059` unless overridden by envi 1. Open Bruno 2. `Collection -> Open Collection` and choose the Bruno collection folder (`apiCollection`) from the AasTwin.DataEngine repository -3. From the top-right environment dropdown select an environment: `local` or `dev` (use `local` for local testing) +3. From the top-right environment dropdown select an environment: `local` or `local_net` (use `local_net` for local testing with development environment) 4. Expand folders to find requests, select a request and click **Send** 5. Inspect the request/response in the right panel @@ -88,17 +85,16 @@ Default values are set as shown. ## Default api-test configuration -* The default configuration includes four shell descriptors with these IDs: +* The default configuration includes three shell descriptors with these IDs: * `https://mm-software.com/ids/aas/000-001` * `https://mm-software.com/ids/aas/000-002` * `https://mm-software.com/ids/aas/000-003` - * `https://mm-software.com/ids/aas/000-004` -* Default submodel templates (under `/infrastructure/config-files/aas`): +* Default submodel templates (under `/Example/aas`): - * `ContactInformation` - * `Nameplate` + * `ContactInformationAAS` + * `DigitalNameplateAAS` * `Reliability` * Default shell template used by all four shells: @@ -150,7 +146,7 @@ By default, DataEngine requests the dev Template Repository, Submodel Registry, ## Troubleshooting -#### ❌ Bruno shows `SSL/TLS handshake failed` +#### Bruno shows `SSL/TLS handshake failed` - Run `dotnet dev-certs https --trust` - Ensure plugin and API endpoints match port and schema (`https://`) diff --git a/apiCollection/Submodel Registry/Get Submodel Descriptor By Id.bru b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Registry/Get Submodel Descriptor By Id.bru similarity index 100% rename from apiCollection/Submodel Registry/Get Submodel Descriptor By Id.bru rename to source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Registry/Get Submodel Descriptor By Id.bru diff --git a/apiCollection/Submodel Registry/folder.bru b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Registry/folder.bru similarity index 100% rename from apiCollection/Submodel Registry/folder.bru rename to source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Registry/folder.bru diff --git a/apiCollection/Submodel Repository/Serialization/Get appropriate serialization.bru b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Repository/Serialization/Get appropriate serialization.bru similarity index 100% rename from apiCollection/Submodel Repository/Serialization/Get appropriate serialization.bru rename to source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Repository/Serialization/Get appropriate serialization.bru diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Repository/Serialization/folder.bru b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Repository/Serialization/folder.bru new file mode 100644 index 0000000..d303a46 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Repository/Serialization/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Serialization + seq: 3 +} + +auth { + mode: inherit +} diff --git a/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - ContactInfo.bru b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - ContactInfo.bru similarity index 100% rename from apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - ContactInfo.bru rename to source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - ContactInfo.bru diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - Markings.bru b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - Markings.bru new file mode 100644 index 0000000..c87b942 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - Markings.bru @@ -0,0 +1,16 @@ +meta { + name: Get Submodel Element - Markings + type: http + seq: 2 +} + +get { + url: {{DataEngineBaseUrl}}/submodels/:submodelIdentifier/submodel-elements/:idShortPath + body: none + auth: inherit +} + +params:path { + submodelIdentifier: {{submodelIdentifierNameplate}} + idShortPath: Markings +} diff --git a/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - Nameplate.bru b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - Nameplate.bru similarity index 100% rename from apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - Nameplate.bru rename to source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - Nameplate.bru diff --git a/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - Reliability.bru b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - Reliability.bru similarity index 100% rename from apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - Reliability.bru rename to source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Repository/Submodel Element/Get Submodel Element - Reliability.bru diff --git a/apiCollection/Submodel Repository/Submodel Element/folder.bru b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Repository/Submodel Element/folder.bru similarity index 100% rename from apiCollection/Submodel Repository/Submodel Element/folder.bru rename to source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Repository/Submodel Element/folder.bru diff --git a/apiCollection/Submodel Repository/Submodel/Get Submodel - ContactInfo.bru b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Repository/Submodel/Get Submodel - ContactInfo.bru similarity index 100% rename from apiCollection/Submodel Repository/Submodel/Get Submodel - ContactInfo.bru rename to source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Repository/Submodel/Get Submodel - ContactInfo.bru diff --git a/apiCollection/Submodel Repository/Submodel/Get Submodel - Nameplate.bru b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Repository/Submodel/Get Submodel - Nameplate.bru similarity index 100% rename from apiCollection/Submodel Repository/Submodel/Get Submodel - Nameplate.bru rename to source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Repository/Submodel/Get Submodel - Nameplate.bru diff --git a/apiCollection/Submodel Repository/Submodel/Get Submodel - Reliability.bru b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Repository/Submodel/Get Submodel - Reliability.bru similarity index 100% rename from apiCollection/Submodel Repository/Submodel/Get Submodel - Reliability.bru rename to source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Repository/Submodel/Get Submodel - Reliability.bru diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Repository/Submodel/folder.bru b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Repository/Submodel/folder.bru new file mode 100644 index 0000000..03deb2e --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Repository/Submodel/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Submodel + seq: 1 +} + +auth { + mode: inherit +} diff --git a/apiCollection/Submodel Repository/folder.bru b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Repository/folder.bru similarity index 100% rename from apiCollection/Submodel Repository/folder.bru rename to source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/Submodel Repository/folder.bru diff --git a/apiCollection/bruno.json b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/bruno.json similarity index 100% rename from apiCollection/bruno.json rename to source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/bruno.json diff --git a/apiCollection/collection.bru b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/collection.bru similarity index 100% rename from apiCollection/collection.bru rename to source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/collection.bru diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/environments/local.bru b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/environments/local.bru new file mode 100644 index 0000000..daae894 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/environments/local.bru @@ -0,0 +1,3 @@ +vars { + DataEngineBaseUrl: http://localhost:8085 +} diff --git a/apiCollection/environments/local.bru b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/environments/local_net.bru similarity index 100% rename from apiCollection/environments/local.bru rename to source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/environments/local_net.bru diff --git a/apiCollection/health.bru b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/health.bru similarity index 100% rename from apiCollection/health.bru rename to source/AAS.TwinEngine.Plugin.TestPlugin/Example/apiCollection/health.bru diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Example/basyx/aas-env.properties b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/basyx/aas-env.properties new file mode 100644 index 0000000..2c3b05d --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/basyx/aas-env.properties @@ -0,0 +1,16 @@ +server.port=8081 +basyx.environment=file:aas +basyx.cors.allowed-origins=* +basyx.cors.allowed-methods=GET,POST,PATCH,DELETE,PUT,OPTIONS,HEAD +basyx.aasrepository.feature.registryintegration=http://aas-template-registry:8080 +basyx.submodelrepository.feature.registryintegration=http://sm-template-regisry:8080 +spring.servlet.multipart.max-file-size=128MB +spring.servlet.multipart.max-request-size=128MB +basyx.backend=MongoDB +spring.data.mongodb.host=mongo +spring.data.mongodb.port=27017 +spring.data.mongodb.database=aasenvironment +spring.data.mongodb.authentication-database=admin +spring.data.mongodb.username=mongoAdmin +spring.data.mongodb.password=mongoPassword + diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Example/docker-compose.yml b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/docker-compose.yml new file mode 100644 index 0000000..a4a0ae1 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/docker-compose.yml @@ -0,0 +1,183 @@ +networks: + dataengine-network: + name: dataengine-network + +services: + nginx: + image: nginx:latest + container_name: nginx + ports: + - "8080:80" + volumes: + - ./nginx/html:/usr/share/nginx/html + - ./nginx/default.conf.template:/etc/nginx/templates/default.conf.template + restart: always + depends_on: + aas-template-registry: + condition: service_healthy + sm-template-regisry: + condition: service_healthy + template-repository: + condition: service_healthy + networks: + - dataengine-network + + twinengine-dataengine: + build: + context: ../../ + dockerfile: ./AAS.TwinEngine.DataEngine/Dockerfile + image: twinengine-dataengine:latest + ports: + - '8085:8080' + container_name: twinengine-dataengine + depends_on: + - mongo + restart: always + environment: + - AasEnvironment__DataEngineRepositoryBaseUrl=http://localhost:8080 + - AasEnvironment__AasEnvironmentRepositoryBaseUrl=http://template-repository:8081 + - AasEnvironment__SubModelRepositoryPath=submodels + - AasEnvironment__AasRegistryBaseUrl=http://aas-template-registry:8080 + - AasEnvironment__AasRegistryPath=shell-descriptors + - AasEnvironment__SubModelRegistryBaseUrl=http://sm-template-regisry:8080 + - AasEnvironment__SubModelRegistryPath=submodel-descriptors + - AasEnvironment__AasRepositoryPath=shells + - AasEnvironment__SubmodelRefPath=submodel-refs + - AasEnvironment__CustomerDomainUrl=https://mm-software.com + - TemplateMappingRules__SubmodelTemplateMappings__0__templateId=https://admin-shell.io/idta/SubmodelTemplate/Reliability/1/0 + - TemplateMappingRules__SubmodelTemplateMappings__0__pattern__0=Reliability + - TemplateMappingRules__SubmodelTemplateMappings__1__templateId=https://admin-shell.io/idta/SubmodelTemplate/DigitalNameplate/3/0 + - TemplateMappingRules__SubmodelTemplateMappings__1__pattern__0=Nameplate + - TemplateMappingRules__SubmodelTemplateMappings__2__templateId=https://admin-shell.io/idta/SubmodelTemplate/ContactInformation/1/0 + - TemplateMappingRules__SubmodelTemplateMappings__2__pattern__0=ContactInformation + - TemplateMappingRules__ShellTemplateMappings__0__templateId=https://mm-software.com/aas/aasTemplate + - TemplateMappingRules__ShellTemplateMappings__0__pattern__0= + - TemplateMappingRules__AasIdExtractionRules__0__pattern=Regex + - TemplateMappingRules__AasIdExtractionRules__0__index=6 + - TemplateMappingRules__AasIdExtractionRules__0__separator=/ + - Semantics__MultiLanguageSemanticPostfixSeparator=_ + - Semantics__SubmodelElementIndexContextPrefix=_aastwinengineindex_ + - Semantics__InternalSemanticId=InternalSemanticId + - AasxOptions__RootFolder=aasx + - AasRegistryPreComputed__ShellDescriptorCron=0 */3 * * * * + - AasRegistryPreComputed__IsPreComputed=false + - MultiPluginConflictOption__HandlingMode=TakeFirst + - PluginConfig__Plugins__0__PluginName=Plugin + - PluginConfig__Plugins__0__PluginUrl=http://twinengine-plugin:8080 + networks: + - dataengine-network + + twinengine-plugin: + build: + context: ../../ + dockerfile: ./AAS.TwinEngine.Plugin.TestPlugin/Dockerfile + image: twinengine-plugin:latest + ports: + - '8086:8080' + container_name: twinengine-plugin + restart: always + volumes: + - ./plugin/mock-metadata.json:/App/Data/mock-metadata.json + - ./plugin/mock-submodel-data.json:/App/Data/mock-submodel-data.json + environment: + - ASPNETCORE_ENVIRONMENT=Production + - Semantics__IndexContextPrefix=_aastwinengineindex_ + - Capabilities__HasShellDescriptor=true + - Capabilities__HasAssetInformation=true + networks: + - dataengine-network + + template-repository: + image: eclipsebasyx/aas-environment:2.0.0-SNAPSHOT + container_name: template-repository + ports: + - "8081:8081" + volumes: + - ./aas:/application/aas + - ./basyx/aas-env.properties:/application/application.properties + environment: + BASYX_EXTERNALURL : http://localhost:8080 + restart: always + depends_on: + aas-template-registry: + condition: service_healthy + sm-template-regisry: + condition: service_healthy + networks: + - dataengine-network + + aas-template-registry: + image: eclipsebasyx/aas-registry-log-mongodb:2.0.0-SNAPSHOT + container_name: aas-template-registry + ports: + - '8082:8080' + restart: always + depends_on: + - mongo + environment: + - SPRING_DATA_MONGODB_URI=mongodb://mongoAdmin:mongoPassword@mongo:27017 + networks: + - dataengine-network + + sm-template-regisry: + image: eclipsebasyx/submodel-registry-log-mongodb:2.0.0-SNAPSHOT + container_name: sm-template-regisry + ports: + - '8083:8080' + depends_on: + - mongo + environment: + - SPRING_DATA_MONGODB_URI=mongodb://mongoAdmin:mongoPassword@mongo:27017 + networks: + - dataengine-network + + shell-template-creator: + image: curlimages/curl:latest + container_name: shell-template-creator + depends_on: + template-repository: + condition: service_healthy + entrypoint: > + sh -c ' + echo "Waiting for template-repository to be ready..."; + sleep 10; + curl -X POST http://template-repository:8081/shells \ + -H "accept: application/json" \ + -H "Content-Type: application/json" \ + -d "{\"id\": \"https://mm-software.com/aas/aasTemplate\", \"assetInformation\": {\"assetKind\": \"Instance\"}, \"submodels\": [{\"type\": \"ModelReference\", \"keys\": [{\"type\": \"Submodel\", \"value\": \"Nameplate\"}]}, {\"type\": \"ModelReference\", \"keys\": [{\"type\": \"Submodel\", \"value\": \"ContactInformation\"}]}, {\"type\": \"ModelReference\", \"keys\": [{\"type\": \"Submodel\", \"value\": \"Reliability\"}]}], \"modelType\": \"AssetAdministrationShell\"}"; + echo "Shell creation request sent."; + ' + networks: + - dataengine-network + + aas-web-ui: + image: eclipsebasyx/aas-gui:SNAPSHOT + container_name: aas-ui + volumes: + - ./logo:/usr/src/app/dist/Logo + environment: + AAS_REGISTRY_PATH: http://localhost:8080/shell-descriptors + SUBMODEL_REGISTRY_PATH: http://localhost:8080/submodel-descriptors + AAS_REPO_PATH: http://localhost:8080/shells + SUBMODEL_REPO_PATH: http://localhost:8080/submodels + CD_REPO_PATH: http://localhost:8080/concept-descriptions + BASE_PATH: "/aas-ui" + LOGO_PATH: "MM_Logo.svg" + PRIMARY_DARK_COLOR: "#EBECED" + PRIMARY_LIGHT_COLOR: "#00F2E5" + restart: always + depends_on: + template-repository: + condition: service_healthy + networks: + - dataengine-network + + mongo: + image: mongo:6.0 + container_name: mongo + environment: + MONGO_INITDB_ROOT_USERNAME: mongoAdmin + MONGO_INITDB_ROOT_PASSWORD: mongoPassword + networks: + - dataengine-network + diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Example/logo/MM_Logo.svg b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/logo/MM_Logo.svg new file mode 100644 index 0000000..35fe73f --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/logo/MM_Logo.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Example/nginx/default.conf.template b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/nginx/default.conf.template new file mode 100644 index 0000000..7663fd9 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/nginx/default.conf.template @@ -0,0 +1,134 @@ +server { + listen 80; + server_name _; + + # Serve static files + location /fileprovider/ { + # Allow only GET and OPTIONS requests, deny all other methods + limit_except GET OPTIONS { + deny all; + } + root /usr/share/nginx/html; + } + + location /aas-ui/ { + proxy_pass http://aas-web-ui:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Authorization $http_authorization; + proxy_set_header Accept $http_accept; + proxy_set_header Content-Type $http_content_type; + } + + + location /shell-descriptors { + limit_except GET OPTIONS { + deny all; + } + proxy_pass http://twinengine-dataengine:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /shells { + # Allow only GET and OPTIONS requests, deny all other methods + limit_except GET OPTIONS { + deny all; + } + proxy_pass http://twinengine-dataengine:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + #Change to TwinEngine when implemented + location /submodels { + # Allow only GET and OPTIONS requests, deny all other methods + limit_except GET OPTIONS { + deny all; + } + proxy_pass http://twinengine-dataengine:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /concept-descriptions/ { + # Allow only GET and OPTIONS requests, deny all other methods + limit_except GET OPTIONS { + deny all; + } + proxy_pass http://template-repository:8081; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /concept-descriptions { + # Allow only GET and OPTIONS requests, deny all other methods + limit_except GET OPTIONS { + deny all; + } + proxy_pass http://template-repository:8081; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /submodel-descriptors/ { + # Allow only GET and OPTIONS requests, deny all other methods + limit_except GET OPTIONS { + deny all; + } + proxy_pass http://twinengine-dataengine:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + } + + location /submodel-descriptors { + # Allow only GET and OPTIONS requests, deny all other methods + limit_except GET OPTIONS { + deny all; + } + proxy_pass http://twinengine-dataengine:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /serialization { + # Allow only GET and OPTIONS requests, deny all other methods + limit_except GET OPTIONS { + deny all; + } + proxy_pass http://twinengine-dataengine:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /description { + # Allow only GET and OPTIONS requests, deny all other methods + limit_except GET OPTIONS { + deny all; + } + proxy_pass http://sm-template-regisry:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} \ No newline at end of file diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Example/plugin/mock-metadata.json b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/plugin/mock-metadata.json new file mode 100644 index 0000000..4d15e7b --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/plugin/mock-metadata.json @@ -0,0 +1,38 @@ +[ + { + "globalAssetId": "https://mm-software.com/ids/assets/000-001", + "idShort": "M&M01", + "id": "https://mm-software.com/ids/aas/000-001", + "specificAssetIds": [], + "assetInformationData": { + "defaultThumbnail": { + "contentType": "image/jpeg", + "path": "https://cdn.pixabay.com/photo/2023/06/06/16/10/computer-8045026_1280.jpg" + } + } + }, + { + "globalAssetId": "https://mm-software.com/ids/assets/000-002", + "idShort": "M&M02", + "id": "https://mm-software.com/ids/aas/000-002", + "specificAssetIds": [], + "assetInformationData": { + "defaultThumbnail": { + "contentType": "image/jpeg", + "path": "https://cdn.pixabay.com/photo/2023/07/08/07/25/ai-generated-8113887_1280.jpg" + } + } + }, + { + "globalAssetId": "https://mm-software.com/ids/assets/000-003", + "idShort": "M&M03", + "id": "https://mm-software.com/ids/aas/000-003", + "specificAssetIds": [], + "assetInformationData": { + "defaultThumbnail": { + "contentType": "image/jpeg", + "path": "https://images.pexels.com/photos/27934787/pexels-photo-27934787.jpeg" + } + } + } +] diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Example/plugin/mock-submodel-data.json b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/plugin/mock-submodel-data.json new file mode 100644 index 0000000..1a5cdf4 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Example/plugin/mock-submodel-data.json @@ -0,0 +1,656 @@ +{ + "https://mm-software.com/submodel/000-001/ContactInformation": { + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations": { + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation": [ + { + "0173-1#02-AAO204#003": "Manager001", + "0173-1#02-AAW001#001": { + "0173-1#02-AAW001#001_en": "Acme Corporation" + }, + "0173-1#02-AAO127#003": { + "0173-1#02-AAO127#003_de": "Marketing" + }, + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ + { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "234-567-8901" + }, + "0173-1#02-AAO137#003": "Office", + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" + } + }, + { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "345-678-9012" + }, + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Montags, 10-16 Uhr" + }, + "0173-1#02-AAO137#003": "Mobile" + } + ], + "0173-1#02-AAQ836#005": [ + { + "0173-1#02-AAO198#002": "john.doe@example.com", + "0173-1#02-AAO199#003": "Work" + } + ], + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/": [ + { + "https://mm-software.com/ipCommunication/AddressOfAdditionalLink": "http://example.com/more-info", + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication": "Chat", + "https://mm-software.com/IPCommunication/AvailableTime": { + "https://mm-software.com/IPCommunication/AvailableTime_de": "8:00 AM to 6:00 PM" + } + } + ], + "0173-1#02-AAO128#002": { + "0173-1#02-AAO128#002_de": "Bahnhofstraße 25" + }, + "0173-1#02-AAO129#002": { + "0173-1#02-AAO129#002_de": "80335" + }, + "0173-1#02-AAO133#002": { + "0173-1#02-AAO133#002_de": "Bayern" + }, + "0173-1#02-AAO205#002": { + "0173-1#02-AAO205#002_en": "John Doe" + }, + "https://mm-software.com/AddressOfAdditionalLink": "http://example.com/more-info" + }, + { + "0173-1#02-AAO204#003": "Marketing Manager", + "0173-1#02-AAW001#001": { + "0173-1#02-AAW001#001_en": "Acme Corporation" + }, + "0173-1#02-AAO127#003": { + "0173-1#02-AAO127#003_de": "Marketing" + }, + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ + { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "234-567-8901" + }, + "0173-1#02-AAO137#003": "Office", + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" + } + }, + { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "345-678-9012" + }, + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Montags, 10-16 Uhr" + }, + "0173-1#02-AAO137#003": "Mobile" + } + ], + "0173-1#02-AAQ836#005": [ + { + "0173-1#02-AAO198#002": "john.doe@example.com", + "0173-1#02-AAO199#003": "Work" + } + ], + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/": [ + { + "https://mm-software.com/ipCommunication/AddressOfAdditionalLink": "http://example.com/more-info", + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication": "Chat", + "https://mm-software.com/IPCommunication/AvailableTime": { + "https://mm-software.com/IPCommunication/AvailableTime_de": "8:00 AM to 6:00 PM" + } + } + ], + "0173-1#02-AAO128#002": { + "0173-1#02-AAO128#002_de": "Bahnhofstraße 25" + }, + "0173-1#02-AAO129#002": { + "0173-1#02-AAO129#002_de": "80335" + }, + "0173-1#02-AAO133#002": { + "0173-1#02-AAO133#002_de": "Bayern" + }, + "0173-1#02-AAO205#002": { + "0173-1#02-AAO205#002_en": "John Doe" + }, + "https://mm-software.com/AddressOfAdditionalLink": "http://example.com/more-info" + } + ] + } + }, + "https://mm-software.com/submodel/000-001/Nameplate": { + "https://admin-shell.io/idta/nameplate/3/0/Nameplate": { + "0112/2///61987#ABN590#002": "https://mm-software.com/Model-000/Serial-Nr-001", + "0112/2///61987#ABA565#009": { + "0112/2///61987#ABA565#009_de": "M&M" + }, + "0112/2///61987#ABP464#002": { + "0112/2///61987#ABP464#002_en": "M&MType ABC" + }, + "0112/2///61987#ABA581#007": "MM11-ABC22-001", + "0112/2///61987#ABB757#007": "2025-01-01", + "0112/2///61987#ABA567#009": { + "0112/2///61987#ABA567#009_en": "\u0022ABC-123\u0022" + }, + "0112/2///61987#ABA950#008": "FMABC1234", + "0112/2///61987#ABA951#009": "12345678", + "0112/2///61987#ABP000#002": "2022", + "0112/2///61360_7#AAS006#001": [ + { + "0112/2///61360_7#AAS009#001": [ + { + "0112/2///61987#ABA231#009": "0173-1#07-DAA603#004", + "0112/2///61987#ABH783#003": "KEMA99IECEX1105/128", + "0112/2///61987#ABO097#001": "2022-01-01", + "0112/2///61987#ABH830#002": "2022-01-01", + "0112/2///61987#ABO100#002": "file", + "0112/2///61987#ABB146#007": "0044" + }, + { + "0112/2///61987#ABA231#009": "0173-1#07-DAA603#004-2", + "0112/2///61987#ABH783#003": "KEMA99IECEX1105/128-2", + "0112/2///61987#ABO097#001": "2025-01-01", + "0112/2///61987#ABH830#002": "2025-01-01", + "0112/2///61987#ABO100#002": "file01", + "0112/2///61987#ABB146#007": "00100" + } + ] + } + ], + "0112/2///61987#ABP462#001": "DE", + "0112/2///61987#ABP463#001": "mm-logo", + "0173-1#02-ABI218#003/0173-1#01-AGZ672#004": [ + { + "https://mm-software.com/AssetSpecificProperties/ArbitraryMLP": { + "https://mm-software.com/AssetSpecificProperties/ArbitraryMLP_en": "M&MTestsample" + }, + "https://mm-software.com/AssetSpecificProperties/ArbitraryFile": "sampleFile", + "0173-1#02-ABI219#003/0173-1#01-AHD205#004": [ + { + "0173-1#01-AHD205#004": [ + { + "0173-1#02-AAO856#002": "", + "https://admin-shell.io/SMT/General/ArbitraryProp": "", + "https://mm-software.com/GuidelineSpecificProperties/ArbitraryFile": "TestFile", + "https://mm-software.com/GuidelineSpecificProperties/ArbitraryMLP": { + "https://mm-software.com/GuidelineSpecificProperties/ArbitraryMLP_en": "\u0022sample\u0022" + } + } + ] + } + ] + } + ] + } + }, + "https://mm-software.com/submodel/000-001/Reliability": { + "https://admin-shell.io/idta/iec62683/1/0/Reliability": { + "0112/2///62683#ACE006#001": "4", + "0112/2///62683#ACG071#001": [ + { + "0112/2///61987#ABA969#007": "DC", + "0112/2///61987#ABA588#004": "14.8", + "0112/2///61987#ABD461#004": "3.8", + "0112/2///61987#ABD462#004": "6.7", + "https://admin-shell.io/idta/FunctionalSafety/RatedOperationalCurrent/1/0": "78.9", + "0112/2///62683#ACE053#001": "TestType001", + "0112/2///62683#ACE070#001": "true", + "0112/2///62683#ACE055#001": "150", + "0112/2///62683#ACE054#001": "45" + } + ], + "0112/2///62683#ACG080#001": [ + { + "0112/2///62683#ACE061#001": "2", + "0112/2///62683#ACE062#001": "7", + "https://admin-shell.io/idta/Reliability/B10/1/0": "1" + } + ] + } + }, + "https://mm-software.com/submodel/000-002/ContactInformation": { + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations": { + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation": [ + { + "0173-1#02-AAO204#003": "Manager001", + "0173-1#02-AAW001#001": { + "0173-1#02-AAW001#001_en": "Acme Corporation" + }, + "0173-1#02-AAO127#003": { + "0173-1#02-AAO127#003_de": "Marketing" + }, + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ + { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "234-567-8901" + }, + "0173-1#02-AAO137#003": "Office", + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" + } + }, + { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "345-678-9012" + }, + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Montags, 10-16 Uhr" + }, + "0173-1#02-AAO137#003": "Mobile" + } + ], + "0173-1#02-AAQ836#005": [ + { + "0173-1#02-AAO198#002": "john.doe@example.com", + "0173-1#02-AAO199#003": "Work" + } + ], + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/": [ + { + "https://mm-software.com/ipCommunication/AddressOfAdditionalLink": "http://example.com/more-info", + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication": "Chat", + "https://mm-software.com/IPCommunication/AvailableTime": { + "https://mm-software.com/IPCommunication/AvailableTime_de": "8:00 AM to 6:00 PM" + } + } + ], + "0173-1#02-AAO128#002": { + "0173-1#02-AAO128#002_de": "Bahnhofstraße 25" + }, + "0173-1#02-AAO129#002": { + "0173-1#02-AAO129#002_de": "80335" + }, + "0173-1#02-AAO133#002": { + "0173-1#02-AAO133#002_de": "Bayern" + }, + "0173-1#02-AAO205#002": { + "0173-1#02-AAO205#002_en": "John Doe" + }, + "https://mm-software.com/AddressOfAdditionalLink": "http://example.com/more-info" + }, + { + "0173-1#02-AAO204#003": "Assistant Manager", + "0173-1#02-AAW001#001": { + "0173-1#02-AAW001#001_en": "Beta Corporation" + }, + "0173-1#02-AAO127#003": { + "0173-1#02-AAO127#003_de": "Vertrieb" + }, + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ + { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "123-456-7890" + }, + "0173-1#02-AAO137#003": "assistant", + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" + } + } + ], + "0173-1#02-AAQ836#005": [ + { + "0173-1#02-AAO198#002": "jane.doe@betacorp.com", + "0173-1#02-AAO199#003": "Work" + } + ], + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/": [ + { + "https://mm-software.com/ipCommunication/AddressOfAdditionalLink": "http://example.com/more-info2", + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication": "Email", + "https://mm-software.com/IPCommunication/AvailableTime": { + "https://mm-software.com/IPCommunication/AvailableTime_de": "9:00 AM to 5:00 PM" + } + } + ], + "0173-1#02-AAO128#002": { + "0173-1#02-AAO128#002_de": "Ringstraße 2" + }, + "0173-1#02-AAO129#002": { + "0173-1#02-AAO129#002_de": "50667" + }, + "0173-1#02-AAO133#002": { + "0173-1#02-AAO133#002_de": "Nordrhein-Westfalen" + }, + "0173-1#02-AAO205#002": { + "0173-1#02-AAO205#002_en": "Jane Doe" + }, + "https://mm-software.com/AddressOfAdditionalLink": "http://example.com/info2" + } + ] + } + }, + "https://mm-software.com/submodel/000-002/Nameplate": { + "https://admin-shell.io/idta/nameplate/3/0/Nameplate": { + "0112/2///61987#ABA567#009": { + "0112/2///61987#ABA567#009_en": "\u0022ABC-123\u0022" + }, + "0112/2///61987#ABN590#002": "https://mm-software.com/Model-000/Serial-Nr-002", + "0112/2///61987#ABA565#009": { + "0112/2///61987#ABA565#009_de": "M&M" + }, + "0112/2///61987#ABP464#002": { + "0112/2///61987#ABP464#002_en": "M&MType ABC002" + }, + "0112/2///61987#ABA581#007": "MM11-ABC22-002", + "0112/2///61987#ABB757#007": "2024-01-01", + "0112/2///61987#ABA950#008": "FMABC1234", + "0112/2///61987#ABA951#009": "12345678", + "0112/2///61987#ABP000#002": "2022", + "0112/2///61360_7#AAS006#001": [ + { + "0112/2///61360_7#AAS009#001": [ + { + "0112/2///61987#ABA231#009": "0173-1#07-DAA603#004", + "0112/2///61987#ABH783#003": "KEMA99IECEX1105/128", + "0112/2///61987#ABO097#001": "2022-01-01", + "0112/2///61987#ABH830#002": "2022-01-01", + "0112/2///61987#ABO100#002": "file", + "0112/2///61987#ABB146#007": "0044" + } + ] + } + ], + "0112/2///61987#ABP462#001": "DE", + "0112/2///61987#ABP463#001": "mm-logo", + "0173-1#02-ABI218#003/0173-1#01-AGZ672#004": [ + { + "https://mm-software.com/AssetSpecificProperties/ArbitraryMLP": { + "https://mm-software.com/AssetSpecificProperties/ArbitraryMLP_en": "M&MTestsample" + }, + "https://mm-software.com/AssetSpecificProperties/ArbitraryFile": "sampleFile", + "0173-1#02-ABI219#003/0173-1#01-AHD205#004": [ + { + "0173-1#01-AHD205#004": [ + { + "0173-1#02-AAO856#002": "", + "https://admin-shell.io/SMT/General/ArbitraryProp": "", + "https://mm-software.com/GuidelineSpecificProperties/ArbitraryFile": "TestFile", + "https://mm-software.com/GuidelineSpecificProperties/ArbitraryMLP": { + "https://mm-software.com/GuidelineSpecificProperties/ArbitraryMLP_en": "\u0022sample\u0022" + } + } + ] + } + ] + } + ] + } + }, + "https://mm-software.com/submodel/000-002/Reliability": { + "https://admin-shell.io/idta/iec62683/1/0/Reliability": { + "0112/2///62683#ACE006#001": "4", + "0112/2///62683#ACG071#001": [ + { + "0112/2///61987#ABA969#007": "DC", + "0112/2///61987#ABA588#004": "14.8", + "0112/2///61987#ABD461#004": "3.8", + "0112/2///61987#ABD462#004": "6.7", + "https://admin-shell.io/idta/FunctionalSafety/RatedOperationalCurrent/1/0": "78.9", + "0112/2///62683#ACE053#001": "TestType002", + "0112/2///62683#ACE070#001": "true", + "0112/2///62683#ACE055#001": "150", + "0112/2///62683#ACE054#001": "45" + } + ], + "0112/2///62683#ACG080#001": [ + { + "0112/2///62683#ACE061#001": "2", + "0112/2///62683#ACE062#001": "7", + "https://admin-shell.io/idta/Reliability/B10/1/0": "1" + } + ] + } + }, + "https://mm-software.com/submodel/000-003/ContactInformation": { + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations": { + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation": [ + { + "0173-1#02-AAO204#003": "Product Manager0003", + "0173-1#02-AAW001#001": { + "0173-1#02-AAW001#001_en": "Acme Corporation" + }, + "0173-1#02-AAO127#003": { + "0173-1#02-AAO127#003_de": "Marketing" + }, + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ + { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "234-567-8901" + }, + "0173-1#02-AAO137#003": "Office", + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" + } + }, + { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "345-678-9012" + }, + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Montags, 10-16 Uhr" + }, + "0173-1#02-AAO137#003": "Mobile" + } + ], + "0173-1#02-AAQ836#005": [ + { + "0173-1#02-AAO198#002": "john.doe@example.com", + "0173-1#02-AAO199#003": "Work" + } + ], + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/": [ + { + "https://mm-software.com/ipCommunication/AddressOfAdditionalLink": "http://example.com/more-info", + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication": "Chat", + "https://mm-software.com/IPCommunication/AvailableTime": { + "https://mm-software.com/IPCommunication/AvailableTime_de": "8:00 AM to 6:00 PM" + } + } + ], + "0173-1#02-AAO128#002": { + "0173-1#02-AAO128#002_de": "Bahnhofstraße 25" + }, + "0173-1#02-AAO129#002": { + "0173-1#02-AAO129#002_de": "80335" + }, + "0173-1#02-AAO133#002": { + "0173-1#02-AAO133#002_de": "Bayern" + }, + "0173-1#02-AAO205#002": { + "0173-1#02-AAO205#002_en": "John Doe" + }, + "https://mm-software.com/AddressOfAdditionalLink": "http://example.com/more-info" + }, + { + "0173-1#02-AAO204#003": "Manager", + "0173-1#02-AAW001#001": { + "0173-1#02-AAW001#001_en": "Acme Corporation" + }, + "0173-1#02-AAO127#003": { + "0173-1#02-AAO127#003_de": "Marketing" + }, + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ + { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "234-567-8901" + }, + "0173-1#02-AAO137#003": "Office", + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" + } + }, + { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "345-678-9012" + }, + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Montags, 10-16 Uhr" + }, + "0173-1#02-AAO137#003": "Mobile" + } + ], + "0173-1#02-AAQ836#005": [ + { + "0173-1#02-AAO198#002": "john.doe@example.com", + "0173-1#02-AAO199#003": "Work" + } + ], + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/": [ + { + "https://mm-software.com/ipCommunication/AddressOfAdditionalLink": "http://example.com/more-info", + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication": "Chat", + "https://mm-software.com/IPCommunication/AvailableTime": { + "https://mm-software.com/IPCommunication/AvailableTime_de": "8:00 AM to 6:00 PM" + } + } + ], + "0173-1#02-AAO128#002": { + "0173-1#02-AAO128#002_de": "Bahnhofstraße 25" + }, + "0173-1#02-AAO129#002": { + "0173-1#02-AAO129#002_de": "80335" + }, + "0173-1#02-AAO133#002": { + "0173-1#02-AAO133#002_de": "Bayern" + }, + "0173-1#02-AAO205#002": { + "0173-1#02-AAO205#002_en": "John Doe" + }, + "https://mm-software.com/AddressOfAdditionalLink": "http://example.com/more-info" + }, + { + "0173-1#02-AAO204#003": "Assistant Manager", + "0173-1#02-AAW001#001": { + "0173-1#02-AAW001#001_en": "Beta Corporation" + }, + "0173-1#02-AAO127#003": { + "0173-1#02-AAO127#003_de": "Vertrieb" + }, + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/Phone": [ + { + "0173-1#02-AAO136#002": { + "0173-1#02-AAO136#002_en": "123-456-7890" + }, + "0173-1#02-AAO137#003": "assistant", + "https://mm-software.com/Phone/AvailableTime": { + "https://mm-software.com/Phone/AvailableTime_de": "Dienstags bis Donnerstags, 9-17 Uhr" + } + } + ], + "0173-1#02-AAQ836#005": [ + { + "0173-1#02-AAO198#002": "jane.doe@betacorp.com", + "0173-1#02-AAO199#003": "Work" + } + ], + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/": [ + { + "https://mm-software.com/ipCommunication/AddressOfAdditionalLink": "http://example.com/more-info2", + "https://admin-shell.io/zvei/nameplate/1/0/ContactInformations/ContactInformation/IPCommunication/TypeOfCommunication": "Email", + "https://mm-software.com/IPCommunication/AvailableTime": { + "https://mm-software.com/IPCommunication/AvailableTime_de": "9:00 AM to 5:00 PM" + } + } + ], + "0173-1#02-AAO128#002": { + "0173-1#02-AAO128#002_de": "Ringstraße 2" + }, + "0173-1#02-AAO129#002": { + "0173-1#02-AAO129#002_de": "50667" + }, + "0173-1#02-AAO133#002": { + "0173-1#02-AAO133#002_de": "Nordrhein-Westfalen" + }, + "0173-1#02-AAO205#002": { + "0173-1#02-AAO205#002_en": "Jane Doe" + }, + "https://mm-software.com/AddressOfAdditionalLink": "http://example.com/info2" + } + ] + } + }, + "https://mm-software.com/submodel/000-003/Nameplate": { + "https://admin-shell.io/idta/nameplate/3/0/Nameplate": { + "0112/2///61987#ABA567#009": { + "0112/2///61987#ABA567#009_en": "\u0022ABC-123\u0022" + }, + "0112/2///61987#ABN590#002": "https://mm-software.com/Model-000/Serial-Nr-003", + "0112/2///61987#ABA565#009": { + "0112/2///61987#ABA565#009_de": "M&M" + }, + "0112/2///61987#ABP464#002": { + "0112/2///61987#ABP464#002_en": "M&MType ABC003" + }, + "0112/2///61987#ABA581#007": "MM11-ABC22-003", + "0112/2///61987#ABB757#007": "2025-01-01", + "0112/2///61360_7#AAS006#001": [ + { + "0112/2///61360_7#AAS009#001": [ + { + "0112/2///61987#ABA231#009": "0173-1#07-DAA603#004", + "0112/2///61987#ABH783#003": "KEMA99IECEX1105/128", + "0112/2///61987#ABO097#001": "2022-01-01", + "0112/2///61987#ABH830#002": "2022-01-01", + "0112/2///61987#ABO100#002": "file", + "0112/2///61987#ABB146#007": "0044" + } + ] + } + ], + "0112/2///61987#ABA950#008": "FMABC1234", + "0112/2///61987#ABA951#009": "12345678", + "0112/2///61987#ABP000#002": "2022", + "0112/2///61987#ABB757#007": "2022-01-01", + "0112/2///61987#ABP462#001": "DE", + "0112/2///61987#ABP463#001": "mm-logo", + "0173-1#02-ABI218#003/0173-1#01-AGZ672#004": [ + { + "https://mm-software.com/AssetSpecificProperties/ArbitraryMLP": { + "https://mm-software.com/AssetSpecificProperties/ArbitraryMLP_en": "\u0022sample\u0022" + }, + "https://mm-software.com/AssetSpecificProperties/ArbitraryFile": "sampleFile", + "0173-1#02-ABI219#003/0173-1#01-AHD205#004": [ + { + "0173-1#01-AHD205#004": [ + { + "0173-1#02-AAO856#002": "", + "https://admin-shell.io/SMT/General/ArbitraryProp": "", + "https://mm-software.com/GuidelineSpecificProperties/ArbitraryFile": "TestFile", + "https://mm-software.com/GuidelineSpecificProperties/ArbitraryMLP": { + "https://mm-software.com/GuidelineSpecificProperties/ArbitraryMLP_en": "\u0022sample\u0022" + } + } + ] + } + ] + } + ] + } + }, + "https://mm-software.com/submodel/000-003/Reliability": { + "https://admin-shell.io/idta/iec62683/1/0/Reliability": { + "0112/2///62683#ACE006#001": "4", + "0112/2///62683#ACG071#001": [ + { + "0112/2///61987#ABA969#007": "DC", + "0112/2///61987#ABA588#004": "14.8", + "0112/2///61987#ABD461#004": "3.8", + "0112/2///61987#ABD462#004": "6.7", + "https://admin-shell.io/idta/FunctionalSafety/RatedOperationalCurrent/1/0": "78.9", + "0112/2///62683#ACE053#001": "TestType003", + "0112/2///62683#ACE070#001": "true", + "0112/2///62683#ACE055#001": "150", + "0112/2///62683#ACE054#001": "45" + } + ], + "0112/2///62683#ACG080#001": [ + { + "0112/2///62683#ACE061#001": "2", + "0112/2///62683#ACE062#001": "7", + "https://admin-shell.io/idta/Reliability/B10/1/0": "1" + } + ] + } + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/DataAccess/Entity/MetaDataEntity.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/DataAccess/Entity/MetaDataEntity.cs new file mode 100644 index 0000000..872b04d --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/DataAccess/Entity/MetaDataEntity.cs @@ -0,0 +1,48 @@ +using System.Text.Json.Serialization; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.DataAccess.Entity; + +public class MetaDataEntity +{ + [JsonPropertyName("globalAssetId")] + public string GlobalAssetId { get; set; } = string.Empty; + + [JsonPropertyName("idShort")] + public string IdShort { get; set; } = string.Empty; + + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("specificAssetIds")] + public List? SpecificAssetIds { get; set; } + + [JsonPropertyName("assetInformationData")] + public AssetInformationDataEntity? AssetInformationData { get; set; } +} + +public class SpecificAssetIdEntity +{ + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("value")] + public string Value { get; set; } = string.Empty; +} + +public class AssetInformationDataEntity +{ + [JsonPropertyName("globalAssetId")] + public string? GlobalAssetId { get; set; } + + [JsonPropertyName("defaultThumbnail")] + public DefaultThumbnailDataEntity? DefaultThumbnail { get; set; } +} + +public class DefaultThumbnailDataEntity +{ + [JsonPropertyName("path")] + public string? Path { get; set; } + + [JsonPropertyName("contentType")] + public string? ContentType { get; set; } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/DataAccess/MapperProfiles/AssetMappingProfile.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/DataAccess/MapperProfiles/AssetMappingProfile.cs new file mode 100644 index 0000000..682f97b --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/DataAccess/MapperProfiles/AssetMappingProfile.cs @@ -0,0 +1,23 @@ +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.MetaData; +using AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.DataAccess.Entity; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.DataAccess.MapperProfiles; + +public static class AssetMappingProfile +{ + public static AssetData ToDomainModel(this AssetInformationDataEntity entity, string globalAssetId, List? specificAssetIds) + { + return new AssetData + { + GlobalAssetId = globalAssetId, + SpecificAssetIds = specificAssetIds, + DefaultThumbnail = entity?.DefaultThumbnail == null + ? null + : new DefaultThumbnailData + { + ContentType = entity.DefaultThumbnail.ContentType, + Path = entity.DefaultThumbnail.Path + } + }; + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/DataAccess/MapperProfiles/ShellDescriptorMappingProfile.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/DataAccess/MapperProfiles/ShellDescriptorMappingProfile.cs new file mode 100644 index 0000000..001e525 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/DataAccess/MapperProfiles/ShellDescriptorMappingProfile.cs @@ -0,0 +1,25 @@ +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.MetaData; +using AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.DataAccess.Entity; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.DataAccess.MapperProfiles; + +public static class ShellDescriptorMappingProfile +{ + public static ShellDescriptorData MapToDomainModel(this MetaDataEntity entity) + { + return new ShellDescriptorData + { + Id = entity.Id, + GlobalAssetId = entity.GlobalAssetId, + IdShort = entity.IdShort, + SpecificAssetIds = entity.SpecificAssetIds?.Select(s => new SpecificAssetIdsData + { + Name = s.Name, + Value = s.Value + }).ToList() ?? [] + }; + } + + public static IList ToDomainModelList(this List dataList) + => dataList?.Select(MapToDomainModel).ToList() ?? []; +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/ManifestProvider/Config/Capabilities.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/ManifestProvider/Config/Capabilities.cs new file mode 100644 index 0000000..6ac2978 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/ManifestProvider/Config/Capabilities.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.Providers.Config; + +public class Capabilities +{ + public const string Section = "Capabilities"; + + [Required] + public bool HasShellDescriptor { get; set; } + + [Required] + public bool HasAssetInformation { get; set; } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/ManifestProvider/Helper/JsonConverter.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/ManifestProvider/Helper/JsonConverter.cs new file mode 100644 index 0000000..d3d77ed --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/ManifestProvider/Helper/JsonConverter.cs @@ -0,0 +1,147 @@ +using System.Text.Json; + +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.Submodel; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.Providers.ManifestProvider.Helper; + +public static class JsonConverter +{ + public static SemanticTreeNode ParseJson(JsonDocument doc) + { + ArgumentNullException.ThrowIfNull(doc); + + var root = doc.RootElement; + + if (root.ValueKind != JsonValueKind.String) + { + return ConvertJsonElement(root); + } + + var jsonString = root.GetString(); + using var nestedDoc = JsonDocument.Parse(jsonString!); + return ConvertJsonElement(nestedDoc.RootElement); + } + + private static SemanticTreeNode ConvertJsonElement(JsonElement element) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + { + var properties = element.EnumerateObject(); + var propertyCount = properties.Count(); + + switch (propertyCount) + { + case 0: + return new SemanticBranchNode(string.Empty, GetDataType(element)); + case 1: + { + var rootProperty = properties.First(); + var rootBranch = new SemanticBranchNode(rootProperty.Name, GetDataType(rootProperty.Value)); + ProcessJsonValue(rootProperty.Value, rootBranch); + return rootBranch; + } + } + + var root = new SemanticBranchNode(string.Empty, GetDataType(element)); + ProcessJsonObject(element, root); + return root; + } + case JsonValueKind.Array: + { + var syntheticRoot = new SemanticBranchNode(string.Empty, GetDataType(element)); + ProcessJsonArray(element, syntheticRoot); + return syntheticRoot; + } + default: + return new SemanticLeafNode(string.Empty, GetDataType(element), element.ToString()); + } + } + + private static void ProcessJsonValue(JsonElement valueElement, SemanticBranchNode parentBranch) + { + switch (valueElement.ValueKind) + { + case JsonValueKind.Object: + ProcessJsonObject(valueElement, parentBranch); + break; + + case JsonValueKind.Array: + ProcessJsonArray(valueElement, parentBranch); + break; + + default: + parentBranch.AddChild(new SemanticLeafNode( + parentBranch.SemanticId, + GetDataType(valueElement), + valueElement.ToString() + )); + break; + } + } + + private static void ProcessJsonObject(JsonElement objectElement, SemanticBranchNode parentBranch) + { + foreach (var property in objectElement.EnumerateObject()) + { + if (IsPrimitiveValue(property.Value)) + { + parentBranch.AddChild(new SemanticLeafNode( + property.Name, + GetDataType(property.Value), + property.Value.ToString() + )); + } + else if (property.Value.ValueKind == JsonValueKind.Array) + { + var baseSemanticId = property.Name; + ProcessJsonArray(property.Value, parentBranch, baseSemanticId); + } + else + { + var branch = new SemanticBranchNode(property.Name, GetDataType(property.Value)); + ProcessJsonValue(property.Value, branch); + parentBranch.AddChild(branch); + } + } + } + + private static void ProcessJsonArray(JsonElement arrayElement, SemanticBranchNode parentBranch, string? baseSemanticId = null) + { + foreach (var item in arrayElement.EnumerateArray()) + { + var semanticId = baseSemanticId ?? parentBranch.SemanticId; + var arrayItemBranch = new SemanticBranchNode(semanticId, GetDataType(item)); + ProcessJsonValue(item, arrayItemBranch); + parentBranch.AddChild(arrayItemBranch); + } + } + + private static bool IsPrimitiveValue(JsonElement element) + { + return element.ValueKind switch + { + JsonValueKind.String or + JsonValueKind.Number or + JsonValueKind.True or + JsonValueKind.False or + JsonValueKind.Null => true, + _ => false + }; + } + + private static DataType GetDataType(JsonElement element) => + element.ValueKind switch + { + JsonValueKind.Object => DataType.Object, + JsonValueKind.Array => DataType.Array, + JsonValueKind.String => DataType.String, + JsonValueKind.Number => DataType.Number, + JsonValueKind.True => DataType.Boolean, + JsonValueKind.False => DataType.Boolean, + JsonValueKind.Null => DataType.Null, + JsonValueKind.Undefined => DataType.Unknown, + _ => DataType.Unknown + }; +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/ManifestProvider/ManifestProvider.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/ManifestProvider/ManifestProvider.cs new file mode 100644 index 0000000..40965be --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/ManifestProvider/ManifestProvider.cs @@ -0,0 +1,60 @@ +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.Manifest; +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.Manifest; +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.Submodel; +using AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.Providers.Config; +using AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.Providers.ManifestProvider.Helper; + +using Microsoft.Extensions.Options; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.Providers.ManifestProvider; + +public class ManifestProvider( + ILogger logger, + IOptions capabilities) : IManifestProvider +{ + private readonly bool _hasShellDescriptor = capabilities.Value.HasShellDescriptor; + private readonly bool _hasAssetInformation = capabilities.Value.HasAssetInformation; + + public ManifestData GetManifestData() + { + logger.LogInformation("Starting getting manifest data"); + + var semanticTreeNode = JsonConverter.ParseJson(MockData.SubmodelData); + + var supportedSemanticIds = GetLeafSemanticIds(semanticTreeNode); + + var manifestData = new ManifestData + { + SupportedSemanticIds = supportedSemanticIds, + Capabilities = new CapabilitiesData { HasAssetInformation = _hasAssetInformation, HasShellDescriptor = _hasShellDescriptor } + }; + return manifestData; + } + + private static IList GetLeafSemanticIds(SemanticTreeNode node) + { + var semanticIds = new List(); + + CollectLeafSemanticIds(node, semanticIds); + return semanticIds.Distinct(StringComparer.Ordinal).ToList(); + } + + private static void CollectLeafSemanticIds(SemanticTreeNode node, List supportedSemanticId) + { + if (node is SemanticLeafNode leaf) + { + supportedSemanticId.Add(leaf.SemanticId); + return; + } + + if (node is not SemanticBranchNode branch) + { + return; + } + + foreach (var child in branch.Children) + { + CollectLeafSemanticIds(child, supportedSemanticId); + } + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/MetaDataProvider/Helper/Paginator.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/MetaDataProvider/Helper/Paginator.cs new file mode 100644 index 0000000..f6faec2 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/MetaDataProvider/Helper/Paginator.cs @@ -0,0 +1,39 @@ +using AAS.TwinEngine.Plugin.TestPlugin.Common.Extensions; +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.MetaData; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.Providers.MetaDataProvider.Helper; + +public static class Paginator +{ + public static (IList Items, PagingMetaData PagingMetaData) GetPagedResult( + IList allItems, + Func getId, + int? limit, + string? cursor) + { + var startIndex = 0; + if (!string.IsNullOrEmpty(cursor)) + { + var lastId = cursor.DecodeBase64(); + startIndex = allItems.ToList().FindIndex(item => getId(item) == lastId) + 1; + } + + var pageSize = limit ?? 100; + var pagedItems = allItems.Skip(startIndex).Take(pageSize).ToList(); + + string? nextCursor = null; + + if (limit == null && cursor == null && pagedItems.Count < pageSize) + { + return (pagedItems, new PagingMetaData { Cursor = nextCursor }); + } + + var lastItem = pagedItems.LastOrDefault(); + if (lastItem != null) + { + nextCursor = getId(lastItem).EncodeToBase64(); + } + + return (pagedItems, new PagingMetaData { Cursor = nextCursor }); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/MetaDataProvider/MetaDataProvider.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/MetaDataProvider/MetaDataProvider.cs new file mode 100644 index 0000000..4ccaf89 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/MetaDataProvider/MetaDataProvider.cs @@ -0,0 +1,99 @@ +using System.Text.Json; + +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Constants; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Exceptions; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.MetaData; +using AAS.TwinEngine.Plugin.TestPlugin.Common.Extensions; +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.MetaData; +using AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.DataAccess.Entity; +using AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.DataAccess.MapperProfiles; +using AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.Providers.MetaDataProvider.Helper; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.Providers.MetaDataProvider; + +public class MetaDataProvider : IMetaDataProvider +{ + private readonly ILogger _logger; + private readonly Dictionary _shellDescriptorLookup = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _assetLookup = new(StringComparer.OrdinalIgnoreCase); + private readonly JsonSerializerOptions _jsonSerializerOptions = new() { PropertyNameCaseInsensitive = true }; + + public MetaDataProvider(ILogger logger) + { + _logger = logger; + BuildDictionaries(MockData.MetaData); + } + + private void BuildDictionaries(JsonDocument jsonData) + { + var entities = jsonData.Deserialize>(_jsonSerializerOptions) ?? []; + + foreach (var entity in entities.Where(e => !string.IsNullOrEmpty(e.Id))) + { + _shellDescriptorLookup[entity.Id] = entity; + + if (entity.AssetInformationData is not null) + { + _assetLookup[entity.Id] = MapToDomainModel(entity); + } + } + + _logger.LogInformation("Loaded {Count} shell-descriptors entries", _shellDescriptorLookup.Count); + _logger.LogInformation("Loaded {Count} asset entries", _assetLookup.Count); + } + + private static AssetData MapToDomainModel(MetaDataEntity entity) + { + return entity.AssetInformationData!.ToDomainModel( + entity.GlobalAssetId, + entity.SpecificAssetIds?.Select(x => new SpecificAssetIdsData + { + Name = x.Name, + Value = x.Value + }).ToList() + ); + } + + public Task GetShellDescriptorsAsync(int? limit, string? cursor, CancellationToken cancellationToken) + { + var domainModels = _shellDescriptorLookup.Values.ToList(); + + var shellDescriptors = domainModels.ToDomainModelList(); + + if (cursor == null || _shellDescriptorLookup.TryGetValue(cursor.DecodeBase64(), out _)) + { + var (pagedItems, pagingMeta) = Paginator.GetPagedResult(shellDescriptors, s => s.Id!, limit, cursor); + + return Task.FromResult(new ShellDescriptorsData() + { + PagingMetaData = pagingMeta, + Result = pagedItems + }); + } + + _logger.LogWarning("Invalid cursor provided."); + throw new NotFoundException(ExceptionMessages.ShellDescriptorDataNotFound); + } + + public Task GetShellDescriptorAsync(string aasIdentifier, CancellationToken cancellationToken) + { + if (_shellDescriptorLookup.TryGetValue(aasIdentifier, out var entity)) + { + return Task.FromResult(entity.MapToDomainModel()); + } + + _logger.LogWarning("Shell-descriptors not found for ID: {AasIdentifier}", aasIdentifier); + throw new NotFoundException(ExceptionMessages.ShellDescriptorDataNotFound); + } + + public Task GetAssetAsync(string shellIdentifier, CancellationToken cancellationToken) + { + if (_assetLookup.TryGetValue(shellIdentifier, out var assetInformation)) + { + return Task.FromResult(assetInformation); + } + + _logger.LogWarning("Asset not found for ID: {ShellIdentifier}", shellIdentifier); + throw new NotFoundException(ExceptionMessages.AssetNotFound); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/MockData.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/MockData.cs new file mode 100644 index 0000000..fa844b8 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/MockData.cs @@ -0,0 +1,9 @@ +using System.Text.Json; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.Providers; + +public record MockData +{ + public static JsonDocument MetaData { get; set; } + public static JsonDocument SubmodelData { get; set; } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/MockDataInitializer.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/MockDataInitializer.cs new file mode 100644 index 0000000..6229031 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/MockDataInitializer.cs @@ -0,0 +1,45 @@ +using System.Text.Json; + +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Constants; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Exceptions; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.Providers; + +public class MockDataInitializer(IHostEnvironment env, ILogger logger) +{ + public void Initialize(CancellationToken cancellationToken) + { + var dataPath = Path.Combine(env.ContentRootPath, "Data"); + + MockData.MetaData = LoadData(Path.Combine(dataPath, "mock-metadata.json")); + + MockData.SubmodelData = LoadData(Path.Combine(dataPath, "mock-submodel-data.json")); + } + + private JsonDocument LoadData(string filePath) + { + if (!File.Exists(filePath)) + { + logger.LogCritical("data file not found at {FilePath}", filePath); + throw new FileNotFoundException("JSON data file not found.", filePath); + } + + try + { + using var fileStream = File.OpenRead(filePath); + using var streamReader = new StreamReader(fileStream); + var jsonContent = streamReader.ReadToEnd(); + return JsonDocument.Parse(jsonContent); + } + catch (JsonException jex) + { + logger.LogError(jex, "Invalid JSON in file {FilePath}", filePath); + throw new InternalServerException(ExceptionMessages.ResourceNotValid); + } + catch (Exception ex) + { + logger.LogError(ex, "Error reading data file {FilePath}", filePath); + throw new InternalServerException(ExceptionMessages.ResourceNotValid); + } + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/SubmodelProviders/Helper/JsonNodeInfo.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/SubmodelProviders/Helper/JsonNodeInfo.cs new file mode 100644 index 0000000..07636c4 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/SubmodelProviders/Helper/JsonNodeInfo.cs @@ -0,0 +1,10 @@ +using System.Text.Json; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.Providers.SubmodelProviders.Helper; + +public class JsonNodeInfo +{ + public JsonValueKind Kind { get; set; } + public int? ArrayLength { get; set; } + public string? StringValue { get; set; } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/SubmodelProviders/SubmodelProvider.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/SubmodelProviders/SubmodelProvider.cs new file mode 100644 index 0000000..75ee251 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Infrastructure/Providers/SubmodelProviders/SubmodelProvider.cs @@ -0,0 +1,274 @@ +using System.Text.Json; + +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Exceptions; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.Submodel; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.Submodel.Config; +using AAS.TwinEngine.Plugin.TestPlugin.DomainModel.Submodel; +using AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.Providers.SubmodelProviders.Helper; + +using Microsoft.Extensions.Options; + +namespace AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.Providers.SubmodelProviders; + +public class SubmodelProvider : ISubmodelProvider +{ + private readonly ILogger _logger; + private readonly string _submodelElementIndexContextPrefix; + private Dictionary> _nodeInfoDictionaries; + + public SubmodelProvider( + ILogger logger, + IOptions semantics) + { + _logger = logger; + _submodelElementIndexContextPrefix = semantics.Value.IndexContextPrefix; + _nodeInfoDictionaries = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + PrecomputeDictionaries(); + } + + private void PrecomputeDictionaries() + { + var rootElement = MockData.SubmodelData.RootElement; + _nodeInfoDictionaries = rootElement + .EnumerateObject() + .Where(p => p.Value.ValueKind == JsonValueKind.Object) + .ToDictionary(keySelector: p => p.Name, + elementSelector: BuildSubmodelDictionary, + comparer: StringComparer.OrdinalIgnoreCase); + } + + private static Dictionary BuildSubmodelDictionary(JsonProperty product) + { + var semanticDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var property in product.Value.EnumerateObject()) + { + BuildDictionaries(property.Value, property.Name, semanticDictionary); + } + + return semanticDictionary; + } + + private static void BuildDictionaries(JsonElement element, string currentPath, Dictionary dictionary) + { + var nodeInfo = new JsonNodeInfo + { + Kind = element.ValueKind + }; + + switch (element.ValueKind) + { + case JsonValueKind.Object: + foreach (var property in element.EnumerateObject()) + { + var newPath = string.IsNullOrEmpty(currentPath) + ? property.Name + : $"{currentPath}.{property.Name}"; + BuildDictionaries(property.Value, newPath, dictionary); + } + + break; + + case JsonValueKind.Array: + nodeInfo.ArrayLength = element.GetArrayLength(); + for (var i = 0; i < element.GetArrayLength(); i++) + { + var newPath = $"{currentPath}.{i}"; + BuildDictionaries(element[i], newPath, dictionary); + } + + break; + + case JsonValueKind.String: + nodeInfo.StringValue = element.GetString()!; + break; + } + + dictionary[currentPath] = nodeInfo; + } + + public SemanticTreeNode EnrichWithData(SemanticTreeNode semanticTreeNode, string submodelId) + { + _logger.LogInformation("Starting semantic tree enrichment for submodelId {SubmodelId}", submodelId); + + if (!_nodeInfoDictionaries.TryGetValue(submodelId, out var nodeInfoDictionary)) + { + _logger.LogError("Submodel identifier {SubmodelId} not found", submodelId); + throw new NotFoundException(); + } + + var result = FillData(semanticTreeNode, [], nodeInfoDictionary); + RemoveIndexPrefix(result); + + _logger.LogInformation("Completed tree enrichment"); + + return result; + } + + private SemanticTreeNode FillData(SemanticTreeNode node, List semanticPath, Dictionary nodeInfoDictionary) + { + var newSemanticPath = new List(semanticPath) { node.SemanticId }; + + switch (node) + { + case SemanticLeafNode leaf: + return HandleLeafNode(leaf, newSemanticPath, nodeInfoDictionary); + case SemanticBranchNode branch: + return HandleBranchNode(branch, newSemanticPath, nodeInfoDictionary); + default: + _logger.LogError("Unknown node type: {Type}", node.GetType().Name); + throw new ArgumentException("Invalid node type"); + } + } + + private string CreateDictionaryKey(List semanticPath) + { + var keyParts = new List(); + + foreach (var id in semanticPath) + { + if (id.Contains(_submodelElementIndexContextPrefix, StringComparison.Ordinal)) + { + var parts = id.Split(_submodelElementIndexContextPrefix); + if (parts.Length == 2 && int.TryParse(parts[1], out var index)) + { + keyParts.Add(parts[0]); + keyParts.Add(index.ToString()); + continue; + } + } + + keyParts.Add(id); + } + + return string.Join('.', keyParts); + } + + private SemanticTreeNode HandleBranchNode(SemanticBranchNode branch, List semanticPath, Dictionary nodeInfoDictionary) + { + var dictKey = CreateDictionaryKey(semanticPath); + + if (!nodeInfoDictionary.TryGetValue(dictKey, out var nodeInfo)) + { + _logger.LogWarning("Path not found in structure: {Path}", dictKey); + throw new NotFoundException($"Value Not found or given Element : {dictKey.LastOrDefault()}"); + } + + switch (nodeInfo.Kind) + { + case JsonValueKind.Array: + var clones = CreateArrayNode(branch, dictKey, nodeInfo.ArrayLength, semanticPath, nodeInfoDictionary); + branch.ReplaceChildren(clones); + break; + case JsonValueKind.Object: + ProcessChildNodes(branch, semanticPath, nodeInfoDictionary); + break; + default: + _logger.LogWarning("Unexpected JSON type {Type} for branch", nodeInfo.Kind); + break; + } + + return branch; + } + + private void ProcessChildNodes(SemanticBranchNode branch, List semanticPath, Dictionary nodeInfoDictionary) + { + var newChildren = new List(); + + foreach (var child in branch.Children) + { + var childPath = new List(semanticPath) { child.SemanticId }; + var childKey = CreateDictionaryKey(childPath); + + if (nodeInfoDictionary.TryGetValue(childKey, out var childInfo) && + childInfo.Kind == JsonValueKind.Array && + child is SemanticBranchNode childBranch) + { + newChildren.AddRange(CreateArrayNode(childBranch, childKey, childInfo.ArrayLength, semanticPath, nodeInfoDictionary)); + } + else + { + var nodes = FillData(child, semanticPath, nodeInfoDictionary); + newChildren.Add(nodes); + } + } + + branch.ReplaceChildren(newChildren); + } + + private List CreateArrayNode(SemanticBranchNode node, string dictKey, int? arrayLength, List parentPath, Dictionary nodeInfoDictionary) + { + if (arrayLength is not > 0) + { + _logger.LogWarning("Invalid array length for {Key}", dictKey); + return []; + } + + var clones = new List(); + for (var i = 0; i < arrayLength.Value; i++) + { + var clone = CloneNode(node) as SemanticBranchNode; + clone!.SemanticId = $"{node.SemanticId}{_submodelElementIndexContextPrefix}{i:D2}"; + + var processedClone = FillData(clone, parentPath, nodeInfoDictionary); + clones.Add(processedClone); + } + + return clones; + } + + private SemanticTreeNode HandleLeafNode(SemanticLeafNode leaf, List semanticPath, Dictionary nodeInfoDictionary) + { + var dictKey = CreateDictionaryKey(semanticPath); + + leaf.Value = nodeInfoDictionary.TryGetValue(dictKey, out var nodeInfo) && nodeInfo.StringValue != null + ? nodeInfo.StringValue + : string.Empty; + return leaf; + } + + private static SemanticTreeNode CloneNode(SemanticTreeNode node) + { + switch (node) + { + case SemanticLeafNode leaf: + return new SemanticLeafNode(leaf.SemanticId, leaf.DataType, leaf.Value); + case SemanticBranchNode branch: + { + var cloned = new SemanticBranchNode(branch.SemanticId, branch.DataType); + foreach (var child in branch.Children) + { + var clonedChild = CloneNode(child); + cloned.AddChild(clonedChild); + } + + return cloned; + } + default: + throw new InvalidOperationException("Unknown node type"); + } + } + + private void RemoveIndexPrefix(SemanticTreeNode node) + { + var id = node.SemanticId; + var idx = id.IndexOf(_submodelElementIndexContextPrefix, StringComparison.Ordinal); + if (idx >= 0) + { + node.SemanticId = id[..idx]; + } + + if (node is not SemanticBranchNode branch) + { + return; + } + + foreach (var child in from child in branch.Children + where node is SemanticBranchNode + select child) + { + RemoveIndexPrefix(child); + } + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Program.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Program.cs new file mode 100644 index 0000000..fa8b263 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Program.cs @@ -0,0 +1,59 @@ +using AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.Providers; +using AAS.TwinEngine.Plugin.TestPlugin.ServiceConfiguration; + +using Asp.Versioning; + +namespace AAS.TwinEngine.Plugin.TestPlugin; + +public static class Program +{ + private static readonly Version ApiVersion = new(1, 0); + private const string ApiTitle = "TestPlugin API"; + + public static async Task Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + builder.ConfigureLogging(builder.Configuration); + + builder.Services.AddHttpContextAccessor(); + builder.Services.ConfigureInfrastructure(builder.Configuration); + builder.Services.ConfigureApplication(builder.Configuration); + builder.Services.AddAuthorization(); + + builder.Services.AddControllers(); + + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddOpenApiDocument(settings => + { + settings.DocumentName = ApiVersion.ToString(); + settings.Title = ApiTitle; + }); + + builder.Services.AddApiVersioning(options => + { + options.DefaultApiVersion = new ApiVersion(ApiVersion.Major, ApiVersion.Minor); + options.AssumeDefaultVersionWhenUnspecified = true; + options.ReportApiVersions = true; + options.ApiVersionReader = new HeaderApiVersionReader("api-version"); + }) + .AddMvc(); + + var app = builder.Build(); + + using (var scope = app.Services.CreateScope()) + { + var initializer = scope.ServiceProvider.GetRequiredService(); + initializer.Initialize(CancellationToken.None); + } + + app.UseExceptionHandler(); + app.UseHttpsRedirection(); + app.UseAuthorization(); + app.UseOpenApi(c => c.PostProcess = (d, _) => d.Servers.Clear()); + app.UseSwaggerUI(c => c.SwaggerEndpoint($"/swagger/{ApiVersion:F1}/swagger.json", ApiTitle)); + app.MapControllers(); + + await app.RunAsync(); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Properties/launchSettings.json b/source/AAS.TwinEngine.Plugin.TestPlugin/Properties/launchSettings.json new file mode 100644 index 0000000..10725d6 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Properties/launchSettings.json @@ -0,0 +1,39 @@ +{ + "profiles": { + "AAS.TwinEngine.Plugin.TestPlugin": { + "commandName": "Project", + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:5057" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Docker": { + "commandName": "Docker", + "launchBrowser": false, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", + "environmentVariables": { + "ASPNETCORE_HTTP_PORTS": "8080" + }, + "publishAllPorts": true, + "useSSL": false + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:13390", + "sslPort": 0 + } + } +} \ No newline at end of file diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/ServiceConfiguration/ApplicationDependencyInjectionExtensions.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/ServiceConfiguration/ApplicationDependencyInjectionExtensions.cs new file mode 100644 index 0000000..c4bf7cb --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/ServiceConfiguration/ApplicationDependencyInjectionExtensions.cs @@ -0,0 +1,33 @@ +using AAS.TwinEngine.Plugin.TestPlugin.Api.Manifest.Handler; +using AAS.TwinEngine.Plugin.TestPlugin.Api.MetaData.Handler; +using AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel.Handler; +using AAS.TwinEngine.Plugin.TestPlugin.Api.Submodel.Services; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Exceptions; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.Manifest; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.MetaData; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.Submodel; + +using FluentValidation; +namespace AAS.TwinEngine.Plugin.TestPlugin.ServiceConfiguration; + +public static class ApplicationDependencyInjectionExtensions +{ + public static void ConfigureApplication(this IServiceCollection services, IConfiguration configuration) + { + services.AddValidatorsFromAssembly(typeof(ApplicationDependencyInjectionExtensions).Assembly); + services.AddExceptionHandler(); + services.AddProblemDetails(); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/ServiceConfiguration/Config/OpenTelemetrySettings.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/ServiceConfiguration/Config/OpenTelemetrySettings.cs new file mode 100644 index 0000000..badf4b9 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/ServiceConfiguration/Config/OpenTelemetrySettings.cs @@ -0,0 +1,9 @@ +namespace AAS.TwinEngine.Plugin.TestPlugin.ServiceConfiguration.Config; + +public class OpenTelemetrySettings +{ + public const string Section = "OpenTelemetry"; + public string OtlpEndpoint { get; set; } = "http://localhost:4317"; + public string ServiceName { get; set; } = "TestPlugin"; + public string ServiceVersion { get; set; } = "1.0.0"; +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs new file mode 100644 index 0000000..63a326b --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs @@ -0,0 +1,24 @@ +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.Manifest; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.MetaData; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.Submodel; +using AAS.TwinEngine.Plugin.TestPlugin.ApplicationLogic.Services.Submodel.Config; +using AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.Providers; +using AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.Providers.Config; +using AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.Providers.ManifestProvider; +using AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.Providers.MetaDataProvider; +using AAS.TwinEngine.Plugin.TestPlugin.Infrastructure.Providers.SubmodelProviders; + +namespace AAS.TwinEngine.Plugin.TestPlugin.ServiceConfiguration; + +public static class InfrastructureDependencyInjectionExtensions +{ + public static void ConfigureInfrastructure(this IServiceCollection services, IConfiguration configuration) + { + services.AddOptions().Bind(configuration.GetSection(Semantics.Section)).ValidateDataAnnotations().ValidateOnStart(); + services.AddOptions().Bind(configuration.GetSection(Capabilities.Section)).ValidateDataAnnotations().ValidateOnStart(); + services.AddScoped(); + services.AddSingleton(); + services.AddSingleton(); + services.AddScoped(); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/ServiceConfiguration/LoggingConfigurationExtension.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/ServiceConfiguration/LoggingConfigurationExtension.cs new file mode 100644 index 0000000..eea58b0 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/ServiceConfiguration/LoggingConfigurationExtension.cs @@ -0,0 +1,65 @@ +using System.Diagnostics.CodeAnalysis; + +using AAS.TwinEngine.Plugin.TestPlugin.ServiceConfiguration.Config; + +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; + +using Serilog; +using Serilog.Core; +using Serilog.Events; + +namespace AAS.TwinEngine.Plugin.TestPlugin.ServiceConfiguration; + +[ExcludeFromCodeCoverage] +internal static class LoggingConfigurationExtension +{ + public static void ConfigureLogging(this WebApplicationBuilder builder, IConfiguration configuration) + { + var otelSettings = configuration.GetSection(OpenTelemetrySettings.Section).Get() ?? new OpenTelemetrySettings(); + var logLevelSwitch = new LoggingLevelSwitch(LogEventLevel.Information); + + _ = builder.Services.AddSingleton(logLevelSwitch); + + _ = builder.Host.UseSerilog((context, loggerConfig) => + { + loggerConfig + .ReadFrom.Configuration(context.Configuration) + .Enrich.FromLogContext() + .MinimumLevel.ControlledBy(logLevelSwitch); + }, writeToProviders: true); + + _ = builder.Logging.ClearProviders(); + + _ = builder.Logging.AddOpenTelemetry(options => + { + options.IncludeScopes = true; + options.IncludeFormattedMessage = true; + options.ParseStateValues = true; + _ = options.AddOtlpExporter(otlp => otlp.Endpoint = new Uri(otelSettings.OtlpEndpoint)); + }); + + _ = builder.Services.AddOpenTelemetry() + .ConfigureResource(resourceConfig => resourceConfig + .AddService( + serviceName: otelSettings.ServiceName, + serviceVersion: otelSettings.ServiceVersion, + serviceInstanceId: Environment.MachineName)) + .WithTracing(tracerProvider => + { + _ = tracerProvider + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddOtlpExporter(otlp => otlp.Endpoint = new Uri(otelSettings.OtlpEndpoint)); + }) + .WithMetrics(metricsProvider => + { + _ = metricsProvider + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddOtlpExporter(otlp => otlp.Endpoint = new Uri(otelSettings.OtlpEndpoint)); + }); + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/appsettings.Development.json b/source/AAS.TwinEngine.Plugin.TestPlugin/appsettings.Development.json new file mode 100644 index 0000000..f678d69 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/appsettings.Development.json @@ -0,0 +1,56 @@ +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Semantics": { + "IndexContextPrefix": "_aastwinengineindex_" + }, + "Capabilities": { + "HasShellDescriptor": true, + "HasAssetInformation": true + }, + "AllowedHosts": "*", + "OpenTelemetry": { + "OtlpEndpoint": "http://localhost:4317", + "ServiceName": "TestPlugin", + "ServiceVersion": "1.0.0." + }, + "Serilog": { + "Using": [ + "Serilog.Sinks.Console" + ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "System": "Error" + } + }, + "WriteTo": [ + { + "Name": "Console", + "Args": { + "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console", + "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level}] : {Message} (CorrelationId: {CorrelationId}) (User: {User}) (SourceContext: {SourceContext}) {Details}{NewLine}{Exception}", + "restrictedToMinimumLevel": "Verbose" + } + }, + { + "Name": "File", + "Args": { + "path": "logs/TestPlugin-.log", + "rollingInterval": "Day", + "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] {Message}{NewLine}{Exception}" + } + } + ], + "Enrich": [ "FromLogContext", "WithThreadId" ], + "Properties": { + "Application": "TestPlugin" + } + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/appsettings.json b/source/AAS.TwinEngine.Plugin.TestPlugin/appsettings.json new file mode 100644 index 0000000..c042643 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/appsettings.json @@ -0,0 +1,55 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Semantics": { + "IndexContextPrefix": "" + }, + "Capabilities": { + "HasShellDescriptor": true, + "HasAssetInformation": true + }, + "AllowedHosts": "*", + "OpenTelemetry": { + "OtlpEndpoint": "http://localhost:4317", + "ServiceName": "TestPlugin", + "ServiceVersion": "1.0.0." + }, + "Serilog": { + "Using": [ + "Serilog.Sinks.Console" + ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "System": "Error" + } + }, + "WriteTo": [ + { + "Name": "Console", + "Args": { + "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console", + "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level}] : {Message} (CorrelationId: {CorrelationId}) (User: {User}) (SourceContext: {SourceContext}) {Details}{NewLine}{Exception}", + "restrictedToMinimumLevel": "Verbose" + } + }, + { + "Name": "File", + "Args": { + "path": "logs/TestPlugin-.log", + "rollingInterval": "Day", + "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] {Message}{NewLine}{Exception}" + } + } + ], + "Enrich": [ "FromLogContext", "WithThreadId" ], + "Properties": { + "Application": "DataEngine" + } + } +} diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/config.nswag b/source/AAS.TwinEngine.Plugin.TestPlugin/config.nswag new file mode 100644 index 0000000..bba8432 --- /dev/null +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/config.nswag @@ -0,0 +1,64 @@ +{ + "runtime": "Net80", + "defaultVariables": "Output=v1.json,Configuration=Debug,Version=1.0", + "documentGenerator": { + "aspNetCoreToOpenApi": { + "project": "AAS.TwinEngine.Plugin.TestPlugin.csproj", + "msBuildProjectExtensionsPath": null, + "configuration": "$(Configuration)", + "runtime": null, + "targetFramework": null, + "noBuild": true, + "msBuildOutputPath": null, + "verbose": true, + "workingDirectory": null, + "requireParametersWithoutDefault": true, + "defaultPropertyNameHandling": "Default", + "defaultReferenceTypeNullHandling": "Null", + "defaultDictionaryValueReferenceTypeNullHandling": "NotNull", + "defaultResponseReferenceTypeNullHandling": "NotNull", + "generateOriginalParameterNames": true, + "defaultEnumHandling": "Integer", + "flattenInheritanceHierarchy": false, + "generateKnownTypes": true, + "generateEnumMappingDescription": false, + "generateXmlObjects": false, + "generateAbstractProperties": false, + "generateAbstractSchemas": true, + "generateResponseTypes": true, + "ignoreObsoleteProperties": false, + "allowReferencesWithProperties": false, + "useXmlDocumentation": true, + "resolveExternalXmlDocumentation": true, + "excludedTypeNames": [], + "serviceHost": null, + "serviceBasePath": null, + "serviceSchemes": [], + "infoTitle": "$(Version)", + "infoDescription": null, + "infoVersion": "$(Version)", + "documentTemplate": null, + "documentProcessorTypes": [], + "operationProcessorTypes": [], + "typeNameGeneratorType": null, + "schemaNameGeneratorType": null, + "contractResolverType": null, + "serializerSettingsType": null, + "useDocumentProvider": true, + "documentName": "$(Version)", + "aspNetCoreEnvironment": null, + "createWebHostBuilderMethod": null, + "startupType": null, + "allowNullableBodyParameters": true, + "useHttpAttributeNameAsOperationId": false, + "output": "$(Output)", + "outputType": "OpenApi3", + "newLineBehavior": "Auto", + "assemblyPaths": [], + "assemblyConfig": null, + "referencePaths": [], + "useNuGetCache": false + } + }, + "codeGenerators": {} +}