From f37b8d84fba79927b8e51e401dc04940f3b7ab91 Mon Sep 17 00:00:00 2001 From: Bertrand Lorentz Date: Fri, 28 Jul 2023 18:04:22 +0200 Subject: [PATCH 01/15] pom.xml: Set version to 2.0.0-SNAPSHOT --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index b5c9a1d..b0f8b3f 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ eu.europa.ted.eforms efx-toolkit-java - 2.0.0-alpha.1 + 2.0.0-SNAPSHOT jar EFX Toolkit for Java @@ -49,7 +49,7 @@ UTF-8 - 2023-05-30T06:25:09Z + 2023-07-28T16:03:53Z s01.oss.sonatype.org From 1288d0edfe4ecec0808872a8301bc0c61c34b300 Mon Sep 17 00:00:00 2001 From: Ioannis Rousochatzakis Date: Tue, 1 Aug 2023 00:41:53 +0200 Subject: [PATCH 02/15] Updated eForms Core Library dependency to 1.0.6-SNAPSHOT in develop. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b0f8b3f..561e15a 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ ${project.build.directory}/eforms-sdk/antlr4 - 1.0.5 + 1.0.6-SNAPSHOT 4.9.3 From d59a72a0fa38522f3be224af710801ad70d9db94 Mon Sep 17 00:00:00 2001 From: Bertrand Lorentz Date: Tue, 10 Oct 2023 17:11:42 +0200 Subject: [PATCH 03/15] Replace use of XPathAttributeLocator with XPathProcessor --- pom.xml | 2 +- .../ted/eforms/sdk/SdkSymbolResolver.java | 31 ++++--------- .../ted/efx/XPathAttributeLocatorTest.java | 43 ------------------- .../efx/mock/sdk1/SymbolResolverMockV1.java | 14 ++++-- .../efx/mock/sdk2/SymbolResolverMockV2.java | 14 ++++-- 5 files changed, 30 insertions(+), 74 deletions(-) delete mode 100644 src/test/java/eu/europa/ted/efx/XPathAttributeLocatorTest.java diff --git a/pom.xml b/pom.xml index 561e15a..c0b128a 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ ${project.build.directory}/eforms-sdk/antlr4 - 1.0.6-SNAPSHOT + 1.2.0-SNAPSHOT 4.9.3 diff --git a/src/main/java/eu/europa/ted/eforms/sdk/SdkSymbolResolver.java b/src/main/java/eu/europa/ted/eforms/sdk/SdkSymbolResolver.java index 0f797ab..a581c92 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/SdkSymbolResolver.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/SdkSymbolResolver.java @@ -16,11 +16,13 @@ import eu.europa.ted.eforms.sdk.repository.SdkFieldRepository; import eu.europa.ted.eforms.sdk.repository.SdkNodeRepository; import eu.europa.ted.eforms.sdk.resource.SdkResourceLoader; +import eu.europa.ted.eforms.xpath.XPathInfo; +import eu.europa.ted.eforms.xpath.XPathProcessor; import eu.europa.ted.efx.interfaces.SymbolResolver; +import eu.europa.ted.efx.model.expressions.Expression; import eu.europa.ted.efx.model.expressions.path.NodePathExpression; import eu.europa.ted.efx.model.expressions.path.PathExpression; import eu.europa.ted.efx.model.types.FieldTypes; -import eu.europa.ted.efx.xpath.XPathAttributeLocator; import eu.europa.ted.efx.xpath.XPathContextualizer; @SdkComponent(versions = { "1", "2" }, componentType = SdkComponentType.SYMBOL_RESOLVER) @@ -181,7 +183,7 @@ public boolean isAttributeField(final String fieldId) { if (!additionalFieldInfoMap.containsKey(fieldId)) { this.cacheAdditionalFieldInfo(fieldId); } - return additionalFieldInfoMap.get(fieldId).isAttribute; + return additionalFieldInfoMap.get(fieldId).isAttribute(); } @Override @@ -189,7 +191,7 @@ public String getAttributeNameFromAttributeField(final String fieldId) { if (!additionalFieldInfoMap.containsKey(fieldId)) { this.cacheAdditionalFieldInfo(fieldId); } - return additionalFieldInfoMap.get(fieldId).attributeName; + return additionalFieldInfoMap.get(fieldId).getAttributeName(); } @Override @@ -197,38 +199,23 @@ public PathExpression getAbsolutePathOfFieldWithoutTheAttribute(final String fie if (!additionalFieldInfoMap.containsKey(fieldId)) { this.cacheAdditionalFieldInfo(fieldId); } - return additionalFieldInfoMap.get(fieldId).pathWithoutAttribute; + return Expression.instantiate(additionalFieldInfoMap.get(fieldId).getPathToLastElement(), NodePathExpression.class); } // #region Temporary helpers ------------------------------------------------ - /** - * Temporary workaround to store additional info about fields. - * - * TODO: Move this additional info to SdkField class, and move the XPathAttributeLocator to the eforms-core-library. - */ - class AdditionalFieldInfo { - public boolean isAttribute; - public String attributeName; - public PathExpression pathWithoutAttribute; - } - /** * Caches the results of xpath parsing to mitigate performance impact. * This is a temporary solution until we move the additional info to the SdkField class. */ - Map additionalFieldInfoMap = new HashMap<>(); + Map additionalFieldInfoMap = new HashMap<>(); private void cacheAdditionalFieldInfo(final String fieldId) { if (additionalFieldInfoMap.containsKey(fieldId)) { return; } - var parsedPath = XPathAttributeLocator.findAttribute(this.getAbsolutePathOfField(fieldId)); - var info = new AdditionalFieldInfo(); - info.isAttribute = parsedPath.hasAttribute(); - info.attributeName = parsedPath.getAttributeName(); - info.pathWithoutAttribute = parsedPath.getElementPath(); - additionalFieldInfoMap.put(fieldId, info); + XPathInfo xpathInfo = XPathProcessor.parse(this.getAbsolutePathOfField(fieldId).getScript()); + additionalFieldInfoMap.put(fieldId, xpathInfo); } // #endregion Temporary helpers ------------------------------------------------ diff --git a/src/test/java/eu/europa/ted/efx/XPathAttributeLocatorTest.java b/src/test/java/eu/europa/ted/efx/XPathAttributeLocatorTest.java deleted file mode 100644 index 8931690..0000000 --- a/src/test/java/eu/europa/ted/efx/XPathAttributeLocatorTest.java +++ /dev/null @@ -1,43 +0,0 @@ -package eu.europa.ted.efx; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; - -import org.junit.jupiter.api.Test; - -import eu.europa.ted.efx.xpath.XPathAttributeLocator; - -class XPathAttributeLocatorTest { - private void testAttribute(final String attributePath, final String expectedPath, - final String expectedAttribute) { - final XPathAttributeLocator locator = - XPathAttributeLocator.findAttribute(attributePath); - - assertEquals(expectedPath, locator.getElementPath().getScript()); - assertEquals(expectedAttribute, locator.getAttributeName()); - } - - @Test - void testXPathAttributeLocator_WithAttribute() { - testAttribute("/path/path/@attribute", "/path/path", "attribute"); - } - - @Test - void testXPathAttributeLocator_WithMultipleAttributes() { - testAttribute("/path/path[@otherAttribute = 'text']/@attribute", - "/path/path[@otherAttribute = 'text']", "attribute"); - } - - @Test - void testXPathAttributeLocator_WithoutAttribute() { - final XPathAttributeLocator locator = XPathAttributeLocator - .findAttribute("/path/path[@otherAttribute = 'text']"); - assertEquals("/path/path[@otherAttribute = 'text']", locator.getElementPath().getScript()); - assertNull(locator.getAttributeName()); - } - - @Test - void testXPathAttributeLocator_WithoutPath() { - testAttribute("@attribute", "", "attribute"); - } -} diff --git a/src/test/java/eu/europa/ted/efx/mock/sdk1/SymbolResolverMockV1.java b/src/test/java/eu/europa/ted/efx/mock/sdk1/SymbolResolverMockV1.java index 8d67e44..8c5e3d5 100644 --- a/src/test/java/eu/europa/ted/efx/mock/sdk1/SymbolResolverMockV1.java +++ b/src/test/java/eu/europa/ted/efx/mock/sdk1/SymbolResolverMockV1.java @@ -9,12 +9,15 @@ import java.util.Map.Entry; import java.util.Optional; +import eu.europa.ted.eforms.xpath.XPathInfo; +import eu.europa.ted.eforms.xpath.XPathProcessor; import eu.europa.ted.efx.mock.AbstractSymbolResolverMock; +import eu.europa.ted.efx.model.expressions.Expression; +import eu.europa.ted.efx.model.expressions.path.NodePathExpression; import eu.europa.ted.efx.model.expressions.path.PathExpression; import eu.europa.ted.efx.sdk1.entity.SdkCodelistV1; import eu.europa.ted.efx.sdk1.entity.SdkFieldV1; import eu.europa.ted.efx.sdk1.entity.SdkNodeV1; -import eu.europa.ted.efx.xpath.XPathAttributeLocator; public class SymbolResolverMockV1 extends AbstractSymbolResolverMock { @@ -57,16 +60,19 @@ protected String getFieldsJsonFilename() { @Override public boolean isAttributeField(final String fieldId) { - return XPathAttributeLocator.findAttribute(this.getAbsolutePathOfField(fieldId)).hasAttribute(); + XPathInfo xpathInfo = XPathProcessor.parse(this.getAbsolutePathOfField(fieldId).getScript()); + return xpathInfo.isAttribute(); } @Override public String getAttributeNameFromAttributeField(String fieldId) { - return XPathAttributeLocator.findAttribute(this.getAbsolutePathOfField(fieldId)).getAttributeName(); + XPathInfo xpathInfo = XPathProcessor.parse(this.getAbsolutePathOfField(fieldId).getScript()); + return xpathInfo.getAttributeName(); } @Override public PathExpression getAbsolutePathOfFieldWithoutTheAttribute(String fieldId) { - return XPathAttributeLocator.findAttribute(this.getAbsolutePathOfField(fieldId)).getElementPath(); + XPathInfo xpathInfo = XPathProcessor.parse(this.getAbsolutePathOfField(fieldId).getScript()); + return Expression.instantiate(xpathInfo.getPathToLastElement(), NodePathExpression.class); } } diff --git a/src/test/java/eu/europa/ted/efx/mock/sdk2/SymbolResolverMockV2.java b/src/test/java/eu/europa/ted/efx/mock/sdk2/SymbolResolverMockV2.java index e2353b1..8a4028c 100644 --- a/src/test/java/eu/europa/ted/efx/mock/sdk2/SymbolResolverMockV2.java +++ b/src/test/java/eu/europa/ted/efx/mock/sdk2/SymbolResolverMockV2.java @@ -10,12 +10,15 @@ import java.util.Optional; import java.util.stream.Collectors; +import eu.europa.ted.eforms.xpath.XPathInfo; +import eu.europa.ted.eforms.xpath.XPathProcessor; import eu.europa.ted.efx.mock.AbstractSymbolResolverMock; +import eu.europa.ted.efx.model.expressions.Expression; +import eu.europa.ted.efx.model.expressions.path.NodePathExpression; import eu.europa.ted.efx.model.expressions.path.PathExpression; import eu.europa.ted.efx.sdk2.entity.SdkCodelistV2; import eu.europa.ted.efx.sdk2.entity.SdkFieldV2; import eu.europa.ted.efx.sdk2.entity.SdkNodeV2; -import eu.europa.ted.efx.xpath.XPathAttributeLocator; public class SymbolResolverMockV2 extends AbstractSymbolResolverMock { @@ -83,16 +86,19 @@ protected String getFieldsJsonFilename() { @Override public boolean isAttributeField(final String fieldId) { - return XPathAttributeLocator.findAttribute(this.getAbsolutePathOfField(fieldId)).hasAttribute(); + XPathInfo xpathInfo = XPathProcessor.parse(this.getAbsolutePathOfField(fieldId).getScript()); + return xpathInfo.isAttribute(); } @Override public String getAttributeNameFromAttributeField(String fieldId) { - return XPathAttributeLocator.findAttribute(this.getAbsolutePathOfField(fieldId)).getAttributeName(); + XPathInfo xpathInfo = XPathProcessor.parse(this.getAbsolutePathOfField(fieldId).getScript()); + return xpathInfo.getAttributeName(); } @Override public PathExpression getAbsolutePathOfFieldWithoutTheAttribute(String fieldId) { - return XPathAttributeLocator.findAttribute(this.getAbsolutePathOfField(fieldId)).getElementPath(); + XPathInfo xpathInfo = XPathProcessor.parse(this.getAbsolutePathOfField(fieldId).getScript()); + return Expression.instantiate(xpathInfo.getPathToLastElement(), NodePathExpression.class); } } From b3802493fd51cd26f5729532219cbe1a5fc7a5e8 Mon Sep 17 00:00:00 2001 From: Bertrand Lorentz Date: Tue, 10 Oct 2023 17:29:43 +0200 Subject: [PATCH 04/15] Remove XPathAttributeLocator class --- .../ted/efx/xpath/XPathAttributeLocator.java | 114 ------------------ 1 file changed, 114 deletions(-) delete mode 100644 src/main/java/eu/europa/ted/efx/xpath/XPathAttributeLocator.java diff --git a/src/main/java/eu/europa/ted/efx/xpath/XPathAttributeLocator.java b/src/main/java/eu/europa/ted/efx/xpath/XPathAttributeLocator.java deleted file mode 100644 index 34d24ef..0000000 --- a/src/main/java/eu/europa/ted/efx/xpath/XPathAttributeLocator.java +++ /dev/null @@ -1,114 +0,0 @@ -package eu.europa.ted.efx.xpath; - -import org.antlr.v4.runtime.CharStream; -import org.antlr.v4.runtime.CharStreams; -import org.antlr.v4.runtime.CommonTokenStream; -import org.antlr.v4.runtime.tree.ParseTree; -import org.antlr.v4.runtime.tree.ParseTreeWalker; -import org.apache.commons.lang3.StringUtils; - -import eu.europa.ted.efx.model.expressions.Expression; -import eu.europa.ted.efx.model.expressions.path.NodePathExpression; -import eu.europa.ted.efx.model.expressions.path.PathExpression; -import eu.europa.ted.efx.xpath.XPath20Parser.AbbrevforwardstepContext; -import eu.europa.ted.efx.xpath.XPath20Parser.PredicateContext; - -/** - * Uses the {@link XPath20Parser} to extract an attribute from an XPath expression. Arguably one - * could try to do the same thing using regular expressions, however, with the parser we can handle - * correctly any XPath expression regardless of how complicated it maybe. - * - * The reason we need to examine if an XPath expression points to an attribute is because some - * eForms Fields represent attribute values. This means that these attributes are effectively hidden - * behind a Field identifier and cannot be visible by the lexical analyzer or the parser. They are - * only visible to the translator after dereferencing such Field identifiers. At this point, the - * translator relies on this class to detect the presence of such attributes and translate - * accordingly. - */ -public class XPathAttributeLocator extends XPath20BaseListener { - - private int inPredicate = 0; - private int splitPosition = -1; - private String path; - private String attribute; - - /** - * Gets the XPath to the XML element that contains the attribute. - * The returned XPath therefore does not contain the attribute itself. - * - * @return A {@link NodePathExpression} pointing to the XML element that contains the attribute. - */ - public NodePathExpression getElementPath() { - return Expression.instantiate(path, NodePathExpression.class); - } - - /** - * Gets the name of the attribute (without the @ prefix). - * If the parsed XPath did not point to an attribute, then this method returns null. - * - * @return The name of the attribute (or null if the parsed XPath did not point to an attribute). - */ - public String getAttributeName() { - return StringUtils.isBlank(attribute) ? null : attribute; - } - - public Boolean hasAttribute() { - return attribute != null; - } - - @Override - public void enterPredicate(PredicateContext ctx) { - this.inPredicate++; - } - - @Override - public void exitPredicate(PredicateContext ctx) { - this.inPredicate--; - } - - @Override - public void exitAbbrevforwardstep(AbbrevforwardstepContext ctx) { - if (this.inPredicate == 0 && ctx.AT() != null) { - this.splitPosition = ctx.AT().getSymbol().getCharPositionInLine(); - this.attribute = ctx.nodetest().getText(); - } - } - - public static XPathAttributeLocator findAttribute(final PathExpression xpath) { - return findAttribute(xpath.getScript()); - } - - public static XPathAttributeLocator findAttribute(final String xpath) { - - final XPathAttributeLocator locator = new XPathAttributeLocator(); - - if (!xpath.contains("@")) { - locator.path = xpath; - locator.attribute = null; - return locator; - } - - final CharStream inputStream = CharStreams.fromString(xpath); - final XPath20Lexer lexer = new XPath20Lexer(inputStream); - final CommonTokenStream tokens = new CommonTokenStream(lexer); - final XPath20Parser parser = new XPath20Parser(tokens); - final ParseTree tree = parser.xpath(); - final ParseTreeWalker walker = new ParseTreeWalker(); - - walker.walk(locator, tree); - - if (locator.splitPosition > -1) { - // The attribute we are looking for is at splitPosition - String path = xpath.substring(0, locator.splitPosition); - while (path.endsWith("/")) { - path = path.substring(0, path.length() - 1); - } - locator.path = path; - } else { - // the XPAth does not point to an attribute - locator.path = xpath; - } - - return locator; - } -} From 70895abab00480191a01c9027ae97cba6df58652 Mon Sep 17 00:00:00 2001 From: Bertrand Lorentz Date: Thu, 12 Oct 2023 16:55:04 +0200 Subject: [PATCH 05/15] Use XPathProcessor for all remaining XPath operations The XPathContextualizer class is only kept to provide utility method to box and unbox XPath strings into PathExpression instances, and just uses XPathProcessor from eforms-core-java. Remove the XPathContextualizerTest class, as all those unit tests are now unit tests of XPathProcessor. --- .../sdk1/xpath/XPathScriptGeneratorV1.java | 6 +- .../ted/efx/xpath/XPathContextualizer.java | 413 +----------------- .../ted/efx/xpath/XPathScriptGenerator.java | 4 +- .../ted/efx/XPathContextualizerTest.java | 149 ------- 4 files changed, 13 insertions(+), 559 deletions(-) delete mode 100644 src/test/java/eu/europa/ted/efx/XPathContextualizerTest.java 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 bbf16e9..8dce460 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 @@ -2,10 +2,11 @@ import eu.europa.ted.eforms.sdk.component.SdkComponent; import eu.europa.ted.eforms.sdk.component.SdkComponentType; +import eu.europa.ted.eforms.xpath.XPathInfo; +import eu.europa.ted.eforms.xpath.XPathProcessor; import eu.europa.ted.efx.interfaces.TranslatorOptions; import eu.europa.ted.efx.model.expressions.path.PathExpression; import eu.europa.ted.efx.model.types.EfxDataType; -import eu.europa.ted.efx.xpath.XPathContextualizer; import eu.europa.ted.efx.xpath.XPathScriptGenerator; @SdkComponent(versions = {"1"}, componentType = SdkComponentType.SCRIPT_GENERATOR) @@ -40,7 +41,8 @@ public XPathScriptGeneratorV1(TranslatorOptions translatorOptions) { */ @Override public PathExpression composeFieldValueReference(PathExpression fieldReference) { - if (fieldReference.is(EfxDataType.MultilingualString.class) && !XPathContextualizer.hasPredicate(fieldReference, "@languageID")) { + XPathInfo xpathInfo = XPathProcessor.parse(fieldReference.getScript()); + if (fieldReference.is(EfxDataType.MultilingualString.class) && !xpathInfo.hasPredicate("@languageID")) { return PathExpression.instantiate("efx:preferred-language-text(" + fieldReference.getScript() + ")", fieldReference.getDataType()); } return super.composeFieldValueReference(fieldReference); 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 499285a..9f95c1c 100644 --- a/src/main/java/eu/europa/ted/efx/xpath/XPathContextualizer.java +++ b/src/main/java/eu/europa/ted/efx/xpath/XPathContextualizer.java @@ -1,64 +1,9 @@ package eu.europa.ted.efx.xpath; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Objects; -import java.util.Queue; -import java.util.function.Function; -import java.util.stream.Collectors; - -import org.antlr.v4.runtime.CharStream; -import org.antlr.v4.runtime.CharStreams; -import org.antlr.v4.runtime.CommonTokenStream; -import org.antlr.v4.runtime.ParserRuleContext; -import org.antlr.v4.runtime.misc.Interval; -import org.antlr.v4.runtime.tree.ParseTree; -import org.antlr.v4.runtime.tree.ParseTreeWalker; - +import eu.europa.ted.eforms.xpath.XPathProcessor; import eu.europa.ted.efx.model.expressions.path.PathExpression; -import eu.europa.ted.efx.xpath.XPath20Parser.AxisstepContext; -import eu.europa.ted.efx.xpath.XPath20Parser.FilterexprContext; -import eu.europa.ted.efx.xpath.XPath20Parser.PredicateContext; - -public class XPathContextualizer extends XPath20BaseListener { - - private final CharStream inputStream; - private final LinkedList steps = new LinkedList<>(); - - public XPathContextualizer(CharStream inputStream) { - this.inputStream = inputStream; - } - - /** - * Parses the XPath represented by th e given {@link PathExpression}} and - * returns a queue containing a {@link StepInfo} object for each step that the - * XPath is comprised of. - */ - private static Queue getSteps(PathExpression xpath) { - return getSteps(xpath.getScript()); - } - - /** - * Parses the given xpath and returns a queue containing a {@link StepInfo} for - * each step that the XPath is comprised of. - */ - private static Queue getSteps(String xpath) { - - final CharStream inputStream = CharStreams.fromString(xpath); - final XPath20Lexer lexer = new XPath20Lexer(inputStream); - final CommonTokenStream tokens = new CommonTokenStream(lexer); - final XPath20Parser parser = new XPath20Parser(tokens); - final ParseTree tree = parser.xpath(); - final ParseTreeWalker walker = new ParseTreeWalker(); - final XPathContextualizer contextualizer = new XPathContextualizer(inputStream); - walker.walk(contextualizer, tree); - - return contextualizer.steps; - } +public class XPathContextualizer { /** * Makes the given xpath relative to the given context xpath. @@ -74,362 +19,16 @@ public static PathExpression contextualize(final PathExpression contextXpath, if (contextXpath == null || contextXpath.getScript().isEmpty()) { return xpath; } - return PathExpression.instantiate(contextualize(contextXpath.getScript(), xpath.getScript()), xpath.getDataType()); - } - - public static String contextualize(final String contextXpath, - final String xpath) { - - // If we are asked to contextualise against a null or empty context - // then we must return the original xpath (instead of throwing an exception). - if (contextXpath == null || contextXpath.isEmpty()) { - return xpath; - } - - Queue contextSteps = new LinkedList(getSteps(contextXpath)); - Queue pathSteps = new LinkedList(getSteps(xpath)); - - return getContextualizedXpath(contextSteps, pathSteps); - } - - public static boolean hasPredicate(final PathExpression xpath, String match) { - return hasPredicate(xpath.getScript(), match); - } - - public static boolean hasPredicate(final String xpath, String match) { - return getSteps(xpath).stream().anyMatch(s -> s.getPredicateText().contains(match)); - } - - public static PathExpression addPredicate(final PathExpression pathExpression, final String predicate) { - return PathExpression.instantiate(addPredicate(pathExpression.getScript(), predicate), pathExpression.getDataType()); - } - - /** - * Attempts to add a predicate to the given xpath. - * It will add the predicate to the last axis-step in the xpath. - * If there is no axis-step in the xpath then it will add the predicate to the last step. - * If the xpath is empty then it will still return a PathExpression but with an empty xpath. - * - * @param xpath the xpath to add the predicate to - * @param predicate the predicate to add - * @return the xpath with the predicate added - */ - public static String addPredicate(final String xpath, final String predicate) { - if (predicate == null) { - return xpath; - } - - String _predicate = predicate.trim(); - - if (_predicate.isEmpty()) { - return xpath; - } - if (!_predicate.startsWith("[")) { - _predicate = "[" + _predicate; - } - - if (!_predicate.endsWith("]")) { - _predicate = _predicate + "]"; - } - - LinkedList steps = new LinkedList<>(getSteps(xpath)); + String result = XPathProcessor.contextualize(contextXpath.getScript(), xpath.getScript()); - StepInfo lastAxisStep = getLastAxisStep(steps); - if (lastAxisStep != null) { - lastAxisStep.predicates.add(_predicate); - } else if (steps.size() > 0) { - steps.getLast().predicates.add(_predicate); - } - return steps.stream().map(s -> s.stepText + s.getPredicateText()).collect(Collectors.joining("/")); - } - - private static StepInfo getLastAxisStep(LinkedList steps) { - int i = steps.size() - 1; - while (i >= 0 && !AxisStepInfo.class.isInstance(steps.get(i))) { - i--; - } - if (i < 0) { - return null; - } - return steps.get(i); + return PathExpression.instantiate(result, xpath.getDataType()); } public static PathExpression join(final PathExpression first, final PathExpression second) { - if (first == null || first.getScript().trim().isEmpty()) { - return second; - } - - if (second == null || second.getScript().trim().isEmpty()) { - return first; - } - - LinkedList firstPartSteps = new LinkedList<>(getSteps(first)); - LinkedList secondPartSteps = new LinkedList<>(getSteps(second)); - - return PathExpression.instantiate(getJoinedXPath(firstPartSteps, secondPartSteps), second.getDataType()); - } - - public static PathExpression addAxis(String axis, PathExpression path) { - LinkedList steps = new LinkedList<>(getSteps(path)); - - while (steps.getFirst().stepText.equals("..")) { - steps.removeFirst(); - } - - return PathExpression.instantiate( - axis + "::" + steps.stream().map(s -> s.stepText).collect(Collectors.joining("/")), path.getDataType()); - } - - private static String getContextualizedXpath(Queue contextQueue, - final Queue pathQueue) { - - // We will store the relative xPath here as we build it. - String relativeXpath = ""; - - if (contextQueue != null) { - - // First we will "consume" all nodes that are the same in both xPaths. - while (!contextQueue.isEmpty() && !pathQueue.isEmpty() - && pathQueue.peek().isTheSameAs(contextQueue.peek())) { - contextQueue.poll(); - pathQueue.poll(); - } + String joinedXPath = XPathProcessor.join(first.getScript(), second.getScript()); - // At this point there are no more matching nodes in the two queues. - - // We look at the first of the remaining steps in both queues and look if - // they are "similar" (meaning that they share the same step-text but but - // the path has different predicates). In this case we want to use a dot step - // with the predicate. - if (!contextQueue.isEmpty() && !pathQueue.isEmpty() && pathQueue.peek().isSimilarTo(contextQueue.peek())) { - contextQueue.poll(); // consume the same step from the contextQueue - if (contextQueue.isEmpty()) { - // Since there are no more steps in the contextQueue, the relative xpath should - // start with a dot step to provide a context for the predicate. - relativeXpath += "." + pathQueue.poll().getPredicateText(); - } else { - // Since there are more steps in the contextQueue which we will need to navigate back to, - // using back-steps, we will use a back-step to provide context of the predicate. - // This avoids an output that looks like ../.[predicate] which is valid but silly. - contextQueue.poll(); // consume the step from the contextQueue - relativeXpath += ".." + pathQueue.poll().getPredicateText(); - } - } - - // We start building the resulting relativeXpath by appending any nodes - // remaining in the pathQueue. - while (!pathQueue.isEmpty()) { - final StepInfo step = pathQueue.poll(); - relativeXpath += "/" + step.stepText + step.getPredicateText(); - } - - // We remove any leading forward slashes from the resulting xPath. - while (relativeXpath.startsWith("/")) { - relativeXpath = relativeXpath.substring(1); - } - - // For each step remaining in the contextQueue we prepend a back-step (..) in - // the resulting relativeXpath. - while (!contextQueue.isEmpty()) { - contextQueue.poll(); // consume the step - relativeXpath = "../" + relativeXpath; // prepend a back-step - } - - // We remove any trailing forward slashes from the resulting xPath. - while (relativeXpath.endsWith("/")) { - relativeXpath = relativeXpath.substring(0, relativeXpath.length() - 1); - } - - - // The relativeXpath will be empty if the path was identical to the context. - // In this case we return a dot. - if (relativeXpath.isEmpty()) { - relativeXpath = "."; - } - } - - return relativeXpath; - } - - - private static String getJoinedXPath(LinkedList first, - final LinkedList second) { - List dotSteps = Arrays.asList("..", "."); - while (second.getFirst().stepText.equals("..") - && !dotSteps.contains(first.getLast().stepText) && !first.getLast().isVariableStep()) { - second.removeFirst(); - first.removeLast(); - } - - return first.stream().map(f -> f.stepText).collect(Collectors.joining("/")) - + "/" + second.stream().map(s -> s.stepText).collect(Collectors.joining("/")); - } - - /** - * Helper method that returns the input text that matched a parser rule context. It is useful - * because {@link ParserRuleContext#getText()} omits whitespace and other lexer tokens in the - * HIDDEN channel. - * - * @param context - * @return - */ - private String getInputText(ParserRuleContext context) { - return this.inputStream - .getText(new Interval(context.start.getStartIndex(), context.stop.getStopIndex())); - } - - int predicateMode = 0; - - private Boolean inPredicateMode() { - return predicateMode > 0; - } - - @Override - public void exitAxisstep(AxisstepContext ctx) { - if (inPredicateMode()) { - return; - } - - // When we recognize a step, we add it to the queue if is is empty. - // If the queue is not empty, and the depth of the new step is not smaller than - // the depth of the last step in the queue, then this step needs to be added to - // the queue too. - // Otherwise, the last step in the queue is a sub-expression of the new step, - // and we need to - // replace it in the queue with the new step. - if (this.steps.isEmpty() || !this.steps.getLast().isPartOf(ctx.getSourceInterval())) { - this.steps.offer(new AxisStepInfo(ctx, this::getInputText)); - } else { - Interval removedInterval = ctx.getSourceInterval(); - while(!this.steps.isEmpty() && this.steps.getLast().isPartOf(removedInterval)) { - this.steps.removeLast(); - } - this.steps.offer(new AxisStepInfo(ctx, this::getInputText)); - } - } - - @Override - public void exitFilterexpr(FilterexprContext ctx) { - if (inPredicateMode()) { - return; - } - - // Same logic as for axis steps here (sse exitAxisstep). - if (this.steps.isEmpty() || !this.steps.getLast().isPartOf(ctx.getSourceInterval())) { - this.steps.offer(new FilterStepInfo(ctx, this::getInputText)); - } else { - Interval removedInterval = ctx.getSourceInterval(); - while(!this.steps.isEmpty() && this.steps.getLast().isPartOf(removedInterval)) { - this.steps.removeLast(); - } - this.steps.offer(new FilterStepInfo(ctx, this::getInputText)); - } - } - - @Override - public void enterPredicate(PredicateContext ctx) { - this.predicateMode++; - } - - @Override - public void exitPredicate(PredicateContext ctx) { - this.predicateMode--; - } - - public class AxisStepInfo extends StepInfo { - - public AxisStepInfo(AxisstepContext ctx, Function getInputText) { - super(ctx.reversestep() != null? getInputText.apply(ctx.reversestep()) : getInputText.apply(ctx.forwardstep()), - ctx.predicatelist().predicate().stream().map(getInputText).collect(Collectors.toList()), ctx.getSourceInterval()); - } - } - - public class FilterStepInfo extends StepInfo { - - public FilterStepInfo(FilterexprContext ctx, Function getInputText) { - super(getInputText.apply(ctx.primaryexpr()), - ctx.predicatelist().predicate().stream().map(getInputText).collect(Collectors.toList()), ctx.getSourceInterval()); - } - } - - public class StepInfo { - String stepText; - List predicates; - int a; - int b; - - protected StepInfo(String stepText, List predicates, Interval interval) { - this.stepText = stepText; - this.predicates = predicates; - this.a = interval.a; - this.b = interval.b; - } - - public Boolean isVariableStep() { - return this.stepText.startsWith("$"); - } - - public String getPredicateText() { - return String.join("", this.predicates); - } - - public Boolean isTheSameAs(final StepInfo contextStep) { - - // First check the step texts are the different. - if (!Objects.equals(contextStep.stepText, this.stepText)) { - return false; - } - - // If one of the two steps has more predicates that the other, - if (this.predicates.size() != contextStep.predicates.size()) { - // then the steps are the same if the path has no predicates - // or all the predicates of the path are also found in the context. - return this.predicates.isEmpty() || contextStep.predicates.containsAll(this.predicates); - } - - // If there are no predicates then the steps are the same. - if (this.predicates.isEmpty()) { - return true; - } - - // If there is only one predicate in each step, then we can do a quick comparison. - if (this.predicates.size() == 1) { - return Objects.equals(contextStep.predicates.get(0), this.predicates.get(0)); - } - - // Both steps contain multiple predicates. - // We need to compare them one by one. - // First we make a copy so that we can sort them without affecting the original lists. - List pathPredicates = new ArrayList<>(this.predicates); - List contextPredicates = new ArrayList<>(contextStep.predicates); - Collections.sort(pathPredicates); - Collections.sort(contextPredicates); - return pathPredicates.equals(contextPredicates); - } - - public Boolean isSimilarTo(final StepInfo contextStep) { - - // First check the step texts are the different. - if (!Objects.equals(contextStep.stepText, this.stepText)) { - return false; - } - - // If one of the two steps has more predicates that the other, - if (this.predicates.size() != contextStep.predicates.size()) { - // then the steps are the same if either of them has no predicates - // or all the predicates of the path are also found in the context. - return this.predicates.isEmpty() || contextStep.predicates.isEmpty() - || contextStep.predicates.containsAll(this.predicates); - } - - assert !this.isTheSameAs(contextStep) : "You should not be calling isSimilarTo() without first checking isTheSameAs()"; - return false; - } - - public Boolean isPartOf(Interval interval) { - return this.a >= interval.a && this.b <= interval.b; - } + return PathExpression.instantiate(joinedXPath, second.getDataType()); } } 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 f04c426..3c33cf2 100644 --- a/src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java +++ b/src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java @@ -14,6 +14,7 @@ import eu.europa.ted.eforms.sdk.component.SdkComponent; import eu.europa.ted.eforms.sdk.component.SdkComponentType; +import eu.europa.ted.eforms.xpath.XPathProcessor; import eu.europa.ted.efx.interfaces.ScriptGenerator; import eu.europa.ted.efx.interfaces.TranslatorOptions; import eu.europa.ted.efx.model.expressions.Expression; @@ -74,7 +75,8 @@ public PathExpression composeFieldReferenceWithPredicate(PathExpression fieldRef @Override public PathExpression composeFieldReferenceWithAxis(final PathExpression fieldReference, final String axis) { - return PathExpression.instantiate(XPathContextualizer.addAxis(axis, fieldReference).getScript(), fieldReference.getDataType()); + String resultXPath = XPathProcessor.addAxis(axis, fieldReference.getScript()); + return PathExpression.instantiate(resultXPath, fieldReference.getDataType()); } @Override diff --git a/src/test/java/eu/europa/ted/efx/XPathContextualizerTest.java b/src/test/java/eu/europa/ted/efx/XPathContextualizerTest.java deleted file mode 100644 index 9e666a2..0000000 --- a/src/test/java/eu/europa/ted/efx/XPathContextualizerTest.java +++ /dev/null @@ -1,149 +0,0 @@ -package eu.europa.ted.efx; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Test; - -import eu.europa.ted.efx.xpath.XPathContextualizer; - -class XPathContextualizerTest { - private String contextualize(final String context, final String xpath) { - return XPathContextualizer.contextualize(context, xpath); - } - - @Test - void testIdentical() { - assertEquals(".", contextualize("/a/b/c", "/a/b/c")); - } - - @Test - void testContextEmpty() { - assertEquals("/a/b/c", contextualize("", "/a/b/c")); - } - - @Test - void testUnderContext() { - assertEquals("c", contextualize("/a/b", "/a/b/c")); - } - - @Test - void testAboveContext() { - assertEquals("..", contextualize("/a/b/c", "/a/b")); - } - - @Test - void testSibling() { - assertEquals("../d", contextualize("/a/b/c", "/a/b/d")); - } - - @Test - void testTwoLevelsDifferent() { - assertEquals("../../x/y", contextualize("/a/b/c/d", "/a/b/x/y")); - } - - @Test - void testAllDifferent() { - assertEquals("../../../x/y/z", contextualize("/a/b/c/d", "/a/x/y/z")); - } - - @Test - void testDifferentRoot() { - // Not realistic, as XML has a single root, but a valid result - assertEquals("../../../x/y/z", contextualize("/a/b/c", "/x/y/z")); - } - - @Test - void testAttributeInXpath() { - assertEquals("../c/@attribute", contextualize("/a/b", "/a/c/@attribute")); - } - - @Test - void testAttributeInContext() { - assertEquals("../c/d", contextualize("/a/b/@attribute", "/a/b/c/d")); - } - - @Test - void testAttributeInBoth() { - assertEquals("../@x", contextualize("/a/b/c/@d", "/a/b/c/@x")); - } - - @Test - void testAttributeInBothSame() { - assertEquals(".", contextualize("/a/b/c/@d", "/a/b/c/@d")); - } - - @Test - void testPredicateInXpathLeaf() { - assertEquals("../d[x/y = 'z']", contextualize("/a/b/c", "/a/b/d[x/y = 'z']")); - } - - @Test - void testPredicateBeingTheOnlyDifference() { - assertEquals(".[x/y = 'z']", contextualize("/a/b/c", "/a/b/c[x/y = 'z']")); - } - - @Test - void testPredicateInContextBeingTheOnlyDifference() { - assertEquals(".", contextualize("/a/b/c[e/f = 'z']", "/a/b/c")); - } - - @Test - void testPredicatesBeingTheOnlyDifferences() { - assertEquals("..[u/v = 'w']/c[x/y = 'z']", contextualize("/a/b/c", "/a/b[u/v = 'w']/c[x/y = 'z']")); - } - - @Test - void testPredicateInContextLeaf() { - assertEquals("../d", contextualize("/a/b/c[e/f = 'z']", "/a/b/d")); - } - - @Test - void testPredicateInBothLeaf() { - assertEquals("../d[x = 'y']", contextualize("/a/b/c[e = 'f']", "/a/b/d[x = 'y']")); - } - - @Test - void testPredicateInXpathMiddle() { - assertEquals("..[x/y = 'z']/d", contextualize("/a/b/c", "/a/b[x/y = 'z']/d")); - } - - @Test - void testPredicateInContextMiddle() { - assertEquals("../d", contextualize("/a/b[e/f = 'z']/c", "/a/b/d")); - } - - @Test - void testPredicateSameInBoth() { - assertEquals("../d", contextualize("/a/b[e/f = 'z']/c", "/a/b[e/f = 'z']/d")); - } - - @Test - void testPredicateDifferentOnSameElement() { - assertEquals("../../b[x = 'y']/d", contextualize("/a/b[e = 'f']/c", "/a/b[x = 'y']/d")); - } - - @Test - void testPredicateDifferent() { - assertEquals(".[x = 'y']/d", contextualize("/a/b[e = 'f']/c", "/a/b/c[x = 'y']/d")); - } - - @Test - void testPredicateMoreInXpath() { - assertEquals("../../b[e][f]/c/d", contextualize("/a/b[e]/c", "/a/b[e][f]/c/d")); - } - - @Test - void testPredicateMoreInContext() { - assertEquals("d", contextualize("/a/b[e][f]/c", "/a/b[e]/c/d")); - } - - @Test - void testSeveralPredicatesIdentical() { - assertEquals("d", contextualize("/a/b[e][f]/c", "/a/b[e][f]/c/d")); - } - - @Test - void testSeveralPredicatesOneDifferent() { - assertEquals("../../b[e][x]/c/d", contextualize("/a/b[e][f]/c", "/a/b[e][x]/c/d")); - } -} From 7ca4342b2aa367b6fd342863a21225a4b79ad98f Mon Sep 17 00:00:00 2001 From: Bertrand Lorentz Date: Fri, 13 Oct 2023 14:43:04 +0200 Subject: [PATCH 06/15] Remove XPath20 grammar, and some cleanup and update in README files --- README.md | 2 +- pom.xml | 1 - .../antlr4/eu/europa/ted/efx/xpath/XPath20.g4 | 343 ------------------ .../java/eu/europa/ted/efx/xpath/README.md | 6 +- 4 files changed, 4 insertions(+), 348 deletions(-) delete mode 100644 src/main/antlr4/eu/europa/ted/efx/xpath/XPath20.g4 diff --git a/README.md b/README.md index d78dcba..972c9f8 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ See ".github/workflows/settings.xml". Unit tests are available under `src/test/java/`. They show in particular a variety of EFX expressions and the corresponding XPath expression. -After running the unit tests with `mvn test`, you can generate a coverage report with`mvn jacoco:report`. +After running the unit tests with `mvn test`, you can generate a coverage report with `mvn jacoco:report`. The report is available under `target/site/jacoco/`, in HTML, CSV, and XML format. ## Download diff --git a/pom.xml b/pom.xml index c0b128a..d8cad1c 100644 --- a/pom.xml +++ b/pom.xml @@ -330,7 +330,6 @@ **/EfxBaseListener.class **/EfxLexer.class **/EfxParser*.class - **/XPath20*.class diff --git a/src/main/antlr4/eu/europa/ted/efx/xpath/XPath20.g4 b/src/main/antlr4/eu/europa/ted/efx/xpath/XPath20.g4 deleted file mode 100644 index 893ef34..0000000 --- a/src/main/antlr4/eu/europa/ted/efx/xpath/XPath20.g4 +++ /dev/null @@ -1,343 +0,0 @@ -// XPath v2.0 -// Author--Ken Domino -// Date--2 Jan 2022 -// -// This is a faithful implementation of the XPath version 2.0 grammar -// from the spec at https://www.w3.org/TR/xpath20/ - -grammar XPath20; - -// [1] -xpath : expr EOF ; -expr : exprsingle ( COMMA exprsingle)* ; -exprsingle : forexpr | quantifiedexpr | ifexpr | orexpr ; -forexpr : simpleforclause KW_RETURN exprsingle ; -// [5] -simpleforclause : KW_FOR DOLLAR varname KW_IN exprsingle ( COMMA DOLLAR varname KW_IN exprsingle )* ; -quantifiedexpr : ( KW_SOME | KW_EVERY) DOLLAR varname KW_IN exprsingle ( COMMA DOLLAR varname KW_IN exprsingle)* KW_SATISFIES exprsingle ; -ifexpr : KW_IF OP expr CP KW_THEN exprsingle KW_ELSE exprsingle ; -orexpr : andexpr ( KW_OR andexpr )* ; -andexpr : comparisonexpr ( KW_AND comparisonexpr )* ; -// [10] -comparisonexpr : rangeexpr ( (valuecomp | generalcomp | nodecomp) rangeexpr )? ; -rangeexpr : additiveexpr ( KW_TO additiveexpr )? ; -additiveexpr : multiplicativeexpr ( (PLUS | MINUS) multiplicativeexpr )* ; -multiplicativeexpr : unionexpr ( (STAR | KW_DIV | KW_IDIV | KW_MOD) unionexpr )* ; -unionexpr : intersectexceptexpr ( (KW_UNION | P) intersectexceptexpr )* ; -// [15] -intersectexceptexpr : instanceofexpr ( ( KW_INTERSECT | KW_EXCEPT) instanceofexpr )* ; -instanceofexpr : treatexpr ( KW_INSTANCE KW_OF sequencetype )? ; -treatexpr : castableexpr ( KW_TREAT KW_AS sequencetype )? ; -castableexpr : castexpr ( KW_CASTABLE KW_AS singletype )? ; -castexpr : unaryexpr ( KW_CAST KW_AS singletype )? ; -// [20] -unaryexpr : ( MINUS | PLUS)* valueexpr ; -valueexpr : pathexpr ; -generalcomp : EQ | NE | LT | LE | GT | GE ; -valuecomp : KW_EQ | KW_NE | KW_LT | KW_LE | KW_GT | KW_GE ; -nodecomp : KW_IS | LL | GG ; -// [25] -pathexpr : ( SLASH relativepathexpr?) | ( SS relativepathexpr) | relativepathexpr ; -relativepathexpr : stepexpr (( SLASH | SS) stepexpr)* ; -stepexpr : filterexpr | axisstep ; -axisstep : (reversestep | forwardstep) predicatelist ; -forwardstep : (forwardaxis nodetest) | abbrevforwardstep ; -// [30] -forwardaxis : ( KW_CHILD COLONCOLON) | ( KW_DESCENDANT COLONCOLON) | ( KW_ATTRIBUTE COLONCOLON) | ( KW_SELF COLONCOLON) | ( KW_DESCENDANT_OR_SELF COLONCOLON) | ( KW_FOLLOWING_SIBLING COLONCOLON) | ( KW_FOLLOWING COLONCOLON) | ( KW_NAMESPACE COLONCOLON) ; -abbrevforwardstep : AT? nodetest ; -reversestep : (reverseaxis nodetest) | abbrevreversestep ; -reverseaxis : ( KW_PARENT COLONCOLON) | ( KW_ANCESTOR COLONCOLON) | ( KW_PRECEDING_SIBLING COLONCOLON) | ( KW_PRECEDING COLONCOLON) | ( KW_ANCESTOR_OR_SELF COLONCOLON) ; -abbrevreversestep : DD ; -// [35] -nodetest : kindtest | nametest ; -nametest : qname | wildcard ; -wildcard : STAR | (NCName CS) | ( SC NCName) ; -filterexpr : primaryexpr predicatelist ; -predicatelist : predicate* ; -// [40] -predicate : OB expr CB ; -primaryexpr : literal | varref | parenthesizedexpr | contextitemexpr | functioncall ; -literal : numericliteral | StringLiteral ; -numericliteral : IntegerLiteral | DecimalLiteral | DoubleLiteral ; -varref : DOLLAR varname ; -// [45] -varname : qname ; -parenthesizedexpr : OP expr? CP ; -contextitemexpr : D ; -functioncall : - { !( - getInputStream().LA(1)==KW_ARRAY - || getInputStream().LA(1)==KW_ATTRIBUTE - || getInputStream().LA(1)==KW_COMMENT - || getInputStream().LA(1)==KW_DOCUMENT_NODE - || getInputStream().LA(1)==KW_ELEMENT - || getInputStream().LA(1)==KW_EMPTY_SEQUENCE - || getInputStream().LA(1)==KW_FUNCTION - || getInputStream().LA(1)==KW_IF - || getInputStream().LA(1)==KW_ITEM - || getInputStream().LA(1)==KW_MAP - || getInputStream().LA(1)==KW_NAMESPACE_NODE - || getInputStream().LA(1)==KW_NODE - || getInputStream().LA(1)==KW_PROCESSING_INSTRUCTION - || getInputStream().LA(1)==KW_SCHEMA_ATTRIBUTE - || getInputStream().LA(1)==KW_SCHEMA_ELEMENT - || getInputStream().LA(1)==KW_TEXT - ) }? - qname OP (exprsingle ( COMMA exprsingle)*)? CP ; -singletype : atomictype QM? ; -// [50] -sequencetype : ( KW_EMPTY_SEQUENCE OP CP) | (itemtype occurrenceindicator?) ; -occurrenceindicator : QM | STAR | PLUS ; -itemtype : kindtest | ( KW_ITEM OP CP) | atomictype ; -atomictype : qname ; -kindtest : documenttest | elementtest | attributetest | schemaelementtest | schemaattributetest | pitest | commenttest | texttest | anykindtest ; -// [55] -anykindtest : KW_NODE OP CP ; -documenttest : KW_DOCUMENT_NODE OP (elementtest | schemaelementtest)? CP ; -texttest : KW_TEXT OP CP ; -commenttest : KW_COMMENT OP CP ; -pitest : KW_PROCESSING_INSTRUCTION OP (NCName | StringLiteral)? CP ; -// [60] -attributetest : KW_ATTRIBUTE OP (attribnameorwildcard ( COMMA typename_)?)? CP ; -attribnameorwildcard : attributename | STAR ; -schemaattributetest : KW_SCHEMA_ATTRIBUTE OP attributedeclaration CP ; -attributedeclaration : attributename ; -elementtest : KW_ELEMENT OP (elementnameorwildcard ( COMMA typename_ QM?)?)? CP ; -// [65] -elementnameorwildcard : elementname | STAR ; -schemaelementtest : KW_SCHEMA_ELEMENT OP elementdeclaration CP ; -elementdeclaration : elementname ; -attributename : qname ; -elementname : qname ; -// [70] -typename_ : qname ; - - -// Error in the spec. EQName also includes acceptable keywords. -qname : QName | URIQualifiedName - | KW_ANCESTOR - | KW_ANCESTOR_OR_SELF - | KW_AND - | KW_ARRAY - | KW_AS - | KW_ATTRIBUTE - | KW_CAST - | KW_CASTABLE - | KW_CHILD - | KW_COMMENT - | KW_DESCENDANT - | KW_DESCENDANT_OR_SELF - | KW_DIV - | KW_DOCUMENT_NODE - | KW_ELEMENT - | KW_ELSE - | KW_EMPTY_SEQUENCE - | KW_EQ - | KW_EVERY - | KW_EXCEPT - | KW_FOLLOWING - | KW_FOLLOWING_SIBLING - | KW_FOR - | KW_FUNCTION - | KW_GE - | KW_GT - | KW_IDIV - | KW_IF - | KW_IN - | KW_INSTANCE - | KW_INTERSECT - | KW_IS - | KW_ITEM - | KW_LE - | KW_LET - | KW_LT - | KW_MAP - | KW_MOD - | KW_NAMESPACE - | KW_NAMESPACE_NODE - | KW_NE - | KW_NODE - | KW_OF - | KW_OR - | KW_PARENT - | KW_PRECEDING - | KW_PRECEDING_SIBLING - | KW_PROCESSING_INSTRUCTION - | KW_RETURN - | KW_SATISFIES - | KW_SCHEMA_ATTRIBUTE - | KW_SCHEMA_ELEMENT - | KW_SELF - | KW_SOME - | KW_TEXT - | KW_THEN - | KW_TREAT - | KW_UNION - ; - -// Not per spec. Specified for testing. -auxilary : (expr SEMI )+ EOF; - - -AT : '@' ; -BANG : '!' ; -CB : ']' ; -CC : '}' ; -CEQ : ':=' ; -COLON : ':' ; -COLONCOLON : '::' ; -COMMA : ',' ; -CP : ')' ; -CS : ':*' ; -D : '.' ; -DD : '..' ; -DOLLAR : '$' ; -EG : '=>' ; -EQ : '=' ; -GE : '>=' ; -GG : '>>' ; -GT : '>' ; -LE : '<=' ; -LL : '<<' ; -LT : '<' ; -MINUS : '-' ; -NE : '!=' ; -OB : '[' ; -OC : '{' ; -OP : '(' ; -P : '|' ; -PLUS : '+' ; -POUND : '#' ; -PP : '||' ; -QM : '?' ; -SC : '*:' ; -SLASH : '/' ; -SS : '//' ; -STAR : '*' ; - -// KEYWORDS - -KW_ANCESTOR : 'ancestor' ; -KW_ANCESTOR_OR_SELF : 'ancestor-or-self' ; -KW_AND : 'and' ; -KW_ARRAY : 'array' ; -KW_AS : 'as' ; -KW_ATTRIBUTE : 'attribute' ; -KW_CAST : 'cast' ; -KW_CASTABLE : 'castable' ; -KW_CHILD : 'child' ; -KW_COMMENT : 'comment' ; -KW_DESCENDANT : 'descendant' ; -KW_DESCENDANT_OR_SELF : 'descendant-or-self' ; -KW_DIV : 'div' ; -KW_DOCUMENT_NODE : 'document-node' ; -KW_ELEMENT : 'element' ; -KW_ELSE : 'else' ; -KW_EMPTY_SEQUENCE : 'empty-sequence' ; -KW_EQ : 'eq' ; -KW_EVERY : 'every' ; -KW_EXCEPT : 'except' ; -KW_FOLLOWING : 'following' ; -KW_FOLLOWING_SIBLING : 'following-sibling' ; -KW_FOR : 'for' ; -KW_FUNCTION : 'function' ; -KW_GE : 'ge' ; -KW_GT : 'gt' ; -KW_IDIV : 'idiv' ; -KW_IF : 'if' ; -KW_IN : 'in' ; -KW_INSTANCE : 'instance' ; -KW_INTERSECT : 'intersect' ; -KW_IS : 'is' ; -KW_ITEM : 'item' ; -KW_LE : 'le' ; -KW_LET : 'let' ; -KW_LT : 'lt' ; -KW_MAP : 'map' ; -KW_MOD : 'mod' ; -KW_NAMESPACE : 'namespace' ; -KW_NAMESPACE_NODE : 'namespace-node' ; -KW_NE : 'ne' ; -KW_NODE : 'node' ; -KW_OF : 'of' ; -KW_OR : 'or' ; -KW_PARENT : 'parent' ; -KW_PRECEDING : 'preceding' ; -KW_PRECEDING_SIBLING : 'preceding-sibling' ; -KW_PROCESSING_INSTRUCTION : 'processing-instruction' ; -KW_RETURN : 'return' ; -KW_SATISFIES : 'satisfies' ; -KW_SCHEMA_ATTRIBUTE : 'schema-attribute' ; -KW_SCHEMA_ELEMENT : 'schema-element' ; -KW_SELF : 'self' ; -KW_SOME : 'some' ; -KW_TEXT : 'text' ; -KW_THEN : 'then' ; -KW_TO : 'to' ; -KW_TREAT : 'treat' ; -KW_UNION : 'union' ; - -// A.2.1. TEMINAL SYMBOLS -// This isn't a complete list of tokens in the language. -// Keywords and symbols are terminals. - -IntegerLiteral : FragDigits ; -DecimalLiteral : ('.' FragDigits) | (FragDigits '.' [0-9]*) ; -DoubleLiteral : (('.' FragDigits) | (FragDigits ('.' [0-9]*)?)) [eE] [+-]? FragDigits ; -StringLiteral : ('"' (FragEscapeQuot | ~[^"])*? '"') | ('\'' (FragEscapeApos | ~['])*? '\'') ; -URIQualifiedName : BracedURILiteral NCName ; -BracedURILiteral : 'Q' '{' [^{}]* '}' ; -// Error in spec: EscapeQuot and EscapeApos are not terminals! -fragment FragEscapeQuot : '""' ; -fragment FragEscapeApos : '\''; -// Error in spec: Comment isn't really a terminal, but an off-channel object. -Comment : '(:' (Comment | CommentContents)*? ':)' -> skip ; -QName : FragQName ; -NCName : FragmentNCName ; -// Error in spec: Char is not a terminal! -fragment Char : FragChar ; -fragment FragDigits : [0-9]+ ; -fragment CommentContents : Char ; -// https://www.w3.org/TR/REC-xml-names/#NT-QName -fragment FragQName : FragPrefixedName | FragUnprefixedName ; -fragment FragPrefixedName : FragPrefix ':' FragLocalPart ; -fragment FragUnprefixedName : FragLocalPart ; -fragment FragPrefix : FragmentNCName ; -fragment FragLocalPart : FragmentNCName ; -fragment FragNCNameStartChar - : 'A'..'Z' - | '_' - | 'a'..'z' - | '\u00C0'..'\u00D6' - | '\u00D8'..'\u00F6' - | '\u00F8'..'\u02FF' - | '\u0370'..'\u037D' - | '\u037F'..'\u1FFF' - | '\u200C'..'\u200D' - | '\u2070'..'\u218F' - | '\u2C00'..'\u2FEF' - | '\u3001'..'\uD7FF' - | '\uF900'..'\uFDCF' - | '\uFDF0'..'\uFFFD' - | '\u{10000}'..'\u{EFFFF}' - ; -fragment FragNCNameChar - : FragNCNameStartChar | '-' | '.' | '0'..'9' - | '\u00B7' | '\u0300'..'\u036F' - | '\u203F'..'\u2040' - ; -fragment FragmentNCName : FragNCNameStartChar FragNCNameChar* ; - -// https://www.w3.org/TR/REC-xml/#NT-Char - -fragment FragChar : '\u0009' | '\u000a' | '\u000d' - | '\u0020'..'\ud7ff' - | '\ue000'..'\ufffd' - | '\u{10000}'..'\u{10ffff}' - ; - -// https://github.com/antlr/grammars-v4/blob/17d3db3fd6a8fc319a12176e0bb735b066ec0616/xpath/xpath31/XPath31.g4#L389 -Whitespace : ('\u000d' | '\u000a' | '\u0020' | '\u0009')+ -> skip ; - -// Not per spec. Specified for testing. -SEMI : ';' ; \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/xpath/README.md b/src/main/java/eu/europa/ted/efx/xpath/README.md index e35001a..324bd0c 100644 --- a/src/main/java/eu/europa/ted/efx/xpath/README.md +++ b/src/main/java/eu/europa/ted/efx/xpath/README.md @@ -1,8 +1,8 @@ # Translating EFX to XPath + This package contains classes used for translating EFX expressions to XPath. * `XPathScriptGenerator`: Implements the `ScriptGenerator` interface for EFX to XPath translation. -* `XPathContextualized`: Used to convert a given absolute XPath to an XPath relative to another absolute XPath. -* `XPathAttributeLocator`: Used to parse an XPath expression and extract any XML attributes it may contain. +* `XPathContextualizer`: Used to convert a given absolute XPath expression to an XPath relative to another absolute XPath. -_Note: There is one more class that is specific to EFX-to-XPath translation which is not contained in this package: the [`SdkSymbolResolver`](../../eforms/sdk/SdkSymbolResolver.java) class. It is XPath specific because it returns XPaths taken from the eForms SDK._ \ No newline at end of file +_Note: There is one more class that is specific to EFX-to-XPath translation which is not contained in this package: the [`SdkSymbolResolver`](../../eforms/sdk/SdkSymbolResolver.java) class. It is XPath specific because it returns XPaths taken from the eForms SDK._ From 7b561c87e10d5d33aae3355c910029d993d46351 Mon Sep 17 00:00:00 2001 From: Ioannis Rousochatzakis Date: Fri, 20 Oct 2023 13:38:14 +0200 Subject: [PATCH 07/15] Sequences of labels now render distinct values --- pom.xml | 2 +- .../ted/efx/sdk1/EfxTemplateTranslatorV1.java | 60 ++++++++++-------- .../ted/efx/sdk2/EfxTemplateTranslatorV2.java | 62 +++++++++++-------- .../efx/sdk1/EfxTemplateTranslatorV1Test.java | 16 ++--- .../efx/sdk2/EfxTemplateTranslatorV2Test.java | 16 ++--- 5 files changed, 85 insertions(+), 71 deletions(-) diff --git a/pom.xml b/pom.xml index 561e15a..c0b128a 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ ${project.build.directory}/eforms-sdk/antlr4 - 1.0.6-SNAPSHOT + 1.2.0-SNAPSHOT 4.9.3 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 460a4eb..983a397 100644 --- a/src/main/java/eu/europa/ted/efx/sdk1/EfxTemplateTranslatorV1.java +++ b/src/main/java/eu/europa/ted/efx/sdk1/EfxTemplateTranslatorV1.java @@ -303,36 +303,42 @@ private void shorthandIndirectLabelReference(final String fieldId) { this.script.composeVariableReference("item", StringExpression.class)); switch (fieldType) { case "indicator": - this.stack.push(this.markup.renderLabelFromExpression(this.script.composeForExpression( - this.script.composeIteratorList( - List.of( - this.script.composeIteratorExpression(loopVariable.declarationExpression, valueReference))), - this.script.composeStringConcatenation( - List.of(this.script.getStringLiteralFromUnquotedString(ASSET_TYPE_INDICATOR), - this.script.getStringLiteralFromUnquotedString("|"), - this.script.getStringLiteralFromUnquotedString(LABEL_TYPE_WHEN), - this.script.getStringLiteralFromUnquotedString("-"), - new StringExpression(loopVariable.referenceExpression.getScript()), - this.script.getStringLiteralFromUnquotedString("|"), - this.script.getStringLiteralFromUnquotedString(fieldId))), - StringSequenceExpression.class))); + this.stack.push(this.markup.renderLabelFromExpression( + this.script.composeDistinctValuesFunction( + this.script.composeForExpression( + this.script.composeIteratorList( + List.of( + this.script.composeIteratorExpression(loopVariable.declarationExpression, valueReference))), + this.script.composeStringConcatenation( + List.of(this.script.getStringLiteralFromUnquotedString(ASSET_TYPE_INDICATOR), + this.script.getStringLiteralFromUnquotedString("|"), + this.script.getStringLiteralFromUnquotedString(LABEL_TYPE_WHEN), + this.script.getStringLiteralFromUnquotedString("-"), + new StringExpression(loopVariable.referenceExpression.getScript()), + this.script.getStringLiteralFromUnquotedString("|"), + this.script.getStringLiteralFromUnquotedString(fieldId))), + StringSequenceExpression.class), + StringSequenceExpression.class))); break; case "code": case "internal-code": - this.stack.push(this.markup.renderLabelFromExpression(this.script.composeForExpression( - this.script.composeIteratorList( - List.of( - this.script.composeIteratorExpression(loopVariable.declarationExpression, valueReference))), - this.script.composeStringConcatenation(List.of( - this.script.getStringLiteralFromUnquotedString(ASSET_TYPE_CODE), - this.script.getStringLiteralFromUnquotedString("|"), - this.script.getStringLiteralFromUnquotedString(LABEL_TYPE_NAME), - this.script.getStringLiteralFromUnquotedString("|"), - this.script.getStringLiteralFromUnquotedString( - this.symbols.getRootCodelistOfField(fieldId)), - this.script.getStringLiteralFromUnquotedString("."), - new StringExpression(loopVariable.referenceExpression.getScript()))), - StringSequenceExpression.class))); + this.stack.push(this.markup.renderLabelFromExpression( + this.script.composeDistinctValuesFunction( + this.script.composeForExpression( + this.script.composeIteratorList( + List.of( + this.script.composeIteratorExpression(loopVariable.declarationExpression, valueReference))), + this.script.composeStringConcatenation(List.of( + this.script.getStringLiteralFromUnquotedString(ASSET_TYPE_CODE), + this.script.getStringLiteralFromUnquotedString("|"), + this.script.getStringLiteralFromUnquotedString(LABEL_TYPE_NAME), + this.script.getStringLiteralFromUnquotedString("|"), + this.script.getStringLiteralFromUnquotedString( + this.symbols.getRootCodelistOfField(fieldId)), + this.script.getStringLiteralFromUnquotedString("."), + new StringExpression(loopVariable.referenceExpression.getScript()))), + StringSequenceExpression.class), + StringSequenceExpression.class))); break; default: throw new ParseCancellationException(String.format( diff --git a/src/main/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2.java b/src/main/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2.java index ed39da2..650843b 100644 --- a/src/main/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2.java +++ b/src/main/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2.java @@ -347,36 +347,44 @@ private void shorthandIndirectLabelReference(final String fieldId, final Numeric this.script.composeVariableReference("item", StringExpression.class)); switch (fieldType) { case "indicator": - this.stack.push(this.markup.renderLabelFromExpression(this.script.composeForExpression( - this.script.composeIteratorList( - List.of( - this.script.composeIteratorExpression(loopVariable.declarationExpression, valueReference))), - this.script.composeStringConcatenation( - List.of(this.script.getStringLiteralFromUnquotedString(ASSET_TYPE_INDICATOR), - this.script.getStringLiteralFromUnquotedString("|"), - this.script.getStringLiteralFromUnquotedString(LABEL_TYPE_WHEN), - this.script.getStringLiteralFromUnquotedString("-"), - new StringExpression(loopVariable.referenceExpression.getScript()), - this.script.getStringLiteralFromUnquotedString("|"), - this.script.getStringLiteralFromUnquotedString(fieldId))), - StringSequenceExpression.class), quantity)); + this.stack.push(this.markup.renderLabelFromExpression( + this.script.composeDistinctValuesFunction( + this.script.composeForExpression( + this.script.composeIteratorList( + List.of( + this.script.composeIteratorExpression(loopVariable.declarationExpression, valueReference))), + this.script.composeStringConcatenation( + List.of(this.script.getStringLiteralFromUnquotedString(ASSET_TYPE_INDICATOR), + this.script.getStringLiteralFromUnquotedString("|"), + this.script.getStringLiteralFromUnquotedString(LABEL_TYPE_WHEN), + this.script.getStringLiteralFromUnquotedString("-"), + new StringExpression(loopVariable.referenceExpression.getScript()), + this.script.getStringLiteralFromUnquotedString("|"), + this.script.getStringLiteralFromUnquotedString(fieldId))), + StringSequenceExpression.class), + StringSequenceExpression.class), + quantity)); break; case "code": case "internal-code": - this.stack.push(this.markup.renderLabelFromExpression(this.script.composeForExpression( - this.script.composeIteratorList( - List.of( - this.script.composeIteratorExpression(loopVariable.declarationExpression, valueReference))), - this.script.composeStringConcatenation(List.of( - this.script.getStringLiteralFromUnquotedString(ASSET_TYPE_CODE), - this.script.getStringLiteralFromUnquotedString("|"), - this.script.getStringLiteralFromUnquotedString(LABEL_TYPE_NAME), - this.script.getStringLiteralFromUnquotedString("|"), - this.script.getStringLiteralFromUnquotedString( - this.symbols.getRootCodelistOfField(fieldId)), - this.script.getStringLiteralFromUnquotedString("."), - new StringExpression(loopVariable.referenceExpression.getScript()))), - StringSequenceExpression.class), quantity)); + this.stack.push(this.markup.renderLabelFromExpression( + this.script.composeDistinctValuesFunction( + this.script.composeForExpression( + this.script.composeIteratorList( + List.of( + this.script.composeIteratorExpression(loopVariable.declarationExpression, valueReference))), + this.script.composeStringConcatenation(List.of( + this.script.getStringLiteralFromUnquotedString(ASSET_TYPE_CODE), + this.script.getStringLiteralFromUnquotedString("|"), + this.script.getStringLiteralFromUnquotedString(LABEL_TYPE_NAME), + this.script.getStringLiteralFromUnquotedString("|"), + this.script.getStringLiteralFromUnquotedString( + this.symbols.getRootCodelistOfField(fieldId)), + this.script.getStringLiteralFromUnquotedString("."), + new StringExpression(loopVariable.referenceExpression.getScript()))), + StringSequenceExpression.class), + StringSequenceExpression.class), + quantity)); break; default: throw new ParseCancellationException(String.format( 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 a98697b..7d3b3d9 100644 --- a/src/test/java/eu/europa/ted/efx/sdk1/EfxTemplateTranslatorV1Test.java +++ b/src/test/java/eu/europa/ted/efx/sdk1/EfxTemplateTranslatorV1Test.java @@ -203,42 +203,42 @@ void testShorthandBtLabelReference_MissingLabelType() { @Test void testShorthandIndirectLabelReferenceForIndicator() { assertEquals( - "let block01() -> { label(for $item in ../IndicatorField return concat('indicator', '|', 'when', '-', $item, '|', 'BT-00-Indicator')) }\nfor-each(/*/PathNode/TextField).call(block01())", + "let block01() -> { label(distinct-values(for $item in ../IndicatorField return concat('indicator', '|', 'when', '-', $item, '|', 'BT-00-Indicator'))) }\nfor-each(/*/PathNode/TextField).call(block01())", translateTemplate("{BT-00-Text} #{BT-00-Indicator}")); } @Test void testShorthandIndirectLabelReferenceForCode() { assertEquals( - "let block01() -> { label(for $item in ../CodeField/normalize-space(text()) return concat('code', '|', 'name', '|', 'main-activity', '.', $item)) }\nfor-each(/*/PathNode/TextField).call(block01())", + "let block01() -> { label(distinct-values(for $item in ../CodeField/normalize-space(text()) return concat('code', '|', 'name', '|', 'main-activity', '.', $item))) }\nfor-each(/*/PathNode/TextField).call(block01())", translateTemplate("{BT-00-Text} #{BT-00-Code}")); } @Test void testShorthandIndirectLabelReferenceForInternalCode() { assertEquals( - "let block01() -> { label(for $item in ../InternalCodeField/normalize-space(text()) return concat('code', '|', 'name', '|', 'main-activity', '.', $item)) }\nfor-each(/*/PathNode/TextField).call(block01())", + "let block01() -> { label(distinct-values(for $item in ../InternalCodeField/normalize-space(text()) return concat('code', '|', 'name', '|', 'main-activity', '.', $item))) }\nfor-each(/*/PathNode/TextField).call(block01())", translateTemplate("{BT-00-Text} #{BT-00-Internal-Code}")); } @Test void testShorthandIndirectLabelReferenceForCodeAttribute() { assertEquals( - "let block01() -> { label(for $item in ../CodeField/@attribute return concat('code', '|', 'name', '|', 'main-activity', '.', $item)) }\nfor-each(/*/PathNode/TextField).call(block01())", + "let block01() -> { label(distinct-values(for $item in ../CodeField/@attribute return concat('code', '|', 'name', '|', 'main-activity', '.', $item))) }\nfor-each(/*/PathNode/TextField).call(block01())", translateTemplate("{BT-00-Text} #{BT-00-CodeAttribute}")); } @Test void testShorthandIndirectLabelReferenceForCodeAttribute_WithSameAttributeInContext() { assertEquals( - "let block01() -> { label(for $item in ../@attribute return concat('code', '|', 'name', '|', 'main-activity', '.', $item)) }\nfor-each(/*/PathNode/CodeField/@attribute).call(block01())", + "let block01() -> { label(distinct-values(for $item in ../@attribute return concat('code', '|', 'name', '|', 'main-activity', '.', $item))) }\nfor-each(/*/PathNode/CodeField/@attribute).call(block01())", translateTemplate("{BT-00-CodeAttribute} #{BT-00-CodeAttribute}")); } @Test void testShorthandIndirectLabelReferenceForCodeAttribute_WithSameElementInContext() { assertEquals( - "let block01() -> { label(for $item in ./@attribute return concat('code', '|', 'name', '|', 'main-activity', '.', $item)) }\nfor-each(/*/PathNode/CodeField).call(block01())", + "let block01() -> { label(distinct-values(for $item in ./@attribute return concat('code', '|', 'name', '|', 'main-activity', '.', $item))) }\nfor-each(/*/PathNode/CodeField).call(block01())", translateTemplate("{BT-00-Code} #{BT-00-CodeAttribute}")); } @@ -297,7 +297,7 @@ void testShorthandLabelReferenceFromContext_WithNodeContext() { @Test void testShorthandIndirectLabelReferenceFromContextField() { assertEquals( - "let block01() -> { label(for $item in ./normalize-space(text()) return concat('code', '|', 'name', '|', 'main-activity', '.', $item)) }\nfor-each(/*/PathNode/CodeField).call(block01())", + "let block01() -> { label(distinct-values(for $item in ./normalize-space(text()) return concat('code', '|', 'name', '|', 'main-activity', '.', $item))) }\nfor-each(/*/PathNode/CodeField).call(block01())", translateTemplate("{BT-00-Code} #value")); } @@ -318,7 +318,7 @@ void testShorthandFieldValueReferenceFromContextField() { @Test void testShorthandFieldValueReferenceFromContextField_WithText() { assertEquals( - "let block01() -> { text('blah ')label(for $item in ./normalize-space(text()) return concat('code', '|', 'name', '|', 'main-activity', '.', $item))text(' ')text('blah ')eval(./normalize-space(text()))text(' ')text('blah') }\nfor-each(/*/PathNode/CodeField).call(block01())", + "let block01() -> { text('blah ')label(distinct-values(for $item in ./normalize-space(text()) return concat('code', '|', 'name', '|', 'main-activity', '.', $item)))text(' ')text('blah ')eval(./normalize-space(text()))text(' ')text('blah') }\nfor-each(/*/PathNode/CodeField).call(block01())", translateTemplate("{BT-00-Code} blah #value blah $value blah")); } diff --git a/src/test/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2Test.java b/src/test/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2Test.java index f78cc07..127f911 100644 --- a/src/test/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2Test.java +++ b/src/test/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2Test.java @@ -292,42 +292,42 @@ void testShorthandBtLabelReference_MissingLabelType() { @Test void testShorthandIndirectLabelReferenceForIndicator() { assertEquals( - "let block01() -> { label(for $item in ../IndicatorField return concat('indicator', '|', 'when', '-', $item, '|', 'BT-00-Indicator')) }\nfor-each(/*/PathNode/TextField).call(block01())", + "let block01() -> { label(distinct-values(for $item in ../IndicatorField return concat('indicator', '|', 'when', '-', $item, '|', 'BT-00-Indicator'))) }\nfor-each(/*/PathNode/TextField).call(block01())", translateTemplate("{BT-00-Text} #{BT-00-Indicator}")); } @Test void testShorthandIndirectLabelReferenceForCode() { assertEquals( - "let block01() -> { label(for $item in ../CodeField/normalize-space(text()) return concat('code', '|', 'name', '|', 'main-activity', '.', $item)) }\nfor-each(/*/PathNode/TextField).call(block01())", + "let block01() -> { label(distinct-values(for $item in ../CodeField/normalize-space(text()) return concat('code', '|', 'name', '|', 'main-activity', '.', $item))) }\nfor-each(/*/PathNode/TextField).call(block01())", translateTemplate("{BT-00-Text} #{BT-00-Code}")); } @Test void testShorthandIndirectLabelReferenceForInternalCode() { assertEquals( - "let block01() -> { label(for $item in ../InternalCodeField/normalize-space(text()) return concat('code', '|', 'name', '|', 'main-activity', '.', $item)) }\nfor-each(/*/PathNode/TextField).call(block01())", + "let block01() -> { label(distinct-values(for $item in ../InternalCodeField/normalize-space(text()) return concat('code', '|', 'name', '|', 'main-activity', '.', $item))) }\nfor-each(/*/PathNode/TextField).call(block01())", translateTemplate("{BT-00-Text} #{BT-00-Internal-Code}")); } @Test void testShorthandIndirectLabelReferenceForCodeAttribute() { assertEquals( - "let block01() -> { label(for $item in ../CodeField/@attribute return concat('code', '|', 'name', '|', 'main-activity', '.', $item)) }\nfor-each(/*/PathNode/TextField).call(block01())", + "let block01() -> { label(distinct-values(for $item in ../CodeField/@attribute return concat('code', '|', 'name', '|', 'main-activity', '.', $item))) }\nfor-each(/*/PathNode/TextField).call(block01())", translateTemplate("{BT-00-Text} #{BT-00-CodeAttribute}")); } @Test void testShorthandIndirectLabelReferenceForCodeAttribute_WithSameAttributeInContext() { assertEquals( - "let block01() -> { label(for $item in ../@attribute return concat('code', '|', 'name', '|', 'main-activity', '.', $item)) }\nfor-each(/*/PathNode/CodeField/@attribute).call(block01())", + "let block01() -> { label(distinct-values(for $item in ../@attribute return concat('code', '|', 'name', '|', 'main-activity', '.', $item))) }\nfor-each(/*/PathNode/CodeField/@attribute).call(block01())", translateTemplate("{BT-00-CodeAttribute} #{BT-00-CodeAttribute}")); } @Test void testShorthandIndirectLabelReferenceForCodeAttribute_WithSameElementInContext() { assertEquals( - "let block01() -> { label(for $item in ./@attribute return concat('code', '|', 'name', '|', 'main-activity', '.', $item)) }\nfor-each(/*/PathNode/CodeField).call(block01())", + "let block01() -> { label(distinct-values(for $item in ./@attribute return concat('code', '|', 'name', '|', 'main-activity', '.', $item))) }\nfor-each(/*/PathNode/CodeField).call(block01())", translateTemplate("{BT-00-Code} #{BT-00-CodeAttribute}")); } @@ -386,7 +386,7 @@ void testShorthandLabelReferenceFromContext_WithNodeContext() { @Test void testShorthandIndirectLabelReferenceFromContextField() { assertEquals( - "let block01() -> { label(for $item in ./normalize-space(text()) return concat('code', '|', 'name', '|', 'main-activity', '.', $item)) }\nfor-each(/*/PathNode/CodeField).call(block01())", + "let block01() -> { label(distinct-values(for $item in ./normalize-space(text()) return concat('code', '|', 'name', '|', 'main-activity', '.', $item))) }\nfor-each(/*/PathNode/CodeField).call(block01())", translateTemplate("{BT-00-Code} #value")); } @@ -407,7 +407,7 @@ void testShorthandFieldValueReferenceFromContextField() { @Test void testShorthandFieldValueReferenceFromContextField_WithText() { assertEquals( - "let block01() -> { text('blah ')label(for $item in ./normalize-space(text()) return concat('code', '|', 'name', '|', 'main-activity', '.', $item))text(' blah ')eval(./normalize-space(text()))text(' blah') }\nfor-each(/*/PathNode/CodeField).call(block01())", + "let block01() -> { text('blah ')label(distinct-values(for $item in ./normalize-space(text()) return concat('code', '|', 'name', '|', 'main-activity', '.', $item)))text(' blah ')eval(./normalize-space(text()))text(' blah') }\nfor-each(/*/PathNode/CodeField).call(block01())", translateTemplate("{BT-00-Code} blah #value blah $value blah")); } From a177d31139ec95d2ac59f510672785a1887c3f38 Mon Sep 17 00:00:00 2001 From: Ioannis Rousochatzakis Date: Wed, 15 Nov 2023 01:28:40 +0100 Subject: [PATCH 08/15] Added a workaround for formatting dates and times in EFX 1 templates. --- .../ted/efx/sdk1/EfxTemplateTranslatorV1.java | 17 ++++++++++++++++- .../efx/sdk1/EfxTemplateTranslatorV1Test.java | 10 ++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) 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 983a397..73d1ff9 100644 --- a/src/main/java/eu/europa/ted/efx/sdk1/EfxTemplateTranslatorV1.java +++ b/src/main/java/eu/europa/ted/efx/sdk1/EfxTemplateTranslatorV1.java @@ -26,6 +26,7 @@ import eu.europa.ted.efx.model.Context.FieldContext; import eu.europa.ted.efx.model.Context.NodeContext; import eu.europa.ted.efx.model.expressions.Expression; +import eu.europa.ted.efx.model.expressions.TypedExpression; import eu.europa.ted.efx.model.expressions.path.PathExpression; import eu.europa.ted.efx.model.expressions.path.StringPathExpression; import eu.europa.ted.efx.model.expressions.scalar.StringExpression; @@ -33,6 +34,7 @@ import eu.europa.ted.efx.model.templates.ContentBlock; import eu.europa.ted.efx.model.templates.ContentBlockStack; import eu.europa.ted.efx.model.templates.Markup; +import eu.europa.ted.efx.model.types.EfxDataType; import eu.europa.ted.efx.model.variables.Variable; import eu.europa.ted.efx.model.variables.VariableList; import eu.europa.ted.efx.sdk1.EfxParser.AssetIdContext; @@ -426,7 +428,20 @@ public void exitAssetId(AssetIdContext ctx) { */ @Override public void exitStandardExpressionBlock(StandardExpressionBlockContext ctx) { - this.stack.push(this.stack.pop(Expression.class)); + var expression = this.stack.pop(Expression.class); + + // This is a hack to make sure that the date and time expressions are rendered in the correct + // format. We had to do this because EFX 1 does not support the format-date() and format-time() + // functions. + if (TypedExpression.class.isAssignableFrom(expression.getClass())) { + if (EfxDataType.Date.class.isAssignableFrom(((TypedExpression) expression).getDataType())) { + expression = new StringExpression("format-date(" + expression.getScript() + ", '[D01]/[M01]/[Y0001]')"); + } else if (EfxDataType.Time.class.isAssignableFrom(((TypedExpression) expression).getDataType())) { + expression = new StringExpression("format-time(" + expression.getScript() + ", '[H01]:[m01] [Z]')"); + } + } + + this.stack.push(expression); } /*** 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 7d3b3d9..f002a9c 100644 --- a/src/test/java/eu/europa/ted/efx/sdk1/EfxTemplateTranslatorV1Test.java +++ b/src/test/java/eu/europa/ted/efx/sdk1/EfxTemplateTranslatorV1Test.java @@ -343,4 +343,14 @@ void testEndOfLineComments() { "let block01() -> { label(concat('field', '|', 'name', '|', 'BT-00-Text'))text(' ')text('blah blah') }\nfor-each(/*).call(block01())", translateTemplate("{ND-Root} #{name|BT-00-Text} blah blah // comment blah blah")); } + + @Test + void testImplicitFormatting_Dates() { + assertEquals("let block01() -> { eval(format-date(PathNode/StartDateField/xs:date(text()), '[D01]/[M01]/[Y0001]')) }\nfor-each(/*).call(block01())", translateTemplate("{ND-Root} ${BT-00-StartDate}")); + } + + @Test + void testImplicitFormatting_Times() { + assertEquals("let block01() -> { eval(format-time(PathNode/StartTimeField/xs:time(text()), '[H01]:[m01] [Z]')) }\nfor-each(/*).call(block01())", translateTemplate("{ND-Root} ${BT-00-StartTime}")); + } } From 335b2627ed9c23b739ab65ba8ae516a5afe7063a Mon Sep 17 00:00:00 2001 From: Ioannis Rousochatzakis Date: Thu, 16 Nov 2023 12:00:18 +0100 Subject: [PATCH 09/15] Added workaround for generating labels from a sequence of asset-ids in EFX 1. --- .../ted/efx/sdk1/EfxTemplateTranslatorV1.java | 65 ++++++++++++++++++- .../efx/sdk1/EfxTemplateTranslatorV1Test.java | 7 ++ 2 files changed, 70 insertions(+), 2 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 73d1ff9..9880486 100644 --- a/src/main/java/eu/europa/ted/efx/sdk1/EfxTemplateTranslatorV1.java +++ b/src/main/java/eu/europa/ted/efx/sdk1/EfxTemplateTranslatorV1.java @@ -246,8 +246,36 @@ public void exitExpressionTemplate(ExpressionTemplateContext ctx) { @Override public void exitStandardLabelReference(StandardLabelReferenceContext ctx) { - StringExpression assetId = ctx.assetId() != null ? this.stack.pop(StringExpression.class) - : this.script.getStringLiteralFromUnquotedString(""); + if (!this.stack.empty() && StringSequenceExpression.class.isAssignableFrom(this.stack.peek().getClass()) && ctx.assetId() != null) { + + // This is a workaround that allows EFX 1 to render a sequence of labels without a special + // syntax. When a standard label reference is processed, the template translator checks if + // the assetId is provided with a SequenceExpression. If this is the case, then the + // translator generates the appropriate code to render a sequence of labels. + // + // For example, this will render a sequence of labels for a label reference of the form + // #{assetType|labelType|${for text:$t in ('assetId1','assetId2') return $t}}:} + // The only restriction is that the assetType and labelType must be the same for all labels in the sequence. + + StringSequenceExpression assetIdSequence = this.stack.pop(StringSequenceExpression.class); + this.exitStandardLabelReference(ctx, assetIdSequence); + } else { + + // Standard implementation as originally intended by EFX 1 + + StringExpression assetId = ctx.assetId() != null ? this.stack.pop(StringExpression.class) + : this.script.getStringLiteralFromUnquotedString(""); + this.exitStandardLabelReference(ctx, assetId); + } + } + + /** + * Renders a single label from a standard label reference. + * + * @param ctx The ParserRuleContext of the standard label reference. + * @param assetId The assetId of the label to render. + */ + private void exitStandardLabelReference(StandardLabelReferenceContext ctx, StringExpression assetId) { StringExpression labelType = ctx.labelType() != null ? this.stack.pop(StringExpression.class) : this.script.getStringLiteralFromUnquotedString(""); StringExpression assetType = ctx.assetType() != null ? this.stack.pop(StringExpression.class) @@ -258,6 +286,39 @@ public void exitStandardLabelReference(StandardLabelReferenceContext ctx) { this.script.getStringLiteralFromUnquotedString("|"), assetId)))); } + /** + * Renders a sequence of labels from a standard label reference. + * + * @param ctx The ParserRuleContext of the standard label reference. + * @param assetIdSequence The sequence of assetIds for the labels to render. + */ + private void exitStandardLabelReference(StandardLabelReferenceContext ctx, StringSequenceExpression assetIdSequence) { + StringExpression labelType = ctx.labelType() != null ? this.stack.pop(StringExpression.class) + : this.script.getStringLiteralFromUnquotedString(""); + StringExpression assetType = ctx.assetType() != null ? this.stack.pop(StringExpression.class) + : this.script.getStringLiteralFromUnquotedString(""); + + Variable loopVariable = new Variable("item", + this.script.composeVariableDeclaration("item", StringExpression.class), StringExpression.empty(), + this.script.composeVariableReference("item", StringExpression.class)); + + this.stack.push(this.markup.renderLabelFromExpression( + this.script.composeDistinctValuesFunction( + this.script.composeForExpression( + this.script.composeIteratorList( + List.of( + this.script.composeIteratorExpression(loopVariable.declarationExpression, assetIdSequence))), + this.script.composeStringConcatenation(List.of( + assetType, + this.script.getStringLiteralFromUnquotedString("|"), + labelType, + this.script.getStringLiteralFromUnquotedString("|"), + new StringExpression(loopVariable.referenceExpression.getScript()))), + StringSequenceExpression.class), + StringSequenceExpression.class))); + } + + @Override public void exitShorthandBtLabelReference(ShorthandBtLabelReferenceContext ctx) { StringExpression assetId = this.script.getStringLiteralFromUnquotedString(ctx.BtId().getText()); 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 f002a9c..4e2c45e 100644 --- a/src/test/java/eu/europa/ted/efx/sdk1/EfxTemplateTranslatorV1Test.java +++ b/src/test/java/eu/europa/ted/efx/sdk1/EfxTemplateTranslatorV1Test.java @@ -180,6 +180,13 @@ void testStandardLabelReference_UsingLabelTypeAsAssetId() { translateTemplate("{BT-00-Text} #{auxiliary|text|value}")); } + @Test + void testStandardLabelReference_WithAssetIdIterator() { + assertEquals( + "let block01() -> { label(distinct-values(for $item in for $t in ./normalize-space(text()) return $t return concat('field', '|', 'name', '|', $item))) }\nfor-each(/*/PathNode/TextField).call(block01())", + translateTemplate("{BT-00-Text} #{field|name|${for text:$t in BT-00-Text return $t}}")); + } + @Test void testShorthandBtLabelReference() { assertEquals( From 8c7272d7a15bfec35dc45afed5d34230f38f4b25 Mon Sep 17 00:00:00 2001 From: Ioannis Rousochatzakis Date: Fri, 17 Nov 2023 00:49:25 +0100 Subject: [PATCH 10/15] Updated teh CHANGELOG. --- CHANGELOG.md | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32890d1..4d79ea5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# EFX Toolkit 2.0.0-alpha.1 Release Notes +# EFX Toolkit 2.0.0-alpha.2 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,17 +6,22 @@ _The EFX Toolkit for Java developers is a library that enables the transpilation ## In this release -This release: +This release improves translation of EFX-1 templates as follows: +- Renders sequences of labels when a sequence expression is used to provide asset-ids. +- Renders distinct labels from sequences +- Improves date and time formatting + +This release also includes a refactoring that moved XPath processing classes to the eForms Core Library 1.2.0 to improve reusability. + +There are no changes in EFX-2 translation included in this release. -- Improves translation of EFX-1. -- Adds support for translating EFX-2 expressions and templates. -- Removes support of the obsolete EFX versions included in pre-release versions of the SDK (SDK 0.x.x). -- Introduces some breaking changes in the interfaces that need to be implemented by new translators (SymbolResolver, ScriptGenerator, MarkupGenerator). ## EFX-1 Support -Although this is a pre-release version of the EFX Toolkit, it provides production-level support for EFX-1 transpilation. -EFX-1 is the current version of EFX released with SDK 1. Transpilation of EFX-1 to XPath is on par with the EFX Toolkit 1.3.0. +Although this is a pre-release version of the EFX Toolkit, it provides production-level support for EFX-1 transpilation. EFX-1 is the current version of EFX released with SDK 1. + +NOTE: Transpilation of EFX-1 to XPath and XSL in this version of the EFX Toolkit is **better than** what is provided by **EFX Toolkit 1.3.0**. + ## EFX-2 Support @@ -35,7 +40,7 @@ Users of the Toolkit that only use the included EFX-to-XPath transpiler will not ## Future development -Further alpha and beta releases of SDK 2 and EFX Toolkit 2 will be issued. While in "alpha" development stage, further braking changes may be introduced. SDK 2 and EFX 2 are expected to continue to be under development util late 2023. +Further alpha and beta releases of SDK 2 and EFX Toolkit 2 will be issued. While in "alpha" development stage, further braking changes may be introduced. SDK 2 and EFX 2 are expected to continue to be under development through the first quarter of 2024. --- @@ -51,4 +56,4 @@ This version of the EFX Toolkit has a compile-time dependency on the following v - eForms SDK 1.x.x - eForms SDK 2.0.0-alpha.1 -It also depends on the [eForms Core Java library](https://github.com/OP-TED/eforms-core-java) version 1.0.5. +It also depends on the [eForms Core Java library](https://github.com/OP-TED/eforms-core-java) version 1.2.0. From 6a595c865ca6607e2435426b27f15d70b694a014 Mon Sep 17 00:00:00 2001 From: Ioannis Rousochatzakis Date: Thu, 7 Dec 2023 12:38:23 +0100 Subject: [PATCH 11/15] Updated dependency to the eforms-core-library --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d8cad1c..70f4cd7 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ ${project.build.directory}/eforms-sdk/antlr4 - 1.2.0-SNAPSHOT + 1.3.0-SNAPSHOT 4.9.3 From 80e4b6998cbd366f14ca3a17d72b44b885f2cfbf Mon Sep 17 00:00:00 2001 From: Ioannis Rousochatzakis Date: Sun, 10 Dec 2023 22:28:41 +0100 Subject: [PATCH 12/15] Copied to EFX-2 recent fixes from EFX-1. --- .../ted/efx/sdk2/EfxTemplateTranslatorV2.java | 94 +++++++++++++++++-- 1 file changed, 86 insertions(+), 8 deletions(-) diff --git a/src/main/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2.java b/src/main/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2.java index 650843b..ccd9e19 100644 --- a/src/main/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2.java +++ b/src/main/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2.java @@ -42,6 +42,7 @@ import eu.europa.ted.efx.model.templates.ContentBlock; import eu.europa.ted.efx.model.templates.ContentBlockStack; import eu.europa.ted.efx.model.templates.Markup; +import eu.europa.ted.efx.model.types.EfxDataType; import eu.europa.ted.efx.model.types.FieldTypes; import eu.europa.ted.efx.model.variables.Variable; import eu.europa.ted.efx.model.variables.VariableList; @@ -279,12 +280,42 @@ public void exitSecondaryTemplate(SecondaryTemplateContext ctx) { @Override public void exitStandardLabelReference(StandardLabelReferenceContext ctx) { - // New in EFX-2: Pluralisation of labels based on a supplied quantity - NumericExpression quantity = ctx.pluraliser() != null ? this.stack.pop(NumericExpression.class) - : NumericExpression.empty(); + if (!this.stack.empty() && StringSequenceExpression.class.isAssignableFrom(this.stack.peek().getClass()) && ctx.assetId() != null) { - StringExpression assetId = ctx.assetId() != null ? this.stack.pop(StringExpression.class) - : this.script.getStringLiteralFromUnquotedString(""); + // TODO: Review this in EFX-2 + + // This is a workaround that allows EFX 1 to render a sequence of labels without a special + // syntax. When a standard label reference is processed, the template translator checks if + // the assetId is provided with a SequenceExpression. If this is the case, then the + // translator generates the appropriate code to render a sequence of labels. + // + // For example, this will render a sequence of labels for a label reference of the form + // #{assetType|labelType|${for text:$t in ('assetId1','assetId2') return $t}}:} + // The only restriction is that the assetType and labelType must be the same for all labels in the sequence. + + StringSequenceExpression assetIdSequence = this.stack.pop(StringSequenceExpression.class); + this.exitStandardLabelReference(ctx, assetIdSequence); // TODO: pluralisation is not addressed here yet + } else { + + // Standard implementation as originally intended by EFX 1 + + // New in EFX-2: Pluralisation of labels based on a supplied quantity + NumericExpression quantity = ctx.pluraliser() != null ? this.stack.pop(NumericExpression.class) + : NumericExpression.empty(); + + StringExpression assetId = ctx.assetId() != null ? this.stack.pop(StringExpression.class) + : this.script.getStringLiteralFromUnquotedString(""); + this.exitStandardLabelReference(ctx, assetId, quantity); + } + } + + /** + * Renders a single label from a standard label reference. + * + * @param ctx The ParserRuleContext of the standard label reference. + * @param assetId The assetId of the label to render. + */ + private void exitStandardLabelReference(StandardLabelReferenceContext ctx, StringExpression assetId, NumericExpression quantity) { StringExpression labelType = ctx.labelType() != null ? this.stack.pop(StringExpression.class) : this.script.getStringLiteralFromUnquotedString(""); StringExpression assetType = ctx.assetType() != null ? this.stack.pop(StringExpression.class) @@ -295,6 +326,39 @@ public void exitStandardLabelReference(StandardLabelReferenceContext ctx) { this.script.getStringLiteralFromUnquotedString("|"), assetId)), quantity)); } + /** + * Renders a sequence of labels from a standard label reference. + * + * @param ctx The ParserRuleContext of the standard label reference. + * @param assetIdSequence The sequence of assetIds for the labels to render. + */ + private void exitStandardLabelReference(StandardLabelReferenceContext ctx, StringSequenceExpression assetIdSequence) { + StringExpression labelType = ctx.labelType() != null ? this.stack.pop(StringExpression.class) + : this.script.getStringLiteralFromUnquotedString(""); + StringExpression assetType = ctx.assetType() != null ? this.stack.pop(StringExpression.class) + : this.script.getStringLiteralFromUnquotedString(""); + + Variable loopVariable = new Variable("item", + this.script.composeVariableDeclaration("item", StringExpression.class), StringExpression.empty(), + this.script.composeVariableReference("item", StringExpression.class)); + + this.stack.push(this.markup.renderLabelFromExpression( + this.script.composeDistinctValuesFunction( + this.script.composeForExpression( + this.script.composeIteratorList( + List.of( + this.script.composeIteratorExpression(loopVariable.declarationExpression, assetIdSequence))), + this.script.composeStringConcatenation(List.of( + assetType, + this.script.getStringLiteralFromUnquotedString("|"), + labelType, + this.script.getStringLiteralFromUnquotedString("|"), + new StringExpression(loopVariable.referenceExpression.getScript()))), + StringSequenceExpression.class), + StringSequenceExpression.class))); + } + + @Override public void exitShorthandBtLabelReference(ShorthandBtLabelReferenceContext ctx) { NumericExpression quantity = ctx.pluraliser() != null ? this.stack.pop(NumericExpression.class) : NumericExpression.empty(); @@ -362,8 +426,7 @@ private void shorthandIndirectLabelReference(final String fieldId, final Numeric this.script.getStringLiteralFromUnquotedString("|"), this.script.getStringLiteralFromUnquotedString(fieldId))), StringSequenceExpression.class), - StringSequenceExpression.class), - quantity)); + StringSequenceExpression.class), quantity)); break; case "code": case "internal-code": @@ -487,7 +550,22 @@ public void exitComputedLabelReference(ComputedLabelReferenceContext ctx) { */ @Override public void exitStandardExpressionBlock(StandardExpressionBlockContext ctx) { - this.stack.push(this.stack.pop(Expression.class)); + var expression = this.stack.pop(Expression.class); + + // TODO: Review this in EFX-2 + + // This is a hack to make sure that the date and time expressions are rendered in the correct + // format. We had to do this because EFX 1 does not support the format-date() and format-time() + // functions. + if (TypedExpression.class.isAssignableFrom(expression.getClass())) { + if (EfxDataType.Date.class.isAssignableFrom(((TypedExpression) expression).getDataType())) { + expression = new StringExpression("format-date(" + expression.getScript() + ", '[D01]/[M01]/[Y0001]')"); + } else if (EfxDataType.Time.class.isAssignableFrom(((TypedExpression) expression).getDataType())) { + expression = new StringExpression("format-time(" + expression.getScript() + ", '[H01]:[m01] [Z]')"); + } + } + + this.stack.push(expression); } /*** From fbe3356b5cd5681c6cbee43222ec1115c0579db0 Mon Sep 17 00:00:00 2001 From: Ioannis Rousochatzakis Date: Sun, 10 Dec 2023 22:33:08 +0100 Subject: [PATCH 13/15] Updated pom.xml --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 70f4cd7..bfd0d30 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.2 jar EFX Toolkit for Java @@ -59,7 +59,7 @@ ${project.build.directory}/eforms-sdk/antlr4 - 1.3.0-SNAPSHOT + 1.3.0 4.9.3 From 46534dbd816f22cf10edd8a552613cdd8fcf7697 Mon Sep 17 00:00:00 2001 From: Ioannis Rousochatzakis Date: Sun, 10 Dec 2023 22:35:08 +0100 Subject: [PATCH 14/15] Updated the changelog. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d79ea5..d5099dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,4 +56,4 @@ This version of the EFX Toolkit has a compile-time dependency on the following v - eForms SDK 1.x.x - eForms SDK 2.0.0-alpha.1 -It also depends on the [eForms Core Java library](https://github.com/OP-TED/eforms-core-java) version 1.2.0. +It also depends on the [eForms Core Java library](https://github.com/OP-TED/eforms-core-java) version 1.3.0. From b242efc6c8126423063d130b478c9479e0b21388 Mon Sep 17 00:00:00 2001 From: Bertrand Lorentz Date: Mon, 11 Dec 2023 16:57:56 +0100 Subject: [PATCH 15/15] CHANGELOG: Small cosmetic fixes --- CHANGELOG.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5099dd..dc2266f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,18 +7,19 @@ _The EFX Toolkit for Java developers is a library that enables the transpilation ## In this release This release improves translation of EFX-1 templates as follows: + - Renders sequences of labels when a sequence expression is used to provide asset-ids. -- Renders distinct labels from sequences -- Improves date and time formatting +- Renders distinct labels from sequences. +- Improves date and time formatting. -This release also includes a refactoring that moved XPath processing classes to the eForms Core Library 1.2.0 to improve reusability. +This release also includes a refactoring that moved XPath processing classes to the eForms Core Library 1.2.0 to improve reusability. There are no changes in EFX-2 translation included in this release. ## EFX-1 Support -Although this is a pre-release version of the EFX Toolkit, it provides production-level support for EFX-1 transpilation. EFX-1 is the current version of EFX released with SDK 1. +Although this is a pre-release version of the EFX Toolkit, it provides production-level support for EFX-1 transpilation. EFX-1 is the current version of EFX released with SDK 1. NOTE: Transpilation of EFX-1 to XPath and XSL in this version of the EFX Toolkit is **better than** what is provided by **EFX Toolkit 1.3.0**.