From 2854c49e1f228eb3f3f80115ea1257b0e4212324 Mon Sep 17 00:00:00 2001 From: Ioannis Rosuochatzakis Date: Fri, 17 Jul 2026 12:41:57 +0200 Subject: [PATCH 01/17] pom: Bump version to 2.0.0-SNAPSHOT and update dependencies --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 6971167..7a5f4b9 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ eu.europa.ted.eforms efx-toolkit-java - 2.0.0-alpha.7 + 2.0.0-SNAPSHOT jar EFX Toolkit for Java @@ -47,8 +47,8 @@ 1.13.0 - 2.0.0-alpha.2 - 1.7.0 + 2.0.0-SNAPSHOT + 1.8.0-SNAPSHOT 4.13.1 From e9d1d97539b860c8cf7a4b8ceab0b56ca4abb17c Mon Sep 17 00:00:00 2001 From: Ioannis Rosuochatzakis Date: Fri, 31 Jul 2026 20:14:05 +0200 Subject: [PATCH 02/17] TEDEFO-5133: Select preferred language only when translating view templates --- .../ted/efx/sdk1/EfxTemplateTranslatorV1.java | 54 +++++++++++++++++- .../sdk1/xpath/XPathScriptGeneratorV1.java | 57 +++++++++---------- .../java/eu/europa/ted/efx/EfxTestsBase.java | 45 +-------------- .../sdk1/EfxExpressionTranslatorV1Test.java | 16 +++++- .../efx/sdk1/EfxTemplateTranslatorV1Test.java | 47 +++++++++++++++ 5 files changed, 143 insertions(+), 76 deletions(-) diff --git a/src/main/java/eu/europa/ted/efx/sdk1/EfxTemplateTranslatorV1.java b/src/main/java/eu/europa/ted/efx/sdk1/EfxTemplateTranslatorV1.java index 286a695..50bce68 100644 --- a/src/main/java/eu/europa/ted/efx/sdk1/EfxTemplateTranslatorV1.java +++ b/src/main/java/eu/europa/ted/efx/sdk1/EfxTemplateTranslatorV1.java @@ -359,7 +359,7 @@ private void shorthandIndirectLabelReference(ParserRuleContext ctx, final String ? this.script.composeFieldAttributeReference( this.script.contextualizePath(this.symbols.getAbsolutePathOfFieldWithoutTheAttribute(fieldId), currentContext.absolutePath()), this.symbols.getAttributeNameFromAttributeField(fieldId), StringPath.class) - : this.script.composeFieldValueReference( + : this.composeFieldValueReference( this.symbols.getRelativePathOfField(fieldId, currentContext.symbol())); Variable loopVariable = new Variable("item", this.script.composeVariableDeclaration("item", StringExpression.class), StringExpression.empty(), @@ -532,12 +532,60 @@ public void exitShorthandFieldValueReferenceFromContextField( if (!this.efxContext.isFieldContext()) { throw InvalidUsageException.shorthandRequiresFieldContext(ctx, "$value"); } - this.stack.push(this.script.composeFieldValueReference( + this.stack.push(this.composeFieldValueReference( this.symbols.getRelativePathOfField(this.efxContext.symbol(), this.efxContext.symbol()))); } // #endregion Expression Blocks ${...} -------------------------------------- - + + // #region Value References ------------------------------------------------- + + /*** + * Multilingual fields are handled by this class, so the value reference is composed here instead + * of directly by the script generator. Anything else is left to the inherited behaviour. + * + * @see #composeFieldValueReference(PathExpression) + */ + @Override + public void exitScalarFromFieldReference(final ScalarFromFieldReferenceContext ctx) { + if (!this.stack.peekType().is(EfxDataType.MultilingualString.class)) { + super.exitScalarFromFieldReference(ctx); + return; + } + this.stack.push(this.composeFieldValueReference(this.stack.pop(PathExpression.class))); + } + + /*** + * @see #exitScalarFromFieldReference(ScalarFromFieldReferenceContext) + */ + @Override + public void exitSequenceFromFieldReference(final SequenceFromFieldReferenceContext ctx) { + if (!this.stack.peekType().is(EfxDataType.MultilingualString.class)) { + super.exitSequenceFromFieldReference(ctx); + return; + } + this.stack.push(this.composeFieldValueReference(this.stack.pop(PathExpression.class))); + } + + /*** + * In a view template the value of a multilingual field must be rendered in the language preferred + * by the reader, which EFX-1 gives the template author no syntax to ask for. Template translation + * therefore selects the preferred language implicitly, for every multilingual field it renders. + * + * Outside of view templates no such selection is possible: the function that performs it is + * provided by the XSL of the notice viewer and exists nowhere else. There the value of a + * multilingual field is retrieved like that of any other text field. + */ + private PathExpression composeFieldValueReference(final PathExpression fieldReference) { + if (fieldReference.is(EfxDataType.MultilingualString.class)) { + return Expression.from(this.script.getTextInPreferredLanguage(fieldReference), + fieldReference.getClass()); + } + return this.script.composeFieldValueReference(fieldReference); + } + + // #endregion Value References ---------------------------------------------- + // #region Context Declaration Blocks {...} --------------------------------- /** diff --git a/src/main/java/eu/europa/ted/efx/sdk1/xpath/XPathScriptGeneratorV1.java b/src/main/java/eu/europa/ted/efx/sdk1/xpath/XPathScriptGeneratorV1.java index 3839138..1a90ac4 100644 --- a/src/main/java/eu/europa/ted/efx/sdk1/xpath/XPathScriptGeneratorV1.java +++ b/src/main/java/eu/europa/ted/efx/sdk1/xpath/XPathScriptGeneratorV1.java @@ -25,7 +25,6 @@ import eu.europa.ted.efx.model.expressions.scalar.NumericExpression; import eu.europa.ted.efx.model.expressions.scalar.StringExpression; import eu.europa.ted.efx.model.expressions.scalar.StringLiteral; -import eu.europa.ted.efx.model.types.EfxDataType; import eu.europa.ted.efx.xpath.XPathScriptGenerator; @SdkComponent(versions = {"1"}, componentType = SdkComponentType.SCRIPT_GENERATOR) @@ -48,29 +47,6 @@ public StringExpression composeToStringConversion(NumericExpression number) { return new StringExpression("format-number(" + number.getScript() + ", '" + formatString + "')"); } - /*** - * This method is overridden to workaround a limitation of EFX 1. - * - * When a multilingual text field is referenced, then a special XPath expression - * is generated to retrieve the value in the "preferred" language. - * Preferred language is the first language among the languages listed in the - * translator options for which a text value is available in the field. - * - * The logic of the workaround is as follows: - * if the fieldReference is a multilingual text field and it does not - * already come with a predicate that filters by @languageID, then we add a - * predicate which, using a for loop, will find the first language for which a - * value is available in the field. - * - * In EFX 1 therefore the selection of the appropriate (preferred) language is - * done implicitly, whereas in EFX 2 it is done explicitly by calling a special - * function designed to perform this task. - * - * Both EFX-1 and EFX-2 implementations of the feature rely on the existence of a - * $PREFERRED_LANGUAGES variable in the XSLT. - * This function returns the list of languages used in the visualisation in the - * order of preference (visualisation language followed by notice language(s)). - */ /** * Preserved V1 behavior: pass EFX string literal through as-is without converting * escape sequences to XPath format. @@ -91,13 +67,36 @@ public BooleanExpression composePatternMatchCondition(StringExpression expressio String.format("fn:matches(normalize-space(%s), %s)", expression.getScript(), pattern)); } + /*** + * Retrieves the value of a multilingual text field in the "preferred" language. + * Preferred language is the first language among the languages listed in the + * translator options for which a text value is available in the field. + * + * This is a workaround for a limitation of EFX 1: the language cannot be selected + * explicitly by the template author, so template translation applies this + * implicitly to every multilingual field it renders. In EFX 2 the selection is + * done explicitly, by calling a function designed to perform this task. + * + * If the reference already comes with a predicate that filters by @languageID, + * then the template author has already pinned a language and the value is + * retrieved as-is. + * + * Both EFX-1 and EFX-2 implementations of the feature rely on the existence of a + * $PREFERRED_LANGUAGES variable in the XSLT. + * This function returns the list of languages used in the visualisation in the + * order of preference (visualisation language followed by notice language(s)). + */ @Override - public PathExpression composeFieldValueReference(PathExpression fieldReference) { - XPathInfo xpathInfo = XPathProcessor.parse(fieldReference.getScript()); - if (fieldReference.is(EfxDataType.MultilingualString.class) && !xpathInfo.hasPredicate("@languageID")) { - return Expression.instantiate("efx:preferred-language-text(" + fieldReference.getScript() + ")", fieldReference.getClass()); + public StringExpression getTextInPreferredLanguage(final PathExpression fieldReference) { + final XPathInfo xpathInfo = XPathProcessor.parse(fieldReference.getScript()); + if (xpathInfo.hasPredicate("@languageID")) { + // The value reference is a PathExpression, which is not a StringExpression and cannot + // be returned as such. Only the generated script matters here: the caller re-creates + // the expression using the type of the field reference it started from. + return Expression.from(super.composeFieldValueReference(fieldReference), + StringExpression.class); } - return super.composeFieldValueReference(fieldReference); + return super.getTextInPreferredLanguage(fieldReference); } @Override diff --git a/src/test/java/eu/europa/ted/efx/EfxTestsBase.java b/src/test/java/eu/europa/ted/efx/EfxTestsBase.java index b477e9c..90a2d06 100644 --- a/src/test/java/eu/europa/ted/efx/EfxTestsBase.java +++ b/src/test/java/eu/europa/ted/efx/EfxTestsBase.java @@ -5,15 +5,10 @@ import eu.europa.ted.efx.interfaces.TranslatorOptions; import eu.europa.ted.efx.mock.DependencyFactoryMock; import eu.europa.ted.efx.model.DecimalFormat; -import net.sf.saxon.s9api.ExtensionFunction; -import net.sf.saxon.s9api.ItemType; -import net.sf.saxon.s9api.OccurrenceIndicator; import net.sf.saxon.s9api.Processor; import net.sf.saxon.s9api.QName; import net.sf.saxon.s9api.SaxonApiException; -import net.sf.saxon.s9api.SequenceType; import net.sf.saxon.s9api.XPathCompiler; -import net.sf.saxon.s9api.XdmValue; public abstract class EfxTestsBase { @@ -26,51 +21,17 @@ public abstract class EfxTestsBase { static { Processor processor = new Processor(false); - // Register custom EFX extension functions so Saxon can validate XPath syntax. - // These are only needed for V1 expression tests, where multilingual field references - // are implicitly wrapped in efx:preferred-language-text() by XPathScriptGeneratorV1. - // V2 bans these functions in expression context (template-only). - processor.registerExtensionFunction(efxFunction("preferred-language")); - processor.registerExtensionFunction(efxFunction("preferred-language-text")); - XPATH_COMPILER = processor.newXPathCompiler(); XPATH_COMPILER.setLanguageVersion("3.1"); XPATH_COMPILER.declareNamespace("fn", "http://www.w3.org/2005/xpath-functions"); XPATH_COMPILER.declareNamespace("xs", "http://www.w3.org/2001/XMLSchema"); + // The functions of the EFX namespace are provided by the XSL of the notice viewer and are + // available to view templates only. None of them is registered here, so an expression that + // calls one fails to compile: that is what keeps them out of validation rules. XPATH_COMPILER.declareNamespace("efx", EFX_NAMESPACE); XPATH_COMPILER.declareVariable(new QName("urlPrefix")); } - /** - * Creates a dummy extension function stub for XPath syntax validation. - * Accepts one argument (node) and returns a string. - */ - private static ExtensionFunction efxFunction(String localName) { - return new ExtensionFunction() { - @Override - public QName getName() { - return new QName(EFX_NAMESPACE, localName); - } - - @Override - public SequenceType getResultType() { - return SequenceType.makeSequenceType(ItemType.STRING, OccurrenceIndicator.ONE); - } - - @Override - public SequenceType[] getArgumentTypes() { - return new SequenceType[] { - SequenceType.makeSequenceType(ItemType.ANY_ITEM, OccurrenceIndicator.ONE_OR_MORE) - }; - } - - @Override - public XdmValue call(XdmValue[] arguments) { - throw new UnsupportedOperationException("Stub for XPath validation only"); - } - }; - } - protected abstract String getSdkVersion(); protected void testExpressionTranslationWithContext(final String expectedTranslation, 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 918c6b2..b9647b6 100644 --- a/src/test/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1Test.java +++ b/src/test/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1Test.java @@ -90,7 +90,7 @@ void testLikePatternCondition_WithNot() { @Test void testFieldValueComparison_UsingTextFields() { testExpressionTranslationWithContext( - "PathNode/TextField/normalize-space(text()) = efx:preferred-language-text(PathNode/TextMultilingualField)", + "PathNode/TextField/normalize-space(text()) = PathNode/TextMultilingualField/normalize-space(text())", "ND-Root", "BT-00-Text == BT-00-Text-Multilingual"); } @@ -1135,12 +1135,24 @@ void testFieldReference_WithAxis() { "ND-Root::preceding::BT-00-Integer"); } + /** + * Outside of view templates there is no preferred language to select: efx:preferred-language-text() + * is defined by the notice viewer's XSL and is unavailable anywhere else, so a multilingual field + * value is retrieved like any other text value. + */ @Test void testMultilingualTextFieldReference() { - testExpressionTranslationWithContext("efx:preferred-language-text(PathNode/TextMultilingualField)", + testExpressionTranslationWithContext("PathNode/TextMultilingualField/normalize-space(text())", "ND-Root", "BT-00-Text-Multilingual"); } + @Test + void testMultilingualTextFieldReference_AsSequence() { + testExpressionTranslationWithContext( + "for $t in PathNode/TextMultilingualField/normalize-space(text()) return $t", "ND-Root", + "for text:$t in BT-00-Text-Multilingual return $t"); + } + @Test void testMultilingualTextFieldReference_WithLanguagePredicate() { testExpressionTranslationWithContext("PathNode/TextMultilingualField[./@languageID = 'eng']/normalize-space(text())", diff --git a/src/test/java/eu/europa/ted/efx/sdk1/EfxTemplateTranslatorV1Test.java b/src/test/java/eu/europa/ted/efx/sdk1/EfxTemplateTranslatorV1Test.java index cf92627..b178b81 100644 --- a/src/test/java/eu/europa/ted/efx/sdk1/EfxTemplateTranslatorV1Test.java +++ b/src/test/java/eu/europa/ted/efx/sdk1/EfxTemplateTranslatorV1Test.java @@ -377,4 +377,51 @@ void testImplicitFormatting_Dates() { void testImplicitFormatting_Times() { assertEquals("TEMPLATES:\nlet block01() -> { eval(for $item in PathNode/StartTimeField/xs:time(text()) return format-time($item, '[H01]:[m01] [Z]')) }\nMAIN:\nfor-each(/*).call(block01())", translateTemplate("{ND-Root} ${BT-00-StartTime}")); } + + /*** Multilingual fields ***/ + + /** + * In a view template the value of a multilingual field must be retrieved in the preferred + * language, so the reference is wrapped in a call to efx:preferred-language-text(). + * This is the behaviour that template translation must keep. + */ + @Test + void testMultilingualTextField_IsRetrievedInPreferredLanguage() { + assertEquals( + "TEMPLATES:\nlet block01() -> { eval(efx:preferred-language-text(PathNode/TextMultilingualField)) }\nMAIN:\nfor-each(/*).call(block01())", + translateTemplate("{ND-Root} ${BT-00-Text-Multilingual}")); + } + + /** + * When the reference already pins a language, the value is retrieved as-is: adding the preferred + * language call on top would override the language the template author asked for. + */ + @Test + void testMultilingualTextField_WithLanguagePredicate_IsRetrievedAsIs() { + assertEquals( + "TEMPLATES:\nlet block01() -> { eval(PathNode/TextMultilingualField[./@languageID = 'eng']/normalize-space(text())) }\nMAIN:\nfor-each(/*).call(block01())", + translateTemplate( + "{ND-Root} ${BT-00-Text-Multilingual[BT-00-Text-Multilingual/@languageID == 'eng']}")); + } + + /** + * A multilingual field used as a sequence goes through its own code path, so it needs its own + * coverage. + */ + @Test + void testMultilingualTextField_AsSequence_IsRetrievedInPreferredLanguage() { + assertEquals( + "TEMPLATES:\nlet block01() -> { eval(for $t in efx:preferred-language-text(PathNode/TextMultilingualField) return $t) }\nMAIN:\nfor-each(/*).call(block01())", + translateTemplate("{ND-Root} ${for text:$t in BT-00-Text-Multilingual return $t}")); + } + + /** + * The $value shorthand goes through a template-only code path, so it needs its own coverage. + */ + @Test + void testMultilingualTextField_ShorthandValueReference_IsRetrievedInPreferredLanguage() { + assertEquals( + "TEMPLATES:\nlet block01() -> { eval(efx:preferred-language-text(.)) }\nMAIN:\nfor-each(/*/PathNode/TextMultilingualField).call(block01())", + translateTemplate("{BT-00-Text-Multilingual} $value")); + } } From 1722c961c40bd1002e7ecf04f3d9621f4d0f812e Mon Sep 17 00:00:00 2001 From: Ioannis Rosuochatzakis Date: Wed, 26 Aug 2026 22:43:47 +0200 Subject: [PATCH 03/17] 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 04/17] 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 05/17] 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 ---------------------------------------------------- } From 90ad1c4f0e08d24746ea649bb502f3c4bce54af5 Mon Sep 17 00:00:00 2001 From: Ioannis Rosuochatzakis Date: Sat, 5 Sep 2026 19:49:57 +0200 Subject: [PATCH 06/17] pom: Set version to 2.0.0-alpha.8 and update dependencies --- pom.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 525baf7..79fd445 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ eu.europa.ted.eforms efx-toolkit-java - 2.0.0-SNAPSHOT + 2.0.0-alpha.8 jar EFX Toolkit for Java @@ -46,9 +46,9 @@ ${project.build.directory}/eforms-sdk/antlr4 - 1.16.0-SNAPSHOT - 2.0.0-SNAPSHOT - 1.8.0-SNAPSHOT + 1.16.0-beta.2 + 2.0.0-alpha.3 + 1.8.0 4.13.1 From e20b6606a7ea8f60636bbea0be9af46fe38be751 Mon Sep 17 00:00:00 2001 From: Ioannis Rosuochatzakis Date: Sat, 5 Sep 2026 19:50:25 +0200 Subject: [PATCH 07/17] docs: Add 2.0.0-alpha.8 changelog --- CHANGELOG.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 490f72e..d2ffd16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# EFX Toolkit 2.0.0-alpha.7 Release Notes +# EFX Toolkit 2.0.0-alpha.8 Release Notes _The EFX Toolkit for Java developers is a library that enables the transpilation of [EFX](https://docs.ted.europa.eu/eforms/latest/efx) expressions and templates to different target languages. It also includes an implementation of an EFX-to-XPath transpiler._ @@ -6,14 +6,15 @@ _The EFX Toolkit for Java developers is a library that enables the transpilation ## In this release -This is an incremental update over 2.0.0-alpha.6. It refines SDK data-type resolution and updates the eForms Core dependency, while retaining full support for the EFX-2 grammar released with SDK 2.0.0-alpha.2. +This is an incremental update over 2.0.0-alpha.7. It adds support for the new EFX selector construct in both versions of the language, corrects the selection of the preferred language outside view templates, and moves to the eForms SDK grammars and eForms Core library released alongside it. -### Changes since 2.0.0-alpha.6 +### Changes since 2.0.0-alpha.7 -- Field data types are now taken directly from the SDK field definitions, removing a temporary `SdkSymbolResolver` heuristic that inferred the `duration` type from `measure` fields using the `duration-unit` codelist. Added regression tests covering measure and duration fields. -- Upgraded the eForms Core Java dependency to 1.7.0. +- **Selectors**: an EFX expression can now identify the XML elements a reference points to, instead of the values held in them. Written `&{reference}` in EFX-1, and `&{reference}` or `WITH context SELECT reference` in EFX-2, it transpiles to the path of the elements rather than to their values. Everywhere else in EFX a field reference means the field's value; a selector is the one place where an application needs to be told where a value lives. The first use of it is `privacy.undisclosedFieldSelector`, which has to name the elements a notice viewer must withhold from publication. Implemented in both translators, and covered by the compute dependency extractor. +- **Preferred language selection**: in EFX-1 the language of a multilingual text field is chosen implicitly, using a variable that the notice viewer's XSL defines. That variable exists only while a view template is being translated, so the selection is now applied there and nowhere else. +- Upgraded the eForms Core Java dependency to 1.8.0, which preserves predicates when paths are joined or given an axis. -The following sections describe the features of the 2.0.0 line, unchanged since 2.0.0-alpha.6. +The following sections describe the features of the 2.0.0 line, unchanged since 2.0.0-alpha.7. ### EFX-2 language support @@ -59,7 +60,7 @@ NOTE: Transpilation of EFX-1 to XPath and XSL in this version of the EFX Toolkit ## Breaking changes -No new breaking changes are introduced in 2.0.0-alpha.7. For the breaking changes introduced earlier in the 2.0.0 line, see the [2.0.0-alpha.6 release notes](https://github.com/OP-TED/efx-toolkit-java/releases/tag/2.0.0-alpha.6). +No new breaking changes are introduced in 2.0.0-alpha.8. For the breaking changes introduced earlier in the 2.0.0 line, see the [2.0.0-alpha.6 release notes](https://github.com/OP-TED/efx-toolkit-java/releases/tag/2.0.0-alpha.6). ## Future development @@ -76,7 +77,7 @@ Documentation for the EFX Toolkit is available at: Date: Sun, 6 Sep 2026 03:10:41 +0200 Subject: [PATCH 08/17] Keep the anchor and its predicates in a context override (TEDEFO-5170) --- CHANGELOG.md | 7 +- .../ted/efx/interfaces/ScriptGenerator.java | 7 +- .../efx/sdk1/EfxExpressionTranslatorV1.java | 10 +-- .../efx/sdk2/EfxExpressionTranslatorV2.java | 5 +- .../ted/efx/xpath/XPathContextualizer.java | 11 ++- .../ted/efx/xpath/XPathScriptGenerator.java | 8 +- .../sdk1/EfxExpressionTranslatorV1Test.java | 14 +++- .../sdk2/EfxExpressionTranslatorV2Test.java | 76 +++++++++++++------ 8 files changed, 94 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2ffd16..102bd1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,14 @@ _The EFX Toolkit for Java developers is a library that enables the transpilation ## In this release -This is an incremental update over 2.0.0-alpha.7. It adds support for the new EFX selector construct in both versions of the language, corrects the selection of the preferred language outside view templates, and moves to the eForms SDK grammars and eForms Core library released alongside it. +This is an incremental update over 2.0.0-alpha.7. It adds support for the new EFX selector construct in both versions of the language, corrects the selection of the preferred language outside view templates and the transpilation of context overrides, and moves to the eForms SDK grammars and eForms Core library released alongside it. ### Changes since 2.0.0-alpha.7 - **Selectors**: an EFX expression can now identify the XML elements a reference points to, instead of the values held in them. Written `&{reference}` in EFX-1, and `&{reference}` or `WITH context SELECT reference` in EFX-2, it transpiles to the path of the elements rather than to their values. Everywhere else in EFX a field reference means the field's value; a selector is the one place where an application needs to be told where a value lives. The first use of it is `privacy.undisclosedFieldSelector`, which has to name the elements a notice viewer must withhold from publication. Implemented in both translators, and covered by the compute dependency extractor. - **Preferred language selection**: in EFX-1 the language of a multilingual text field is chosen implicitly, using a variable that the notice viewer's XSL defines. That variable exists only while a view template is being translated, so the selection is now applied there and nowhere else. -- Upgraded the eForms Core Java dependency to 1.8.0, which preserves predicates when paths are joined or given an axis. +- **Context overrides**: `context::field` now transpiles to a path that keeps the walk to the context. The override means the value of the field by a path that starts at the context, so where the context is not present in the notice there is no starting point and nothing is selected. Until now that walk was removed as redundant, which made `${field}` and `${context::field}` transpile to the same thing and left the override with no effect. A predicate written on the context, as in `ND-Lot[cbc:ID = 'LOT-0001']::BT-137`, was discarded as well, so the condition the author wrote had no effect either. Expressions using a context override therefore produce a longer path than before, one that yields nothing where the context is absent, and one that applies any predicate written on the context. No SDK content is affected: context overrides are not used in any released SDK. +- Upgraded the eForms Core Java dependency to 1.9.0, which preserves predicates when paths are joined or given an axis, and lets the caller decide how far a joined path is shortened. The following sections describe the features of the 2.0.0 line, unchanged since 2.0.0-alpha.7. @@ -80,4 +81,4 @@ This version of the EFX Toolkit has a compile-time dependency on the following e - eForms SDK 1.x.x (SDK 1.16.0-beta.2 grammar) - eForms SDK 2.0.0-alpha.3 -It also depends on the [eForms Core Java library](https://github.com/OP-TED/eforms-core-java) version 1.8.0. +It also depends on the [eForms Core Java library](https://github.com/OP-TED/eforms-core-java) version 1.9.0. diff --git a/src/main/java/eu/europa/ted/efx/interfaces/ScriptGenerator.java b/src/main/java/eu/europa/ted/efx/interfaces/ScriptGenerator.java index 8b29079..63acfbc 100644 --- a/src/main/java/eu/europa/ted/efx/interfaces/ScriptGenerator.java +++ b/src/main/java/eu/europa/ted/efx/interfaces/ScriptGenerator.java @@ -328,7 +328,12 @@ default PathExpression composeFieldInExternalReference(final PathExpression exte /** * Joins two given path expressions into one by placing the second after the first and using the * proper delimiter. - * + * + *

+ * This is how the path of a context override is built: the second path is relative to the first, + * which is the context the override names. What that implies for the joined path is for the + * implementation to decide, in the terms of the language it targets. + * * @param first The part of the path that goes before the delimiter. * @param second The part of the path that goes after the delimiter. * @return The joined path expression. 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 4f8d9c5..fdac204 100644 --- a/src/main/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1.java +++ b/src/main/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1.java @@ -1134,12 +1134,11 @@ public void exitSequenceFromAttributeReference(SequenceFromAttributeReferenceCon */ @Override public void exitContextFieldSpecifier(ContextFieldSpecifierContext ctx) { - this.stack.pop(PathExpression.class); // Discard the PathExpression placed in the stack for - // the context field. + final PathExpression contextFieldPath = this.stack.pop(PathExpression.class); final String contextFieldId = getFieldId(ctx.fieldContext()); this.efxContext .push(new FieldContext(contextFieldId, this.symbols.getAbsolutePathOfField(contextFieldId), - this.symbols.getRelativePathOfField(contextFieldId, this.efxContext.symbol()))); + contextFieldPath)); } @@ -1163,12 +1162,11 @@ public void exitFieldReferenceWithFieldContextOverride( */ @Override public void exitContextNodeSpecifier(ContextNodeSpecifierContext ctx) { - this.stack.pop(PathExpression.class); // Discard the PathExpression placed in the stack for - // the context node. + final PathExpression contextNodePath = this.stack.pop(PathExpression.class); final String contextNodeId = getNodeId(ctx.node); this.efxContext .push(new NodeContext(contextNodeId, this.symbols.getAbsolutePathOfNode(contextNodeId), - this.symbols.getRelativePathOfNode(contextNodeId, this.efxContext.symbol()))); + contextNodePath)); } /** 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 7180787..e4907b0 100644 --- a/src/main/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2.java +++ b/src/main/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2.java @@ -2074,12 +2074,11 @@ public void exitFieldReferenceWithFieldContextOverride( */ @Override public void exitContextNodeSpecifier(ContextNodeSpecifierContext ctx) { - this.stack.pop(PathExpression.class); // Discard the PathExpression placed in the stack for - // the context node. + final PathExpression contextNodePath = this.stack.pop(PathExpression.class); final String contextNodeId = getNodeId(ctx.node); this.efxContext .push(new NodeContext(contextNodeId, this.symbols.getAbsolutePathOfNode(contextNodeId), - this.symbols.getRelativePathOfNode(contextNodeId, this.efxContext.symbol()))); + contextNodePath)); } /** diff --git a/src/main/java/eu/europa/ted/efx/xpath/XPathContextualizer.java b/src/main/java/eu/europa/ted/efx/xpath/XPathContextualizer.java index cfd0eb5..7d2c2bc 100644 --- a/src/main/java/eu/europa/ted/efx/xpath/XPathContextualizer.java +++ b/src/main/java/eu/europa/ted/efx/xpath/XPathContextualizer.java @@ -13,6 +13,7 @@ */ package eu.europa.ted.efx.xpath; +import eu.europa.ted.eforms.xpath.Simplification; import eu.europa.ted.eforms.xpath.XPathProcessor; import eu.europa.ted.efx.model.expressions.Expression; import eu.europa.ted.efx.model.expressions.PathExpression; @@ -39,9 +40,15 @@ public static PathExpression contextualize(final PathExpression contextXpath, return Expression.instantiate(result, xpath.getClass()); } - public static PathExpression join(final PathExpression first, final PathExpression second) { + /** + * Joins the path of a context to a path that is relative to it, shortening the result as far as + * the caller asks for. + */ + public static PathExpression join(final PathExpression first, final PathExpression second, + final Simplification simplification) { - String joinedXPath = XPathProcessor.join(first.getScript(), second.getScript()); + String joinedXPath = + XPathProcessor.join(first.getScript(), second.getScript(), simplification); return Expression.instantiate(joinedXPath, second.getClass()); } diff --git a/src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java b/src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java index 83c90a1..43d037e 100644 --- a/src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java +++ b/src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java @@ -27,6 +27,7 @@ import eu.europa.ted.eforms.sdk.component.SdkComponent; import eu.europa.ted.eforms.sdk.component.SdkComponentType; +import eu.europa.ted.eforms.xpath.Simplification; import eu.europa.ted.efx.interfaces.ScriptGenerator; import eu.europa.ted.efx.interfaces.TranslatorOptions; import eu.europa.ted.efx.model.expressions.Expression; @@ -298,7 +299,12 @@ public T composeParenthesizedExpression(T expression, Cla @Override public PathExpression joinPaths(final PathExpression first, final PathExpression second) { - return XPathContextualizer.join(first, second); + // Every step of both paths is kept. A context override means the value of the field by a path + // that starts at the context, so where the context is not present in the notice there is no + // starting point and nothing is selected. Shortening the path would remove the walk to the + // context and with it that condition, leaving an override indistinguishable from a plain + // reference. + return XPathContextualizer.join(first, second, Simplification.NONE); } @Override 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 dc0586b..8d1206b 100644 --- a/src/test/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1Test.java +++ b/src/test/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1Test.java @@ -1089,13 +1089,21 @@ void testFieldReferenceInOtherNotice() { @Test void testFieldReferenceWithFieldContextOverride() { - testExpressionTranslationWithContext("../TextField/normalize-space(text())", "BT-00-Code", + testExpressionTranslationWithContext("../ChildNode/SubLevelTextField/../../TextField/normalize-space(text())", "BT-00-Code", "BT-01-SubLevel-Text::BT-00-Text"); } + @Test + void testFieldReferenceWithFieldContextOverride_WithPredicate() { + testExpressionTranslationWithContext( + "../ChildNode/SubLevelTextField['a' = 'a']/../../TextField/normalize-space(text())", + "BT-00-Code", + "BT-01-SubLevel-Text['a' == 'a']::BT-00-Text"); + } + @Test void testFieldReferenceWithFieldContextOverride_WithIntegerField() { - testExpressionTranslationWithContext("../IntegerField/number()", "BT-00-Code", + testExpressionTranslationWithContext("../ChildNode/SubLevelTextField/../../IntegerField/number()", "BT-00-Code", "BT-01-SubLevel-Text::BT-00-Integer"); } @@ -1107,7 +1115,7 @@ void testFieldReferenceWithNodeContextOverride() { @Test void testFieldReferenceWithNodeContextOverride_WithPredicate() { - testExpressionTranslationWithContext("../../PathNode/IntegerField/number()", "BT-00-Text", + testExpressionTranslationWithContext("../..[PathNode/IndicatorField = true()]/PathNode/IntegerField/number()", "BT-00-Text", "ND-Root[BT-00-Indicator == TRUE]::BT-00-Integer"); } 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 eebde7e..6768382 100644 --- a/src/test/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2Test.java +++ b/src/test/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2Test.java @@ -1265,13 +1265,13 @@ void testFieldReferenceWithPredicate_WithFieldReferenceInPredicate() { @Test void testFieldReferenceWithFieldContextOverride() { - testExpressionTranslationWithContext("../TextField/normalize-space(text())", "BT-00-Code", + testExpressionTranslationWithContext("../ChildNode/SubLevelTextField/../../TextField/normalize-space(text())", "BT-00-Code", "BT-01-SubLevel-Text::BT-00-Text"); } @Test void testFieldReferenceWithFieldContextOverride_WithIntegerField() { - testExpressionTranslationWithContext("../IntegerField/number()", "BT-00-Code", + testExpressionTranslationWithContext("../ChildNode/SubLevelTextField/../../IntegerField/number()", "BT-00-Code", "BT-01-SubLevel-Text::integerField"); } @@ -1281,9 +1281,32 @@ void testFieldReferenceWithNodeContextOverride() { "ND-Root::integerField"); } + @Test + void testFieldReferenceWithNodeContextOverride_AbsoluteAnchor() { + testExpressionTranslationWithContext("/*/PathNode/TextField", "BT-00-Text", + "/ND-Root::BT-00-Text is present"); + } + + @Test + void testFieldReferenceWithFieldContextOverride_AbsoluteAnchor() { + testExpressionTranslationWithContext( + "/*/PathNode/ChildNode/SubLevelTextField/../../TextField", "BT-00-Code", + "/BT-01-SubLevel-Text::BT-00-Text is present"); + } + + @Test + void testFieldReferenceWithNodeContextOverride_WithPredicateFromTheSdk() { + // The anchor's path carries a predicate that comes from the SDK rather than from the + // expression. It has to survive for the same reason a written one does. + testExpressionTranslationWithContext( + "../FieldsPrivacy[FieldIdentifierCode/text()='rep-text-priv']/ReasonCode", + "BT-00-Text", + "ND-PrivacyForRepeatableText::BT-197(BT-00)-Repeatable-Text is present"); + } + @Test void testFieldReferenceWithNodeContextOverride_WithPredicate() { - testExpressionTranslationWithContext("../../PathNode/IntegerField/number()", "BT-00-Text", + testExpressionTranslationWithContext("../..[PathNode/IndicatorField = true()]/PathNode/IntegerField/number()", "BT-00-Text", "ND-Root[BT-00-Indicator == TRUE]::integerField"); } @@ -3820,19 +3843,21 @@ void testLinkedProperty_InSequenceContext() { @Test void testFieldContextOverride_WithPredicateOnContext() { - // joinPaths simplifies: ../ChildNode/SubLevelTextField['a'='a']/../../TextField → ../TextField - // because navigating UP from a predicated node reaches the parent regardless + // The walk to the context is kept: the override says the value is reached by a path that + // starts at the context, so where no SubLevelTextField satisfies the predicate there is + // nothing to start from and nothing is selected. testExpressionTranslationWithContext( - "../TextField/normalize-space(text())", + "../ChildNode/SubLevelTextField['a' = 'a']/../../TextField/normalize-space(text())", "BT-00-Code", "BT-01-SubLevel-Text['a' == 'a']::BT-00-Text"); } @Test void testFieldContextOverride_WithIndexerOnContext() { - // joinPaths simplifies the indexed context path the same way as predicates + // An indexed context is kept for the same reason as a predicated one: without a first + // SubLevelTextField there is no starting point. testExpressionTranslationWithContext( - "../TextField/normalize-space(text())", + "../ChildNode/SubLevelTextField[1]/../../TextField/normalize-space(text())", "BT-00-Code", "BT-01-SubLevel-Text[1]::BT-00-Text"); } @@ -3840,7 +3865,7 @@ void testFieldContextOverride_WithIndexerOnContext() { @Test void testFieldContextOverride_InSequenceContext() { testExpressionTranslationWithContext( - "'test' = ../TextField/normalize-space(text())", + "'test' = ../ChildNode/SubLevelTextField/../../TextField/normalize-space(text())", "BT-00-Code", "'test' in BT-01-SubLevel-Text::BT-00-Text"); } @@ -3855,9 +3880,10 @@ void testNodeContextOverride_InSequenceContext() { @Test void testStackedOverride_NodeAndField() { - // joinPaths simplifies: .../ChildNode/SubLevelTextField/../../TextField → .../TextField + // The walk to the context is kept even with nothing written on it, so the override + // selects nothing where the context is absent. testExpressionTranslationWithContext( - "../../PathNode/TextField/normalize-space(text())", + "../../PathNode/ChildNode/SubLevelTextField/../../TextField/normalize-space(text())", "BT-00-Text", "ND-Root::BT-01-SubLevel-Text::BT-00-Text"); } @@ -3866,7 +3892,7 @@ void testStackedOverride_NodeAndField() { @Test void testFieldContextOverride_InPresenceCondition() { testExpressionTranslationWithContext( - "../TextField", + "../ChildNode/SubLevelTextField/../../TextField", "BT-00-Code", "BT-01-SubLevel-Text::BT-00-Text is present"); } @@ -3875,7 +3901,7 @@ void testFieldContextOverride_InPresenceCondition() { @Test void testFieldContextOverride_InAttributeContext() { testExpressionTranslationWithContext( - "../CodeField/@listName", + "../ChildNode/SubLevelTextField/../../CodeField/@listName", "BT-00-Code", "BT-01-SubLevel-Text::BT-00-Code/@listName"); } @@ -3884,7 +3910,7 @@ void testFieldContextOverride_InAttributeContext() { @Test void testFieldContextOverride_WithPredicate_InSequenceContext() { testExpressionTranslationWithContext( - "'test' = ../TextField/normalize-space(text())", + "'test' = ../ChildNode/SubLevelTextField['a' = 'a']/../../TextField/normalize-space(text())", "BT-00-Code", "'test' in BT-01-SubLevel-Text['a' == 'a']::BT-00-Text"); } @@ -3893,7 +3919,7 @@ void testFieldContextOverride_WithPredicate_InSequenceContext() { @Test void testFieldContextOverride_WithPredicate_InPresenceCondition() { testExpressionTranslationWithContext( - "../TextField", + "../ChildNode/SubLevelTextField['a' = 'a']/../../TextField", "BT-00-Code", "BT-01-SubLevel-Text['a' == 'a']::BT-00-Text is present"); } @@ -3902,7 +3928,7 @@ void testFieldContextOverride_WithPredicate_InPresenceCondition() { @Test void testFieldContextOverride_WithPredicate_InAttributeContext() { testExpressionTranslationWithContext( - "../CodeField/@listName", + "../ChildNode/SubLevelTextField['a' = 'a']/../../CodeField/@listName", "BT-00-Code", "BT-01-SubLevel-Text['a' == 'a']::BT-00-Code/@listName"); } @@ -3911,7 +3937,7 @@ void testFieldContextOverride_WithPredicate_InAttributeContext() { @Test void testFieldContextOverride_WithIndexer_InSequenceContext() { testExpressionTranslationWithContext( - "'test' = ../TextField/normalize-space(text())", + "'test' = ../ChildNode/SubLevelTextField[1]/../../TextField/normalize-space(text())", "BT-00-Code", "'test' in BT-01-SubLevel-Text[1]::BT-00-Text"); } @@ -3920,7 +3946,7 @@ void testFieldContextOverride_WithIndexer_InSequenceContext() { @Test void testFieldContextOverride_WithIndexer_InPresenceCondition() { testExpressionTranslationWithContext( - "../TextField", + "../ChildNode/SubLevelTextField[1]/../../TextField", "BT-00-Code", "BT-01-SubLevel-Text[1]::BT-00-Text is present"); } @@ -3929,7 +3955,7 @@ void testFieldContextOverride_WithIndexer_InPresenceCondition() { @Test void testFieldContextOverride_WithIndexer_InAttributeContext() { testExpressionTranslationWithContext( - "../CodeField/@listName", + "../ChildNode/SubLevelTextField[1]/../../CodeField/@listName", "BT-00-Code", "BT-01-SubLevel-Text[1]::BT-00-Code/@listName"); } @@ -3956,7 +3982,7 @@ void testNodeContextOverride_InAttributeContext() { @Test void testNodeContextOverride_WithPredicate_InSequenceContext() { testExpressionTranslationWithContext( - "'test' = ../../PathNode/TextField/normalize-space(text())", + "'test' = ../..[PathNode/IndicatorField = true()]/PathNode/TextField/normalize-space(text())", "BT-00-Text", "'test' in ND-Root[BT-00-Indicator == TRUE]::BT-00-Text"); } @@ -3965,7 +3991,7 @@ void testNodeContextOverride_WithPredicate_InSequenceContext() { @Test void testNodeContextOverride_WithPredicate_InPresenceCondition() { testExpressionTranslationWithContext( - "../../PathNode/TextField", + "../..[PathNode/IndicatorField = true()]/PathNode/TextField", "BT-00-Text", "ND-Root[BT-00-Indicator == TRUE]::BT-00-Text is present"); } @@ -3974,7 +4000,7 @@ void testNodeContextOverride_WithPredicate_InPresenceCondition() { @Test void testNodeContextOverride_WithPredicate_InAttributeContext() { testExpressionTranslationWithContext( - "../../PathNode/CodeField/@listName", + "../..[PathNode/IndicatorField = true()]/PathNode/CodeField/@listName", "BT-00-Text", "ND-Root[BT-00-Indicator == TRUE]::BT-00-Code/@listName"); } @@ -4010,7 +4036,7 @@ void testVariableContextOverride_InAttributeContext() { @Test void testStackedOverride_NodeAndField_InSequenceContext() { testExpressionTranslationWithContext( - "'test' = ../../PathNode/TextField/normalize-space(text())", + "'test' = ../../PathNode/ChildNode/SubLevelTextField/../../TextField/normalize-space(text())", "BT-00-Text", "'test' in ND-Root::BT-01-SubLevel-Text::BT-00-Text"); } @@ -4019,7 +4045,7 @@ void testStackedOverride_NodeAndField_InSequenceContext() { @Test void testStackedOverride_NodeAndField_InPresenceCondition() { testExpressionTranslationWithContext( - "../../PathNode/TextField", + "../../PathNode/ChildNode/SubLevelTextField/../../TextField", "BT-00-Text", "ND-Root::BT-01-SubLevel-Text::BT-00-Text is present"); } @@ -4028,7 +4054,7 @@ void testStackedOverride_NodeAndField_InPresenceCondition() { @Test void testStackedOverride_NodeAndField_InAttributeContext() { testExpressionTranslationWithContext( - "../../PathNode/CodeField/@listName", + "../../PathNode/ChildNode/SubLevelTextField/../../CodeField/@listName", "BT-00-Text", "ND-Root::BT-01-SubLevel-Text::BT-00-Code/@listName"); } From 970fe4cb35b1b759a7c736b6a405890b2f380c13 Mon Sep 17 00:00:00 2001 From: Ioannis Rosuochatzakis Date: Sun, 6 Sep 2026 03:10:52 +0200 Subject: [PATCH 09/17] pom: Depend on eForms Core 1.9.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 79fd445..3a007c9 100644 --- a/pom.xml +++ b/pom.xml @@ -48,7 +48,7 @@ 1.16.0-beta.2 2.0.0-alpha.3 - 1.8.0 + 1.9.0-SNAPSHOT 4.13.1 From 1e72f9fadde85b69e37f6e4e971d6e040251657f Mon Sep 17 00:00:00 2001 From: Ioannis Rosuochatzakis Date: Sun, 6 Sep 2026 11:19:13 +0200 Subject: [PATCH 10/17] ci: Publish when a release is published, not when it is created --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 83d2e67..588593c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -7,7 +7,7 @@ on: - 'main' release: - types: [created] + types: [published] # Allows to run this workflow manually from the Actions tab workflow_dispatch: From bd90ed9ddaea28bc8b49f2527e3c88c24e6e0514 Mon Sep 17 00:00:00 2001 From: Ioannis Rosuochatzakis Date: Sun, 6 Sep 2026 13:18:31 +0200 Subject: [PATCH 11/17] Follow Simplification into XPathProcessor --- src/main/java/eu/europa/ted/efx/xpath/XPathContextualizer.java | 2 +- src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/europa/ted/efx/xpath/XPathContextualizer.java b/src/main/java/eu/europa/ted/efx/xpath/XPathContextualizer.java index 7d2c2bc..4d5da9c 100644 --- a/src/main/java/eu/europa/ted/efx/xpath/XPathContextualizer.java +++ b/src/main/java/eu/europa/ted/efx/xpath/XPathContextualizer.java @@ -13,7 +13,7 @@ */ package eu.europa.ted.efx.xpath; -import eu.europa.ted.eforms.xpath.Simplification; +import eu.europa.ted.eforms.xpath.XPathProcessor.Simplification; import eu.europa.ted.eforms.xpath.XPathProcessor; import eu.europa.ted.efx.model.expressions.Expression; import eu.europa.ted.efx.model.expressions.PathExpression; diff --git a/src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java b/src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java index 43d037e..51bf005 100644 --- a/src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java +++ b/src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java @@ -27,7 +27,7 @@ import eu.europa.ted.eforms.sdk.component.SdkComponent; import eu.europa.ted.eforms.sdk.component.SdkComponentType; -import eu.europa.ted.eforms.xpath.Simplification; +import eu.europa.ted.eforms.xpath.XPathProcessor.Simplification; import eu.europa.ted.efx.interfaces.ScriptGenerator; import eu.europa.ted.efx.interfaces.TranslatorOptions; import eu.europa.ted.efx.model.expressions.Expression; From 7a02dddc07034e401bd50515c5004310ec22916b Mon Sep 17 00:00:00 2001 From: Ioannis Rosuochatzakis Date: Sun, 6 Sep 2026 13:18:37 +0200 Subject: [PATCH 12/17] pom: Pin eForms Core to the released 1.9.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3a007c9..408340b 100644 --- a/pom.xml +++ b/pom.xml @@ -48,7 +48,7 @@ 1.16.0-beta.2 2.0.0-alpha.3 - 1.9.0-SNAPSHOT + 1.9.0 4.13.1 From 187974cb2545e9533cbda52b8fcaf3df51746f31 Mon Sep 17 00:00:00 2001 From: Ioannis Rosuochatzakis Date: Sun, 6 Sep 2026 14:20:27 +0200 Subject: [PATCH 13/17] docs: Reword the 2.0.0-alpha.8 release notes --- CHANGELOG.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 102bd1c..89fdf5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,14 +6,14 @@ _The EFX Toolkit for Java developers is a library that enables the transpilation ## In this release -This is an incremental update over 2.0.0-alpha.7. It adds support for the new EFX selector construct in both versions of the language, corrects the selection of the preferred language outside view templates and the transpilation of context overrides, and moves to the eForms SDK grammars and eForms Core library released alongside it. +This is an incremental update over 2.0.0-alpha.7. ### Changes since 2.0.0-alpha.7 -- **Selectors**: an EFX expression can now identify the XML elements a reference points to, instead of the values held in them. Written `&{reference}` in EFX-1, and `&{reference}` or `WITH context SELECT reference` in EFX-2, it transpiles to the path of the elements rather than to their values. Everywhere else in EFX a field reference means the field's value; a selector is the one place where an application needs to be told where a value lives. The first use of it is `privacy.undisclosedFieldSelector`, which has to name the elements a notice viewer must withhold from publication. Implemented in both translators, and covered by the compute dependency extractor. -- **Preferred language selection**: in EFX-1 the language of a multilingual text field is chosen implicitly, using a variable that the notice viewer's XSL defines. That variable exists only while a view template is being translated, so the selection is now applied there and nowhere else. -- **Context overrides**: `context::field` now transpiles to a path that keeps the walk to the context. The override means the value of the field by a path that starts at the context, so where the context is not present in the notice there is no starting point and nothing is selected. Until now that walk was removed as redundant, which made `${field}` and `${context::field}` transpile to the same thing and left the override with no effect. A predicate written on the context, as in `ND-Lot[cbc:ID = 'LOT-0001']::BT-137`, was discarded as well, so the condition the author wrote had no effect either. Expressions using a context override therefore produce a longer path than before, one that yields nothing where the context is absent, and one that applies any predicate written on the context. No SDK content is affected: context overrides are not used in any released SDK. -- Upgraded the eForms Core Java dependency to 1.9.0, which preserves predicates when paths are joined or given an axis, and lets the caller decide how far a joined path is shortened. +- **Selectors**: an EFX expression can now yield the path of the XML elements a reference points to, instead of the values held in them. Write `&{reference}` in EFX-1, and `&{reference}` or `WITH context SELECT reference` in EFX-2. The first use of this new feature is in the `privacy.undisclosedFieldSelector` property in `fields.json` of SDK 1.16.0-beta.2 and SDK 2.0.0-alpha.3. The new property identifies the elements that must be masked and effectively withheld from publication. +- **Preferred language selection**: fixed an issue with the implicit invocation in EFX-1 of the `preferred-language-text` function when multilingual text fields are referenced. The function is now called correctly only when transpiling view templates, which is the only context where this functionality is applicable. EFX-2 requires explicit invocation of the function and is therefore unaffected by this fix. +- **Context overrides**: fixed multiple issues that caused the transpiler to produce valid but inaccurate XPaths when using a context override (`context::field`) to modify the path through which the value of a field is reached. Transpilation now preserves navigation steps and predicates accurately, and expressions using a context override will now produce a longer XPath, which however honours all predicates and enforces the presence of the context to select the designated value(s). This feature has not been used so far in any published SDK, so this change does not have an impact on the interpretation of existing rules and templates published in any SDK version. +- The eForms Core Java dependency is now 1.9.0. The following sections describe the features of the 2.0.0 line, unchanged since 2.0.0-alpha.7. @@ -78,7 +78,7 @@ Documentation for the EFX Toolkit is available at: Date: Sun, 6 Sep 2026 14:43:19 +0200 Subject: [PATCH 14/17] docs: Describe the preferred-language methods where they are implemented --- .../ted/efx/interfaces/ScriptGenerator.java | 4 +++- .../efx/sdk1/xpath/XPathScriptGeneratorV1.java | 5 ----- .../ted/efx/xpath/XPathScriptGenerator.java | 16 ++++++++++++++++ 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/europa/ted/efx/interfaces/ScriptGenerator.java b/src/main/java/eu/europa/ted/efx/interfaces/ScriptGenerator.java index 63acfbc..50d8d7d 100644 --- a/src/main/java/eu/europa/ted/efx/interfaces/ScriptGenerator.java +++ b/src/main/java/eu/europa/ted/efx/interfaces/ScriptGenerator.java @@ -804,7 +804,9 @@ public StringExpression composeReplaceRegexFunction(StringExpression text, * Given a reference to a multilingual field, this function should generate the target language script * that returns the text value of the field in the preferred language. * - * Calling the function in EFX 2 + * In EFX-2 the template author calls this explicitly, through the + * {@code preferred-language-text} function. EFX-1 has no such function, so template + * translation applies it implicitly to every multilingual field it renders. * * @since SDK 2.0.0 * @see #getPreferredLanguage(PathExpression) diff --git a/src/main/java/eu/europa/ted/efx/sdk1/xpath/XPathScriptGeneratorV1.java b/src/main/java/eu/europa/ted/efx/sdk1/xpath/XPathScriptGeneratorV1.java index 1a90ac4..82d92d5 100644 --- a/src/main/java/eu/europa/ted/efx/sdk1/xpath/XPathScriptGeneratorV1.java +++ b/src/main/java/eu/europa/ted/efx/sdk1/xpath/XPathScriptGeneratorV1.java @@ -80,11 +80,6 @@ public BooleanExpression composePatternMatchCondition(StringExpression expressio * If the reference already comes with a predicate that filters by @languageID, * then the template author has already pinned a language and the value is * retrieved as-is. - * - * Both EFX-1 and EFX-2 implementations of the feature rely on the existence of a - * $PREFERRED_LANGUAGES variable in the XSLT. - * This function returns the list of languages used in the visualisation in the - * order of preference (visualisation language followed by notice language(s)). */ @Override public StringExpression getTextInPreferredLanguage(final PathExpression fieldReference) { diff --git a/src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java b/src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java index 51bf005..f221913 100644 --- a/src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java +++ b/src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java @@ -790,11 +790,27 @@ public StringLiteral getStringLiteralFromUnquotedString(String value) { return new StringLiteral("'" + value + "'"); } + /** + * Emits a call to {@code efx:preferred-language}, a function of the notice viewer's XSLT runtime + * library. It returns the identifier of the first language, among those the visualisation + * prefers, for which the field holds a value. + * + * @see #getTextInPreferredLanguage(PathExpression) + */ @Override public StringExpression getPreferredLanguage(PathExpression fieldReference) { return new StringExpression("efx:preferred-language(" + fieldReference.getScript() + ")"); } + /** + * Emits a call to {@code efx:preferred-language-text}, a function of the notice viewer's XSLT + * runtime library. + * + * Both EFX-1 and EFX-2 reach this method, and the function they call relies on a + * {@code $PREFERRED_LANGUAGES} variable defined by the XSLT. That variable holds the languages + * used in the visualisation, in order of preference: the visualisation language followed by the + * notice languages. + */ @Override public StringExpression getTextInPreferredLanguage(PathExpression fieldReference) { return new StringExpression("efx:preferred-language-text(" + fieldReference.getScript() + ")"); From ca35ea0e83adceba82eead88db61d6927e1ae84e Mon Sep 17 00:00:00 2001 From: Ioannis Rosuochatzakis Date: Sun, 6 Sep 2026 15:05:09 +0200 Subject: [PATCH 15/17] ci: Check the javadoc on every push --- .github/workflows/javadoc.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/javadoc.yml diff --git a/.github/workflows/javadoc.yml b/.github/workflows/javadoc.yml new file mode 100644 index 0000000..5c308ac --- /dev/null +++ b/.github/workflows/javadoc.yml @@ -0,0 +1,21 @@ +name: Check the javadoc + +on: + push: + + # Allows to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + javadoc: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + java-version: '11' + distribution: 'adopt' + - name: Check javadoc + run: mvn --batch-mode clean test-compile javadoc:javadoc javadoc:test-javadoc From be45e20a5d219dd5528f3751f807b4f812923f4f Mon Sep 17 00:00:00 2001 From: Ioannis Rosuochatzakis Date: Sun, 6 Sep 2026 15:44:29 +0200 Subject: [PATCH 16/17] docs: Record the XPathContextualizer.join signature change as breaking --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89fdf5d..53b7be4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,7 +61,7 @@ NOTE: Transpilation of EFX-1 to XPath and XSL in this version of the EFX Toolkit ## Breaking changes -No new breaking changes are introduced in 2.0.0-alpha.8. For the breaking changes introduced earlier in the 2.0.0 line, see the [2.0.0-alpha.6 release notes](https://github.com/OP-TED/efx-toolkit-java/releases/tag/2.0.0-alpha.6). +`XPathContextualizer.join` now takes a third argument, which says how far the joined path may be shortened; the two-argument form has been removed. For the breaking changes introduced earlier in the 2.0.0 line, see the [2.0.0-alpha.6 release notes](https://github.com/OP-TED/efx-toolkit-java/releases/tag/2.0.0-alpha.6). ## Future development From 00db6e6b42cde9284fc7bd032ef961c190d5e291 Mon Sep 17 00:00:00 2001 From: Ioannis Rosuochatzakis Date: Sun, 6 Sep 2026 15:44:58 +0200 Subject: [PATCH 17/17] ci: Publish a version once, when its release is published --- .github/workflows/publish.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 588593c..bc153f5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,7 +4,6 @@ on: push: branches: - 'develop' - - 'main' release: types: [published]