Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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";

Expand Down Expand Up @@ -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:
*
* <ul>
* <li>a leading IRI prefix on the AAS value is tolerated via {@code endsWith};
* <li>a trailing eCl@ss/IRDI version suffix on the AAS value (e.g. {@code
* 0173-1#02-AAO677#002}) is tolerated by also comparing the value with that suffix
* stripped — {@code endsWith} alone cannot see past it.
* </ul>
*/
private boolean matchesSemanticId(JsonObject element, String targetSemanticId) {
JsonObject semanticIdObj = getObject(element, SEMANTIC_ID);
Expand All @@ -171,17 +181,34 @@ 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;
}
}
}
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
// -------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Map<String, Object>> 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 {
Expand Down
183 changes: 183 additions & 0 deletions core/src/test/resources/example-dpp/dpp-aas-versioned.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}

]
}
Loading