From 029ec926042cb4781a2d91fa932d6b26ed5c2e46 Mon Sep 17 00:00:00 2001 From: Marco Volpini Date: Tue, 31 Mar 2026 15:14:31 +0200 Subject: [PATCH 1/2] aas support --- README.md | 262 +++++++++++--- .../extraction/test-db-extraction-1.json | 13 + .../extraction/test-db-extraction-2.json | 9 + .../extraction/test-rest-core-extraction.json | 20 ++ .../extractor/config/AasOntology.java | 38 ++ .../config/ExtractionConfiguration.java | 41 ++- .../extractor/config/field/AasFieldSpec.java | 75 ++++ .../strategies/AasExtractorStrategy.java | 324 ++++++++++++++++++ .../strategies/ExtractionStrategyType.java | 1 + .../strategies/SearchKeyExtractor.java | 12 + .../mocks/MockDPPMetadataRepository.java | 7 +- .../extractor/mocks/MockProducers.java | 7 +- .../strategies/SearchKeyExtractorTest.java | 17 + .../test/resources/example-dpp/dpp-aas.json | 211 ++++++++++++ .../extraction/db-test-core-extraction.json | 12 + .../extraction/fs-test-core-extraction.json | 13 + .../extraction/test-core-extraction.json | 109 +++--- .../extraction/db-2-test-core-extraction.json | 13 + .../extraction/db-test-core-extraction.json | 12 + .../extraction/db-2-test-core-extraction.json | 13 + .../extraction/db-test-core-extraction.json | 12 + .../main/resources/extraction/extraction.json | 83 +++++ 22 files changed, 1190 insertions(+), 114 deletions(-) create mode 100644 core/src/main/java/it/extrared/extractor/config/AasOntology.java create mode 100644 core/src/main/java/it/extrared/extractor/config/field/AasFieldSpec.java create mode 100644 core/src/main/java/it/extrared/extractor/strategies/AasExtractorStrategy.java create mode 100644 core/src/test/resources/example-dpp/dpp-aas.json diff --git a/README.md b/README.md index b68d140..b8e4b74 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # DPP Data Extractor -A service able to extract data from JSON, JSON-LD or RDF DPPs, and make them available in a storage to enable search over the decentralized repository. +A service able to extract data from JSON, JSON-LD, RDF or AAS (Asset Administration Shell) DPPs, and make them available in a storage to enable search over the decentralized repository. © CIRPASS-2 Consortium, 2024-2027 @@ -20,7 +20,7 @@ This application leverages the Mock EU Registry repository to retrieve the lates - **RESTful API** for capabilities exposure and extraction configuration management - **Flexible Extraction Configuration** with runtime customization support -- **Multi Format DPP support**: JSON, JSON-LD, RDF-XML and other RDF formats like Turtle, N3, N-Quads +- **Multi Format DPP support**: JSON, JSON-LD, RDF-XML, other RDF formats (Turtle, N3, N-Quads), and **AAS JSON** (Asset Administration Shell, IDTA metamodel v3) - **Multiple database backends** (PostgreSQL, MariaDB) - **OpenID Connect authentication** with role-based access control @@ -495,11 +495,22 @@ spec: ## Extraction Configuration -The application uses a JSON configuration to determine how to extract values from DPPs. The configuration is organized into four top-level sections: `searchFields`, `knownOntology`, `noOntology`, and `unknownOntology`. The three strategy sections are not mutually exclusive: the extractor determines which strategy to apply based on the format and semantic content of each incoming DPP document, and applies the corresponding section of the configuration. +The application uses a JSON configuration to determine how to extract values from DPPs. The configuration is organized into five top-level sections: `searchFields`, `knownOntology`, `noOntology`, `unknownOntology`, and `aasOntology`. The four strategy sections are not mutually exclusive: the extractor determines which strategy to apply based on the format and semantic content of each incoming DPP document, and applies the corresponding section of the configuration. + +### Strategy Selection + +The extractor inspects each incoming document and routes it to exactly one strategy according to the following priority order: + +1. **AAS JSON** — the document is a JSON root object whose `modelType` field equals one of `AssetAdministrationShellEnvironment`, `Submodel`, or `AssetAdministrationShell`. Detection is a single O(1) field lookup with no further inspection required. +2. **Known Ontology** — the document is a JSON-LD object whose `@context` contains a URI matching `extractor.dpp.reference-ontology.contexts`, or whose `@vocab` matches `extractor.dpp.reference-ontology.vocabularies`. +3. **Unknown Ontology** — the document is a JSON-LD object with a `@context` that does not match any reference ontology URI, but carries `@type` annotations that can anchor extraction. +4. **Plain JSON** — any other JSON document with no semantic layer. + +AAS detection takes precedence over JSON-LD detection because an AAS environment document may legally contain a `@context` in some serialisation profiles, which would otherwise trigger an incorrect JSON-LD route. ### `searchFields` -An array of objects that declares the complete set of fields the extractor will attempt to populate for each DPP. Every field name referenced in any of the three strategy sections must have a corresponding entry here. Fields not listed in `searchFields` are ignored even if matched by a strategy. +An array of objects that declares the complete set of fields the extractor will attempt to populate for each DPP. Every field name referenced in any of the strategy sections must have a corresponding entry here. Fields not listed in `searchFields` are ignored even if matched by a strategy. Each entry supports the following properties: @@ -593,6 +604,86 @@ The section contains a single `fields` object whose keys are `fieldName` values } ``` +### `aasOntology` + +Configures extraction from **Asset Administration Shell (AAS) JSON** documents compliant with the IDTA metamodel v3 (IEC 63278). This strategy navigates the `submodels → submodelElements` tree using two complementary matching mechanisms applied in priority order for each configured field. + +#### AAS Document Detection + +A document is identified as AAS JSON when its root `modelType` field is one of: + +| `modelType` value | Description | +|------------------------------------------|----------------------------------------------| +| `AssetAdministrationShellEnvironment` | Full AAS environment (multiple submodels) | +| `Submodel` | Standalone submodel serialisation | +| `AssetAdministrationShell` | AAS descriptor without submodel payload | + +Detection is a single field lookup and takes precedence over all JSON-LD routes. + +#### Matching Mechanisms + +For each configured field, the extractor applies the following mechanisms in order, stopping at the first successful match: + +1. **`semanticId` scan** (preferred) — performs a full-document scan collecting all `SubmodelElement` nodes and checks each node's `semanticId.keys[].value` against the configured value. Matching is suffix-insensitive to tolerate ECLASS version suffixes (e.g. `0173-1#02-AAO677` matches both `#002` and `#004`). This mechanism is ontology-stable and works regardless of how the producer has named the submodel or its elements. + +2. **`idShortPath` navigation** (structural fallback) — navigates the submodel tree top-down by matching `idShort` values at each level. The first path segment identifies the target submodel, and the remaining segments navigate `submodelElements` (or `value` arrays for `SubmodelElementList`). This mechanism is useful when `semanticId` is absent, non-standard, or when targeting proprietary submodels. + +When both `semanticId` and `idShortPath` are configured for the same field, `semanticId` is always attempted first. + +#### Value Extraction + +Once a matching `SubmodelElement` node is found, the extractor reads its `value` field according to the node's `modelType`: + +| `modelType` | Extraction behaviour | +|--------------------------|------------------------------------------------------------------------------------------------------------------------------| +| `Property` | Reads `value` directly as a scalar. | +| `MultiLanguageProperty` | Reads the `text` field from the language entry matching `preferLanguage`. Falls back to the first available language entry. | + +#### Field Specification + +The `aasOntology` section contains a single `fields` object whose keys are `fieldName` values from `searchFields`. Each field entry supports: + +| Property | Required | Description | +|------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `semanticId` | no* | The semantic ID value to match against `semanticId.keys[].value` of each `SubmodelElement`. Matching is suffix-insensitive: the configured value is matched if the element's semantic ID ends with it (case-insensitive), allowing version-suffix-agnostic configuration. | +| `idShortPath` | no* | Ordered list of `idShort` segments. The first segment identifies the target submodel (or a submodel idShort prefix); subsequent segments navigate the `submodelElements` hierarchy. `SubmodelElementList` children are traversed automatically. | +| `preferLanguage` | no | BCP 47 language tag used for `MultiLanguageProperty` value selection. Defaults to `"en"`. | + +*At least one of `semanticId` or `idShortPath` must be provided. + +#### Semantic ID References + +The table below lists the well-known ECLASS IRDIs and IDTA IRIs used in the default AAS configuration, along with the IDTA submodel template each one originates from. Version suffixes are intentionally omitted from configured values to ensure forward compatibility. + +| Field | Semantic ID (configured, no version suffix) | Source submodel template | +|----------------------|---------------------------------------------------------------------------------|---------------------------------------| +| `manufacturerName` | `0173-1#02-AAO677` | IDTA-02006 Digital Nameplate | +| `productName` | `0173-1#02-AAW338` | IDTA-02006 Digital Nameplate | +| `codeValue` | `https://admin-shell.io/ZVEI/TechnicalData/ProductClassId/1/1` | IDTA-02003 Technical Data | +| `codeSet` | `https://admin-shell.io/ZVEI/TechnicalData/ClassificationSystem/1/1` | IDTA-02003 Technical Data | +| `carbonFootprint` | `0173-1#02-ABG855` | IDTA-02023 Carbon Footprint | +| `carbonFootprintUom` | `0173-1#02-ABG856` | IDTA-02023 Carbon Footprint | +| `weight` | `0173-1#02-AAB419` | ECLASS (used in IDTA-02003) | +| `recyclingRate` | `0173-1#02-ABE994` | ECLASS / IDTA-02035 Circularity (draft) | +| `durability` | `0173-1#02-AAY812` | ECLASS | +| `energyConsumption` | `0173-1#02-AAI667` | ECLASS (used in IDTA-02053/54) | + +> **Note:** Semantic IDs for `recyclingRate`, `durability`, `energyConsumption`, and their companion unit fields are based on ECLASS IRDIs or early IDTA drafts. They will be updated when IDTA finalises the Circularity (IDTA-02035) and Energy Data Sheet (IDTA-02053/54) submodel templates. + +#### Example + +Extracting `manufacturerName` from an AAS Nameplate submodel using both mechanisms: + +```json +"manufacturerName": { + "semanticId": "0173-1#02-AAO677", + "idShortPath": ["Nameplate", "ManufacturerName"], + "preferLanguage": "en" +} +``` + +The extractor first scans all submodel elements for a `semanticId` ending in `0173-1#02-AAO677`. If found, it reads the `MultiLanguageProperty` value for language `"en"`. If the `semanticId` scan yields no result (e.g. the submodel omits semantic IDs), it falls back to navigating `submodels[idShort="Nameplate"] → submodelElements[idShort="ManufacturerName"]`. + ### Default Configuration The application ships with a default configuration that extracts the following fields: @@ -600,7 +691,8 @@ The application ships with a default configuration that extracts the following f - `productName` — product or model name - `codeValue` — product classification code (e.g. ECLASS, UNSPSC) - `codeSet` — URI of the classification scheme the code belongs to -- `environmentalFootprint` / `environmentalFootprintUom` — aggregated environmental footprint value and unit +- `manufacturerName` — legally registered manufacturer name +- `carbonFootprint` / `carbonFootprintUom` — carbon footprint value and unit - `recyclingRate` / `recyclingRateUom` — recycling rate value and unit - `energyConsumption` / `energyConsumptionUom` — energy consumption value and unit - `weight` / `weightUom` — product weight and unit @@ -901,8 +993,8 @@ The application ships with a default configuration that extracts the following f "weight": { "variants": ["weight", "weightG", "weightKg"], "variantsWithContext": { - "context": ["dimension", "dimensions", "physicalDimension","weight"], - "field": ["weight", "weightG", "weightKg"] + "context": ["dimension", "weight","dimensions", "physicalDimension"], + "field": ["weight", "weightG", "weightKg","numericalValue"] } }, "weightUom": { @@ -991,6 +1083,89 @@ The application ships with a default configuration that extracts the following f "typeHints": ["Durability", "ProductDurability", "LifeExpectancy"] } } + }, + "aasOntology": { + "fields": { + + "manufacturerName": { + "comment": "IDTA-02006 Nameplate. ECLASS 0173-1#02-AAO677 (ManufacturerName). Version suffix intentionally omitted to match #002, #003, #004.", + "semanticId": "0173-1#02-AAO677", + "idShortPath": ["Nameplate", "ManufacturerName"], + "preferLanguage": "en" + }, + + "productName": { + "comment": "IDTA-02006 Nameplate. ECLASS 0173-1#02-AAW338 (ManufacturerProductDesignation). Also try ManufacturerProductFamily as secondary path if empty.", + "semanticId": "0173-1#02-AAW338", + "idShortPath": ["Nameplate", "ManufacturerProductDesignation"], + "preferLanguage": "en" + }, + + "codeValue": { + "comment": "IDTA-02003 TechnicalData. admin-shell.io IRI for ProductClassId. Path targets first classification item. SML indexing (..Item00) is positional — first entry is the primary classification.", + "semanticId": "https://admin-shell.io/ZVEI/TechnicalData/ProductClassId/1/1", + "idShortPath": ["TechnicalData", "ProductClassifications", "ProductClassificationItem00", "ProductClassId"] + }, + + "codeSet": { + "comment": "IDTA-02003 TechnicalData. Paired with codeValue — same SML item, different property.", + "semanticId": "https://admin-shell.io/ZVEI/TechnicalData/ClassificationSystem/1/1", + "idShortPath": ["TechnicalData", "ProductClassifications", "ProductClassificationItem00", "ClassificationSystem"] + }, + + "carbonFootprint": { + "comment": "IDTA-02023 CarbonFootprint v1.0. ECLASS 0173-1#02-ABG855 (PcfCO2eq). Path descends into first ProductCarbonFootprint instance in the SML.", + "semanticId": "0173-1#02-ABG855", + "idShortPath": ["CarbonFootprint", "ProductCarbonFootprints", "ProductCarbonFootprint00", "PcfCO2eq"] + }, + + "carbonFootprintUom": { + "comment": "IDTA-02023. ECLASS 0173-1#02-ABG856 (ReferenceImpactUnitForCalculation). Quantity unit for the PCF value, e.g. 'piece', 'kg'.", + "semanticId": "0173-1#02-ABG856", + "idShortPath": ["CarbonFootprint", "ProductCarbonFootprints", "ProductCarbonFootprint00", "ReferenceImpactUnitForCalculation"] + }, + + "weight": { + "comment": "ECLASS 0173-1#02-AAB419 (net weight). Typically under TechnicalData/TechnicalProperties. No dedicated IDTA submodel template — idShortPath is a well-known convention.", + "semanticId": "0173-1#02-AAB419", + "idShortPath": ["TechnicalData", "TechnicalProperties", "NetWeight"] + }, + + "weightUom": { + "comment": "No standardized semanticId for unit companion properties in TechnicalData — units are encoded in the ECLASS ConceptDescription. idShortPath covers implementors who add an explicit unit property.", + "idShortPath": ["TechnicalData", "TechnicalProperties", "NetWeightUnit"] + }, + + "energyConsumption": { + "comment": "IDTA energy submodels (02054 Energy Consumption in Production, 02053 Energy Data Sheet) are under development / niche. ECLASS 0173-1#02-AAI667 covers nominal power. Fallback to common idShort conventions.", + "semanticId": "0173-1#02-AAI667", + "idShortPath": ["EnergyConsumption", "EnergyConsumptionDetails", "EnergyConsumptionValue"] + }, + + "energyConsumptionUom": { + "comment": "No stable IDTA standard semanticId for energy unit companion. idShortPath fallback only.", + "idShortPath": ["EnergyConsumption", "EnergyConsumptionDetails", "EnergyConsumptionUnit"] + }, + + "recyclingRate": { + "comment": "IDTA-02035-7 (Battery Passport Circularity) in development. No universal IRDI. Common convention used in sustainability submodels. Update when IDTA standardizes.", + "idShortPath": ["Circularity", "RecyclingInformation", "RecyclingRate"] + }, + + "recyclingRateUom": { + "idShortPath": ["Circularity", "RecyclingInformation", "RecyclingRateUnit"] + }, + + "durability": { + "comment": "No IDTA SMT yet. ECLASS 0173-1#02-AAY812 covers service life/durability in some releases — treat as advisory only. idShortPath based on ESPR-aligned DPP conventions.", + "semanticId": "0173-1#02-AAY812", + "idShortPath": ["Circularity", "DurabilityIndicators", "Durability"] + }, + + "durabilityUom": { + "idShortPath": ["Circularity", "DurabilityIndicators", "DurabilityUnit"] + } + } } } ``` @@ -1046,19 +1221,20 @@ GET /capabilities/v1 ```json [ - { "fieldName": "productName", "targetType": "STRING" }, - { "fieldName": "codeValue", "targetType": "STRING" }, - { "fieldName": "codeSet", "targetType": "STRING" }, - { "fieldName": "environmentalFootprint", "targetType": "DECIMAL" }, - { "fieldName": "environmentalFootprintUom", "targetType": "STRING", "dependsOn": "environmentalFootprint" }, - { "fieldName": "recyclingRate", "targetType": "DECIMAL" }, - { "fieldName": "recyclingRateUom", "targetType": "STRING", "dependsOn": "recyclingRate" }, - { "fieldName": "energyConsumption", "targetType": "DECIMAL" }, - { "fieldName": "energyConsumptionUom", "targetType": "STRING", "dependsOn": "energyConsumption" }, - { "fieldName": "weight", "targetType": "DECIMAL" }, - { "fieldName": "weightUom", "targetType": "STRING", "dependsOn": "weight" }, - { "fieldName": "durability", "targetType": "DECIMAL" }, - { "fieldName": "durabilityUom", "targetType": "STRING", "dependsOn": "durability" } + { "fieldName": "productName", "targetType": "STRING" }, + { "fieldName": "codeValue", "targetType": "STRING" }, + { "fieldName": "codeSet", "targetType": "STRING" }, + { "fieldName": "manufacturerName", "targetType": "STRING" }, + { "fieldName": "carbonFootprint", "targetType": "DECIMAL" }, + { "fieldName": "carbonFootprintUom", "targetType": "STRING", "dependsOn": "carbonFootprint" }, + { "fieldName": "recyclingRate", "targetType": "DECIMAL" }, + { "fieldName": "recyclingRateUom", "targetType": "STRING", "dependsOn": "recyclingRate" }, + { "fieldName": "energyConsumption", "targetType": "DECIMAL" }, + { "fieldName": "energyConsumptionUom", "targetType": "STRING", "dependsOn": "energyConsumption" }, + { "fieldName": "weight", "targetType": "DECIMAL" }, + { "fieldName": "weightUom", "targetType": "STRING", "dependsOn": "weight" }, + { "fieldName": "durability", "targetType": "DECIMAL" }, + { "fieldName": "durabilityUom", "targetType": "STRING", "dependsOn": "durability" } ] ``` @@ -1078,51 +1254,31 @@ Content-Type: application/json { "searchFields": [ - { "fieldName": "productName", "targetType": "STRING" }, + { "fieldName": "productName", "targetType": "STRING" }, { "fieldName": "carbonFootprint", "targetType": "DECIMAL" } ], "knownOntology": { "fields": { - "productName": { - "reference": { "key": "productName", "nativeType": "STRING" } - }, - "carbonFootprint": { - "reference": { - "@type": "CarbonFootprint", - "key": "hasProperty", - "child": { "key": "numericalValue", "nativeType": "DECIMAL" } - } - } + "productName": { "reference": { "key": "productName", "nativeType": "STRING" } }, + "carbonFootprint": { "reference": { "@type": "CarbonFootprint", "key": "hasProperty", "child": { "key": "numericalValue", "nativeType": "DECIMAL" } } } } }, "noOntology": { "fields": { - "productName": { - "variants": ["productName", "modelName"], - "variantsWithContext": { - "context": ["model", "product"], - "field": ["productName", "modelName", "name"] - } - }, - "carbonFootprint": { - "variants": ["carbonFootprint"], - "variantsWithContext": { - "context": ["carbonFootprint"], - "field": ["value", "totalKgCo2", "kgCo2", "numericalValue"] - } - } + "productName": { "variants": ["productName", "modelName"], "variantsWithContext": { "context": ["model", "product"], "field": ["productName", "modelName", "name"] } }, + "carbonFootprint": { "variants": ["carbonFootprint"], "variantsWithContext": { "context": ["carbonFootprint"], "field": ["value", "totalKgCo2", "kgCo2", "numericalValue"] } } } }, "unknownOntology": { "fields": { - "productName": { - "variants": ["productName", "modelName", "name"], - "typeHints": ["Product", "ProductData", "DPP", "DPPData", "Model", "ModelData"] - }, - "carbonFootprint": { - "variants": ["numericalValue", "value", "totalKgCo2"], - "typeHints": ["CarbonFootprint", "CO2Footprint", "CarbonEmission"] - } + "productName": { "variants": ["productName", "modelName", "name"], "typeHints": ["Product", "ProductData", "DPP", "DPPData", "Model", "ModelData"] }, + "carbonFootprint": { "variants": ["numericalValue", "value", "totalKgCo2"], "typeHints": ["CarbonFootprint", "CO2Footprint", "CarbonEmission"] } + } + }, + "aasOntology": { + "fields": { + "productName": { "semanticId": "0173-1#02-AAW338", "idShortPath": ["Nameplate", "ManufacturerProductDesignation"], "preferLanguage": "en" }, + "carbonFootprint": { "semanticId": "0173-1#02-ABG855", "idShortPath": ["CarbonFootprint", "ProductCarbonFootprints", "ProductCarbonFootprint00", "PcfCO2eq"] } } } } diff --git a/api/rest/src/test/resources/extraction/test-db-extraction-1.json b/api/rest/src/test/resources/extraction/test-db-extraction-1.json index d60ebea..f2cd59c 100644 --- a/api/rest/src/test/resources/extraction/test-db-extraction-1.json +++ b/api/rest/src/test/resources/extraction/test-db-extraction-1.json @@ -60,5 +60,18 @@ "typeHints": ["CarbonFootprint", "CO2Footprint", "CarbonEmission"] } } + }, + "aasOntology": { + "fields": { + "productName": { + "semanticId": "0173-1#02-AAW338", + "idShortPath": ["Nameplate", "ManufacturerProductDesignation"], + "preferLanguage": "en" + }, + "carbonFootprint": { + "semanticId": "0173-1#02-ABG855", + "idShortPath": ["CarbonFootprint", "ProductCarbonFootprints", "ProductCarbonFootprint00", "PcfCO2eq"] + } + } } } \ No newline at end of file diff --git a/api/rest/src/test/resources/extraction/test-db-extraction-2.json b/api/rest/src/test/resources/extraction/test-db-extraction-2.json index 9195c11..de3e2c5 100644 --- a/api/rest/src/test/resources/extraction/test-db-extraction-2.json +++ b/api/rest/src/test/resources/extraction/test-db-extraction-2.json @@ -35,5 +35,14 @@ "typeHints": ["Product","ProductData","DPP","DPPData","Model","ModelData"] } } + }, + "aasOntology": { + "fields": { + "productName": { + "semanticId": "0173-1#02-AAW338", + "idShortPath": ["Nameplate", "ManufacturerProductDesignation"], + "preferLanguage": "en" + } + } } } \ No newline at end of file diff --git a/api/rest/src/test/resources/extraction/test-rest-core-extraction.json b/api/rest/src/test/resources/extraction/test-rest-core-extraction.json index dd10350..92ba8fa 100644 --- a/api/rest/src/test/resources/extraction/test-rest-core-extraction.json +++ b/api/rest/src/test/resources/extraction/test-rest-core-extraction.json @@ -106,5 +106,25 @@ "typeHints": ["Height","QuantitativeValue"] } } + }, + "aasOntology": { + "fields": { + "productName": { + "semanticId": "0173-1#02-AAW338", + "idShortPath": ["Nameplate", "ManufacturerProductDesignation"], + "preferLanguage": "en" + }, + "weight": { + "semanticId": "0173-1#02-AAB419", + "idShortPath": ["TechnicalData", "TechnicalProperties", "NetWeight"] + }, + "carbonFootprint": { + "semanticId": "0173-1#02-ABG855", + "idShortPath": ["CarbonFootprint", "ProductCarbonFootprints", "ProductCarbonFootprint00", "PcfCO2eq"] + }, + "granularity": { + "idShortPath": ["DPPAdministration", "Granularity"] + } + } } } \ No newline at end of file diff --git a/core/src/main/java/it/extrared/extractor/config/AasOntology.java b/core/src/main/java/it/extrared/extractor/config/AasOntology.java new file mode 100644 index 0000000..b1deb2f --- /dev/null +++ b/core/src/main/java/it/extrared/extractor/config/AasOntology.java @@ -0,0 +1,38 @@ +package it.extrared.extractor.config; + +import it.extrared.extractor.config.field.AasFieldSpec; +import java.util.Map; +import java.util.Objects; + +/** + * Configuration to extract values from Asset Administration Shell (AAS) JSON documents. Supports + * both idShort path navigation and semanticId-based matching. + */ +public class AasOntology { + + private Map fields; + + public Map getFields() { + return fields; + } + + public void setFields(Map fields) { + this.fields = fields; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + return Objects.equals(fields, ((AasOntology) o).fields); + } + + @Override + public int hashCode() { + return Objects.hashCode(fields); + } + + @Override + public String toString() { + return "AasOntology{fields=" + fields + '}'; + } +} diff --git a/core/src/main/java/it/extrared/extractor/config/ExtractionConfiguration.java b/core/src/main/java/it/extrared/extractor/config/ExtractionConfiguration.java index dc21273..3ae7b22 100644 --- a/core/src/main/java/it/extrared/extractor/config/ExtractionConfiguration.java +++ b/core/src/main/java/it/extrared/extractor/config/ExtractionConfiguration.java @@ -15,6 +15,7 @@ */ package it.extrared.extractor.config; +import it.extrared.extractor.config.field.AasFieldSpec; import it.extrared.extractor.config.field.FieldType; import it.extrared.extractor.config.field.SearchField; import it.extrared.extractor.exceptions.InvalidExtractionConfigException; @@ -39,6 +40,8 @@ public class ExtractionConfiguration { private UnknownOntology unknownOntology; + private AasOntology aasOntology; + /** * @return the Known Ontology configuration. */ @@ -126,6 +129,7 @@ public void validate() { KnownOntology knownOntology = getKnownOntology(); UnknownOntology unknownOntology = getUnknownOntology(); NoOntology noOntology = getNoOntology(); + AasOntology aasOntology = getAasOntology(); if (knownOntology == null) throw new InvalidExtractionConfigException( "Known Ontology configuration must be provided."); @@ -135,11 +139,15 @@ public void validate() { if (unknownOntology == null) throw new InvalidExtractionConfigException( "Unknown Ontology configuration must be provided."); + if (aasOntology == null) + throw new InvalidExtractionConfigException( + "AAS Ontology configuration must be provided."); checkSpecFieldNames(KnownOntology.class, knownOntology.getFields().keySet(), targetTypes); checkSpecFieldNames( UnknownOntology.class, unknownOntology.getFields().keySet(), targetTypes); checkSpecFieldNames(NoOntology.class, noOntology.getFields().keySet(), targetTypes); + checkSpecFieldNames(AasFieldSpec.class, aasOntology.getFields().keySet(), targetTypes); } private void checkSpecFieldNames( @@ -151,19 +159,12 @@ private void checkSpecFieldNames( .formatted(subConfigType.getSimpleName(), k)); } - @Override - public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) return false; - ExtractionConfiguration that = (ExtractionConfiguration) o; - return Objects.equals(searchFields, that.searchFields) - && Objects.equals(knownOntology, that.knownOntology) - && Objects.equals(noOntology, that.noOntology) - && Objects.equals(unknownOntology, that.unknownOntology); + public AasOntology getAasOntology() { + return aasOntology; } - @Override - public int hashCode() { - return Objects.hash(searchFields, knownOntology, noOntology, unknownOntology); + public void setAasOntology(AasOntology aasOntology) { + this.aasOntology = aasOntology; } @Override @@ -177,6 +178,24 @@ public String toString() { + noOntology + ", unknownOntology=" + unknownOntology + + ", assOntology=" + + aasOntology + '}'; } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + ExtractionConfiguration that = (ExtractionConfiguration) o; + return Objects.equals(searchFields, that.searchFields) + && Objects.equals(knownOntology, that.knownOntology) + && Objects.equals(noOntology, that.noOntology) + && Objects.equals(unknownOntology, that.unknownOntology) + && Objects.equals(aasOntology, that.aasOntology); + } + + @Override + public int hashCode() { + return Objects.hash(searchFields, knownOntology, noOntology, unknownOntology, aasOntology); + } } diff --git a/core/src/main/java/it/extrared/extractor/config/field/AasFieldSpec.java b/core/src/main/java/it/extrared/extractor/config/field/AasFieldSpec.java new file mode 100644 index 0000000..f332b8b --- /dev/null +++ b/core/src/main/java/it/extrared/extractor/config/field/AasFieldSpec.java @@ -0,0 +1,75 @@ +package it.extrared.extractor.config.field; + +import java.util.List; +import java.util.Objects; + +/** + * Field specification for AAS JSON extraction. + * + *

Supports two complementary matching strategies: + * + *

    + *
  • semanticId: matches against {@code semanticId.keys[].value} — more precise, + * ontology-stable across different AAS producers. + *
  • idShortPath: dot-navigates the submodelElement tree by {@code idShort} — useful when + * semanticId is absent or non-standard. + *
+ * + * When both are configured, semanticId match takes priority. For {@code MultiLanguageProperty}, + * {@code preferLanguage} drives value selection (default "en"). + */ +public class AasFieldSpec { + + /** + * Ordered list of idShort segments to navigate: e.g. ["Nameplate", "ManufacturerName"]. May be + * null if semanticId matching is sufficient. + */ + private List idShortPath; + + /** + * The semantic ID value to match against {@code semanticId.keys[].value}. May be null if + * idShortPath matching is sufficient. + */ + private String semanticId; + + /** Preferred language tag for {@code MultiLanguageProperty} values. Defaults to "en". */ + private String preferLanguage = "en"; + + public List getIdShortPath() { + return idShortPath; + } + + public void setIdShortPath(List idShortPath) { + this.idShortPath = idShortPath; + } + + public String getSemanticId() { + return semanticId; + } + + public void setSemanticId(String semanticId) { + this.semanticId = semanticId; + } + + public String getPreferLanguage() { + return preferLanguage; + } + + public void setPreferLanguage(String preferLanguage) { + this.preferLanguage = preferLanguage; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + AasFieldSpec that = (AasFieldSpec) o; + return Objects.equals(idShortPath, that.idShortPath) + && Objects.equals(semanticId, that.semanticId) + && Objects.equals(preferLanguage, that.preferLanguage); + } + + @Override + public int hashCode() { + return Objects.hash(idShortPath, semanticId, preferLanguage); + } +} diff --git a/core/src/main/java/it/extrared/extractor/strategies/AasExtractorStrategy.java b/core/src/main/java/it/extrared/extractor/strategies/AasExtractorStrategy.java new file mode 100644 index 0000000..3ea1b6c --- /dev/null +++ b/core/src/main/java/it/extrared/extractor/strategies/AasExtractorStrategy.java @@ -0,0 +1,324 @@ +package it.extrared.extractor.strategies; + +import static it.extrared.extractor.utils.CommonUtils.warn; + +import com.apicatalog.jsonld.document.JsonDocument; +import io.smallrye.mutiny.Uni; +import it.extrared.extractor.config.AasOntology; +import it.extrared.extractor.config.ExtractionConfiguration; +import it.extrared.extractor.config.field.AasFieldSpec; +import it.extrared.extractor.config.field.FieldType; +import it.extrared.extractor.utils.JsonUtils; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.json.*; +import java.util.*; +import org.jboss.logging.Logger; + +/** + * Extraction strategy for Asset Administration Shell (AAS) JSON documents. + * + *

Navigates the {@code submodels[].submodelElements} tree. For each configured field: + * + *

    + *
  1. Attempts a full-document scan matching {@code semanticId.keys[].value} (preferred). + *
  2. Falls back to idShort-path navigation if semanticId is absent or unmatched. + *
+ * + *

Handles the following AAS modelTypes: + * + *

    + *
  • {@code Property} — scalar value + *
  • {@code MultiLanguageProperty} — picks value by {@link AasFieldSpec#getPreferLanguage()}, + * falls back to first available language + *
  • {@code SubmodelElementCollection} / {@code SubmodelElementList} — transparent during + * idShort path navigation + *
+ */ +@ApplicationScoped +public class AasExtractorStrategy implements SearchKeyExtractorStrategy { + + private static final Logger LOGGER = Logger.getLogger(AasExtractorStrategy.class); + + private static final String SUBMODELS = "submodels"; + private static final String SUBMODEL_ELEMENTS = "submodelElements"; + private static final String ID_SHORT = "idShort"; + private static final String MODEL_TYPE = "modelType"; + private static final String VALUE = "value"; + private static final String SEMANTIC_ID = "semanticId"; + private static final String KEYS = "keys"; + private static final String LANGUAGE = "language"; + private static final String TEXT = "text"; + + @Override + public boolean canHandle(ExtractionStrategyType type) { + return ExtractionStrategyType.AAS_JSON == type; + } + + @Override + public Uni> extractSearchKeys( + ExtractionConfiguration configuration, JsonDocument jsonDocument) { + + Optional opStructure = jsonDocument.getJsonContent(); + if (opStructure.isEmpty()) return Uni.createFrom().nullItem(); + + JsonStructure root = opStructure.get(); + if (root.getValueType() != JsonValue.ValueType.OBJECT) { + warn(LOGGER, () -> "AAS document root is not a JSON object, skipping"); + return Uni.createFrom().item(Collections.emptyMap()); + } + + JsonObject rootObj = root.asJsonObject(); + AasOntology aasOntology = configuration.getAasOntology(); + Map targetTypes = configuration.searchFieldsAsMap(); + Map results = new HashMap<>(); + + // Collect ALL submodelElements across all submodels into a flat list for semanticId scan + List allElements = collectAllElements(rootObj); + + for (Map.Entry entry : aasOntology.getFields().entrySet()) { + String targetName = entry.getKey(); + AasFieldSpec spec = entry.getValue(); + FieldType targetType = targetTypes.get(targetName); + + Object value = null; + + // 1 — semanticId scan (precise, ontology-stable) + if (spec.getSemanticId() != null) { + value = findBySemanticId(allElements, spec, targetType); + } + + // idShort path navigation (structural fallback) + if (value == null + && spec.getIdShortPath() != null + && !spec.getIdShortPath().isEmpty()) { + value = findByIdShortPath(rootObj, spec, targetType); + } + + if (value != null) { + results.put(targetName, value); + } else { + warn(LOGGER, () -> "No value found for field: " + targetName); + } + } + + return Uni.createFrom().item(results); + } + + // ------------------------------------------------------------------------- + // Semantic ID scan + // ------------------------------------------------------------------------- + + /** + * Recursively collects every leaf/intermediate JsonObject in the AAS element tree, so + * semanticId matching can be done in a single flat pass regardless of nesting depth. + */ + private List collectAllElements(JsonObject root) { + List acc = new ArrayList<>(); + JsonArray submodels = getArray(root, SUBMODELS); + if (submodels == null) return acc; + for (JsonValue sm : submodels) { + if (sm.getValueType() == JsonValue.ValueType.OBJECT) { + collectElementsRecursive(sm.asJsonObject(), acc); + } + } + return acc; + } + + private void collectElementsRecursive(JsonObject node, List acc) { + acc.add(node); + JsonArray children = getArray(node, SUBMODEL_ELEMENTS); + if (children != null) { + for (JsonValue child : children) { + if (child.getValueType() == JsonValue.ValueType.OBJECT) { + collectElementsRecursive(child.asJsonObject(), acc); + } + } + } + // SubmodelElementList stores children directly in "value" + JsonValue valueNode = node.get(VALUE); + if (valueNode != null && valueNode.getValueType() == JsonValue.ValueType.ARRAY) { + for (JsonValue child : valueNode.asJsonArray()) { + if (child.getValueType() == JsonValue.ValueType.OBJECT) { + JsonObject childObj = child.asJsonObject(); + if (childObj.containsKey(MODEL_TYPE)) { // it's an AAS element, not a lang map + collectElementsRecursive(childObj, acc); + } + } + } + } + } + + private Object findBySemanticId( + List elements, AasFieldSpec spec, FieldType targetType) { + for (JsonObject el : elements) { + if (matchesSemanticId(el, spec.getSemanticId())) { + return extractValue(el, spec.getPreferLanguage(), targetType); + } + } + return null; + } + + /** + * Checks if {@code semanticId.keys[].value} of the element matches the configured semantic ID. + * Matching is suffix-insensitive to tolerate version suffixes (e.g. {@code #002}). + */ + private boolean matchesSemanticId(JsonObject element, String targetSemanticId) { + JsonObject semanticIdObj = getObject(element, SEMANTIC_ID); + if (semanticIdObj == null) return false; + JsonArray keys = getArray(semanticIdObj, KEYS); + if (keys == null) return false; + for (JsonValue key : keys) { + if (key.getValueType() != JsonValue.ValueType.OBJECT) continue; + JsonValue keyValue = key.asJsonObject().get(VALUE); + if (keyValue instanceof JsonString js) { + // exact match OR the AAS value ends with our target (version-suffix tolerant) + String candidate = js.getString(); + if (candidate.equalsIgnoreCase(targetSemanticId) + || candidate.toUpperCase().endsWith(targetSemanticId.toUpperCase())) { + return true; + } + } + } + return false; + } + + // ------------------------------------------------------------------------- + // idShort path navigation + // ------------------------------------------------------------------------- + + private Object findByIdShortPath(JsonObject root, AasFieldSpec spec, FieldType targetType) { + List path = spec.getIdShortPath(); + + // Start from submodels array — first segment identifies the submodel (or its idShort) + JsonArray submodels = getArray(root, SUBMODELS); + if (submodels == null) return null; + + for (JsonValue sm : submodels) { + if (sm.getValueType() != JsonValue.ValueType.OBJECT) continue; + JsonObject smObj = sm.asJsonObject(); + + // path[0] matches the submodel idShort; remaining segments navigate submodelElements + if (path.size() == 1) { + // The field is directly on the submodel level (unusual but supported) + if (idShortMatches(smObj, path.getFirst())) { + return extractValue(smObj, spec.getPreferLanguage(), targetType); + } + } else { + if (idShortMatches(smObj, path.getFirst())) { + JsonObject found = navigatePath(smObj, path.subList(1, path.size())); + if (found != null) { + return extractValue(found, spec.getPreferLanguage(), targetType); + } + } + } + } + return null; + } + + /** + * Recursively descends into {@code submodelElements} following the idShort path segments. + * + * @param current current AAS node + * @param remainingPath remaining idShort segments to match + * @return the matching leaf JsonObject, or null + */ + private JsonObject navigatePath(JsonObject current, List remainingPath) { + if (remainingPath.isEmpty()) return current; + + String nextIdShort = remainingPath.getFirst(); + List rest = remainingPath.subList(1, remainingPath.size()); + + JsonArray children = getArray(current, SUBMODEL_ELEMENTS); + if (children == null) { + // SubmodelElementList: children are in "value" + JsonValue valueNode = current.get(VALUE); + if (valueNode != null && valueNode.getValueType() == JsonValue.ValueType.ARRAY) { + children = valueNode.asJsonArray(); + } + } + if (children == null) return null; + + for (JsonValue child : children) { + if (child.getValueType() != JsonValue.ValueType.OBJECT) continue; + JsonObject childObj = child.asJsonObject(); + if (idShortMatches(childObj, nextIdShort)) { + return rest.isEmpty() ? childObj : navigatePath(childObj, rest); + } + } + return null; + } + + private boolean idShortMatches(JsonObject obj, String idShort) { + JsonValue v = obj.get(ID_SHORT); + return v instanceof JsonString js && js.getString().equalsIgnoreCase(idShort); + } + + /** + * Extracts the value from an AAS element node, handling the main modelTypes: {@code Property}, + * {@code MultiLanguageProperty}. + */ + private Object extractValue(JsonObject element, String preferLanguage, FieldType targetType) { + String modelType = getString(element, MODEL_TYPE); + JsonValue rawValue = element.get(VALUE); + if (rawValue == null || rawValue.getValueType() == JsonValue.ValueType.NULL) return null; + + if ("MultiLanguageProperty".equalsIgnoreCase(modelType) + && rawValue.getValueType() == JsonValue.ValueType.ARRAY) { + return extractMultiLangValue(rawValue.asJsonArray(), preferLanguage, targetType); + } + + if (rawValue.getValueType() == JsonValue.ValueType.STRING + || rawValue.getValueType() == JsonValue.ValueType.NUMBER + || rawValue.getValueType() == JsonValue.ValueType.TRUE + || rawValue.getValueType() == JsonValue.ValueType.FALSE) { + return JsonUtils.convertToTargetType(rawValue, targetType); + } + + warn( + LOGGER, + () -> + "Unsupported value type %s for modelType %s" + .formatted(rawValue.getValueType(), modelType)); + return null; + } + + /** + * Picks the text value from a {@code MultiLanguageProperty} value array. Tries {@code + * preferLanguage} first, then falls back to the first available entry. + */ + private Object extractMultiLangValue( + JsonArray langArray, String preferLanguage, FieldType targetType) { + JsonValue fallback = null; + for (JsonValue item : langArray) { + if (item.getValueType() != JsonValue.ValueType.OBJECT) continue; + JsonObject langObj = item.asJsonObject(); + JsonValue textVal = langObj.get(TEXT); + if (textVal == null) continue; + String lang = getString(langObj, LANGUAGE); + if (preferLanguage.equalsIgnoreCase(lang)) { + return JsonUtils.convertToTargetType(textVal, targetType); + } + if (fallback == null) fallback = textVal; + } + return fallback != null ? JsonUtils.convertToTargetType(fallback, targetType) : null; + } + + private JsonArray getArray(JsonObject obj, String key) { + JsonValue v = obj.get(key); + return (v != null && v.getValueType() == JsonValue.ValueType.ARRAY) + ? v.asJsonArray() + : null; + } + + private JsonObject getObject(JsonObject obj, String key) { + JsonValue v = obj.get(key); + return (v != null && v.getValueType() == JsonValue.ValueType.OBJECT) + ? v.asJsonObject() + : null; + } + + private String getString(JsonObject obj, String key) { + JsonValue v = obj.get(key); + return (v instanceof JsonString js) ? js.getString() : null; + } +} diff --git a/core/src/main/java/it/extrared/extractor/strategies/ExtractionStrategyType.java b/core/src/main/java/it/extrared/extractor/strategies/ExtractionStrategyType.java index 53c9498..d12d3a8 100644 --- a/core/src/main/java/it/extrared/extractor/strategies/ExtractionStrategyType.java +++ b/core/src/main/java/it/extrared/extractor/strategies/ExtractionStrategyType.java @@ -18,6 +18,7 @@ /** Enumeration of the supported extraction strategies. */ public enum ExtractionStrategyType { PLAIN_JSON, + AAS_JSON, KNOW_ONTOLOGY, UNKNOWN_ONTOLOGY } diff --git a/core/src/main/java/it/extrared/extractor/strategies/SearchKeyExtractor.java b/core/src/main/java/it/extrared/extractor/strategies/SearchKeyExtractor.java index f25138f..2326d51 100644 --- a/core/src/main/java/it/extrared/extractor/strategies/SearchKeyExtractor.java +++ b/core/src/main/java/it/extrared/extractor/strategies/SearchKeyExtractor.java @@ -45,6 +45,9 @@ public class SearchKeyExtractor { private static final String VOCAB = "@vocab"; private static final Logger LOGGER = Logger.getLogger(SearchKeyExtractor.class); + private static final Set AAS_ROOT_MODEL_TYPES = + Set.of("AssetAdministrationShellEnvironment", "Submodel", "AssetAdministrationShell"); + @Inject ExtractionConfigCache configCache; @Inject ExtractorConfig config; @@ -98,6 +101,10 @@ private ExtractionStrategyType detectFromDocument(JsonDocument document) throws return ExtractionStrategyType.PLAIN_JSON; } + if (isAasDocument(jsonLd)) { + return ExtractionStrategyType.AAS_JSON; + } + if (!jsonLd.containsKey(CONTEXT)) { return ExtractionStrategyType.PLAIN_JSON; } @@ -118,6 +125,11 @@ private ExtractionStrategyType detectFromDocument(JsonDocument document) throws return ExtractionStrategyType.UNKNOWN_ONTOLOGY; } + private boolean isAasDocument(JsonObject root) { + JsonValue modelType = root.get("modelType"); + return modelType instanceof JsonString js && AAS_ROOT_MODEL_TYPES.contains(js.getString()); + } + private boolean matchesContextContent(JsonValue contextValue, List contexts) { // context uri match diff --git a/core/src/test/java/it/extrared/extractor/mocks/MockDPPMetadataRepository.java b/core/src/test/java/it/extrared/extractor/mocks/MockDPPMetadataRepository.java index fcde702..b7b05b7 100644 --- a/core/src/test/java/it/extrared/extractor/mocks/MockDPPMetadataRepository.java +++ b/core/src/test/java/it/extrared/extractor/mocks/MockDPPMetadataRepository.java @@ -52,6 +52,11 @@ public Uni> findByRegistryIds( entry1.setUpi("76544567"); entry1.setModifiedAt(LocalDateTime.now()); entry1.setRegistryId(registryIds.getFirst()); - return Uni.createFrom().item(List.of(entry1)); + DPPMetadataEntry entry2 = new DPPMetadataEntry(); + entry2.setLiveURL("http://localhost:8080/dpp4"); + entry2.setUpi("665445688"); + entry2.setModifiedAt(LocalDateTime.now()); + entry2.setRegistryId(registryIds.getFirst()); + return Uni.createFrom().item(List.of(entry2)); } } diff --git a/core/src/test/java/it/extrared/extractor/mocks/MockProducers.java b/core/src/test/java/it/extrared/extractor/mocks/MockProducers.java index c3045e1..5a28d73 100644 --- a/core/src/test/java/it/extrared/extractor/mocks/MockProducers.java +++ b/core/src/test/java/it/extrared/extractor/mocks/MockProducers.java @@ -54,6 +54,10 @@ public WebClient webClient() throws IOException { Mockito.doReturn(mockRequest(ExtractionStrategyType.UNKNOWN_ONTOLOGY)) .when(webClient) .getAbs(eq("http://localhost:8080/dpp3")); + Mockito.doReturn(mockRequest(ExtractionStrategyType.AAS_JSON)) + .when(webClient) + .getAbs(eq("http://localhost:8080/dpp4")); + return webClient; } @@ -69,12 +73,13 @@ private HttpResponse mockResponse(ExtractionStrategyType type) throws IO switch (type) { case PLAIN_JSON -> bytes = readResourceAsBytes("/example-dpp/dpp.json"); case KNOW_ONTOLOGY -> bytes = readResourceAsBytes("/example-dpp/dpp-ld.json"); + case AAS_JSON -> bytes = readResourceAsBytes("/example-dpp/dpp-aas.json"); default -> bytes = readResourceAsBytes("/example-dpp/dpp-unknown-ld.json"); } Buffer buffer = Buffer.buffer(bytes); HttpResponse response = Mockito.mock(HttpResponse.class); Mockito.doReturn(buffer).when(response).bodyAsBuffer(); - if (type == ExtractionStrategyType.PLAIN_JSON) { + if (type == ExtractionStrategyType.PLAIN_JSON || type == ExtractionStrategyType.AAS_JSON) { Mockito.doReturn("application/json") .when(response) .getHeader(ArgumentMatchers.eq("Content-Type")); diff --git a/core/src/test/java/it/extrared/extractor/strategies/SearchKeyExtractorTest.java b/core/src/test/java/it/extrared/extractor/strategies/SearchKeyExtractorTest.java index 21db58b..fd05687 100644 --- a/core/src/test/java/it/extrared/extractor/strategies/SearchKeyExtractorTest.java +++ b/core/src/test/java/it/extrared/extractor/strategies/SearchKeyExtractorTest.java @@ -43,6 +43,23 @@ public void extractFromJson(UniAsserter uniAsserter) throws IOException { } } + @Test + @RunOnVertxContext + public void extractFromAAS(UniAsserter uniAsserter) throws IOException { + try (InputStream is = getClass().getResourceAsStream("/example-dpp/dpp-aas.json")) { + Uni> result = searchKeyExtractor.extractSearchKeys(is); + uniAsserter.assertThat( + () -> result, + r -> { + assertEquals(4, r.size()); + assertEquals("product", r.get("granularity")); + assertEquals(45.8, r.get("carbonFootprint")); + assertEquals(189.5, r.get("weight")); + assertEquals("EcoPhone X Pro AAS", r.get("productName")); + }); + } + } + @Test @RunOnVertxContext public void extractFromJsonLd(UniAsserter uniAsserter) throws IOException { diff --git a/core/src/test/resources/example-dpp/dpp-aas.json b/core/src/test/resources/example-dpp/dpp-aas.json new file mode 100644 index 0000000..ef9e9ea --- /dev/null +++ b/core/src/test/resources/example-dpp/dpp-aas.json @@ -0,0 +1,211 @@ +{ + "modelType": "AssetAdministrationShellEnvironment", + + "assetAdministrationShells": [ + { + "modelType": "AssetAdministrationShell", + "id": "urn:example:aas:smartphone-001", + "idShort": "SmartphoneEcoPhoneXPro", + "assetInformation": { + "assetKind": "Instance", + "globalAssetId": "urn:epc:id:sgtin:0614141.112345.001", + "specificAssetIds": [ + { "name": "GTIN", "value": "8712345678901" }, + { "name": "commodityCode", "value": "8517120000" } + ] + }, + "submodels": [ + { "type": "ExternalReference", "keys": [{ "type": "Submodel", "value": "urn:example:sm:nameplate:smartphone-001" }] }, + { "type": "ExternalReference", "keys": [{ "type": "Submodel", "value": "urn:example:sm:technicaldata:smartphone-001" }] }, + { "type": "ExternalReference", "keys": [{ "type": "Submodel", "value": "urn:example:sm:carbonfootprint:smartphone-001" }] }, + { "type": "ExternalReference", "keys": [{ "type": "Submodel", "value": "urn:example:sm:dppadmin:smartphone-001" }] } + ] + } + ], + + "submodels": [ + + { + "modelType": "Submodel", + "id": "urn:example:sm:nameplate:smartphone-001", + "idShort": "Nameplate", + "semanticId": { + "type": "ExternalReference", + "keys": [{ "type": "GlobalReference", "value": "https://admin-shell.io/idta/nameplate/3/0/Nameplate" }] + }, + "submodelElements": [ + { + "modelType": "MultiLanguageProperty", + "idShort": "ManufacturerName", + "semanticId": { + "type": "ExternalReference", + "keys": [{ "type": "GlobalReference", "value": "0173-1#02-AAO677#002" }] + }, + "value": [ + { "language": "en", "text": "Alpha Electronics Inc." } + ] + }, + { + "modelType": "MultiLanguageProperty", + "idShort": "ManufacturerProductDesignation", + "semanticId": { + "type": "ExternalReference", + "keys": [{ "type": "GlobalReference", "value": "0173-1#02-AAW338#001" }] + }, + "value": [ + { "language": "en", "text": "EcoPhone X Pro AAS" } + ] + } + ] + }, + + { + "modelType": "Submodel", + "id": "urn:example:sm:technicaldata:smartphone-001", + "idShort": "TechnicalData", + "semanticId": { + "type": "ExternalReference", + "keys": [{ "type": "GlobalReference", "value": "https://admin-shell.io/ZVEI/TechnicalData/Submodel/1/2" }] + }, + "submodelElements": [ + { + "modelType": "SubmodelElementCollection", + "idShort": "TechnicalProperties", + "semanticId": { + "type": "ExternalReference", + "keys": [{ "type": "GlobalReference", "value": "https://admin-shell.io/ZVEI/TechnicalData/MainSection/1/1" }] + }, + "value": [ + { + "modelType": "Property", + "idShort": "NetWeight", + "semanticId": { + "type": "ExternalReference", + "keys": [{ "type": "GlobalReference", "value": "0173-1#02-AAB419#012" }] + }, + "valueType": "xs:decimal", + "value": "189.5" + }, + { + "modelType": "Property", + "idShort": "NetWeightUnit", + "valueType": "xs:string", + "value": "g" + } + ] + } + ] + }, + + { + "modelType": "Submodel", + "id": "urn:example:sm:carbonfootprint:smartphone-001", + "idShort": "CarbonFootprint", + "semanticId": { + "type": "ExternalReference", + "keys": [{ "type": "GlobalReference", "value": "https://admin-shell.io/idta/CarbonFootprint/CarbonFootprint/1/0" }] + }, + "submodelElements": [ + { + "modelType": "SubmodelElementList", + "idShort": "ProductCarbonFootprints", + "semanticId": { + "type": "ExternalReference", + "keys": [{ "type": "GlobalReference", "value": "https://admin-shell.io/idta/CarbonFootprint/ProductCarbonFootprints/1/0" }] + }, + "typeValueListElement": "SubmodelElementCollection", + "value": [ + { + "modelType": "SubmodelElementCollection", + "idShort": "ProductCarbonFootprint00", + "semanticId": { + "type": "ExternalReference", + "keys": [{ "type": "GlobalReference", "value": "https://admin-shell.io/idta/CarbonFootprint/ProductCarbonFootprint/1/0" }] + }, + "value": [ + { + "modelType": "Property", + "idShort": "PcfCO2eq", + "semanticId": { + "type": "ExternalReference", + "keys": [{ "type": "GlobalReference", "value": "0173-1#02-ABG855#003" }] + }, + "valueType": "xs:decimal", + "value": "45.8" + }, + { + "modelType": "Property", + "idShort": "ReferenceImpactUnitForCalculation", + "semanticId": { + "type": "ExternalReference", + "keys": [{ "type": "GlobalReference", "value": "0173-1#02-ABG856#003" }] + }, + "valueType": "xs:string", + "value": "piece" + } + ] + } + ] + } + ] + }, + + { + "modelType": "Submodel", + "id": "urn:example:sm:dppadmin:smartphone-001", + "idShort": "DPPAdministration", + "displayName": [{ "language": "en", "text": "DPP Administration Metadata" }], + "submodelElements": [ + { + "modelType": "Property", + "idShort": "UniqueDPPID", + "valueType": "xs:string", + "value": "urn:uuid:a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d" + }, + { + "modelType": "Property", + "idShort": "Granularity", + "valueType": "xs:string", + "value": "product" + }, + { + "modelType": "Property", + "idShort": "Status", + "valueType": "xs:string", + "value": "Active" + }, + { + "modelType": "Property", + "idShort": "SchemaVersion", + "valueType": "xs:string", + "value": "1.7.0" + }, + { + "modelType": "Property", + "idShort": "ValidFrom", + "valueType": "xs:dateTime", + "value": "2024-01-15T08:00:00Z" + }, + { + "modelType": "Property", + "idShort": "ValidUntil", + "valueType": "xs:dateTime", + "value": "2034-01-15T08:00:00Z" + }, + { + "modelType": "Property", + "idShort": "LastUpdate", + "valueType": "xs:dateTime", + "value": "2024-11-20T14:30:00Z" + }, + { + "modelType": "Property", + "idShort": "FacilityID", + "valueType": "xs:string", + "value": "FAC-IT-MI-001" + } + ] + } + + ] +} \ No newline at end of file diff --git a/core/src/test/resources/extraction/db-test-core-extraction.json b/core/src/test/resources/extraction/db-test-core-extraction.json index 9195c11..0934e41 100644 --- a/core/src/test/resources/extraction/db-test-core-extraction.json +++ b/core/src/test/resources/extraction/db-test-core-extraction.json @@ -35,5 +35,17 @@ "typeHints": ["Product","ProductData","DPP","DPPData","Model","ModelData"] } } + }, + "aasOntology": { + "fields": { + "productName": { + "semanticId": "0173-1#02-AAW338", + "idShortPath": [ + "Nameplate", + "ManufacturerProductDesignation" + ], + "preferLanguage": "en" + } + } } } \ No newline at end of file diff --git a/core/src/test/resources/extraction/fs-test-core-extraction.json b/core/src/test/resources/extraction/fs-test-core-extraction.json index d60ebea..f2cd59c 100644 --- a/core/src/test/resources/extraction/fs-test-core-extraction.json +++ b/core/src/test/resources/extraction/fs-test-core-extraction.json @@ -60,5 +60,18 @@ "typeHints": ["CarbonFootprint", "CO2Footprint", "CarbonEmission"] } } + }, + "aasOntology": { + "fields": { + "productName": { + "semanticId": "0173-1#02-AAW338", + "idShortPath": ["Nameplate", "ManufacturerProductDesignation"], + "preferLanguage": "en" + }, + "carbonFootprint": { + "semanticId": "0173-1#02-ABG855", + "idShortPath": ["CarbonFootprint", "ProductCarbonFootprints", "ProductCarbonFootprint00", "PcfCO2eq"] + } + } } } \ No newline at end of file diff --git a/core/src/test/resources/extraction/test-core-extraction.json b/core/src/test/resources/extraction/test-core-extraction.json index 20c877f..01f6fba 100644 --- a/core/src/test/resources/extraction/test-core-extraction.json +++ b/core/src/test/resources/extraction/test-core-extraction.json @@ -1,52 +1,40 @@ { - "searchFields":[ - { - "fieldName": "productName", - "targetType": "STRING" - }, - { - "fieldName": "weight", - "targetType": "DECIMAL" - }, - { - "fieldName": "carbonFootprint", - "targetType": "DECIMAL" - }, - { - "fieldName": "granularity", - "targetType": "STRING" - } + "searchFields": [ + { "fieldName": "productName", "targetType": "STRING" }, + { "fieldName": "weight", "targetType": "DECIMAL" }, + { "fieldName": "carbonFootprint", "targetType": "DECIMAL" }, + { "fieldName": "granularity", "targetType": "STRING" } ], - "knownOntology":{ - "fields": - { + + "knownOntology": { + "fields": { "productName": { "reference": { - "key":"productName", + "key": "productName", "nativeType": "STRING" } }, "weight": { "reference": { - "@type":"Weight", - "key": "hasProperty", - "child": { - "key":"numericalValue", - "nativeType": "DECIMAL" - } + "@type": "Weight", + "key": "hasProperty", + "child": { + "key": "numericalValue", + "nativeType": "DECIMAL" } + } }, - "carbonFootprint":{ + "carbonFootprint": { "reference": { - "@type":"CarbonFootprint", + "@type": "CarbonFootprint", "key": "hasProperty", "child": { - "nativeType": "DECIMAL", - "key":"numericalValue" + "key": "numericalValue", + "nativeType": "DECIMAL" } } }, - "granularity":{ + "granularity": { "reference": { "@type": "DPP", "key": "hasDPP", @@ -58,40 +46,45 @@ } } }, + "noOntology": { - "fields": - { + "fields": { "productName": { - "variants":["productName","modelName"], - "variantsWithContext":{ - "context": ["model","product"], - "field": ["productName","modelName","name"] + "variants": ["productName", "modelName"], + "variantsWithContext": { + "context": ["model", "product"], + "field": ["productName", "modelName", "name"] } }, "weight": { - "variants": ["weight","weightG","weightKg"], + "variants": ["weight", "weightG", "weightKg"], "variantsWithContext": { - "context": ["dimension"], - "field": ["weight","weightg","weightkg"] + "context": ["dimension", "weight"], + "field": ["weight", "weightG", "weightKg", "numericalValue"] } }, "carbonFootprint": { "variants": ["carbonFootprint"], "variantsWithContext": { - "context": ["carbonFootprint"], - "field": ["value","totalKgCo2","kgCo2","numericalValue"] + "context": ["carbonFootprint", "co2"], + "field": ["value", "totalKgCo2", "kgCo2", "numericalValue"] } }, "granularity": { - "variants": ["granularity","granularityLevel"] + "variants": ["granularity", "granularityLevel"], + "variantsWithContext": { + "context": ["dpp", "digitalProductPassport", "hasDPP"], + "field": ["granularity", "granularityLevel", "granularityInfo"] + } } } }, + "unknownOntology": { "fields": { "productName": { - "variants": ["productName","modelName","name"], - "typeHints": ["Product","ProductData","DPP","DPPData","Model","ModelData"] + "variants": ["productName", "modelName", "name"], + "typeHints": ["Product", "ProductData", "DPP", "DPPData", "Model", "ModelData"] }, "weight": { "variants": ["numericalValue", "value", "amount"], @@ -102,8 +95,28 @@ "typeHints": ["CarbonFootprint", "CO2Footprint", "CarbonEmission"] }, "granularity": { - "variants": ["granularity","granularityLevel","granularityInfo"], - "typeHints": ["DPP","DigitalProductPassport"] + "variants": ["granularity", "granularityLevel", "granularityInfo"], + "typeHints": ["DPP", "DigitalProductPassport"] + } + } + }, + "aasOntology": { + "fields": { + "productName": { + "semanticId": "0173-1#02-AAW338", + "idShortPath": ["Nameplate", "ManufacturerProductDesignation"], + "preferLanguage": "en" + }, + "weight": { + "semanticId": "0173-1#02-AAB419", + "idShortPath": ["TechnicalData", "TechnicalProperties", "NetWeight"] + }, + "carbonFootprint": { + "semanticId": "0173-1#02-ABG855", + "idShortPath": ["CarbonFootprint", "ProductCarbonFootprints", "ProductCarbonFootprint00", "PcfCO2eq"] + }, + "granularity": { + "idShortPath": ["DPPAdministration", "Granularity"] } } } diff --git a/datastore/mariadb/src/test/resources/extraction/db-2-test-core-extraction.json b/datastore/mariadb/src/test/resources/extraction/db-2-test-core-extraction.json index d60ebea..f2cd59c 100644 --- a/datastore/mariadb/src/test/resources/extraction/db-2-test-core-extraction.json +++ b/datastore/mariadb/src/test/resources/extraction/db-2-test-core-extraction.json @@ -60,5 +60,18 @@ "typeHints": ["CarbonFootprint", "CO2Footprint", "CarbonEmission"] } } + }, + "aasOntology": { + "fields": { + "productName": { + "semanticId": "0173-1#02-AAW338", + "idShortPath": ["Nameplate", "ManufacturerProductDesignation"], + "preferLanguage": "en" + }, + "carbonFootprint": { + "semanticId": "0173-1#02-ABG855", + "idShortPath": ["CarbonFootprint", "ProductCarbonFootprints", "ProductCarbonFootprint00", "PcfCO2eq"] + } + } } } \ No newline at end of file diff --git a/datastore/mariadb/src/test/resources/extraction/db-test-core-extraction.json b/datastore/mariadb/src/test/resources/extraction/db-test-core-extraction.json index 9195c11..0934e41 100644 --- a/datastore/mariadb/src/test/resources/extraction/db-test-core-extraction.json +++ b/datastore/mariadb/src/test/resources/extraction/db-test-core-extraction.json @@ -35,5 +35,17 @@ "typeHints": ["Product","ProductData","DPP","DPPData","Model","ModelData"] } } + }, + "aasOntology": { + "fields": { + "productName": { + "semanticId": "0173-1#02-AAW338", + "idShortPath": [ + "Nameplate", + "ManufacturerProductDesignation" + ], + "preferLanguage": "en" + } + } } } \ No newline at end of file diff --git a/datastore/pgsql/src/test/resources/extraction/db-2-test-core-extraction.json b/datastore/pgsql/src/test/resources/extraction/db-2-test-core-extraction.json index d60ebea..f2cd59c 100644 --- a/datastore/pgsql/src/test/resources/extraction/db-2-test-core-extraction.json +++ b/datastore/pgsql/src/test/resources/extraction/db-2-test-core-extraction.json @@ -60,5 +60,18 @@ "typeHints": ["CarbonFootprint", "CO2Footprint", "CarbonEmission"] } } + }, + "aasOntology": { + "fields": { + "productName": { + "semanticId": "0173-1#02-AAW338", + "idShortPath": ["Nameplate", "ManufacturerProductDesignation"], + "preferLanguage": "en" + }, + "carbonFootprint": { + "semanticId": "0173-1#02-ABG855", + "idShortPath": ["CarbonFootprint", "ProductCarbonFootprints", "ProductCarbonFootprint00", "PcfCO2eq"] + } + } } } \ No newline at end of file diff --git a/datastore/pgsql/src/test/resources/extraction/db-test-core-extraction.json b/datastore/pgsql/src/test/resources/extraction/db-test-core-extraction.json index 9195c11..0934e41 100644 --- a/datastore/pgsql/src/test/resources/extraction/db-test-core-extraction.json +++ b/datastore/pgsql/src/test/resources/extraction/db-test-core-extraction.json @@ -35,5 +35,17 @@ "typeHints": ["Product","ProductData","DPP","DPPData","Model","ModelData"] } } + }, + "aasOntology": { + "fields": { + "productName": { + "semanticId": "0173-1#02-AAW338", + "idShortPath": [ + "Nameplate", + "ManufacturerProductDesignation" + ], + "preferLanguage": "en" + } + } } } \ No newline at end of file diff --git a/main/src/main/resources/extraction/extraction.json b/main/src/main/resources/extraction/extraction.json index f876932..a229d3b 100644 --- a/main/src/main/resources/extraction/extraction.json +++ b/main/src/main/resources/extraction/extraction.json @@ -379,5 +379,88 @@ "typeHints": ["Durability", "ProductDurability", "LifeExpectancy"] } } + }, + "aasOntology": { + "fields": { + + "manufacturerName": { + "comment": "IDTA-02006 Nameplate. ECLASS 0173-1#02-AAO677 (ManufacturerName). Version suffix intentionally omitted to match #002, #003, #004.", + "semanticId": "0173-1#02-AAO677", + "idShortPath": ["Nameplate", "ManufacturerName"], + "preferLanguage": "en" + }, + + "productName": { + "comment": "IDTA-02006 Nameplate. ECLASS 0173-1#02-AAW338 (ManufacturerProductDesignation). Also try ManufacturerProductFamily as secondary path if empty.", + "semanticId": "0173-1#02-AAW338", + "idShortPath": ["Nameplate", "ManufacturerProductDesignation"], + "preferLanguage": "en" + }, + + "codeValue": { + "comment": "IDTA-02003 TechnicalData. admin-shell.io IRI for ProductClassId. Path targets first classification item. SML indexing (..Item00) is positional — first entry is the primary classification.", + "semanticId": "https://admin-shell.io/ZVEI/TechnicalData/ProductClassId/1/1", + "idShortPath": ["TechnicalData", "ProductClassifications", "ProductClassificationItem00", "ProductClassId"] + }, + + "codeSet": { + "comment": "IDTA-02003 TechnicalData. Paired with codeValue — same SML item, different property.", + "semanticId": "https://admin-shell.io/ZVEI/TechnicalData/ClassificationSystem/1/1", + "idShortPath": ["TechnicalData", "ProductClassifications", "ProductClassificationItem00", "ClassificationSystem"] + }, + + "carbonFootprint": { + "comment": "IDTA-02023 CarbonFootprint v1.0. ECLASS 0173-1#02-ABG855 (PcfCO2eq). Path descends into first ProductCarbonFootprint instance in the SML.", + "semanticId": "0173-1#02-ABG855", + "idShortPath": ["CarbonFootprint", "ProductCarbonFootprints", "ProductCarbonFootprint00", "PcfCO2eq"] + }, + + "carbonFootprintUom": { + "comment": "IDTA-02023. ECLASS 0173-1#02-ABG856 (ReferenceImpactUnitForCalculation). Quantity unit for the PCF value, e.g. 'piece', 'kg'.", + "semanticId": "0173-1#02-ABG856", + "idShortPath": ["CarbonFootprint", "ProductCarbonFootprints", "ProductCarbonFootprint00", "ReferenceImpactUnitForCalculation"] + }, + + "weight": { + "comment": "ECLASS 0173-1#02-AAB419 (net weight). Typically under TechnicalData/TechnicalProperties. No dedicated IDTA submodel template — idShortPath is a well-known convention.", + "semanticId": "0173-1#02-AAB419", + "idShortPath": ["TechnicalData", "TechnicalProperties", "NetWeight"] + }, + + "weightUom": { + "comment": "No standardized semanticId for unit companion properties in TechnicalData — units are encoded in the ECLASS ConceptDescription. idShortPath covers implementors who add an explicit unit property.", + "idShortPath": ["TechnicalData", "TechnicalProperties", "NetWeightUnit"] + }, + + "energyConsumption": { + "comment": "IDTA energy submodels (02054 Energy Consumption in Production, 02053 Energy Data Sheet) are under development / niche. ECLASS 0173-1#02-AAI667 covers nominal power. Fallback to common idShort conventions.", + "semanticId": "0173-1#02-AAI667", + "idShortPath": ["EnergyConsumption", "EnergyConsumptionDetails", "EnergyConsumptionValue"] + }, + + "energyConsumptionUom": { + "comment": "No stable IDTA standard semanticId for energy unit companion. idShortPath fallback only.", + "idShortPath": ["EnergyConsumption", "EnergyConsumptionDetails", "EnergyConsumptionUnit"] + }, + + "recyclingRate": { + "comment": "IDTA-02035-7 (Battery Passport Circularity) in development. No universal IRDI. Common convention used in sustainability submodels. Update when IDTA standardizes.", + "idShortPath": ["Circularity", "RecyclingInformation", "RecyclingRate"] + }, + + "recyclingRateUom": { + "idShortPath": ["Circularity", "RecyclingInformation", "RecyclingRateUnit"] + }, + + "durability": { + "comment": "No IDTA SMT yet. ECLASS 0173-1#02-AAY812 covers service life/durability in some releases — treat as advisory only. idShortPath based on ESPR-aligned DPP conventions.", + "semanticId": "0173-1#02-AAY812", + "idShortPath": ["Circularity", "DurabilityIndicators", "Durability"] + }, + + "durabilityUom": { + "idShortPath": ["Circularity", "DurabilityIndicators", "DurabilityUnit"] + } + } } } \ No newline at end of file From 84de44059a737586be42b6e5b12be89310363baa Mon Sep 17 00:00:00 2001 From: Marco Volpini Date: Tue, 31 Mar 2026 15:21:22 +0200 Subject: [PATCH 2/2] fix test config aas --- .../test/resources/extraction/test-rest-core-extraction.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api/rest/src/test/resources/extraction/test-rest-core-extraction.json b/api/rest/src/test/resources/extraction/test-rest-core-extraction.json index 92ba8fa..132e2d0 100644 --- a/api/rest/src/test/resources/extraction/test-rest-core-extraction.json +++ b/api/rest/src/test/resources/extraction/test-rest-core-extraction.json @@ -122,8 +122,9 @@ "semanticId": "0173-1#02-ABG855", "idShortPath": ["CarbonFootprint", "ProductCarbonFootprints", "ProductCarbonFootprint00", "PcfCO2eq"] }, - "granularity": { - "idShortPath": ["DPPAdministration", "Granularity"] + "height": { + "semanticId": "0173-1#02-AAB419", + "idShortPath": ["TechnicalData", "TechnicalProperties", "NetHeight"] } } }