From 1722c961c40bd1002e7ecf04f3d9621f4d0f812e Mon Sep 17 00:00:00 2001 From: Ioannis Rosuochatzakis Date: Wed, 26 Aug 2026 22:43:47 +0200 Subject: [PATCH 1/3] TEDEFO-5142: Transpile EFX-1 selector-blocks to element paths --- pom.xml | 2 +- .../efx/sdk1/EfxExpressionTranslatorV1.java | 17 +++ .../sdk1/EfxExpressionTranslatorV1Test.java | 127 ++++++++++++++++++ 3 files changed, 145 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7a5f4b9..525baf7 100644 --- a/pom.xml +++ b/pom.xml @@ -46,7 +46,7 @@ ${project.build.directory}/eforms-sdk/antlr4 - 1.13.0 + 1.16.0-SNAPSHOT 2.0.0-SNAPSHOT 1.8.0-SNAPSHOT diff --git a/src/main/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1.java b/src/main/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1.java index 5f1ff96..4f8d9c5 100644 --- a/src/main/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1.java +++ b/src/main/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1.java @@ -1095,6 +1095,23 @@ public void exitSequenceFromFieldReference(SequenceFromFieldReferenceContext ctx } } + /** + * A selector-block yields the reference itself rather than its value: the value step that every + * other reference position applies is deliberately not composed here. The path is otherwise + * resolved exactly as it would be in an expression-block, relative to the declared context + * unless the author wrote it as an absolute reference. + */ + @Override + public void exitSelection(final SelectionContext ctx) { + if (ctx.attributeReference() != null) { + // attributeReference has no exit handler of its own, so the attribute step is composed here + // rather than globally, which would double-compose it for the scalar and sequence positions. + this.stack.push(this.script.composeFieldAttributeReference( + this.stack.pop(PathExpression.class), + ctx.attributeReference().Identifier().getText(), StringPath.class)); + } + } + @Override public void exitScalarFromAttributeReference(ScalarFromAttributeReferenceContext ctx) { this.stack.push(this.script.composeFieldAttributeReference(this.stack.pop(PathExpression.class), diff --git a/src/test/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1Test.java b/src/test/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1Test.java index b9647b6..dc0586b 100644 --- a/src/test/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1Test.java +++ b/src/test/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1Test.java @@ -1653,4 +1653,131 @@ void testParameterizedExpression_WithDurationParameter() { // #endregion: Compare sequences // #endregion Sequence Functions + + // #region: Selectors ------------------------------------------------------- + + /** + * A selector yields the same path an expression would use, without the value step that an + * expression appends. It is relative or absolute according to how the reference was written, + * exactly as in any other position in the language. + */ + @Test + void testSelector_YieldsTheSamePathAsTheEquivalentExpression() { + assertEquals(translateExpression("{ND-Root} ${BT-00-Text}"), + translateExpression("{ND-Root} &{BT-00-Text}") + "/normalize-space(text())"); + assertEquals(translateExpression("{ND-SubNode} ${BT-00-Text}"), + translateExpression("{ND-SubNode} &{BT-00-Text}") + "/normalize-space(text())"); + assertEquals(translateExpression("{ND-Root} ${/BT-00-Text}"), + translateExpression("{ND-Root} &{/BT-00-Text}") + "/normalize-space(text())"); + assertEquals(translateExpression("{ND-Root} ${BT-00-Integer}"), + translateExpression("{ND-Root} &{BT-00-Integer}") + "/number()"); + } + + @Test + void testSelector_FieldReference() { + testExpressionTranslation("PathNode/TextField", "{ND-Root} &{BT-00-Text}"); + } + + @Test + void testSelector_AbsoluteFieldReference() { + testExpressionTranslation("/*/PathNode/TextField", "{ND-Root} &{/BT-00-Text}"); + } + + @Test + void testSelector_WithPredicate() { + testExpressionTranslation( + "/*/PathNode/TextField[../CodeField/normalize-space(text()) = 'x']", + "{ND-Root} &{/BT-00-Text[BT-00-Code == 'x']}"); + } + + @Test + void testSelector_NumericFieldHasNoValueStep() { + testExpressionTranslation("PathNode/IntegerField", "{ND-Root} &{BT-00-Integer}"); + } + + @Test + void testSelector_DurationFieldHasNoValueStep() { + testExpressionTranslation("PathNode/DurationField", "{ND-Root} &{BT-00-Duration}"); + } + + @Test + void testSelector_IndicatorField() { + testExpressionTranslation("PathNode/IndicatorField", "{ND-Root} &{BT-00-Indicator}"); + } + + @Test + void testSelector_MultilingualField() { + testExpressionTranslation("PathNode/TextMultilingualField", + "{ND-Root} &{BT-00-Text-Multilingual}"); + } + + @Test + void testSelector_AttributeField() { + testExpressionTranslation("PathNode/TextField/@Attribute", "{ND-Root} &{BT-00-Attribute}"); + } + + @Test + void testSelector_ExplicitAttributeReference() { + testExpressionTranslation("/*/PathNode/TextField/@Attribute", + "{ND-Root} &{/BT-00-Text/@Attribute}"); + } + + @Test + void testSelector_NodeReference() { + testExpressionTranslation("SubNode", "{ND-Root} &{ND-SubNode}"); + } + + @Test + void testSelector_AbsoluteNodeReference() { + testExpressionTranslation("/*/SubNode", "{ND-Root} &{/ND-SubNode}"); + } + + @Test + void testSelector_FieldUnderAnotherNode() { + testExpressionTranslation("SubNode/SubTextField", "{ND-Root} &{BT-01-SubNode-Text}"); + } + + @Test + void testSelector_ContextIsTheReferencedFieldItself() { + testExpressionTranslation(".", "{BT-00-Text} &{BT-00-Text}"); + } + + @Test + void testSelector_ContextIsAnotherNode() { + testExpressionTranslation("SubTextField", "{ND-SubNode} &{BT-01-SubNode-Text}"); + testExpressionTranslation("../PathNode/TextField", "{ND-SubNode} &{BT-00-Text}"); + } + + @Test + void testSelector_ContextIsAField() { + testExpressionTranslation("../CodeField", "{BT-00-Text} &{BT-00-Code}"); + testExpressionTranslation("../../SubNode/SubTextField", "{BT-00-Text} &{BT-01-SubNode-Text}"); + } + + @Test + void testSelector_ContextIsAFieldWithPredicate() { + testExpressionTranslation(".[../CodeField/normalize-space(text()) = 'x']", + "{BT-00-Text} &{BT-00-Text[BT-00-Code == 'x']}"); + testExpressionTranslation( + "SubTextField[../../PathNode/CodeField/normalize-space(text()) = 'x']", + "{ND-SubNode} &{BT-01-SubNode-Text[BT-00-Code == 'x']}"); + } + + /** + * An absolute selector is obtained the same way as an absolute reference anywhere else: by + * writing the leading slash. Consumers that evaluate against a whole document need that. + */ + @Test + void testSelector_IsAbsoluteOnlyWhenWrittenAsAbsolute() { + testExpressionTranslation("/*/PathNode/TextField", "{ND-SubNode} &{/BT-00-Text}"); + testExpressionTranslation("/*/PathNode/CodeField", "{BT-00-Text} &{/BT-00-Code}"); + } + + @Test + void testSelector_RejectsValueExpression() { + assertThrows(ParseCancellationException.class, + () -> translateExpression("{ND-Root} &{BT-00-Text == 'x'}")); + } + + // #endregion: Selectors ---------------------------------------------------- } From c3caf9179250ac54653079e21a741e13aabfd78f Mon Sep 17 00:00:00 2001 From: Ioannis Rosuochatzakis Date: Wed, 26 Aug 2026 22:43:47 +0200 Subject: [PATCH 2/3] TEDEFO-5144: Transpile EFX-2 selectors to element paths --- .../efx/sdk2/EfxExpressionTranslatorV2.java | 19 +++ .../sdk2/EfxExpressionTranslatorV2Test.java | 109 ++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/src/main/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2.java b/src/main/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2.java index 1b00287..7180787 100644 --- a/src/main/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2.java +++ b/src/main/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2.java @@ -1919,6 +1919,25 @@ public void exitSequenceFromFieldReference(SequenceFromFieldReferenceContext ctx this.resolveAndPushFieldReference(ctx, result, fieldId); } + /** + * A selector yields the reference itself rather than its value: the value step that every other + * reference position applies is deliberately not composed here. The path is otherwise resolved + * exactly as it would be in an expression, relative to the declared context unless the author + * wrote it as an absolute reference. + * + *

The reference tier has already left the path on the stack. Only an attribute reference + * needs work, because {@code attributeReference} has no exit handler of its own; composing the + * attribute step globally would double-compose it for the scalar and sequence positions. + */ + @Override + public void exitSelection(final SelectionContext ctx) { + if (ctx.attributeReference() != null) { + this.stack.push(this.script.composeFieldAttributeReference( + this.stack.pop(PathExpression.class), + ctx.attributeReference().attributeName.getText(), StringPath.class)); + } + } + @Override public void exitScalarFromAttributeReference(ScalarFromAttributeReferenceContext ctx) { PathExpression result = this.script.composeFieldAttributeReference(this.stack.pop(PathExpression.class), diff --git a/src/test/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2Test.java b/src/test/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2Test.java index 7b8b103..eebde7e 100644 --- a/src/test/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2Test.java +++ b/src/test/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2Test.java @@ -4674,4 +4674,113 @@ void testCompute_CaseInsensitive() { } // #endregion: EFX-2 COMPUTE syntax + + // #region: Selectors ------------------------------------------------------- + + /** + * A selector yields the same path an expression would use, without the value step that an + * expression appends. It is relative or absolute according to how the reference was written. + */ + @Test + void testSelector_YieldsTheSamePathAsTheEquivalentExpression() { + assertEquals(translateExpression("WITH ND-Root COMPUTE BT-00-Text"), + translateExpression("WITH ND-Root SELECT BT-00-Text") + "/normalize-space(text())"); + assertEquals(translateExpression("WITH ND-Root COMPUTE BT-00-Integer"), + translateExpression("WITH ND-Root SELECT BT-00-Integer") + "/number()"); + assertEquals(translateExpression("WITH ND-SubNode COMPUTE BT-00-Text"), + translateExpression("WITH ND-SubNode SELECT BT-00-Text") + "/normalize-space(text())"); + } + + /** Both spellings of the selector must produce the same result. */ + @Test + void testSelector_BothSpellingsAgree() { + assertEquals(translateExpression("WITH ND-Root SELECT /BT-00-Text"), + translateExpression("{ND-Root} &{/BT-00-Text}")); + assertEquals(translateExpression("WITH ND-Root SELECT /BT-00-Text[BT-00-Code == 'x']"), + translateExpression("{ND-Root} &{/BT-00-Text[BT-00-Code == 'x']}")); + } + + @Test + void testSelector_FieldReference() { + testExpressionTranslation("PathNode/TextField", "WITH ND-Root SELECT BT-00-Text"); + } + + @Test + void testSelector_AbsoluteFieldReference() { + testExpressionTranslation("/*/PathNode/TextField", "WITH ND-Root SELECT /BT-00-Text"); + } + + @Test + void testSelector_WithPredicate() { + testExpressionTranslation("/*/PathNode/TextField[../CodeField/normalize-space(text()) = 'x']", + "WITH ND-Root SELECT /BT-00-Text[BT-00-Code == 'x']"); + } + + @Test + void testSelector_NumericFieldHasNoValueStep() { + testExpressionTranslation("PathNode/IntegerField", "WITH ND-Root SELECT BT-00-Integer"); + } + + @Test + void testSelector_DurationFieldHasNoValueStep() { + testExpressionTranslation("PathNode/DurationField", "WITH ND-Root SELECT BT-00-Duration"); + } + + @Test + void testSelector_NodeReference() { + testExpressionTranslation("SubNode", "WITH ND-Root SELECT ND-SubNode"); + testExpressionTranslation("/*/SubNode", "WITH ND-Root SELECT /ND-SubNode"); + } + + @Test + void testSelector_AttributeReference() { + testExpressionTranslation("/*/PathNode/TextField/@Attribute", + "WITH ND-Root SELECT /BT-00-Text/@Attribute"); + } + + @Test + void testSelector_IsRelativeToTheDeclaredContext() { + testExpressionTranslation("../PathNode/TextField", "WITH ND-SubNode SELECT BT-00-Text"); + testExpressionTranslation("/*/PathNode/TextField", "WITH ND-SubNode SELECT /BT-00-Text"); + } + + @Test + void testSelector_KeywordIsCaseInsensitive() { + assertEquals(translateExpression("WITH ND-Root SELECT BT-00-Text"), + translateExpression("with ND-Root select BT-00-Text")); + } + + @Test + void testSelector_RejectsValueExpression() { + assertThrows(ParseCancellationException.class, + () -> translateExpression("WITH ND-Root SELECT BT-00-Text == 'x'")); + } + + + /** + * A selector's indexer is a node-level indexer, like every other {@code fieldContext} in the + * language: it selects the nth occurrence within each parent, not the nth item of the sequence + * overall. This is why an indexed selector differs from an indexed expression, where the index + * applies to the sequence of values - the one case where a selector is not simply the expression + * without its value step. + * + *

Do not "correct" this into a parenthesised form. Indexing at node level is deliberate and is + * shared with {@code :rawValue}, context iterators and context overrides; changing it here alone + * would make the selector the odd one out, and changing it everywhere would redefine the language. + */ + @Test + void testSelector_IndexerAppliesAtNodeLevel() { + testExpressionTranslation("PathNode/TextField[1]", "WITH ND-Root SELECT BT-00-Text[1]"); + testExpressionTranslation("(PathNode/TextField/normalize-space(text()))[1]", + "WITH ND-Root COMPUTE BT-00-Text[1]"); + } + + @Test + void testSelector_IndexerAfterPredicate() { + testExpressionTranslation( + "/*/PathNode/TextField[../CodeField/normalize-space(text()) = 'x'][1]", + "WITH ND-Root SELECT /BT-00-Text[BT-00-Code == 'x'][1]"); + } + + // #endregion: Selectors ---------------------------------------------------- } From ff783b34de97aef0e13782308b4f406a143e41d4 Mon Sep 17 00:00:00 2001 From: Ioannis Rosuochatzakis Date: Wed, 26 Aug 2026 22:53:02 +0200 Subject: [PATCH 3/3] TEDEFO-5144: Cover selectors in the compute dependency extractor tests --- .../EfxComputeDependencyExtractorTest.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/test/java/eu/europa/ted/efx/sdk2/EfxComputeDependencyExtractorTest.java b/src/test/java/eu/europa/ted/efx/sdk2/EfxComputeDependencyExtractorTest.java index 89545dc..37b51b1 100644 --- a/src/test/java/eu/europa/ted/efx/sdk2/EfxComputeDependencyExtractorTest.java +++ b/src/test/java/eu/europa/ted/efx/sdk2/EfxComputeDependencyExtractorTest.java @@ -132,4 +132,41 @@ void testDuplicateFieldReferences() { } // #endregion: Deduplication + + // #region: Selectors ------------------------------------------------------- + + /** + * The extractor parses the same top-level rule as the translators, so it sees the selector + * alternative added to the grammar although it has no handler of its own for it. It nevertheless + * derives the correct dependencies, because it listens to the reference exits rather than to the + * top-level block. These tests record that, so that the behaviour is not lost inadvertently. + */ + @Test + void testSelector_YieldsTheSameDependenciesAsTheEquivalentExpression() { + assertEquals(extract("WITH ND-Root COMPUTE BT-00-Text"), + extract("WITH ND-Root SELECT BT-00-Text")); + } + + @Test + void testSelector_IncludesReferencesFromThePredicate() { + Set deps = extract("WITH ND-Root SELECT /BT-00-Text[BT-00-Code == 'x']"); + assertTrue(deps.contains("BT-00-Text")); + assertTrue(deps.contains("BT-00-Code")); + assertTrue(deps.contains("ND-Root")); + } + + @Test + void testSelector_IncludesANodeReference() { + Set deps = extract("WITH ND-Root SELECT ND-SubNode"); + assertTrue(deps.contains("ND-SubNode")); + assertTrue(deps.contains("ND-Root")); + } + + @Test + void testSelector_BraceSpellingBehavesTheSame() { + assertEquals(extract("WITH ND-Root SELECT /BT-00-Text"), + extract("{ND-Root} &{/BT-00-Text}")); + } + + // #endregion: Selectors ---------------------------------------------------- }