diff --git a/core/src/main/java/it/extrared/extractor/strategies/AasExtractorStrategy.java b/core/src/main/java/it/extrared/extractor/strategies/AasExtractorStrategy.java index 3ea1b6c..08b2063 100644 --- a/core/src/main/java/it/extrared/extractor/strategies/AasExtractorStrategy.java +++ b/core/src/main/java/it/extrared/extractor/strategies/AasExtractorStrategy.java @@ -12,6 +12,7 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.json.*; import java.util.*; +import java.util.regex.Pattern; import org.jboss.logging.Logger; /** @@ -46,6 +47,7 @@ public class AasExtractorStrategy implements SearchKeyExtractorStrategy { private static final String VALUE = "value"; private static final String SEMANTIC_ID = "semanticId"; private static final String KEYS = "keys"; + private static final Pattern IRDI_VERSION_SUFFIX = Pattern.compile("#\\d+$"); private static final String LANGUAGE = "language"; private static final String TEXT = "text"; @@ -160,7 +162,15 @@ private Object findBySemanticId( /** * 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}). + * Two tolerances let version-less configured targets (e.g. {@code 0173-1#02-AAO677}) match + * real-world AAS values: + * + * */ private boolean matchesSemanticId(JsonObject element, String targetSemanticId) { JsonObject semanticIdObj = getObject(element, SEMANTIC_ID); @@ -171,10 +181,9 @@ private boolean matchesSemanticId(JsonObject element, String targetSemanticId) { 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())) { + if (matchesTarget(candidate, targetSemanticId) + || matchesTarget(withoutVersionSuffix(candidate), targetSemanticId)) { return true; } } @@ -182,6 +191,24 @@ private boolean matchesSemanticId(JsonObject element, String targetSemanticId) { return false; } + /** + * Exact match (case-insensitive) OR the AAS value ends with the target (IRI-prefix tolerant). + */ + private boolean matchesTarget(String candidate, String target) { + return candidate.equalsIgnoreCase(target) + || candidate.toUpperCase().endsWith(target.toUpperCase()); + } + + /** + * Strips a trailing IRDI version segment ({@code 0173-1#02-AAO677#002} → {@code + * 0173-1#02-AAO677}). Applied only when another {@code #} remains, so plain IRIs with a numeric + * fragment are left untouched. + */ + private String withoutVersionSuffix(String semanticId) { + String stripped = IRDI_VERSION_SUFFIX.matcher(semanticId).replaceFirst(""); + return stripped.contains("#") ? stripped : semanticId; + } + // ------------------------------------------------------------------------- // idShort path navigation // ------------------------------------------------------------------------- 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 fd05687..5d88bea 100644 --- a/core/src/test/java/it/extrared/extractor/strategies/SearchKeyExtractorTest.java +++ b/core/src/test/java/it/extrared/extractor/strategies/SearchKeyExtractorTest.java @@ -60,6 +60,28 @@ public void extractFromAAS(UniAsserter uniAsserter) throws IOException { } } + @Test + @RunOnVertxContext + public void extractFromAASWithVersionedSemanticIds(UniAsserter uniAsserter) throws IOException { + // Same data as dpp-aas.json, but with vendor-specific idShorts and versioned eCl@ss + // semanticIds (e.g. 0173-1#02-AAW338#001) — the form real IDTA submodels carry. The + // idShort-path fallback cannot match here, so extraction must succeed via the + // semanticId scan alone; 'granularity' (idShortPath-only) keeps working as before. + try (InputStream is = + getClass().getResourceAsStream("/example-dpp/dpp-aas-versioned.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-versioned.json b/core/src/test/resources/example-dpp/dpp-aas-versioned.json new file mode 100644 index 0000000..dbe871a --- /dev/null +++ b/core/src/test/resources/example-dpp/dpp-aas-versioned.json @@ -0,0 +1,183 @@ +{ + "modelType": "AssetAdministrationShellEnvironment", + + "assetAdministrationShells": [ + { + "modelType": "AssetAdministrationShell", + "id": "urn:example:aas:smartphone-002", + "idShort": "SmartphoneEcoPhoneXProVendor", + "assetInformation": { + "assetKind": "Instance", + "globalAssetId": "urn:epc:id:sgtin:0614141.112345.002", + "specificAssetIds": [ + { "name": "GTIN", "value": "8712345678901" }, + { "name": "commodityCode", "value": "8517120000" } + ] + }, + "submodels": [ + { "type": "ExternalReference", "keys": [{ "type": "Submodel", "value": "urn:example:sm:nameplate:smartphone-002" }] }, + { "type": "ExternalReference", "keys": [{ "type": "Submodel", "value": "urn:example:sm:technicaldata:smartphone-002" }] }, + { "type": "ExternalReference", "keys": [{ "type": "Submodel", "value": "urn:example:sm:carbonfootprint:smartphone-002" }] }, + { "type": "ExternalReference", "keys": [{ "type": "Submodel", "value": "urn:example:sm:dppadmin:smartphone-002" }] } + ] + } + ], + + "_comment": "Same data as dpp-aas.json, but with vendor-specific (non-standard) idShorts and versioned eCl@ss semanticIds (the form real IDTA submodels carry, e.g. 0173-1#02-AAW338#001). Extraction of productName/weight/carbonFootprint must succeed via the semanticId scan alone: the idShort-path fallback cannot match these names. Only 'granularity' (configured by idShortPath alone) keeps its standard idShorts.", + + "submodels": [ + + { + "modelType": "Submodel", + "id": "urn:example:sm:nameplate:smartphone-002", + "idShort": "VendorNameplate", + "semanticId": { + "type": "ExternalReference", + "keys": [{ "type": "GlobalReference", "value": "https://admin-shell.io/idta/nameplate/3/0/Nameplate" }] + }, + "submodelElements": [ + { + "modelType": "MultiLanguageProperty", + "idShort": "BrandName", + "semanticId": { + "type": "ExternalReference", + "keys": [{ "type": "GlobalReference", "value": "0173-1#02-AAO677#002" }] + }, + "value": [ + { "language": "en", "text": "Alpha Electronics Inc." } + ] + }, + { + "modelType": "MultiLanguageProperty", + "idShort": "ProductDesignation", + "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-002", + "idShort": "TechData", + "semanticId": { + "type": "ExternalReference", + "keys": [{ "type": "GlobalReference", "value": "https://admin-shell.io/ZVEI/TechnicalData/Submodel/1/2" }] + }, + "submodelElements": [ + { + "modelType": "SubmodelElementCollection", + "idShort": "GeneralProperties", + "semanticId": { + "type": "ExternalReference", + "keys": [{ "type": "GlobalReference", "value": "https://admin-shell.io/ZVEI/TechnicalData/MainSection/1/1" }] + }, + "value": [ + { + "modelType": "Property", + "idShort": "ProductNetWeight", + "semanticId": { + "type": "ExternalReference", + "keys": [{ "type": "GlobalReference", "value": "0173-1#02-AAB419#012" }] + }, + "valueType": "xs:decimal", + "value": "189.5" + }, + { + "modelType": "Property", + "idShort": "ProductNetWeightUnit", + "valueType": "xs:string", + "value": "g" + } + ] + } + ] + }, + + { + "modelType": "Submodel", + "id": "urn:example:sm:carbonfootprint:smartphone-002", + "idShort": "PCF", + "semanticId": { + "type": "ExternalReference", + "keys": [{ "type": "GlobalReference", "value": "https://admin-shell.io/idta/CarbonFootprint/CarbonFootprint/1/0" }] + }, + "submodelElements": [ + { + "modelType": "SubmodelElementList", + "idShort": "Footprints", + "semanticId": { + "type": "ExternalReference", + "keys": [{ "type": "GlobalReference", "value": "https://admin-shell.io/idta/CarbonFootprint/ProductCarbonFootprints/1/0" }] + }, + "typeValueListElement": "SubmodelElementCollection", + "value": [ + { + "modelType": "SubmodelElementCollection", + "idShort": "Footprint00", + "semanticId": { + "type": "ExternalReference", + "keys": [{ "type": "GlobalReference", "value": "https://admin-shell.io/idta/CarbonFootprint/ProductCarbonFootprint/1/0" }] + }, + "value": [ + { + "modelType": "Property", + "idShort": "CO2eq", + "semanticId": { + "type": "ExternalReference", + "keys": [{ "type": "GlobalReference", "value": "0173-1#02-ABG855#003" }] + }, + "valueType": "xs:decimal", + "value": "45.8" + }, + { + "modelType": "Property", + "idShort": "CO2eqUnit", + "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-002", + "idShort": "DPPAdministration", + "displayName": [{ "language": "en", "text": "DPP Administration Metadata" }], + "submodelElements": [ + { + "modelType": "Property", + "idShort": "UniqueDPPID", + "valueType": "xs:string", + "value": "urn:uuid:b2c3d4e5-f6a7-4b5c-9d0e-1f2a3b4c5d6e" + }, + { + "modelType": "Property", + "idShort": "Granularity", + "valueType": "xs:string", + "value": "product" + }, + { + "modelType": "Property", + "idShort": "Status", + "valueType": "xs:string", + "value": "Active" + } + ] + } + + ] +}