From a58966b1ec39f98701b2c43afa0917cd317e356b Mon Sep 17 00:00:00 2001 From: Ioannis Rosuochatzakis Date: Mon, 12 Jan 2026 01:48:04 +0100 Subject: [PATCH 1/4] TEDEFO-4821: Add hierarchy traversal and repeatability to SDK entities - SdkNode: Add parent reference and getAncestry() with caching - SdkField: Add repeatable property and parentNode reference - SdkNodeRepository: Two-pass loading to wire parent relationships - SdkFieldRepository: Optional parentNode wiring via SdkNodeRepository - MapFromJson: Context-aware constructor for dependency injection --- .../ted/eforms/sdk/entity/SdkField.java | 36 +++++++++++++++++++ .../europa/ted/eforms/sdk/entity/SdkNode.java | 29 ++++++++++++++- .../eforms/sdk/repository/MapFromJson.java | 29 +++++++++++++-- .../sdk/repository/SdkFieldRepository.java | 19 ++++++++++ .../sdk/repository/SdkNodeRepository.java | 19 ++++++++++ .../component/SdkComponentFactoryTest.java | 6 +--- 6 files changed, 130 insertions(+), 8 deletions(-) diff --git a/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkField.java b/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkField.java index 9213798..50aa16c 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkField.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkField.java @@ -10,6 +10,8 @@ public abstract class SdkField implements Comparable { private final String parentNodeId; private final String type; private final String codelistId; + private final boolean repeatable; + private SdkNode parentNode; @SuppressWarnings("unused") private SdkField() { @@ -18,12 +20,19 @@ private SdkField() { protected SdkField(final String id, final String type, final String parentNodeId, final String xpathAbsolute, final String xpathRelative, final String codelistId) { + this(id, type, parentNodeId, xpathAbsolute, xpathRelative, codelistId, false); + } + + protected SdkField(final String id, final String type, final String parentNodeId, + final String xpathAbsolute, final String xpathRelative, final String codelistId, + final boolean repeatable) { this.id = id; this.parentNodeId = parentNodeId; this.xpathAbsolute = xpathAbsolute; this.xpathRelative = xpathRelative; this.type = type; this.codelistId = codelistId; + this.repeatable = repeatable; } protected SdkField(final JsonNode fieldNode) { @@ -33,6 +42,7 @@ protected SdkField(final JsonNode fieldNode) { this.xpathRelative = fieldNode.get("xpathRelative").asText(null); this.type = fieldNode.get("type").asText(null); this.codelistId = extractCodelistId(fieldNode); + this.repeatable = extractRepeatable(fieldNode); } protected String extractCodelistId(final JsonNode fieldNode) { @@ -49,6 +59,20 @@ protected String extractCodelistId(final JsonNode fieldNode) { return valueNode.get("id").asText(null); } + protected boolean extractRepeatable(final JsonNode fieldNode) { + final JsonNode repeatableNode = fieldNode.get("repeatable"); + if (repeatableNode == null) { + return false; + } + + final JsonNode valueNode = repeatableNode.get("value"); + if (valueNode == null) { + return false; + } + + return valueNode.asBoolean(false); + } + public String getId() { return id; } @@ -73,6 +97,18 @@ public String getCodelistId() { return codelistId; } + public boolean isRepeatable() { + return repeatable; + } + + public SdkNode getParentNode() { + return parentNode; + } + + public void setParentNode(SdkNode parentNode) { + this.parentNode = parentNode; + } + /** * Helps with hash maps collisions. Should be consistent with equals. */ diff --git a/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkNode.java b/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkNode.java index 9166eaf..6cee2d2 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkNode.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkNode.java @@ -1,6 +1,9 @@ package eu.europa.ted.eforms.sdk.entity; +import java.util.Collections; +import java.util.LinkedHashSet; import java.util.Objects; +import java.util.Set; import com.fasterxml.jackson.databind.JsonNode; /** @@ -12,6 +15,8 @@ public abstract class SdkNode implements Comparable { private final String xpathRelative; private final String parentId; private final boolean repeatable; + private SdkNode parent; + private Set cachedAncestry; protected SdkNode(final String id, final String parentId, final String xpathAbsolute, final String xpathRelative, final boolean repeatable) { @@ -51,9 +56,31 @@ public boolean isRepeatable() { return repeatable; } + public SdkNode getParent() { + return parent; + } + + public void setParent(SdkNode parent) { + this.parent = parent; + this.cachedAncestry = null; + } + + public Set getAncestry() { + if (cachedAncestry == null) { + Set ancestry = new LinkedHashSet<>(); + SdkNode current = this; + while (current != null) { + ancestry.add(current.getId()); + current = current.getParent(); + } + cachedAncestry = Collections.unmodifiableSet(ancestry); + } + return cachedAncestry; + } + @Override public int compareTo(SdkNode o) { - return o.getId().compareTo(o.getId()); + return this.getId().compareTo(o.getId()); } @Override diff --git a/src/main/java/eu/europa/ted/eforms/sdk/repository/MapFromJson.java b/src/main/java/eu/europa/ted/eforms/sdk/repository/MapFromJson.java index 1badd9b..11cdd54 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/repository/MapFromJson.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/repository/MapFromJson.java @@ -39,7 +39,20 @@ protected MapFromJson(final String sdkVersion, final Path jsonPath) } } - private final void populateMap(final Path jsonPath) throws IOException, InstantiationException { + protected MapFromJson(final String sdkVersion, final Path jsonPath, final Object... context) + throws InstantiationException { + this.sdkVersion = sdkVersion; + + try { + populateMap(jsonPath, context); + } catch (IOException e) { + throw new RuntimeException(MessageFormat + .format("Failed to set resource filepath to [{0}]. Error was: {1}", jsonPath, e)); + } + } + + private final void populateMap(final Path jsonPath, final Object... context) + throws IOException, InstantiationException { logger.debug("Populating maps for context, jsonPath={}", jsonPath); final ObjectMapper mapper = buildStandardJacksonObjectMapper(); @@ -54,12 +67,24 @@ private final void populateMap(final Path jsonPath) throws IOException, Instanti } final JsonNode json = mapper.readTree(fieldsJsonInputStream); - populateMap(json); + populateMap(json, context); } } + /** + * Abstract method for populating the map from JSON. Existing subclasses implement this. + */ protected abstract void populateMap(final JsonNode json) throws InstantiationException; + /** + * Context-aware population method. Default implementation delegates to the abstract method, + * ignoring context. Subclasses that need context should override this method. + */ + protected void populateMap(final JsonNode json, final Object... context) + throws InstantiationException { + populateMap(json); + } + /** * @return A reusable Jackson object mapper instance. */ diff --git a/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkFieldRepository.java b/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkFieldRepository.java index e901fc9..670bad5 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkFieldRepository.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkFieldRepository.java @@ -14,12 +14,31 @@ public SdkFieldRepository(String sdkVersion, Path jsonPath) throws Instantiation super(sdkVersion, jsonPath); } + public SdkFieldRepository(String sdkVersion, Path jsonPath, SdkNodeRepository nodeRepository) + throws InstantiationException { + super(sdkVersion, jsonPath, nodeRepository); + } + @Override protected void populateMap(final JsonNode json) throws InstantiationException { + populateMap(json, new Object[0]); + } + + @Override + protected void populateMap(final JsonNode json, final Object... context) + throws InstantiationException { + SdkNodeRepository nodes = (context.length > 0 && context[0] instanceof SdkNodeRepository) + ? (SdkNodeRepository) context[0] + : null; + final ArrayNode fields = (ArrayNode) json.get(SdkConstants.FIELDS_JSON_FIELDS_KEY); for (final JsonNode field : fields) { final SdkField sdkField = SdkEntityFactory.getSdkField(sdkVersion, field); put(sdkField.getId(), sdkField); + + if (nodes != null && sdkField.getParentNodeId() != null) { + sdkField.setParentNode(nodes.get(sdkField.getParentNodeId())); + } } } } diff --git a/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkNodeRepository.java b/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkNodeRepository.java index adc2e56..c853153 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkNodeRepository.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkNodeRepository.java @@ -1,6 +1,8 @@ package eu.europa.ted.eforms.sdk.repository; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import eu.europa.ted.eforms.sdk.SdkConstants; @@ -17,9 +19,26 @@ public SdkNodeRepository(String sdkVersion, Path jsonPath) throws InstantiationE @Override protected void populateMap(final JsonNode json) throws InstantiationException { final ArrayNode nodes = (ArrayNode) json.get(SdkConstants.FIELDS_JSON_XML_STRUCTURE_KEY); + List needsParentWiring = new ArrayList<>(); + + // First pass: create all nodes, optimistically set parent if already loaded for (final JsonNode node : nodes) { final SdkNode sdkNode = SdkEntityFactory.getSdkNode(sdkVersion, node); put(sdkNode.getId(), sdkNode); + + if (sdkNode.getParentId() != null) { + SdkNode parent = get(sdkNode.getParentId()); + if (parent != null) { + sdkNode.setParent(parent); + } else { + needsParentWiring.add(sdkNode); + } + } + } + + // Second pass: wire up any nodes whose parent wasn't loaded yet + for (SdkNode sdkNode : needsParentWiring) { + sdkNode.setParent(get(sdkNode.getParentId())); } } } diff --git a/src/test/java/eu/europa/ted/eforms/sdk/component/SdkComponentFactoryTest.java b/src/test/java/eu/europa/ted/eforms/sdk/component/SdkComponentFactoryTest.java index 4153763..2fca295 100644 --- a/src/test/java/eu/europa/ted/eforms/sdk/component/SdkComponentFactoryTest.java +++ b/src/test/java/eu/europa/ted/eforms/sdk/component/SdkComponentFactoryTest.java @@ -44,7 +44,7 @@ void testComponentNotFound() throws InstantiationException { // No component for this type assertThrows(IllegalArgumentException.class, () -> - factory.getComponentImpl("1.0", SdkComponentType.CODELIST, TestComponent.class)); + factory.getComponentImpl("1.0", SdkComponentType.VALIDATOR_GENERATOR, TestComponent.class)); // No component for this qualifier assertThrows(IllegalArgumentException.class, () -> @@ -53,9 +53,5 @@ void testComponentNotFound() throws InstantiationException { // Only component for this version and type does not have a qualifier assertThrows(IllegalArgumentException.class, () -> factory.getComponentImpl("0.5", SdkComponentType.EFX_EXPRESSION_TRANSLATOR, "BAD", TestComponent.class)); - - // Only component for this version and type has a qualifier - assertThrows(IllegalArgumentException.class, () -> - factory.getComponentImpl("1.0", SdkComponentType.NODE, TestComponent.class)); } } From 57087cac3bb39baeb4a86b9e1d39b4f912835462 Mon Sep 17 00:00:00 2001 From: Ioannis Rosuochatzakis Date: Mon, 12 Jan 2026 01:50:36 +0100 Subject: [PATCH 2/4] TEDEFO-2129: Move versioned SDK entity classes from EXT to ECL --- .../eforms/sdk/entity/v1/SdkCodelistV1.java | 21 ++++++ .../ted/eforms/sdk/entity/v1/SdkFieldV1.java | 65 +++++++++++++++++++ .../ted/eforms/sdk/entity/v1/SdkNodeV1.java | 29 +++++++++ .../sdk/entity/v1/SdkNoticeSubtypeV1.java | 21 ++++++ .../eforms/sdk/entity/v2/SdkCodelistV2.java | 21 ++++++ .../ted/eforms/sdk/entity/v2/SdkFieldV2.java | 43 ++++++++++++ .../ted/eforms/sdk/entity/v2/SdkNodeV2.java | 37 +++++++++++ .../sdk/entity/v2/SdkNoticeSubtypeV2.java | 21 ++++++ 8 files changed, 258 insertions(+) create mode 100644 src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkCodelistV1.java create mode 100644 src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkFieldV1.java create mode 100644 src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkNodeV1.java create mode 100644 src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkNoticeSubtypeV1.java create mode 100644 src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkCodelistV2.java create mode 100644 src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkFieldV2.java create mode 100644 src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkNodeV2.java create mode 100644 src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkNoticeSubtypeV2.java diff --git a/src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkCodelistV1.java b/src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkCodelistV1.java new file mode 100644 index 0000000..d186edc --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkCodelistV1.java @@ -0,0 +1,21 @@ +package eu.europa.ted.eforms.sdk.entity.v1; + +import java.util.List; +import java.util.Optional; +import eu.europa.ted.eforms.sdk.component.SdkComponent; +import eu.europa.ted.eforms.sdk.component.SdkComponentType; +import eu.europa.ted.eforms.sdk.entity.SdkCodelist; + +/** + * Representation of an SdkCodelist for usage in the symbols map. + * + * @author rouschr + */ +@SdkComponent(versions = {"1"}, componentType = SdkComponentType.CODELIST) +public class SdkCodelistV1 extends SdkCodelist { + + public SdkCodelistV1(final String codelistId, final String codelistVersion, + final List codes, final Optional parentId) { + super(codelistId, codelistVersion, codes, parentId); + } +} diff --git a/src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkFieldV1.java b/src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkFieldV1.java new file mode 100644 index 0000000..0a7457b --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkFieldV1.java @@ -0,0 +1,65 @@ +package eu.europa.ted.eforms.sdk.entity.v1; + +import java.util.Map; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import eu.europa.ted.eforms.sdk.component.SdkComponent; +import eu.europa.ted.eforms.sdk.component.SdkComponentType; +import eu.europa.ted.eforms.sdk.entity.SdkField; + +@SdkComponent(versions = {"1"}, componentType = SdkComponentType.FIELD) +public class SdkFieldV1 extends SdkField { + + public SdkFieldV1(final String id, final String type, final String parentNodeId, + final String xpathAbsolute, final String xpathRelative, final String codelistId, + final boolean repeatable) { + super(id, type, parentNodeId, xpathAbsolute, xpathRelative, codelistId, repeatable); + } + + public SdkFieldV1(final JsonNode field) { + super(field); + } + + @JsonCreator + public SdkFieldV1( + @JsonProperty("id") final String id, + @JsonProperty("type") final String type, + @JsonProperty("parentNodeId") final String parentNodeId, + @JsonProperty("xpathAbsolute") final String xpathAbsolute, + @JsonProperty("xpathRelative") final String xpathRelative, + @JsonProperty("codeList") final Map> codelist, + @JsonProperty("repeatable") final Map repeatable) { + this(id, type, parentNodeId, xpathAbsolute, xpathRelative, getCodelistId(codelist), + getRepeatable(repeatable)); + } + + protected static String getCodelistId(Map> codelist) { + if (codelist == null) { + return null; + } + + Map value = codelist.get("value"); + if (value == null) { + return null; + } + + return value.get("id"); + } + + protected static boolean getRepeatable(Map repeatable) { + if (repeatable == null) { + return false; + } + + // If there are constraints, the field may repeat conditionally - treat as repeatable + Object constraints = repeatable.get("constraints"); + if (constraints != null) { + return true; + } + + // Otherwise check the default value + Object value = repeatable.get("value"); + return Boolean.TRUE.equals(value); + } +} diff --git a/src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkNodeV1.java b/src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkNodeV1.java new file mode 100644 index 0000000..69de45c --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkNodeV1.java @@ -0,0 +1,29 @@ +package eu.europa.ted.eforms.sdk.entity.v1; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import eu.europa.ted.eforms.sdk.component.SdkComponent; +import eu.europa.ted.eforms.sdk.component.SdkComponentType; +import eu.europa.ted.eforms.sdk.entity.SdkNode; + +/** + * A node is something like a section. Nodes can be parents of other nodes or parents of fields. + */ +@SdkComponent(versions = {"1"}, componentType = SdkComponentType.NODE) +public class SdkNodeV1 extends SdkNode { + + @JsonCreator + public SdkNodeV1( + @JsonProperty("id") String id, + @JsonProperty("parentId") String parentId, + @JsonProperty("xpathAbsolute") String xpathAbsolute, + @JsonProperty("xpathRelative") String xpathRelative, + @JsonProperty("repeatable") boolean repeatable) { + super(id, parentId, xpathAbsolute, xpathRelative, repeatable); + } + + public SdkNodeV1(JsonNode node) { + super(node); + } +} diff --git a/src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkNoticeSubtypeV1.java b/src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkNoticeSubtypeV1.java new file mode 100644 index 0000000..2175b03 --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkNoticeSubtypeV1.java @@ -0,0 +1,21 @@ +package eu.europa.ted.eforms.sdk.entity.v1; + +import com.fasterxml.jackson.databind.JsonNode; +import eu.europa.ted.eforms.sdk.component.SdkComponent; +import eu.europa.ted.eforms.sdk.component.SdkComponentType; +import eu.europa.ted.eforms.sdk.entity.SdkNoticeSubtype; + +/** + * Represents a notice subtype from the SDK's notice-types.json file. + */ +@SdkComponent(versions = {"1"}, componentType = SdkComponentType.NOTICE_TYPE) +public class SdkNoticeSubtypeV1 extends SdkNoticeSubtype { + + public SdkNoticeSubtypeV1(String subTypeId, String documentType, String type) { + super(subTypeId, documentType, type); + } + + public SdkNoticeSubtypeV1(JsonNode json) { + super(json); + } +} diff --git a/src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkCodelistV2.java b/src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkCodelistV2.java new file mode 100644 index 0000000..bc4249d --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkCodelistV2.java @@ -0,0 +1,21 @@ +package eu.europa.ted.eforms.sdk.entity.v2; + +import java.util.List; +import java.util.Optional; +import eu.europa.ted.eforms.sdk.component.SdkComponent; +import eu.europa.ted.eforms.sdk.component.SdkComponentType; +import eu.europa.ted.eforms.sdk.entity.v1.SdkCodelistV1; + +/** + * Representation of an SdkCodelist for usage in the symbols map. + * + * @author rouschr + */ +@SdkComponent(versions = {"2"}, componentType = SdkComponentType.CODELIST) +public class SdkCodelistV2 extends SdkCodelistV1 { + + public SdkCodelistV2(final String codelistId, final String codelistVersion, + final List codes, final Optional parentId) { + super(codelistId, codelistVersion, codes, parentId); + } +} diff --git a/src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkFieldV2.java b/src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkFieldV2.java new file mode 100644 index 0000000..d69ad30 --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkFieldV2.java @@ -0,0 +1,43 @@ +package eu.europa.ted.eforms.sdk.entity.v2; + +import java.util.Map; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import eu.europa.ted.eforms.sdk.component.SdkComponent; +import eu.europa.ted.eforms.sdk.component.SdkComponentType; +import eu.europa.ted.eforms.sdk.entity.v1.SdkFieldV1; + +@SdkComponent(versions = {"2"}, componentType = SdkComponentType.FIELD) +public class SdkFieldV2 extends SdkFieldV1 { + private final String alias; + + public SdkFieldV2(String id, String type, String parentNodeId, String xpathAbsolute, + String xpathRelative, String rootCodelistId, boolean repeatable, String alias) { + super(id, type, parentNodeId, xpathAbsolute, xpathRelative, rootCodelistId, repeatable); + this.alias = alias; + } + + public SdkFieldV2(JsonNode fieldNode) { + super(fieldNode); + this.alias = fieldNode.has("alias") ? fieldNode.get("alias").asText(null) : null; + } + + @JsonCreator + public SdkFieldV2( + @JsonProperty("id") final String id, + @JsonProperty("type") final String type, + @JsonProperty("parentNodeId") final String parentNodeId, + @JsonProperty("xpathAbsolute") final String xpathAbsolute, + @JsonProperty("xpathRelative") final String xpathRelative, + @JsonProperty("codeList") final Map> codelist, + @JsonProperty("repeatable") final Map repeatable, + @JsonProperty("alias") final String alias) { + this(id, type, parentNodeId, xpathAbsolute, xpathRelative, getCodelistId(codelist), + getRepeatable(repeatable), alias); + } + + public String getAlias() { + return alias; + } +} diff --git a/src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkNodeV2.java b/src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkNodeV2.java new file mode 100644 index 0000000..9d98cee --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkNodeV2.java @@ -0,0 +1,37 @@ +package eu.europa.ted.eforms.sdk.entity.v2; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import eu.europa.ted.eforms.sdk.component.SdkComponent; +import eu.europa.ted.eforms.sdk.component.SdkComponentType; +import eu.europa.ted.eforms.sdk.entity.v1.SdkNodeV1; + +/** + * A node is something like a section. Nodes can be parents of other nodes or parents of fields. + */ +@SdkComponent(versions = {"2"}, componentType = SdkComponentType.NODE) +public class SdkNodeV2 extends SdkNodeV1 { + private final String alias; + + @JsonCreator + public SdkNodeV2( + @JsonProperty("id") String id, + @JsonProperty("parentId") String parentId, + @JsonProperty("xpathAbsolute") String xpathAbsolute, + @JsonProperty("xpathRelative") String xpathRelative, + @JsonProperty("repeatable") boolean repeatable, + @JsonProperty("alias") String alias) { + super(id, parentId, xpathAbsolute, xpathRelative, repeatable); + this.alias = alias; + } + + public SdkNodeV2(JsonNode node) { + super(node); + this.alias = node.has("alias") ? node.get("alias").asText(null) : null; + } + + public String getAlias() { + return alias; + } +} diff --git a/src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkNoticeSubtypeV2.java b/src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkNoticeSubtypeV2.java new file mode 100644 index 0000000..086a29c --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkNoticeSubtypeV2.java @@ -0,0 +1,21 @@ +package eu.europa.ted.eforms.sdk.entity.v2; + +import com.fasterxml.jackson.databind.JsonNode; +import eu.europa.ted.eforms.sdk.component.SdkComponent; +import eu.europa.ted.eforms.sdk.component.SdkComponentType; +import eu.europa.ted.eforms.sdk.entity.v1.SdkNoticeSubtypeV1; + +/** + * Represents a notice subtype from the SDK's notice-types.json file. + */ +@SdkComponent(versions = {"2"}, componentType = SdkComponentType.NOTICE_TYPE) +public class SdkNoticeSubtypeV2 extends SdkNoticeSubtypeV1 { + + public SdkNoticeSubtypeV2(String subTypeId, String documentType, String type) { + super(subTypeId, documentType, type); + } + + public SdkNoticeSubtypeV2(JsonNode json) { + super(json); + } +} From 047396dcbd2dd5028a79e11b10591d6291e75217 Mon Sep 17 00:00:00 2001 From: Ioannis Rosuochatzakis Date: Mon, 12 Jan 2026 09:39:28 +0100 Subject: [PATCH 3/4] TEDEFO-4822 Add getXpathInfo() to SdkField Lazy-initialized XPathInfo provides parsed XPath structure. --- .../eu/europa/ted/eforms/sdk/entity/SdkField.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkField.java b/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkField.java index 50aa16c..88c2613 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkField.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkField.java @@ -2,6 +2,8 @@ import java.util.Objects; import com.fasterxml.jackson.databind.JsonNode; +import eu.europa.ted.eforms.xpath.XPathInfo; +import eu.europa.ted.eforms.xpath.XPathProcessor; public abstract class SdkField implements Comparable { private final String id; @@ -12,6 +14,7 @@ public abstract class SdkField implements Comparable { private final String codelistId; private final boolean repeatable; private SdkNode parentNode; + private XPathInfo xpathInfo; @SuppressWarnings("unused") private SdkField() { @@ -109,6 +112,18 @@ public void setParentNode(SdkNode parentNode) { this.parentNode = parentNode; } + /** + * Returns parsed XPath information for this field. + * Provides access to attribute info, path decomposition, and predicate checks. + * Lazily initialized on first access. + */ + public XPathInfo getXpathInfo() { + if (this.xpathInfo == null) { + this.xpathInfo = XPathProcessor.parse(this.xpathAbsolute); + } + return this.xpathInfo; + } + /** * Helps with hash maps collisions. Should be consistent with equals. */ From c23a05e502ac76e90417a4381148c9e2e1a34970 Mon Sep 17 00:00:00 2001 From: Ioannis Rosuochatzakis Date: Tue, 3 Feb 2026 15:37:05 +0100 Subject: [PATCH 4/4] Change getAncestry() return type from Set to List for clearer ordering semantics --- .../europa/ted/eforms/sdk/entity/SdkNode.java | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkNode.java b/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkNode.java index 6cee2d2..1057b4a 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkNode.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkNode.java @@ -1,9 +1,9 @@ package eu.europa.ted.eforms.sdk.entity; +import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedHashSet; +import java.util.List; import java.util.Objects; -import java.util.Set; import com.fasterxml.jackson.databind.JsonNode; /** @@ -16,7 +16,7 @@ public abstract class SdkNode implements Comparable { private final String parentId; private final boolean repeatable; private SdkNode parent; - private Set cachedAncestry; + private List cachedAncestry; protected SdkNode(final String id, final String parentId, final String xpathAbsolute, final String xpathRelative, final boolean repeatable) { @@ -60,20 +60,35 @@ public SdkNode getParent() { return parent; } + /** + * Sets the parent node and invalidates the cached ancestry. + * Should only be called during SDK initialization (two-pass loading). + * + * @param parent the parent node + */ public void setParent(SdkNode parent) { this.parent = parent; this.cachedAncestry = null; } - public Set getAncestry() { + /** + * Returns the ancestry chain from this node to the root. + * The list includes this node as the first element, followed by its parent, + * grandparent, and so on up to the root node. + * + * The result is cached and recomputed only when the parent changes. + * + * @return unmodifiable list of node IDs ordered from child (this node) to root + */ + public List getAncestry() { if (cachedAncestry == null) { - Set ancestry = new LinkedHashSet<>(); + List ancestry = new ArrayList<>(); SdkNode current = this; while (current != null) { ancestry.add(current.getId()); current = current.getParent(); } - cachedAncestry = Collections.unmodifiableSet(ancestry); + cachedAncestry = Collections.unmodifiableList(ancestry); } return cachedAncestry; }