diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..098a49a0 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,21 @@ +name: Build the project +on: + push: + + # Allows to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'adopt' + - name: Build package + run: mvn --batch-mode clean install -s .github/workflows/settings.xml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4399ecca..4c5c702d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,6 +1,11 @@ name: Publish package to the Maven Central Repository on: + push: + branches: + - 'develop' + - 'main' + release: types: [created] @@ -16,20 +21,17 @@ jobs: steps: - uses: actions/checkout@v3 - name: Import GPG Key - uses: crazy-max/ghaction-import-gpg@v1 - env: - GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} - PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + uses: crazy-max/ghaction-import-gpg@v5 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.GPG_PASSPHRASE }} - name: Set up Java for publishing to Maven Central Repository uses: actions/setup-java@v3 with: java-version: '11' distribution: 'adopt' - server-id: ossrh - server-username: MAVEN_USERNAME - server-password: MAVEN_PASSWORD - name: Publish to the Maven Central Repository - run: mvn --batch-mode deploy -Dgpg.passphrase='${{ secrets.GPG_PASSPHRASE }}' -Prelease + run: mvn --batch-mode deploy -Dgpg.passphrase='${{ secrets.GPG_PASSPHRASE }}' -Prelease -s .github/workflows/settings.xml env: MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }} diff --git a/.github/workflows/settings.xml b/.github/workflows/settings.xml new file mode 100644 index 00000000..a36dfa7d --- /dev/null +++ b/.github/workflows/settings.xml @@ -0,0 +1,34 @@ + + + + + ossrh + ${env.MAVEN_USERNAME} + ${env.MAVEN_PASSWORD} + + + + + + repositories + + true + + + + ossrh + OSSRH Snapshots + https://s01.oss.sonatype.org/content/repositories/snapshots + + false + + + true + + + + + + diff --git a/CHANGELOG.md b/CHANGELOG.md index 683aea97..1b17c913 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,18 +1,18 @@ -# EFX Toolkit 1.2.0 Release Notes +# EFX Toolkit 1.3.0 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._ --- ## In this release: -- We fixed a bug in the `XPathScriptGenerator` that was causing references to fields of type `measure` (duration) to throw an exception when multiple values where matched by the reference. -- We fixed an issue in the `SdkSymbolResolver` that was causing some code labels to be resolved incorrectly. The `SdkSymbolResolver` now correctly looks for the root codelist associated with a field in the codelist metadata provided in the `codelists` folder, instead of relying on the codelist constraint metadata provided in `fields.json`. - :warning: _**CAUTION:** If you have implemented your own `SymbolResolver` make sure that your implementation of `getRootCodelistOfField` retrieves the parent codelist information from `codelists/codelists.json` or directly from the `.gc` files in the `codelists` folder of the eForms SDK._ -- We refactored the code to move to the [eForms Core Java](https://github.com/OP-TED/eforms-core-java) library some common entity classes that were not specific to EFX (`SdkEntityFactory`, `SdkField`, `SdkNode`, `SdkCodelist`). We also moved into the EFX Toolkit some reusable classes (`SdkSymbolResolver`, `ComponentFactory`) from the [eForms Notice Viewer](https://github.com/OP-TED/eforms-notice-viewer) sample application. The result of this refactoring is `efx-toolkit-java`-`1.2.0`, `eforms-core-java`-`1.0.0` and `eforms-notice-viewer`-`0.6.0`. +- Updated the XPath 2.0 parser, XPathContextualizer and XPathScriptGenerator to correctly translate sequences. +- Improved numeric formatting. The EfxTranslator API now includes overloaded methods that permit control of numeric formatting. The existing API has been preserved. +- Improved handling of multilingual text fields by adding automatic selection of the visualisation language. + --- You can download the latest EFX Toolkit from Maven Central. -[![Maven Central](https://img.shields.io/maven-central/v/eu.europa.ted.eforms/efx-toolkit-java?label=Download%20&style=flat-square)](https://search.maven.org/search?q=g:%22eu.europa.ted.eforms%22%20AND%20a:%22efx-toolkit-java%22) +[![Maven Central](https://img.shields.io/maven-central/v/eu.europa.ted.eforms/efx-toolkit-java?label=Download%20&style=flat-square)](https://central.sonatype.com/artifact/eu.europa.ted.eforms/efx-toolkit-java) Documentation for the EFX Toolkit is available at: https://docs.ted.europa.eu/eforms/latest/efx-toolkit @@ -23,4 +23,4 @@ This version of the EFX Toolkit has a compile-time dependency on the following v - eForms SDK 0.7.x - eForms SDK 1.x.x -It also depends on the [eForms Core Java library](https://github.com/OP-TED/eforms-core-java) version 1.0.0. +It also depends on the [eForms Core Java library](https://github.com/OP-TED/eforms-core-java) version 1.0.5. diff --git a/README.md b/README.md index db62b12b..d78dcba6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -**[:memo: Latest Release Notes](CHANGELOG.md)** | **[:package: Latest Release Artifacts](https://search.maven.org/search?q=g:%22eu.europa.ted.eforms%22%20AND%20a:%22efx-toolkit-java%22)** +**[:memo: Latest Release Notes](CHANGELOG.md)** | **[:package: Latest Release Artifacts](https://central.sonatype.com/artifact/eu.europa.ted.eforms/efx-toolkit-java)** --- # Java toolkit for the eForms Expression Language (EFX) @@ -36,6 +36,40 @@ You can build this project as usual using Maven. The build process uses the grammar files provided in the [eForms SDK](https://github.com/OP-TED/eForms-SDK/tree/develop/efx-grammar) to generate a parser, using [ANTLR4](https://www.antlr.org). +In order to be able to use snapshot versions of dependencies, the following should be added to the "profiles" section of the Maven configuration file "settings.xml" (normally under ${HOME}/.m2): + +``` + + + ossrh + ${env.MAVEN_USERNAME} + ${env.MAVEN_PASSWORD} + + + + + repositories + + true + + + + ossrh + OSSRH Snapshots + https://s01.oss.sonatype.org/content/repositories/snapshots + + false + + + true + + + + +``` + +See ".github/workflows/settings.xml". + ## Testing Unit tests are available under `src/test/java/`. They show in particular a variety of EFX expressions and the corresponding XPath expression. @@ -47,10 +81,10 @@ The report is available under `target/site/jacoco/`, in HTML, CSV, and XML forma You can download the latest EFX Toolkit from Maven Central. -[![Maven Central](https://img.shields.io/maven-central/v/eu.europa.ted.eforms/efx-toolkit-java?label=Download%20&style=flat-square)](https://search.maven.org/search?q=g:%22eu.europa.ted.eforms%22%20AND%20a:%22efx-toolkit-java%22) +[![Maven Central](https://img.shields.io/maven-central/v/eu.europa.ted.eforms/efx-toolkit-java?label=Download%20&style=flat-square)](https://central.sonatype.com/artifact/eu.europa.ted.eforms/efx-toolkit-java) [^1]: _Copyright 2022 European Union_ _Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European Commission – subsequent versions of the EUPL (the "Licence");_ _You may not use this work except in compliance with the Licence. You may obtain [a copy of the Licence here](LICENSE)._ -_Unless required by applicable law or agreed to in writing, software distributed under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the Licence for the specific language governing permissions and limitations under the Licence._ \ No newline at end of file +_Unless required by applicable law or agreed to in writing, software distributed under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the Licence for the specific language governing permissions and limitations under the Licence._ diff --git a/pom.xml b/pom.xml index 259a1695..d8280c33 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ eu.europa.ted.eforms efx-toolkit-java - 1.2.0 + 1.3.0 jar EFX Toolkit for Java @@ -39,16 +39,19 @@ ossrh - https://s01.oss.sonatype.org/content/repositories/snapshots + https://${sonatype.server.url}/content/repositories/snapshots ossrh - https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ + https://${sonatype.server.url}/service/local/staging/deploy/maven2/ UTF-8 + 2023-05-30T06:25:09Z + + s01.oss.sonatype.org 11 11 @@ -56,7 +59,7 @@ ${project.build.directory}/eforms-sdk/antlr4 - 1.0.0 + 1.0.5 4.9.3 @@ -72,8 +75,10 @@ 3.3.0 2.5.2 0.8.8 + 3.2.0 3.4.0 1.5 + 1.6.7 3.2.1 3.0.0-M7 @@ -194,6 +199,11 @@ jacoco-maven-plugin ${version.jacoco.plugin} + + org.apache.maven.plugins + maven-jar-plugin + ${version.jar.plugin} + org.apache.maven.plugins maven-javadoc-plugin @@ -209,6 +219,11 @@ maven-source-plugin ${version.source.plugin} + + org.sonatype.plugins + nexus-staging-maven-plugin + ${version.nexus-staging.plugin} + @@ -231,7 +246,7 @@ eu.europa.ted.eforms eforms-sdk - 1.2.1 + 1.7.0 jar eforms-sdk/efx-grammar/**/*.g4 ${sdk.antlr4.dir}/eu/europa/ted/efx/sdk1 @@ -342,6 +357,9 @@ release + + false + @@ -388,6 +406,16 @@ + + org.sonatype.plugins + nexus-staging-maven-plugin + true + + ossrh + https://${sonatype.server.url}/ + true + + diff --git a/src/main/antlr4/eu/europa/ted/efx/xpath/XPath20.g4 b/src/main/antlr4/eu/europa/ted/efx/xpath/XPath20.g4 index 50e996ed..893ef349 100644 --- a/src/main/antlr4/eu/europa/ted/efx/xpath/XPath20.g4 +++ b/src/main/antlr4/eu/europa/ted/efx/xpath/XPath20.g4 @@ -4,13 +4,9 @@ // // This is a faithful implementation of the XPath version 2.0 grammar // from the spec at https://www.w3.org/TR/xpath20/ -// -// Note: Some minor adoptations were done -// to simplify the translator using this grammar. grammar XPath20; - // [1] xpath : expr EOF ; expr : exprsingle ( COMMA exprsingle)* ; @@ -43,8 +39,8 @@ nodecomp : KW_IS | LL | GG ; // [25] pathexpr : ( SLASH relativepathexpr?) | ( SS relativepathexpr) | relativepathexpr ; relativepathexpr : stepexpr (( SLASH | SS) stepexpr)* ; -stepexpr : step predicatelist; -step: primaryexpr | reversestep | forwardstep; +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) ; @@ -56,6 +52,7 @@ abbrevreversestep : DD ; nodetest : kindtest | nametest ; nametest : qname | wildcard ; wildcard : STAR | (NCName CS) | ( SC NCName) ; +filterexpr : primaryexpr predicatelist ; predicatelist : predicate* ; // [40] predicate : OB expr CB ; @@ -343,4 +340,4 @@ fragment FragChar : '\u0009' | '\u000a' | '\u000d' Whitespace : ('\u000d' | '\u000a' | '\u0020' | '\u0009')+ -> skip ; // Not per spec. Specified for testing. -SEMI : ';' ; +SEMI : ';' ; \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/eforms/sdk/ComponentFactory.java b/src/main/java/eu/europa/ted/eforms/sdk/ComponentFactory.java index 736fc563..a88dd0fc 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/ComponentFactory.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/ComponentFactory.java @@ -4,11 +4,13 @@ import java.text.MessageFormat; import java.util.HashMap; import java.util.Map; + import eu.europa.ted.eforms.sdk.component.SdkComponentFactory; import eu.europa.ted.eforms.sdk.component.SdkComponentType; import eu.europa.ted.efx.interfaces.MarkupGenerator; import eu.europa.ted.efx.interfaces.ScriptGenerator; import eu.europa.ted.efx.interfaces.SymbolResolver; +import eu.europa.ted.efx.interfaces.TranslatorOptions; public class ComponentFactory extends SdkComponentFactory { public static final ComponentFactory INSTANCE = new ComponentFactory(); @@ -43,15 +45,15 @@ public static SymbolResolver getSymbolResolver(final String sdkVersion, final Pa }); } - public static MarkupGenerator getMarkupGenerator(final String sdkVersion) + public static MarkupGenerator getMarkupGenerator(final String sdkVersion, TranslatorOptions options) throws InstantiationException { return ComponentFactory.INSTANCE.getComponentImpl(sdkVersion, - SdkComponentType.MARKUP_GENERATOR, MarkupGenerator.class); + SdkComponentType.MARKUP_GENERATOR, MarkupGenerator.class, options); } - public static ScriptGenerator getScriptGenerator(final String sdkVersion) + public static ScriptGenerator getScriptGenerator(final String sdkVersion, TranslatorOptions options) throws InstantiationException { return ComponentFactory.INSTANCE.getComponentImpl(sdkVersion, - SdkComponentType.SCRIPT_GENERATOR, ScriptGenerator.class); + SdkComponentType.SCRIPT_GENERATOR, ScriptGenerator.class, options); } } 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 12581f4e..ef93b247 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/SdkSymbolResolver.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/SdkSymbolResolver.java @@ -3,8 +3,9 @@ import java.nio.file.Path; import java.util.List; import java.util.Map; + import org.antlr.v4.runtime.misc.ParseCancellationException; -import eu.europa.ted.eforms.sdk.SdkConstants; + import eu.europa.ted.eforms.sdk.component.SdkComponent; import eu.europa.ted.eforms.sdk.component.SdkComponentType; import eu.europa.ted.eforms.sdk.entity.SdkCodelist; diff --git a/src/main/java/eu/europa/ted/efx/EfxTranslator.java b/src/main/java/eu/europa/ted/efx/EfxTranslator.java index 4c09c53f..c7a86a70 100644 --- a/src/main/java/eu/europa/ted/efx/EfxTranslator.java +++ b/src/main/java/eu/europa/ted/efx/EfxTranslator.java @@ -18,6 +18,7 @@ import java.nio.file.Path; import eu.europa.ted.efx.component.EfxTranslatorFactory; import eu.europa.ted.efx.interfaces.TranslatorDependencyFactory; +import eu.europa.ted.efx.interfaces.TranslatorOptions; /** * Provided for convenience, this class exposes static methods that allow you to quickly instantiate @@ -25,6 +26,8 @@ */ public class EfxTranslator { + private static TranslatorOptions defaultOptions = EfxTranslatorOptions.DEFAULT; + /** * Instantiates an EFX expression translator and translates a given expression. * @@ -39,11 +42,17 @@ public class EfxTranslator { * @throws InstantiationException */ public static String translateExpression(final TranslatorDependencyFactory dependencyFactory, final String sdkVersion, - final String expression, final String... expressionParameters) throws InstantiationException { - return EfxTranslatorFactory.getEfxExpressionTranslator(sdkVersion, dependencyFactory) + final String expression, TranslatorOptions options, final String... expressionParameters) + throws InstantiationException { + return EfxTranslatorFactory.getEfxExpressionTranslator(sdkVersion, dependencyFactory, options) .translateExpression(expression, expressionParameters); } + public static String translateExpression(final TranslatorDependencyFactory dependencyFactory, final String sdkVersion, + final String expression, final String... expressionParameters) throws InstantiationException { + return translateExpression(dependencyFactory, sdkVersion, expression, defaultOptions, expressionParameters); + } + /** * Instantiates an EFX template translator and translates the EFX template contained in the given * file. @@ -58,13 +67,19 @@ public static String translateExpression(final TranslatorDependencyFactory depen * @throws IOException * @throws InstantiationException */ - public static String translateTemplate(final TranslatorDependencyFactory dependencyFactory, final String sdkVersion, - final Path pathname) + public static String translateTemplate(final TranslatorDependencyFactory dependencyFactory, final String sdkVersion, + final Path pathname, TranslatorOptions options) throws IOException, InstantiationException { - return EfxTranslatorFactory.getEfxTemplateTranslator(sdkVersion, dependencyFactory) + return EfxTranslatorFactory.getEfxTemplateTranslator(sdkVersion, dependencyFactory, options) .renderTemplate(pathname); } + public static String translateTemplate(final TranslatorDependencyFactory dependencyFactory, final String sdkVersion, + final Path pathname) + throws IOException, InstantiationException { + return translateTemplate(dependencyFactory, sdkVersion, pathname, defaultOptions); + } + /** * Instantiates an EFX template translator and translates the given EFX template. * @@ -78,12 +93,18 @@ public static String translateTemplate(final TranslatorDependencyFactory depende * @throws InstantiationException */ public static String translateTemplate(final TranslatorDependencyFactory dependencyFactory, final String sdkVersion, - final String template) + final String template, TranslatorOptions options) throws InstantiationException { - return EfxTranslatorFactory.getEfxTemplateTranslator(sdkVersion, dependencyFactory) + return EfxTranslatorFactory.getEfxTemplateTranslator(sdkVersion, dependencyFactory, options) .renderTemplate(template); } + public static String translateTemplate(final TranslatorDependencyFactory dependencyFactory, final String sdkVersion, + final String template) + throws InstantiationException { + return translateTemplate(dependencyFactory, sdkVersion, template, defaultOptions); + } + /** * Instantiates an EFX template translator and translates the EFX template contained in the given * InputStream. @@ -98,10 +119,16 @@ public static String translateTemplate(final TranslatorDependencyFactory depende * @throws IOException * @throws InstantiationException */ - public static String translateTemplate(final TranslatorDependencyFactory dependencyFactory, final String sdkVersion, - final InputStream stream) + public static String translateTemplate(final TranslatorDependencyFactory dependencyFactory, final String sdkVersion, + final InputStream stream, TranslatorOptions options) throws IOException, InstantiationException { - return EfxTranslatorFactory.getEfxTemplateTranslator(sdkVersion, dependencyFactory) + return EfxTranslatorFactory.getEfxTemplateTranslator(sdkVersion, dependencyFactory, options) .renderTemplate(stream); } + + public static String translateTemplate(final TranslatorDependencyFactory dependencyFactory, final String sdkVersion, + final InputStream stream) + throws IOException, InstantiationException { + return translateTemplate(dependencyFactory, sdkVersion, stream, defaultOptions); + } } diff --git a/src/main/java/eu/europa/ted/efx/EfxTranslatorOptions.java b/src/main/java/eu/europa/ted/efx/EfxTranslatorOptions.java new file mode 100644 index 00000000..26e1f376 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/EfxTranslatorOptions.java @@ -0,0 +1,75 @@ +package eu.europa.ted.efx; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +import eu.europa.ted.efx.interfaces.TranslatorOptions; +import eu.europa.ted.efx.model.DecimalFormat; + +public class EfxTranslatorOptions implements TranslatorOptions { + + // Change to EfxDecimalFormatSymbols.EFX_DEFAULT to use the decimal format + // preferred by OP (space as thousands separator and comma as decimal separator). + public static final EfxTranslatorOptions DEFAULT = new EfxTranslatorOptions(DecimalFormat.XSL_DEFAULT, Locale.ENGLISH); + + private final DecimalFormat symbols; + private Locale primaryLocale; + private ArrayList otherLocales; + + public EfxTranslatorOptions(DecimalFormat symbols) { + this(symbols, Locale.ENGLISH); + } + + public EfxTranslatorOptions(DecimalFormat symbols, String primaryLanguage, String... otherLanguages) { + this(symbols, Locale.forLanguageTag(primaryLanguage), Arrays.stream(otherLanguages).map(Locale::forLanguageTag).toArray(Locale[]::new)); + } + + public EfxTranslatorOptions(DecimalFormat symbols, Locale primaryLocale, Locale... otherLocales) { + this.symbols = symbols; + this.primaryLocale = primaryLocale; + this.otherLocales = new ArrayList<>(Arrays.asList(otherLocales)); + } + + + @Override + public DecimalFormat getDecimalFormat() { + return this.symbols; + } + + @Override + public String getPrimaryLanguage2LetterCode() { + return this.primaryLocale.getLanguage(); + } + + @Override + public String getPrimaryLanguage3LetterCode() { + return this.primaryLocale.getISO3Language(); + } + + @Override + public String[] getAllLanguage2LetterCodes() { + List languages = new ArrayList<>(); + languages.add(primaryLocale.getLanguage()); + for (Locale locale : otherLocales) { + languages.add(locale.getLanguage()); + } + return languages.toArray(new String[0]); + } + + @Override + public String[] getAllLanguage3LetterCodes() { + List languages = new ArrayList<>(); + languages.add(primaryLocale.getISO3Language()); + for (Locale locale : otherLocales) { + languages.add(locale.getISO3Language()); + } + return languages.toArray(new String[0]); + } + + public EfxTranslatorOptions withLanguage(String language) { + this.primaryLocale = Locale.forLanguageTag(language); + return this; + } +} \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/component/EfxTranslatorFactory.java b/src/main/java/eu/europa/ted/efx/component/EfxTranslatorFactory.java index f3e7313b..e40ae6bb 100644 --- a/src/main/java/eu/europa/ted/efx/component/EfxTranslatorFactory.java +++ b/src/main/java/eu/europa/ted/efx/component/EfxTranslatorFactory.java @@ -5,6 +5,7 @@ import eu.europa.ted.efx.interfaces.EfxExpressionTranslator; import eu.europa.ted.efx.interfaces.EfxTemplateTranslator; import eu.europa.ted.efx.interfaces.TranslatorDependencyFactory; +import eu.europa.ted.efx.interfaces.TranslatorOptions; public class EfxTranslatorFactory extends SdkComponentFactory { public static final EfxTranslatorFactory INSTANCE = new EfxTranslatorFactory(); @@ -14,18 +15,18 @@ private EfxTranslatorFactory() { } public static EfxExpressionTranslator getEfxExpressionTranslator(final String sdkVersion, - final TranslatorDependencyFactory factory) throws InstantiationException { + final TranslatorDependencyFactory factory, TranslatorOptions options) throws InstantiationException { return EfxTranslatorFactory.INSTANCE.getComponentImpl(sdkVersion, SdkComponentType.EFX_EXPRESSION_TRANSLATOR, EfxExpressionTranslator.class, - factory.createSymbolResolver(sdkVersion), factory.createScriptGenerator(sdkVersion), + factory.createSymbolResolver(sdkVersion), factory.createScriptGenerator(sdkVersion, options), factory.createErrorListener()); } public static EfxTemplateTranslator getEfxTemplateTranslator(final String sdkVersion, - final TranslatorDependencyFactory factory) throws InstantiationException { + final TranslatorDependencyFactory factory, TranslatorOptions options) throws InstantiationException { return EfxTranslatorFactory.INSTANCE.getComponentImpl(sdkVersion, SdkComponentType.EFX_TEMPLATE_TRANSLATOR, EfxTemplateTranslator.class, - factory.createMarkupGenerator(sdkVersion), factory.createSymbolResolver(sdkVersion), - factory.createScriptGenerator(sdkVersion), factory.createErrorListener()); + factory.createMarkupGenerator(sdkVersion, options), factory.createSymbolResolver(sdkVersion), + factory.createScriptGenerator(sdkVersion, options), factory.createErrorListener()); } } diff --git a/src/main/java/eu/europa/ted/efx/interfaces/ScriptGenerator.java b/src/main/java/eu/europa/ted/efx/interfaces/ScriptGenerator.java index 3fc28ed3..c62d03b7 100644 --- a/src/main/java/eu/europa/ted/efx/interfaces/ScriptGenerator.java +++ b/src/main/java/eu/europa/ted/efx/interfaces/ScriptGenerator.java @@ -21,7 +21,6 @@ import eu.europa.ted.efx.model.Expression.IteratorExpression; import eu.europa.ted.efx.model.Expression.IteratorListExpression; import eu.europa.ted.efx.model.Expression.ListExpression; -import eu.europa.ted.efx.model.Expression.ListExpressionBase; import eu.europa.ted.efx.model.Expression.NumericExpression; import eu.europa.ted.efx.model.Expression.NumericListExpression; import eu.europa.ted.efx.model.Expression.PathExpression; @@ -250,7 +249,7 @@ public NumericExpression composeNumericOperation(NumericExpression leftOperand, @Deprecated(since = "0.7.0", forRemoval = true) public NumericExpression composeCountOperation(final PathExpression set); - public NumericExpression composeCountOperation(final ListExpressionBase list); + public NumericExpression composeCountOperation(final ListExpression list); public NumericExpression composeToNumberConversion(StringExpression text); @@ -292,7 +291,7 @@ public StringExpression composeSubstringExtraction(StringExpression text, Numeri public BooleanExpression composeUniqueValueCondition(PathExpression needle, PathExpression haystack); - public BooleanExpression composeSequenceEqualFunction(ListExpressionBase one, ListExpressionBase two); + public BooleanExpression composeSequenceEqualFunction(ListExpression one, ListExpression two); /* * Date Functions diff --git a/src/main/java/eu/europa/ted/efx/interfaces/TranslatorDependencyFactory.java b/src/main/java/eu/europa/ted/efx/interfaces/TranslatorDependencyFactory.java index e06dcd00..8efd6349 100644 --- a/src/main/java/eu/europa/ted/efx/interfaces/TranslatorDependencyFactory.java +++ b/src/main/java/eu/europa/ted/efx/interfaces/TranslatorDependencyFactory.java @@ -47,26 +47,62 @@ public interface TranslatorDependencyFactory { * This method is called by the EFX translator to instantiate the ScriptGenerator it will use to * translate EFX expressions to the target script language. * + * @deprecated Use {@link #createScriptGenerator(String, TranslatorOptions)} instead. * @param sdkVersion The version of the SDK that contains the version of the EFX grammar that the * EFX translator will attempt to translate. This is important as it defines the EFX * language features that ScriptGenerator instance should be able to handle. * @return An instance of ScriptGenerator to be used by the EFX translator. */ + @Deprecated(since = "1.3.0", forRemoval = true) public ScriptGenerator createScriptGenerator(String sdkVersion); + /** + * Creates a ScriptGenerator instance. + * + * This method is called by the EFX translator to instantiate the ScriptGenerator it will use to + * translate EFX expressions to the target script language. + * + * @param sdkVersion The version of the SDK that contains the version of the EFX grammar that the + * EFX translator will attempt to translate. This is important as it defines the EFX + * language features that ScriptGenerator instance should be able to handle. + * @param options The translator options to be used by the ScriptGenerator instance. + * @return An instance of ScriptGenerator to be used by the EFX translator. + */ + default public ScriptGenerator createScriptGenerator(String sdkVersion, TranslatorOptions options) { + return createScriptGenerator(sdkVersion); + } + /** * Creates a MarkupGenerator instance. * * This method is called by the EFX translator to instantiate the MarkupGenerator it will use to * translate EFX templates to the target markup language. * + * @deprecated Use {@link #createMarkupGenerator(String, TranslatorOptions)} instead. * @param sdkVersion The version of the SDK that contains the version of the EFX grammar that the * EFX translator will attempt to translate. This is important as it defines the EFX * language features that MarkupGenerator instance should be able to handle. * @return The instance of MarkupGenerator to be used by the EFX translator. */ + @Deprecated(since = "1.3.0", forRemoval = true) public MarkupGenerator createMarkupGenerator(String sdkVersion); + /** + * Creates a MarkupGenerator instance. + * + * This method is called by the EFX translator to instantiate the MarkupGenerator it will use to + * translate EFX templates to the target markup language. + * + * @param sdkVersion The version of the SDK that contains the version of the EFX grammar that the + * EFX translator will attempt to translate. This is important as it defines the EFX + * language features that MarkupGenerator instance should be able to handle. + * @param options The translator options to be used by the MarkupGenerator instance. + * @return The instance of MarkupGenerator to be used by the EFX translator. + */ + default public MarkupGenerator createMarkupGenerator(String sdkVersion, TranslatorOptions options) { + return createMarkupGenerator(sdkVersion); + } + /** * Creates an error listener instance. * diff --git a/src/main/java/eu/europa/ted/efx/interfaces/TranslatorOptions.java b/src/main/java/eu/europa/ted/efx/interfaces/TranslatorOptions.java new file mode 100644 index 00000000..76540b3c --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/interfaces/TranslatorOptions.java @@ -0,0 +1,15 @@ +package eu.europa.ted.efx.interfaces; + +import eu.europa.ted.efx.model.DecimalFormat; + +public interface TranslatorOptions { + public DecimalFormat getDecimalFormat(); + + public String getPrimaryLanguage2LetterCode(); + + public String getPrimaryLanguage3LetterCode(); + + public String[] getAllLanguage2LetterCodes(); + + public String[] getAllLanguage3LetterCodes(); +} diff --git a/src/main/java/eu/europa/ted/efx/model/CallStack.java b/src/main/java/eu/europa/ted/efx/model/CallStack.java index 723f3e33..2a7f03a3 100644 --- a/src/main/java/eu/europa/ted/efx/model/CallStack.java +++ b/src/main/java/eu/europa/ted/efx/model/CallStack.java @@ -2,67 +2,277 @@ import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.Stack; + import org.antlr.v4.runtime.misc.ParseCancellationException; -public class CallStack extends Stack { +/** + * The call stack is a stack of stack frames. Each stack frame represents a + * scope. The top of the stack is the current scope. The bottom of the stack is + * the global scope. + */ +public class CallStack { + + private static final String TYPE_MISMATCH = "Type mismatch. Expected %s instead of %s."; + private static final String UNDECLARED_IDENTIFIER = "Identifier not declared: "; + private static final String IDENTIFIER_ALREADY_DECLARED = "Identifier already declared: "; + private static final String STACK_UNDERFLOW = "Stack underflow. Return values were available in the dropped frame, but no stack frame is left to consume them."; + + /** + * Stack frames are means of controlling the scope of variables and parameters. + * Certain sub-expressions are scoped, meaning that variables and parameters are + * only available within the scope of the sub-expression. + */ + class StackFrame extends Stack { + + /** + * Keeps a list of all identifiers declared in the current scope as well as + * their type. + */ + Map> typeRegister = new HashMap>(); + + /** + * Keeps a list of all parameter values declared in the current scope. + */ + Map valueRegister = new HashMap(); + + /** + * Registers a parameter identifier and pushes a parameter declaration on the + * current stack frame. Also stores the parameter value. + */ + void pushParameterDeclaration(String parameterName, Expression parameterDeclarationExpression, + Expression parameterValue) { + this.declareIdentifier(parameterName, parameterDeclarationExpression.getClass()); + this.storeValue(parameterName, parameterValue); + this.push(parameterDeclarationExpression); + } + + /** + * Registers a variable identifier and pushes a variable declaration on the + * current stack frame. + */ + void pushVariableDeclaration(String variableName, Expression variableDeclarationExpression) { + this.declareIdentifier(variableName, variableDeclarationExpression.getClass()); + this.push(variableDeclarationExpression); + } + + /** + * Registers an identifier in the current scope. + * This registration is later used to check if an identifier is declared in the + * current scope. + */ + void declareIdentifier(String identifier, Class type) { + this.typeRegister.put(identifier, type); + } + + /** + * Used to store parameter values. + */ + void storeValue(String identifier, Expression value) { + this.valueRegister.put(identifier, value); + } + + /** + * Returns the object at the top of the stack and removes it from the stack. + * The object must be of the expected type. + */ + synchronized T pop(Class expectedType) { + Class actualType = peek().getClass(); + if (!expectedType.isAssignableFrom(actualType) && !actualType.equals(Expression.class)) { + throw new ParseCancellationException(String.format(TYPE_MISMATCH, expectedType.getSimpleName(), this.peek().getClass().getSimpleName())); + } + return expectedType.cast(this.pop()); + } + + /** + * Clears the stack frame and all its registers. + */ + @Override + public void clear() { + super.clear(); + this.typeRegister.clear(); + this.valueRegister.clear(); + } + } - Map> variableTypes = - new HashMap>(); + /** + * The stack of stack frames. + */ + Stack frames; - Map parameterValues = - new HashMap(); + /** + * Default and only constructor. + * Adds a global scope to the stack. + */ + public CallStack() { + this.frames = new Stack<>(); + this.frames.push(new StackFrame()); // The global scope + } + + /** + * Creates a new stack frame and pushes it on top of the call stack. + * + * This method is called at the begin boundary of scoped sub-expression to + * allow for the declaration of local variables. + */ + public void pushStackFrame() { + this.frames.push(new StackFrame()); + } - public CallStack() {} + /** + * Drops the current stack frame and passes the return values to the previous + * stack frame. + * + * This method is called at the end boundary of scoped sub-expressions. + * Variables local to the sub-expression must go out of scope and the return + * values are passed to the parent expression. + */ + public void popStackFrame() { + StackFrame droppedFrame = this.frames.pop(); + + // If the dropped frame is not empty, then it contains return values that should + // be passed to the next frame on the stack. + if (droppedFrame.size() > 0) { + if (this.frames.empty()) { + throw new ParseCancellationException(STACK_UNDERFLOW); + } + this.frames.peek().addAll(droppedFrame); + } + } + /** + * Pushes a parameter declaration on the current stack frame. + * Checks if another identifier with the same name is already declared in the + * current scope. + */ public void pushParameterDeclaration(String parameterName, Expression parameterDeclaration, Expression parameterValue) { - if (this.variableTypes.containsKey(parameterName)) { - throw new ParseCancellationException("A parameter with the name " + parameterDeclaration.script + " already exists"); - } else if (parameterDeclaration.getClass() == Expression.class) { - throw new ParseCancellationException(); - } else { - this.variableTypes.put(parameterName, parameterDeclaration.getClass()); - this.parameterValues.put(parameterName, parameterValue); - this.push(parameterDeclaration); + if (this.inScope(parameterName)) { + throw new ParseCancellationException(IDENTIFIER_ALREADY_DECLARED + parameterDeclaration.script); } + this.frames.peek().pushParameterDeclaration(parameterName, parameterDeclaration, parameterValue); } + /** + * Pushes a variable declaration on the current stack frame. + * Checks if another identifier with the same name is already declared in the + * current scope. + */ public void pushVariableDeclaration(String variableName, Expression variableDeclaration) { - if (parameterValues.containsKey(variableName)) { - throw new ParseCancellationException("A parameter with the name " + variableDeclaration.script + " has already been declared."); - } else if (this.variableTypes.containsKey(variableName)) { - throw new ParseCancellationException("A variable with the name " + variableDeclaration.script + " has already been declared."); - } else if (variableDeclaration.getClass() == Expression.class) { - throw new ParseCancellationException(); - } else { - this.variableTypes.put(variableName, variableDeclaration.getClass()); - this.push(variableDeclaration); + if (this.inScope(variableName)) { + throw new ParseCancellationException(IDENTIFIER_ALREADY_DECLARED + variableDeclaration.script); } + this.frames.peek().pushVariableDeclaration(variableName, variableDeclaration); } - public void pushVariableReference(String variableName, Expression variableReference) { - if (this.parameterValues.containsKey(variableName)) { - this.push(parameterValues.get(variableName)); - } else if (this.variableTypes.containsKey(variableName)) { - this.push(Expression.instantiate(variableReference.script, variableTypes.get(variableName))); - } else { - throw new ParseCancellationException("A variable or parameter with the name " + variableName + " has not been declared."); + /** + * Declares a template variable. Template variables are tracked to ensure proper + * scoping. However, their declaration is not pushed on the stack as they are + * declared at the template level (in Markup) and not at the expression level + * (not in the target language script). + */ + public void declareTemplateVariable(String variableName, Class variableType) { + if (this.inScope(variableName)) { + throw new ParseCancellationException(IDENTIFIER_ALREADY_DECLARED + variableName); } + this.frames.peek().declareIdentifier(variableName, variableType); } - public synchronized T pop(Class expectedType) { - Class actualType = peek().getClass(); - if (!expectedType.isAssignableFrom(actualType) && !actualType.equals(Expression.class)) { - throw new ParseCancellationException("Type mismatch. Expected " + expectedType.getSimpleName() - + " instead of " + this.peek().getClass().getSimpleName()); - } - return expectedType.cast(this.pop()); + /** + * Checks if an identifier is declared in the current scope. + */ + boolean inScope(String identifier) { + return this.frames.stream().anyMatch(f -> f.typeRegister.containsKey(identifier) || f.valueRegister.containsKey(identifier) ); + } + + /** + * Returns the stack frame containing the given identifier. + */ + StackFrame findFrameContaining(String identifier) { + return this.frames.stream().filter(f -> f.typeRegister.containsKey(identifier) || f.valueRegister.containsKey(identifier)).findFirst().orElse(null); + } + + /** + * Gets the value of a parameter. + */ + Optional getParameter(String identifier) { + return this.frames.stream().filter(f -> f.valueRegister.containsKey(identifier)).findFirst().map(x -> x.valueRegister.get(identifier)); + } + + /** + * Gets the type of a variable. + */ + Optional> getVariable(String identifier) { + return this.frames.stream().filter(f -> f.typeRegister.containsKey(identifier)).findFirst().map(x -> x.typeRegister.get(identifier)); + } + + /** + * Pushes a variable reference on the current stack frame. + * Makes sure there is no name collision with other identifiers already in + * scope. + */ + public void pushVariableReference(String variableName, Expression variableReference) { + getParameter(variableName).ifPresentOrElse(parameterValue -> this.push(parameterValue), + () -> getVariable(variableName).ifPresentOrElse( + variableType -> this.pushVariableReference(variableReference, variableType), + () -> { + throw new ParseCancellationException(UNDECLARED_IDENTIFIER + variableName); + })); + } + + /** + * Pushes a variable reference on the current stack frame. + * This method is private because it is only used for to improve the readability + * of its public counterpart. + */ + private void pushVariableReference(Expression variableReference, Class variableType) { + this.frames.peek().push(Expression.instantiate(variableReference.script, variableType)); + } + + /** + * Pushes an object on the current stack frame. + * No checks, no questions asked. + */ + public void push(CallStackObject item) { + this.frames.peek().push(item); + } + + /** + * Returns the object at the top of the current stack frame and removes it from + * the stack. + * + * @param expectedType The that the returned object is expected to have. + */ + public synchronized T pop(Class expectedType) { + return this.frames.peek().pop(expectedType); + } + + /** + * Returns the object at the top of the current stack frame without removing it + * from the stack. + */ + public synchronized CallStackObject peek() { + return this.frames.peek().peek(); + } + + /** + * Returns the number of elements in the current stack frame. + */ + public int size() { + return this.frames.peek().size(); + } + + /** + * Returns true if the current stack frame is empty. + */ + public boolean empty() { + return this.frames.peek().empty(); } - @Override + /** + * Clears the current stack frame. + */ public void clear() { - super.clear(); - this.variableTypes.clear(); - this.parameterValues.clear(); + this.frames.peek().clear(); } } diff --git a/src/main/java/eu/europa/ted/efx/model/CallStackObjectBase.java b/src/main/java/eu/europa/ted/efx/model/CallStackObject.java similarity index 66% rename from src/main/java/eu/europa/ted/efx/model/CallStackObjectBase.java rename to src/main/java/eu/europa/ted/efx/model/CallStackObject.java index 63126da4..07a3d3e7 100644 --- a/src/main/java/eu/europa/ted/efx/model/CallStackObjectBase.java +++ b/src/main/java/eu/europa/ted/efx/model/CallStackObject.java @@ -1,12 +1,12 @@ package eu.europa.ted.efx.model; /** - * Base class for objects pushed in the Sdk6EfxExpressionTranslator. + * Base class for objects pushed in the EfxExpressionTranslator. * call-stack. * * As the EfxExpressionTranslator translates EFX to a target language, the objects in the call-stack * are typically code snippets in the target language. */ -public abstract class CallStackObjectBase { +public abstract class CallStackObject { } diff --git a/src/main/java/eu/europa/ted/efx/model/ContentBlock.java b/src/main/java/eu/europa/ted/efx/model/ContentBlock.java index f57b1950..26817626 100644 --- a/src/main/java/eu/europa/ted/efx/model/ContentBlock.java +++ b/src/main/java/eu/europa/ted/efx/model/ContentBlock.java @@ -1,10 +1,16 @@ package eu.europa.ted.efx.model; import java.util.Comparator; +import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Queue; +import java.util.Set; +import java.util.stream.Collectors; + import org.antlr.v4.runtime.misc.ParseCancellationException; +import org.apache.commons.lang3.tuple.Pair; + import eu.europa.ted.efx.interfaces.MarkupGenerator; public class ContentBlock { @@ -15,6 +21,7 @@ public class ContentBlock { private final Context context; private final Queue children = new LinkedList<>(); private final int number; + private final VariableList variables; private ContentBlock() { this.parent = null; @@ -23,38 +30,40 @@ private ContentBlock() { this.content = new Markup(""); this.context = null; this.number = 0; + this.variables = new VariableList(); } public ContentBlock(final ContentBlock parent, final String id, final int number, - final Markup content, Context contextPath) { + final Markup content, Context contextPath, VariableList variables) { this.parent = parent; this.id = id; this.indentationLevel = parent.indentationLevel + 1; this.content = content; this.context = contextPath; this.number = number; + this.variables = variables; } public static ContentBlock newRootBlock() { return new ContentBlock(); } - public ContentBlock addChild(final int number, final Markup content, final Context context) { + public ContentBlock addChild(final int number, final Markup content, final Context context, final VariableList variables) { // number < 0 means "autogenerate", number == 0 means "no number", number > 0 means "use this number" final int outlineNumber = number >= 0 ? number : children.stream().map(b -> b.number).max(Comparator.naturalOrder()).orElse(0) + 1; String newBlockId = String.format("%s%02d", this.id, this.children.size() + 1); - ContentBlock newBlock = new ContentBlock(this, newBlockId, outlineNumber, content, context); + ContentBlock newBlock = new ContentBlock(this, newBlockId, outlineNumber, content, context, variables); this.children.add(newBlock); return newBlock; } - public ContentBlock addSibling(final int number, final Markup content, final Context context) { + public ContentBlock addSibling(final int number, final Markup content, final Context context, final VariableList variables) { if (this.parent == null) { throw new ParseCancellationException("Cannot add sibling to root block"); } - return this.parent.addChild(number, content, context); + return this.parent.addChild(number, content, context, variables); } public ContentBlock findParentByLevel(final int parentIndentationLevel) { @@ -104,6 +113,29 @@ public Context getParentContext() { return this.parent.getContext(); } + public Set> getOwnVariables() { + Set> variables = new LinkedHashSet<>(); + if (this.context != null && this.context.variable() != null) { + variables.add(this.context.variable()); + } + variables.addAll(this.variables.asList()); + return variables; + } + + public Set> getAllVariables() { + if (this.parent == null) { + return new LinkedHashSet<>(this.getOwnVariables()); + } + final Set> merged = new LinkedHashSet<>(); + merged.addAll(parent.getAllVariables()); + merged.addAll(this.getOwnVariables()); + return merged; + } + + public Set getTemplateParameters() { + return this.getAllVariables().stream().map(v -> v.name).collect(Collectors.toSet()); + } + public Markup renderContent(MarkupGenerator markupGenerator) { StringBuilder sb = new StringBuilder(); sb.append(this.content.script); @@ -122,6 +154,11 @@ public void renderTemplate(MarkupGenerator markupGenerator, List templat } public Markup renderCallTemplate(MarkupGenerator markupGenerator) { + Set> variables = new LinkedHashSet<>(); + if (this.parent != null) { + variables.addAll(parent.getAllVariables().stream().map(v -> Pair.of(v.name, v.referenceExpression.script)).collect(Collectors.toList())); + } + variables.addAll(this.getOwnVariables().stream().map(v -> Pair.of(v.name, v.initializationExpression.script)).collect(Collectors.toList())); return markupGenerator.renderFragmentInvocation(this.id, this.context.relativePath()); } } diff --git a/src/main/java/eu/europa/ted/efx/model/ContentBlockStack.java b/src/main/java/eu/europa/ted/efx/model/ContentBlockStack.java index f3c4c97a..d1d4e142 100644 --- a/src/main/java/eu/europa/ted/efx/model/ContentBlockStack.java +++ b/src/main/java/eu/europa/ted/efx/model/ContentBlockStack.java @@ -8,16 +8,16 @@ public class ContentBlockStack extends Stack { * Adds a new child block to the top of the stack. When the child is later removed, its parent * will return to the top of the stack again. */ - public void pushChild(final int number, final Markup content, final Context context) { - this.push(this.peek().addChild(number, content, context)); + public void pushChild(final int number, final Markup content, final Context context, final VariableList variables) { + this.push(this.peek().addChild(number, content, context, variables)); } /** * Removes the block at the top of the stack and replaces it by a new sibling block. When the last * sibling is later removed, their parent block will return to the top of the stack again. */ - public void pushSibling(final int number, final Markup content, Context context) { - this.push(this.pop().addSibling(number, content, context)); + public void pushSibling(final int number, final Markup content, Context context, final VariableList variables) { + this.push(this.pop().addSibling(number, content, context, variables)); } /** diff --git a/src/main/java/eu/europa/ted/efx/model/Context.java b/src/main/java/eu/europa/ted/efx/model/Context.java index 4c1d7f19..7383fa32 100644 --- a/src/main/java/eu/europa/ted/efx/model/Context.java +++ b/src/main/java/eu/europa/ted/efx/model/Context.java @@ -19,11 +19,20 @@ public abstract class Context { */ public static class FieldContext extends Context { + public FieldContext(final String fieldId, final PathExpression absolutePath, + final PathExpression relativePath, final Variable variable) { + super(fieldId, absolutePath, relativePath, variable); + } + public FieldContext(final String fieldId, final PathExpression absolutePath, final PathExpression relativePath) { super(fieldId, absolutePath, relativePath); } + public FieldContext(final String fieldId, final PathExpression absolutePath, final Variable variable) { + super(fieldId, absolutePath, variable); + } + public FieldContext(final String fieldId, final PathExpression absolutePath) { super(fieldId, absolutePath); } @@ -47,14 +56,25 @@ public NodeContext(final String nodeId, final PathExpression absolutePath) { private final String symbol; private final PathExpression absolutePath; private final PathExpression relativePath; + private final Variable variable; protected Context(final String symbol, final PathExpression absolutePath, - final PathExpression relativePath) { + final PathExpression relativePath, final Variable variable) { + this.variable = variable; this.symbol = symbol; this.absolutePath = absolutePath; this.relativePath = relativePath == null ? absolutePath : relativePath; } + protected Context(final String symbol, final PathExpression absolutePath, + final PathExpression relativePath) { + this(symbol, absolutePath, relativePath, null); + } + + protected Context(final String symbol, final PathExpression absolutePath, final Variable variable) { + this(symbol, absolutePath, absolutePath, variable); + } + protected Context(final String symbol, final PathExpression absolutePath) { this(symbol, absolutePath, absolutePath); } @@ -67,6 +87,10 @@ public Boolean isNodeContext() { return this.getClass().equals(NodeContext.class); } + public Variable variable() { + return variable; + } + /** * Returns the [field or node] identifier that was used to create this context. */ diff --git a/src/main/java/eu/europa/ted/efx/model/DecimalFormat.java b/src/main/java/eu/europa/ted/efx/model/DecimalFormat.java new file mode 100644 index 00000000..001060f1 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/DecimalFormat.java @@ -0,0 +1,69 @@ +package eu.europa.ted.efx.model; + +import java.text.DecimalFormatSymbols; +import java.util.Locale; + +public class DecimalFormat extends DecimalFormatSymbols { + + public final static DecimalFormat XSL_DEFAULT = DecimalFormat.getXslDefault(); + public final static DecimalFormat EFX_DEFAULT = DecimalFormat.getEfxDefault(); + + DecimalFormat(Locale locale) { + super(locale); + } + + private static DecimalFormat getXslDefault() { + DecimalFormat symbols = new DecimalFormat(Locale.US); + symbols.setDecimalSeparator('.'); + symbols.setGroupingSeparator(','); + symbols.setMinusSign('-'); + symbols.setPercent('%'); + symbols.setPerMill('‰'); + symbols.setZeroDigit('0'); + symbols.setDigit('#'); + symbols.setPatternSeparator(';'); + symbols.setInfinity("Infinity"); + symbols.setNaN("NaN"); + return symbols; + } + + private static DecimalFormat getEfxDefault() { + DecimalFormat symbols = DecimalFormat.getXslDefault(); + symbols.setDecimalSeparator(','); + symbols.setGroupingSeparator(' '); + return symbols; + } + + public String adaptFormatString(final String originalString) { + + final char decimalSeparatorPlaceholder = '\uE000'; + final char groupingSeparatorPlaceholder = '\uE001'; + final char minusSignPlaceholder = '\uE002'; + final char percentPlaceholder = '\uE003'; + final char perMillePlaceholder = '\uE004'; + final char zeroDigitPlaceholder = '\uE005'; + final char digitPlaceholder = '\uE006'; + final char patternSeparatorPlaceholder = '\uE007'; + + String adaptedString = originalString; + adaptedString = adaptedString.replace(XSL_DEFAULT.getDecimalSeparator(), decimalSeparatorPlaceholder); + adaptedString = adaptedString.replace(XSL_DEFAULT.getGroupingSeparator(), groupingSeparatorPlaceholder); + adaptedString = adaptedString.replace(XSL_DEFAULT.getMinusSign(), minusSignPlaceholder); + adaptedString = adaptedString.replace(XSL_DEFAULT.getPercent(), percentPlaceholder); + adaptedString = adaptedString.replace(XSL_DEFAULT.getPerMill(), perMillePlaceholder); + adaptedString = adaptedString.replace(XSL_DEFAULT.getZeroDigit(), zeroDigitPlaceholder); + adaptedString = adaptedString.replace(XSL_DEFAULT.getDigit(), digitPlaceholder); + adaptedString = adaptedString.replace(XSL_DEFAULT.getPatternSeparator(), patternSeparatorPlaceholder); + + adaptedString = adaptedString.replace(decimalSeparatorPlaceholder, this.getDecimalSeparator()); + adaptedString = adaptedString.replace(groupingSeparatorPlaceholder, this.getGroupingSeparator()); + adaptedString = adaptedString.replace(minusSignPlaceholder, this.getMinusSign()); + adaptedString = adaptedString.replace(percentPlaceholder, this.getPercent()); + adaptedString = adaptedString.replace(perMillePlaceholder, this.getPerMill()); + adaptedString = adaptedString.replace(zeroDigitPlaceholder, this.getZeroDigit()); + adaptedString = adaptedString.replace(digitPlaceholder, this.getDigit()); + adaptedString = adaptedString.replace(patternSeparatorPlaceholder, this.getPatternSeparator()); + + return adaptedString; + } +} \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/Expression.java b/src/main/java/eu/europa/ted/efx/model/Expression.java index 701a6df8..2fd1f569 100644 --- a/src/main/java/eu/europa/ted/efx/model/Expression.java +++ b/src/main/java/eu/europa/ted/efx/model/Expression.java @@ -16,7 +16,7 @@ * language. It also enables to EFX translator to perform type checking of EFX expressions. * */ -public class Expression extends CallStackObjectBase { +public class Expression extends CallStackObject { /** * eForms types are mapped to Expression types. @@ -25,7 +25,7 @@ public class Expression extends CallStackObjectBase { Map.ofEntries(entry("id", StringExpression.class), // entry("id-ref", StringExpression.class), // entry("text", StringExpression.class), // - entry("text-multilingual", StringExpression.class), // + entry("text-multilingual", MultilingualStringExpression.class), // entry("indicator", BooleanExpression.class), // entry("amount", NumericExpression.class), // entry("number", NumericExpression.class), // @@ -47,7 +47,7 @@ public class Expression extends CallStackObjectBase { entry("id", StringListExpression.class), // entry("id-ref", StringListExpression.class), // entry("text", StringListExpression.class), // - entry("text-multilingual", StringListExpression.class), // + entry("text-multilingual", MultilingualStringListExpression.class), // entry("indicator", BooleanListExpression.class), // entry("amount", NumericListExpression.class), // entry("number", NumericListExpression.class), // @@ -67,8 +67,16 @@ public class Expression extends CallStackObjectBase { */ public final String script; + public final Boolean isLiteral; + public Expression(final String script) { this.script = script; + this.isLiteral = false; + } + + public Expression(final String script, final Boolean isLiteral) { + this.script = script; + this.isLiteral = isLiteral; } public static T instantiate(String script, Class type) { @@ -125,6 +133,10 @@ public static class BooleanExpression extends Expression { public BooleanExpression(final String script) { super(script); } + + public BooleanExpression(final String script, final Boolean isLiteral) { + super(script, isLiteral); + } } /** @@ -135,6 +147,10 @@ public static class NumericExpression extends Expression { public NumericExpression(final String script) { super(script); } + + public NumericExpression(final String script, final Boolean isLiteral) { + super(script, isLiteral); + } } /** @@ -145,6 +161,21 @@ public static class StringExpression extends Expression { public StringExpression(final String script) { super(script); } + + public StringExpression(final String script, final Boolean isLiteral) { + super(script, isLiteral); + } + } + + public static class MultilingualStringExpression extends StringExpression { + + public MultilingualStringExpression(final String script) { + super(script); + } + + public MultilingualStringExpression(final String script, final Boolean isLiteral) { + super(script, isLiteral); + } } /** @@ -155,6 +186,10 @@ public static class DateExpression extends Expression { public DateExpression(final String script) { super(script); } + + public DateExpression(final String script, final Boolean isLiteral) { + super(script, isLiteral); + } } /** @@ -165,6 +200,10 @@ public static class TimeExpression extends Expression { public TimeExpression(final String script) { super(script); } + + public TimeExpression(final String script, final Boolean isLiteral) { + super(script, isLiteral); + } } /** @@ -175,19 +214,16 @@ public static class DurationExpression extends Expression { public DurationExpression(final String script) { super(script); } - } - - public static class ListExpressionBase extends Expression { - - public ListExpressionBase(final String script) { - super(script); + + public DurationExpression(final String script, final Boolean isLiteral) { + super(script, isLiteral); } } /** * Used to represent a list of strings in the target language. */ - public static class ListExpression extends ListExpressionBase { + public static class ListExpression extends Expression { public ListExpression(final String script) { super(script); @@ -204,6 +240,13 @@ public StringListExpression(final String script) { } } + public static class MultilingualStringListExpression extends StringListExpression { + + public MultilingualStringListExpression(final String script) { + super(script); + } + } + /** * Used to represent a list of numbers in the target language. */ diff --git a/src/main/java/eu/europa/ted/efx/model/Identifier.java b/src/main/java/eu/europa/ted/efx/model/Identifier.java new file mode 100644 index 00000000..a73e03b9 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/Identifier.java @@ -0,0 +1,11 @@ +package eu.europa.ted.efx.model; + +public class Identifier { + public String name; + public T referenceExpression; + + public Identifier(String name, T referenceExpression) { + this.name = name; + this.referenceExpression = referenceExpression; + } +} \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/Markup.java b/src/main/java/eu/europa/ted/efx/model/Markup.java index 2fd9fa6b..ed55e2a2 100644 --- a/src/main/java/eu/europa/ted/efx/model/Markup.java +++ b/src/main/java/eu/europa/ted/efx/model/Markup.java @@ -3,7 +3,7 @@ /** * Represents markup in the target template language. */ -public class Markup extends CallStackObjectBase { +public class Markup extends CallStackObject { /** * Stores the markup script in the target language. diff --git a/src/main/java/eu/europa/ted/efx/model/Variable.java b/src/main/java/eu/europa/ted/efx/model/Variable.java new file mode 100644 index 00000000..e5d1a093 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/Variable.java @@ -0,0 +1,16 @@ +package eu.europa.ted.efx.model; + +public class Variable extends Identifier { + public T initializationExpression; + + public Variable(String variableName, T initializationExpression, T referenceExpression) { + super(variableName, referenceExpression); + this.name = variableName; + this.initializationExpression = initializationExpression; + } + + public Variable(Identifier identifier, T initializationExpression) { + super(identifier.name, identifier.referenceExpression); + this.initializationExpression = initializationExpression; + } +} \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/model/VariableList.java b/src/main/java/eu/europa/ted/efx/model/VariableList.java new file mode 100644 index 00000000..ffc281c3 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/VariableList.java @@ -0,0 +1,29 @@ +package eu.europa.ted.efx.model; + +import java.util.LinkedList; +import java.util.List; + +public class VariableList extends CallStackObject { + + LinkedList> variables; + + public VariableList() { + this.variables = new LinkedList<>(); + } + + public void push(Variable variable) { + this.variables.push(variable); + } + + public synchronized Variable pop() { + return this.variables.pop(); + } + + public boolean isEmpty() { + return this.variables.isEmpty(); + } + + public List> asList() { + return this.variables; + } +} \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/efx/sdk0/v6/EfxTemplateTranslator06.java b/src/main/java/eu/europa/ted/efx/sdk0/v6/EfxTemplateTranslator06.java index 8afd4f48..9f43d2c0 100644 --- a/src/main/java/eu/europa/ted/efx/sdk0/v6/EfxTemplateTranslator06.java +++ b/src/main/java/eu/europa/ted/efx/sdk0/v6/EfxTemplateTranslator06.java @@ -29,6 +29,7 @@ import eu.europa.ted.efx.model.Expression.PathExpression; import eu.europa.ted.efx.model.Expression.StringExpression; import eu.europa.ted.efx.model.Markup; +import eu.europa.ted.efx.model.VariableList; import eu.europa.ted.efx.sdk0.v6.EfxParser.AssetIdContext; import eu.europa.ted.efx.sdk0.v6.EfxParser.AssetTypeContext; import eu.europa.ted.efx.sdk0.v6.EfxParser.ContextDeclarationBlockContext; @@ -439,13 +440,14 @@ public void exitContextDeclarationBlock(ContextDeclarationBlockContext ctx) { @Override public void exitTemplateLine(TemplateLineContext ctx) { + final VariableList variables = new VariableList(); // template variables not supported by EFX prior to 2.0.0 final Context lineContext = this.efxContext.pop(); final int indentLevel = this.getIndentLevel(ctx); final int indentChange = indentLevel - this.blockStack.currentIndentationLevel(); final Markup content = ctx.template() != null ? this.stack.pop(Markup.class) : new Markup(""); final Integer outlineNumber = ctx.OutlineNumber() != null ? Integer.parseInt(ctx.OutlineNumber().getText().trim()) : -1; - assert this.stack.isEmpty() : "Stack should be empty at this point."; + assert this.stack.empty() : "Stack should be empty at this point."; if (indentChange > 1) { throw new ParseCancellationException(INDENTATION_LEVEL_SKIPPED); @@ -453,7 +455,7 @@ public void exitTemplateLine(TemplateLineContext ctx) { if (this.blockStack.isEmpty()) { throw new ParseCancellationException(START_INDENT_AT_ZERO); } - this.blockStack.pushChild(outlineNumber, content, this.relativizeContext(lineContext, this.blockStack.currentContext())); + this.blockStack.pushChild(outlineNumber, content, this.relativizeContext(lineContext, this.blockStack.currentContext()), variables); } else if (indentChange < 0) { // lower indent level for (int i = indentChange; i < 0; i++) { @@ -462,14 +464,14 @@ public void exitTemplateLine(TemplateLineContext ctx) { this.blockStack.pop(); } assert this.blockStack.currentIndentationLevel() == indentLevel : UNEXPECTED_INDENTATION; - this.blockStack.pushSibling(outlineNumber, content, this.relativizeContext(lineContext, this.blockStack.parentContext())); + this.blockStack.pushSibling(outlineNumber, content, this.relativizeContext(lineContext, this.blockStack.parentContext()), variables); } else if (indentChange == 0) { if (blockStack.isEmpty()) { assert indentLevel == 0 : UNEXPECTED_INDENTATION; - this.blockStack.push(this.rootBlock.addChild(outlineNumber, content, this.relativizeContext(lineContext, this.rootBlock.getContext()))); + this.blockStack.push(this.rootBlock.addChild(outlineNumber, content, this.relativizeContext(lineContext, this.rootBlock.getContext()), variables)); } else { - this.blockStack.pushSibling(outlineNumber, content, this.relativizeContext(lineContext, this.blockStack.parentContext())); + this.blockStack.pushSibling(outlineNumber, content, this.relativizeContext(lineContext, this.blockStack.parentContext()), variables); } } } diff --git a/src/main/java/eu/europa/ted/efx/sdk0/v7/EfxExpressionTranslator07.java b/src/main/java/eu/europa/ted/efx/sdk0/v7/EfxExpressionTranslator07.java index d7cf16d8..c14d38de 100644 --- a/src/main/java/eu/europa/ted/efx/sdk0/v7/EfxExpressionTranslator07.java +++ b/src/main/java/eu/europa/ted/efx/sdk0/v7/EfxExpressionTranslator07.java @@ -19,7 +19,7 @@ import eu.europa.ted.efx.interfaces.ScriptGenerator; import eu.europa.ted.efx.interfaces.SymbolResolver; import eu.europa.ted.efx.model.CallStack; -import eu.europa.ted.efx.model.CallStackObjectBase; +import eu.europa.ted.efx.model.CallStackObject; import eu.europa.ted.efx.model.Context.FieldContext; import eu.europa.ted.efx.model.Context.NodeContext; import eu.europa.ted.efx.model.ContextStack; @@ -546,7 +546,7 @@ private > void exitList(int li @Override public void exitUntypedConditonalExpression(UntypedConditonalExpressionContext ctx) { - Class typeWhenFalse = this.stack.peek().getClass(); + Class typeWhenFalse = this.stack.peek().getClass(); if (typeWhenFalse == BooleanExpression.class) { this.exitConditionalBooleanExpression(); } else if (typeWhenFalse == NumericExpression.class) { @@ -1233,7 +1233,8 @@ public void exitEndsWithFunction(EndsWithFunctionContext ctx) { @Override public void exitCountFunction(CountFunctionContext ctx) { - this.stack.push(this.script.composeCountOperation(this.stack.pop(ListExpression.class))); + ListExpression expression = this.stack.pop(ListExpression.class); + this.stack.push(this.script.composeCountOperation(expression)); } @Override diff --git a/src/main/java/eu/europa/ted/efx/sdk0/v7/EfxTemplateTranslator07.java b/src/main/java/eu/europa/ted/efx/sdk0/v7/EfxTemplateTranslator07.java index f1d8ebb6..b76fd1ac 100644 --- a/src/main/java/eu/europa/ted/efx/sdk0/v7/EfxTemplateTranslator07.java +++ b/src/main/java/eu/europa/ted/efx/sdk0/v7/EfxTemplateTranslator07.java @@ -29,6 +29,7 @@ import eu.europa.ted.efx.model.Expression.PathExpression; import eu.europa.ted.efx.model.Expression.StringExpression; import eu.europa.ted.efx.model.Markup; +import eu.europa.ted.efx.model.VariableList; import eu.europa.ted.efx.sdk0.v7.EfxParser.AssetIdContext; import eu.europa.ted.efx.sdk0.v7.EfxParser.AssetTypeContext; import eu.europa.ted.efx.sdk0.v7.EfxParser.ContextDeclarationBlockContext; @@ -439,13 +440,14 @@ public void exitContextDeclarationBlock(ContextDeclarationBlockContext ctx) { @Override public void exitTemplateLine(TemplateLineContext ctx) { + final VariableList variables = new VariableList(); // template variables not supported prior to EFX 2 final Context lineContext = this.efxContext.pop(); final int indentLevel = this.getIndentLevel(ctx); final int indentChange = indentLevel - this.blockStack.currentIndentationLevel(); final Markup content = ctx.template() != null ? this.stack.pop(Markup.class) : new Markup(""); final Integer outlineNumber = ctx.OutlineNumber() != null ? Integer.parseInt(ctx.OutlineNumber().getText().trim()) : -1; - assert this.stack.isEmpty() : "Stack should be empty at this point."; + assert this.stack.empty() : "Stack should be empty at this point."; if (indentChange > 1) { throw new ParseCancellationException(INDENTATION_LEVEL_SKIPPED); @@ -453,7 +455,7 @@ public void exitTemplateLine(TemplateLineContext ctx) { if (this.blockStack.isEmpty()) { throw new ParseCancellationException(START_INDENT_AT_ZERO); } - this.blockStack.pushChild(outlineNumber, content, this.relativizeContext(lineContext, this.blockStack.currentContext())); + this.blockStack.pushChild(outlineNumber, content, this.relativizeContext(lineContext, this.blockStack.currentContext()), variables); } else if (indentChange < 0) { // lower indent level for (int i = indentChange; i < 0; i++) { @@ -462,14 +464,14 @@ public void exitTemplateLine(TemplateLineContext ctx) { this.blockStack.pop(); } assert this.blockStack.currentIndentationLevel() == indentLevel : UNEXPECTED_INDENTATION; - this.blockStack.pushSibling(outlineNumber, content, this.relativizeContext(lineContext, this.blockStack.parentContext())); + this.blockStack.pushSibling(outlineNumber, content, this.relativizeContext(lineContext, this.blockStack.parentContext()), variables); } else if (indentChange == 0) { if (blockStack.isEmpty()) { assert indentLevel == 0 : UNEXPECTED_INDENTATION; - this.blockStack.push(this.rootBlock.addChild(outlineNumber, content, this.relativizeContext(lineContext, this.rootBlock.getContext()))); + this.blockStack.push(this.rootBlock.addChild(outlineNumber, content, this.relativizeContext(lineContext, this.rootBlock.getContext()), variables)); } else { - this.blockStack.pushSibling(outlineNumber, content, this.relativizeContext(lineContext, this.blockStack.parentContext())); + this.blockStack.pushSibling(outlineNumber, content, this.relativizeContext(lineContext, this.blockStack.parentContext()), variables); } } } diff --git a/src/main/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1.java b/src/main/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1.java index c0ddd161..ca6dbcc8 100644 --- a/src/main/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1.java +++ b/src/main/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1.java @@ -6,6 +6,7 @@ import java.util.LinkedList; import java.util.List; import java.util.stream.Collectors; + import org.antlr.v4.runtime.BaseErrorListener; import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CommonTokenStream; @@ -15,13 +16,15 @@ import org.antlr.v4.runtime.tree.ParseTree; import org.antlr.v4.runtime.tree.ParseTreeWalker; import org.antlr.v4.runtime.tree.TerminalNode; +import org.apache.commons.lang3.StringUtils; + import eu.europa.ted.eforms.sdk.component.SdkComponent; import eu.europa.ted.eforms.sdk.component.SdkComponentType; import eu.europa.ted.efx.interfaces.EfxExpressionTranslator; import eu.europa.ted.efx.interfaces.ScriptGenerator; import eu.europa.ted.efx.interfaces.SymbolResolver; import eu.europa.ted.efx.model.CallStack; -import eu.europa.ted.efx.model.CallStackObjectBase; +import eu.europa.ted.efx.model.CallStackObject; import eu.europa.ted.efx.model.Context; import eu.europa.ted.efx.model.Context.FieldContext; import eu.europa.ted.efx.model.Context.NodeContext; @@ -287,7 +290,7 @@ public void exitLogicalOrCondition(EfxParser.LogicalOrConditionContext ctx) { public void exitFieldValueComparison(FieldValueComparisonContext ctx) { Expression right = this.stack.pop(Expression.class); Expression left = this.stack.pop(Expression.class); - if (!left.getClass().equals(right.getClass())) { + if (!left.getClass().isAssignableFrom(right.getClass()) && !right.getClass().isAssignableFrom(left.getClass())) { throw new ParseCancellationException(TYPE_MISMATCH_CANNOT_COMPARE_VALUES_OF_DIFFERENT_TYPES + left.getClass() + " and " + right.getClass()); } @@ -560,7 +563,7 @@ private > void exitList(int li @Override public void exitUntypedConditionalExpression(UntypedConditionalExpressionContext ctx) { - Class typeWhenFalse = this.stack.peek().getClass(); + Class typeWhenFalse = this.stack.peek().getClass(); if (typeWhenFalse == BooleanExpression.class) { this.exitConditionalBooleanExpression(); } else if (typeWhenFalse == NumericExpression.class) { @@ -1109,8 +1112,8 @@ public void exitCodelistReference(CodelistReferenceContext ctx) { @Override public void exitVariableReference(VariableReferenceContext ctx) { - this.stack.pushVariableReference(ctx.Variable().getText(), - this.script.composeVariableReference(ctx.Variable().getText(), Expression.class)); + this.stack.pushVariableReference(this.getVariableName(ctx), + this.script.composeVariableReference(this.getVariableName(ctx), Expression.class)); } /*** Parameter Declarations ***/ @@ -1118,32 +1121,32 @@ public void exitVariableReference(VariableReferenceContext ctx) { @Override public void exitStringParameterDeclaration(StringParameterDeclarationContext ctx) { - this.exitParameterDeclaration(ctx.Variable().getText(), StringExpression.class); + this.exitParameterDeclaration(this.getVariableName(ctx), StringExpression.class); } @Override public void exitNumericParameterDeclaration(NumericParameterDeclarationContext ctx) { - this.exitParameterDeclaration(ctx.Variable().getText(), NumericExpression.class); + this.exitParameterDeclaration(this.getVariableName(ctx), NumericExpression.class); } @Override public void exitBooleanParameterDeclaration(BooleanParameterDeclarationContext ctx) { - this.exitParameterDeclaration(ctx.Variable().getText(), BooleanExpression.class); + this.exitParameterDeclaration(this.getVariableName(ctx), BooleanExpression.class); } @Override public void exitDateParameterDeclaration(DateParameterDeclarationContext ctx) { - this.exitParameterDeclaration(ctx.Variable().getText(), DateExpression.class); + this.exitParameterDeclaration(this.getVariableName(ctx), DateExpression.class); } @Override public void exitTimeParameterDeclaration(TimeParameterDeclarationContext ctx) { - this.exitParameterDeclaration(ctx.Variable().getText(), TimeExpression.class); + this.exitParameterDeclaration(this.getVariableName(ctx), TimeExpression.class); } @Override public void exitDurationParameterDeclaration(DurationParameterDeclarationContext ctx) { - this.exitParameterDeclaration(ctx.Variable().getText(), DurationExpression.class); + this.exitParameterDeclaration(this.getVariableName(ctx), DurationExpression.class); } private void exitParameterDeclaration(String parameterName, Class parameterType) { @@ -1160,44 +1163,51 @@ private void exitParameterDeclaration(String parameterNam @Override public void exitStringVariableDeclaration(StringVariableDeclarationContext ctx) { - this.stack.pushVariableDeclaration(ctx.Variable().getText(), - this.script.composeVariableDeclaration(ctx.Variable().getText(), StringExpression.class)); + String variableName = this.getVariableName(ctx); + this.stack.pushVariableDeclaration(variableName, + this.script.composeVariableDeclaration(variableName, StringExpression.class)); } @Override public void exitBooleanVariableDeclaration(BooleanVariableDeclarationContext ctx) { - this.stack.pushVariableDeclaration(ctx.Variable().getText(), - this.script.composeVariableDeclaration(ctx.Variable().getText(), BooleanExpression.class)); + String variableName = this.getVariableName(ctx); + this.stack.pushVariableDeclaration(variableName, + this.script.composeVariableDeclaration(variableName, BooleanExpression.class)); } @Override public void exitNumericVariableDeclaration(NumericVariableDeclarationContext ctx) { - this.stack.pushVariableDeclaration(ctx.Variable().getText(), - this.script.composeVariableDeclaration(ctx.Variable().getText(), NumericExpression.class)); + String variableName = this.getVariableName(ctx); + this.stack.pushVariableDeclaration(variableName, + this.script.composeVariableDeclaration(variableName, NumericExpression.class)); } @Override public void exitDateVariableDeclaration(DateVariableDeclarationContext ctx) { - this.stack.pushVariableDeclaration(ctx.Variable().getText(), - this.script.composeVariableDeclaration(ctx.Variable().getText(), DateExpression.class)); + String variableName = this.getVariableName(ctx); + this.stack.pushVariableDeclaration(variableName, + this.script.composeVariableDeclaration(variableName, DateExpression.class)); } @Override public void exitTimeVariableDeclaration(TimeVariableDeclarationContext ctx) { - this.stack.pushVariableDeclaration(ctx.Variable().getText(), - this.script.composeVariableDeclaration(ctx.Variable().getText(), TimeExpression.class)); + String variableName = this.getVariableName(ctx); + this.stack.pushVariableDeclaration(variableName, + this.script.composeVariableDeclaration(variableName, TimeExpression.class)); } @Override public void exitDurationVariableDeclaration(DurationVariableDeclarationContext ctx) { - this.stack.pushVariableDeclaration(ctx.Variable().getText(), - this.script.composeVariableDeclaration(ctx.Variable().getText(), DurationExpression.class)); + String variableName = this.getVariableName(ctx); + this.stack.pushVariableDeclaration(variableName, + this.script.composeVariableDeclaration(variableName, DurationExpression.class)); } @Override public void exitContextVariableDeclaration(ContextVariableDeclarationContext ctx) { - this.stack.pushVariableDeclaration(ctx.Variable().getText(), - this.script.composeVariableDeclaration(ctx.Variable().getText(), ContextExpression.class)); + String variableName = this.getVariableName(ctx); + this.stack.pushVariableDeclaration(variableName, + this.script.composeVariableDeclaration(variableName, ContextExpression.class)); } /*** Boolean functions ***/ @@ -1239,7 +1249,8 @@ public void exitSequenceEqualFunction(SequenceEqualFunctionContext ctx) { @Override public void exitCountFunction(CountFunctionContext ctx) { - this.stack.push(this.script.composeCountOperation(this.stack.pop(ListExpression.class))); + final ListExpression expression = this.stack.pop(ListExpression.class); + this.stack.push(this.script.composeCountOperation(expression)); } @Override @@ -1448,7 +1459,6 @@ public void exitExceptFunction(ExceptFunctionContext ctx) { } } - private > void exitExceptFunction( Class listType) { final L two = this.stack.pop(listType); @@ -1456,4 +1466,63 @@ private > void exitExceptFunct this.stack.push(this.script.composeExceptFunction(one, two, listType)); } + private String getVariableName(String efxVariableIdentifier) { + return StringUtils.stripStart(efxVariableIdentifier, "$"); + } + + private String getVariableName(VariableReferenceContext ctx) { + return this.getVariableName(ctx.Variable().getText()); + } + + protected String getVariableName(ContextVariableDeclarationContext ctx) { + return this.getVariableName(ctx.Variable().getText()); + } + + private String getVariableName(StringVariableDeclarationContext ctx) { + return this.getVariableName(ctx.Variable().getText()); + } + + private String getVariableName(NumericVariableDeclarationContext ctx) { + return this.getVariableName(ctx.Variable().getText()); + } + + private String getVariableName(BooleanVariableDeclarationContext ctx) { + return this.getVariableName(ctx.Variable().getText()); + } + + private String getVariableName(DateVariableDeclarationContext ctx) { + return this.getVariableName(ctx.Variable().getText()); + } + + private String getVariableName(TimeVariableDeclarationContext ctx) { + return this.getVariableName(ctx.Variable().getText()); + } + + private String getVariableName(DurationVariableDeclarationContext ctx) { + return this.getVariableName(ctx.Variable().getText()); + } + + private String getVariableName(StringParameterDeclarationContext ctx) { + return this.getVariableName(ctx.Variable().getText()); + } + + private String getVariableName(NumericParameterDeclarationContext ctx) { + return this.getVariableName(ctx.Variable().getText()); + } + + private String getVariableName(BooleanParameterDeclarationContext ctx) { + return this.getVariableName(ctx.Variable().getText()); + } + + private String getVariableName(DateParameterDeclarationContext ctx) { + return this.getVariableName(ctx.Variable().getText()); + } + + private String getVariableName(TimeParameterDeclarationContext ctx) { + return this.getVariableName(ctx.Variable().getText()); + } + + private String getVariableName(DurationParameterDeclarationContext ctx) { + return this.getVariableName(ctx.Variable().getText()); + } } 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 5b4696e4..0be1f12e 100644 --- a/src/main/java/eu/europa/ted/efx/sdk1/EfxTemplateTranslatorV1.java +++ b/src/main/java/eu/europa/ted/efx/sdk1/EfxTemplateTranslatorV1.java @@ -30,6 +30,7 @@ import eu.europa.ted.efx.model.Expression.StringExpression; import eu.europa.ted.efx.model.Expression.StringListExpression; import eu.europa.ted.efx.model.Markup; +import eu.europa.ted.efx.model.VariableList; import eu.europa.ted.efx.sdk1.EfxParser.*; import eu.europa.ted.efx.xpath.XPathAttributeLocator; @@ -284,19 +285,18 @@ private void shorthandIndirectLabelReference(final String fieldId) { : this.script.composeFieldValueReference( symbols.getRelativePathOfField(fieldId, currentContext.absolutePath()), PathExpression.class); - final String loopVariableName = "$item"; - + final StringExpression loopVariable = 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(loopVariableName, valueReference))), + List.of(this.script.composeIteratorExpression(loopVariable.script, valueReference))), this.script.composeStringConcatenation( List.of(this.script.getStringLiteralFromUnquotedString(ASSET_TYPE_INDICATOR), this.script.getStringLiteralFromUnquotedString("|"), this.script.getStringLiteralFromUnquotedString(LABEL_TYPE_WHEN), this.script.getStringLiteralFromUnquotedString("-"), - this.script.composeVariableReference(loopVariableName, StringExpression.class), + loopVariable, this.script.getStringLiteralFromUnquotedString("|"), this.script.getStringLiteralFromUnquotedString(fieldId))), StringListExpression.class))); @@ -305,7 +305,7 @@ private void shorthandIndirectLabelReference(final String fieldId) { case "internal-code": this.stack.push(this.markup.renderLabelFromExpression(this.script.composeForExpression( this.script.composeIteratorList( - List.of(this.script.composeIteratorExpression(loopVariableName, valueReference))), + List.of(this.script.composeIteratorExpression(loopVariable.script, valueReference))), this.script.composeStringConcatenation(List.of( this.script.getStringLiteralFromUnquotedString(ASSET_TYPE_CODE), this.script.getStringLiteralFromUnquotedString("|"), @@ -314,7 +314,7 @@ private void shorthandIndirectLabelReference(final String fieldId) { this.script.getStringLiteralFromUnquotedString( this.symbols.getRootCodelistOfField(fieldId)), this.script.getStringLiteralFromUnquotedString("."), - this.script.composeVariableReference(loopVariableName, StringExpression.class))), + loopVariable)), StringListExpression.class))); break; default: @@ -456,13 +456,14 @@ public void exitContextDeclarationBlock(ContextDeclarationBlockContext ctx) { @Override public void exitTemplateLine(TemplateLineContext ctx) { + final VariableList variables = new VariableList(); // template variables not supported prior to EFX 2 final Context lineContext = this.efxContext.pop(); final int indentLevel = this.getIndentLevel(ctx); final int indentChange = indentLevel - this.blockStack.currentIndentationLevel(); final Markup content = ctx.template() != null ? this.stack.pop(Markup.class) : new Markup(""); final Integer outlineNumber = ctx.OutlineNumber() != null ? Integer.parseInt(ctx.OutlineNumber().getText().trim()) : -1; - assert this.stack.isEmpty() : "Stack should be empty at this point."; + assert this.stack.empty() : "Stack should be empty at this point."; this.stack.clear(); // Variable scope boundary. Clear declared variables if (indentChange > 1) { @@ -471,7 +472,7 @@ public void exitTemplateLine(TemplateLineContext ctx) { if (this.blockStack.isEmpty()) { throw new ParseCancellationException(START_INDENT_AT_ZERO); } - this.blockStack.pushChild(outlineNumber, content, this.relativizeContext(lineContext, this.blockStack.currentContext())); + this.blockStack.pushChild(outlineNumber, content, this.relativizeContext(lineContext, this.blockStack.currentContext()), variables); } else if (indentChange < 0) { // lower indent level for (int i = indentChange; i < 0; i++) { @@ -480,14 +481,14 @@ public void exitTemplateLine(TemplateLineContext ctx) { this.blockStack.pop(); } assert this.blockStack.currentIndentationLevel() == indentLevel : UNEXPECTED_INDENTATION; - this.blockStack.pushSibling(outlineNumber, content, this.relativizeContext(lineContext, this.blockStack.parentContext())); + this.blockStack.pushSibling(outlineNumber, content, this.relativizeContext(lineContext, this.blockStack.parentContext()), variables); } else if (indentChange == 0) { if (blockStack.isEmpty()) { assert indentLevel == 0 : UNEXPECTED_INDENTATION; - this.blockStack.push(this.rootBlock.addChild(outlineNumber, content, this.relativizeContext(lineContext, this.rootBlock.getContext()))); + this.blockStack.push(this.rootBlock.addChild(outlineNumber, content, this.relativizeContext(lineContext, this.rootBlock.getContext()), variables)); } else { - this.blockStack.pushSibling(outlineNumber, content, this.relativizeContext(lineContext, this.blockStack.parentContext())); + this.blockStack.pushSibling(outlineNumber, content, this.relativizeContext(lineContext, this.blockStack.parentContext()), variables); } } } 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 f1413ec2..d7d0bf35 100644 --- a/src/main/java/eu/europa/ted/efx/xpath/XPathContextualizer.java +++ b/src/main/java/eu/europa/ted/efx/xpath/XPathContextualizer.java @@ -9,6 +9,7 @@ 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; @@ -16,22 +17,37 @@ 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.efx.model.Expression.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; -import eu.europa.ted.efx.xpath.XPath20Parser.StepexprContext; public class XPathContextualizer extends XPath20BaseListener { private final CharStream inputStream; - private final Queue steps = new LinkedList<>(); + 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.script); + } + + /** + * 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.script); + final CharStream inputStream = CharStreams.fromString(xpath); final XPath20Lexer lexer = new XPath20Lexer(inputStream); final CommonTokenStream tokens = new CommonTokenStream(lexer); final XPath20Parser parser = new XPath20Parser(tokens); @@ -44,7 +60,12 @@ private static Queue getSteps(PathExpression xpath) { return contextualizer.steps; } - + /** + * Makes the given xpath relative to the given context xpath. + * @param contextXpath + * @param xpath + * @return + */ public static PathExpression contextualize(final PathExpression contextXpath, final PathExpression xpath) { @@ -60,6 +81,65 @@ public static PathExpression contextualize(final PathExpression contextXpath, return getContextualizedXpath(contextSteps, pathSteps); } + public static boolean hasPredicate(final PathExpression xpath, String match) { + return hasPredicate(xpath.script, 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 new PathExpression(addPredicate(pathExpression.script, predicate)); + } + + /** + * 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. + */ + 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)); + + 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); + } + public static PathExpression join(final PathExpression first, final PathExpression second) { if (first == null || first.script.trim().isEmpty()) { @@ -173,9 +253,44 @@ private Boolean inPredicateMode() { } @Override - public void exitStepexpr(StepexprContext ctx) { - if (!inPredicateMode()) { - this.steps.offer(new StepInfo(ctx, this::getInputText)); + 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)); } } @@ -189,14 +304,33 @@ public void exitPredicate(PredicateContext ctx) { this.predicateMode--; } - private class StepInfo { + 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; - - public StepInfo(StepexprContext ctx, Function getInputText) { - this.stepText = getInputText.apply(ctx.step()); - this.predicates = - ctx.predicatelist().predicate().stream().map(getInputText).collect(Collectors.toList()); + 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() { @@ -240,5 +374,9 @@ public Boolean isTheSameAs(final StepInfo contextStep) { Collections.sort(contextPredicates); return pathPredicates.equals(contextPredicates); } + + public Boolean isPartOf(Interval interval) { + return this.a >= interval.a && this.b <= interval.b; + } } } 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 03624b47..27562586 100644 --- a/src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java +++ b/src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java @@ -1,6 +1,7 @@ package eu.europa.ted.efx.xpath; import static java.util.Map.entry; + import java.lang.reflect.Constructor; import java.util.List; import java.util.Map; @@ -8,23 +9,31 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; + import org.antlr.v4.runtime.misc.ParseCancellationException; + import eu.europa.ted.eforms.sdk.component.SdkComponent; import eu.europa.ted.eforms.sdk.component.SdkComponentType; import eu.europa.ted.efx.interfaces.ScriptGenerator; +import eu.europa.ted.efx.interfaces.TranslatorOptions; import eu.europa.ted.efx.model.Expression; import eu.europa.ted.efx.model.Expression.BooleanExpression; import eu.europa.ted.efx.model.Expression.DateExpression; +import eu.europa.ted.efx.model.Expression.DateListExpression; import eu.europa.ted.efx.model.Expression.DurationExpression; +import eu.europa.ted.efx.model.Expression.DurationListExpression; import eu.europa.ted.efx.model.Expression.IteratorExpression; import eu.europa.ted.efx.model.Expression.IteratorListExpression; import eu.europa.ted.efx.model.Expression.ListExpression; -import eu.europa.ted.efx.model.Expression.ListExpressionBase; +import eu.europa.ted.efx.model.Expression.MultilingualStringExpression; +import eu.europa.ted.efx.model.Expression.MultilingualStringListExpression; import eu.europa.ted.efx.model.Expression.NumericExpression; import eu.europa.ted.efx.model.Expression.NumericListExpression; import eu.europa.ted.efx.model.Expression.PathExpression; import eu.europa.ted.efx.model.Expression.StringExpression; +import eu.europa.ted.efx.model.Expression.StringListExpression; import eu.europa.ted.efx.model.Expression.TimeExpression; +import eu.europa.ted.efx.model.Expression.TimeListExpression; @SdkComponent(versions = {"0.6", "0.7", "1"}, componentType = SdkComponentType.SCRIPT_GENERATOR) public class XPathScriptGenerator implements ScriptGenerator { @@ -44,6 +53,11 @@ public class XPathScriptGenerator implements ScriptGenerator { entry(">", ">"), // entry(">=", ">=")); + protected TranslatorOptions translatorOptions; + + public XPathScriptGenerator(TranslatorOptions translatorOptions) { + this.translatorOptions = translatorOptions; + } @Override public T composeNodeReferenceWithPredicate(PathExpression nodeReference, @@ -66,20 +80,24 @@ public T composeFieldReferenceWithAxis(final PathExpressi @Override public T composeFieldValueReference(PathExpression fieldReference, Class type) { - - if (StringExpression.class.isAssignableFrom(type)) { + if ((MultilingualStringExpression.class.isAssignableFrom(type) + || MultilingualStringListExpression.class.isAssignableFrom(type)) + && !XPathContextualizer.hasPredicate(fieldReference, "@languageID")) { + return Expression.instantiate("efx:preferred-language-text(" + fieldReference.script + ")", type); + } + if (StringExpression.class.isAssignableFrom(type) || StringListExpression.class.isAssignableFrom(type)) { return Expression.instantiate(fieldReference.script + "/normalize-space(text())", type); } - if (NumericExpression.class.isAssignableFrom(type)) { + if (NumericExpression.class.isAssignableFrom(type) || NumericListExpression.class.isAssignableFrom(type)) { return Expression.instantiate(fieldReference.script + "/number()", type); } - if (DateExpression.class.isAssignableFrom(type)) { + if (DateExpression.class.isAssignableFrom(type) || DateListExpression.class.isAssignableFrom(type)) { return Expression.instantiate(fieldReference.script + "/xs:date(text())", type); } - if (TimeExpression.class.isAssignableFrom(type)) { + if (TimeExpression.class.isAssignableFrom(type) || TimeListExpression.class.isAssignableFrom(type)) { return Expression.instantiate(fieldReference.script + "/xs:time(text())", type); } - if (DurationExpression.class.isAssignableFrom(type)) { + if (DurationExpression.class.isAssignableFrom(type) || DurationListExpression.class.isAssignableFrom(type)) { return Expression.instantiate("(for $F in " + fieldReference.script + " return (if ($F/@unitCode='WEEK')" + // " then xs:dayTimeDuration(concat('P', $F/number() * 7, 'D'))" + // " else if ($F/@unitCode='DAY')" + // @@ -104,12 +122,12 @@ public T composeFieldAttributeReference(PathExpression fi @Override public T composeVariableReference(String variableName, Class type) { - return Expression.instantiate(variableName, type); + return Expression.instantiate("$" + variableName, type); } @Override public T composeVariableDeclaration(String variableName, Class type) { - return Expression.instantiate(variableName, type); + return Expression.instantiate("$" + variableName, type); } @Override @@ -134,40 +152,40 @@ public > L composeList(List @Override public NumericExpression getNumericLiteralEquivalent(String literal) { - return new NumericExpression(literal); + return new NumericExpression(literal, true); } @Override public StringExpression getStringLiteralEquivalent(String literal) { - return new StringExpression(literal); + return new StringExpression(literal, true); } @Override public BooleanExpression getBooleanEquivalent(boolean value) { - return new BooleanExpression(value ? "true()" : "false()"); + return new BooleanExpression(value ? "true()" : "false()", true); } @Override public DateExpression getDateLiteralEquivalent(String literal) { - return new DateExpression("xs:date(" + quoted(literal) + ")"); + return new DateExpression("xs:date(" + quoted(literal) + ")", true); } @Override public TimeExpression getTimeLiteralEquivalent(String literal) { - return new TimeExpression("xs:time(" + quoted(literal) + ")"); + return new TimeExpression("xs:time(" + quoted(literal) + ")", true); } @Override public DurationExpression getDurationLiteralEquivalent(final String literal) { if (literal.contains("M") || literal.contains("Y")) { - return new DurationExpression("xs:yearMonthDuration(" + quoted(literal) + ")"); + return new DurationExpression("xs:yearMonthDuration(" + quoted(literal) + ")", true); } if (literal.contains("W")) { final int weeks = this.getWeeksFromDurationLiteral(literal); return new DurationExpression( - "xs:dayTimeDuration(" + quoted(String.format("P%dD", weeks * 7)) + ")"); + "xs:dayTimeDuration(" + quoted(String.format("P%dD", weeks * 7)) + ")", true); } - return new DurationExpression("xs:dayTimeDuration(" + quoted(literal) + ")"); + return new DurationExpression("xs:dayTimeDuration(" + quoted(literal) + ")", true); } @Override @@ -348,8 +366,8 @@ public BooleanExpression composeComparisonOperation(Expression leftOperand, Stri } @Override - public BooleanExpression composeSequenceEqualFunction(ListExpressionBase one, - ListExpressionBase two) { + public BooleanExpression composeSequenceEqualFunction(ListExpression one, + ListExpression two) { return new BooleanExpression("deep-equal(sort(" + one.script + "), sort(" + two.script + "))"); } @@ -361,7 +379,7 @@ public NumericExpression composeCountOperation(PathExpression nodeSet) { } @Override - public NumericExpression composeCountOperation(ListExpressionBase list) { + public NumericExpression composeCountOperation(ListExpression list) { return new NumericExpression("count(" + list.script + ")"); } @@ -410,7 +428,8 @@ public StringExpression composeSubstringExtraction(StringExpression text, @Override public StringExpression composeToStringConversion(NumericExpression number) { - return new StringExpression("format-number(" + number.script + ", '0.##########')"); + String formatString = this.translatorOptions.getDecimalFormat().adaptFormatString("0.##########"); + return new StringExpression("format-number(" + number.script + ", '" + formatString + "')"); } @Override @@ -422,12 +441,13 @@ public StringExpression composeStringConcatenation(List list) @Override public StringExpression composeNumberFormatting(NumericExpression number, StringExpression format) { - return new StringExpression("format-number(" + number.script + ", " + format.script + ")"); + String formatString = format.isLiteral ? this.translatorOptions.getDecimalFormat().adaptFormatString(format.script) : format.script; + return new StringExpression("format-number(" + number.script + ", " + formatString + ")"); } @Override public StringExpression getStringLiteralFromUnquotedString(String value) { - return new StringExpression("'" + value + "'"); + return new StringExpression("'" + value + "'", true); } @@ -504,15 +524,13 @@ public > L composeUnionFunctio } @Override - public > L composeIntersectFunction(L listOne, - L listTwo, Class listType) { - return Expression.instantiate("distinct-values(" + listOne.script + "[.= " + listTwo.script + "])", listType); + public > L composeIntersectFunction(L listOne, L listTwo, Class listType) { + return Expression.instantiate("distinct-values(for $L1 in " + listOne.script + " return if (some $L2 in " + listTwo.script + " satisfies $L1 = $L2) then $L1 else ())", listType); } @Override - public > L composeExceptFunction(L listOne, - L listTwo, Class listType) { - return Expression.instantiate("distinct-values(" + listOne.script + "[not(. = " + listTwo.script + ")])", listType); + public > L composeExceptFunction(L listOne, L listTwo, Class listType) { + return Expression.instantiate("distinct-values(for $L1 in " + listOne.script + " return if (every $L2 in " + listTwo.script + " satisfies $L1 != $L2) then $L1 else ())", listType); } diff --git a/src/test/java/eu/europa/ted/efx/EfxExpressionCombinedTest.java b/src/test/java/eu/europa/ted/efx/EfxExpressionCombinedTest.java index ecf9a344..dad29bff 100644 --- a/src/test/java/eu/europa/ted/efx/EfxExpressionCombinedTest.java +++ b/src/test/java/eu/europa/ted/efx/EfxExpressionCombinedTest.java @@ -2,18 +2,22 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.Test; + +import eu.europa.ted.efx.interfaces.TranslatorOptions; import eu.europa.ted.efx.mock.DependencyFactoryMock; +import eu.europa.ted.efx.model.DecimalFormat; /** * Test for EFX expressions that combine several aspects of the language. */ class EfxExpressionCombinedTest { final private String SDK_VERSION = "eforms-sdk-1.0"; + final private TranslatorOptions TRANSLATOR_OPTIONS = new EfxTranslatorOptions(DecimalFormat.EFX_DEFAULT); private String test(final String context, final String expression) { try { return EfxTranslator.translateExpression(DependencyFactoryMock.INSTANCE, - SDK_VERSION, String.format("{%s} ${%s}", context, expression)); + SDK_VERSION, String.format("{%s} ${%s}", context, expression), TRANSLATOR_OPTIONS); } catch (InstantiationException e) { throw new RuntimeException(e); } @@ -32,18 +36,18 @@ void testNotPresentAndNotPresent() { @Test void testCountWithNodeContextOverride() { - assertEquals("count(../../PathNode/CodeField) = 1", + assertEquals("count(../../PathNode/CodeField/normalize-space(text())) = 1", test("BT-00-Text", "count(ND-Root::BT-00-Code) == 1")); } @Test void testCountWithAbsoluteFieldReference() { - assertEquals("count(/*/PathNode/CodeField) = 1", test("BT-00-Text", "count(/BT-00-Code) == 1")); + assertEquals("count(/*/PathNode/CodeField/normalize-space(text())) = 1", test("BT-00-Text", "count(/BT-00-Code) == 1")); } @Test void testCountWithAbsoluteFieldReferenceAndPredicate() { - assertEquals("count(/*/PathNode/CodeField[../IndicatorField = true()]) = 1", + assertEquals("count(/*/PathNode/CodeField[../IndicatorField = true()]/normalize-space(text())) = 1", test("BT-00-Text", "count(/BT-00-Code[BT-00-Indicator == TRUE]) == 1")); } } diff --git a/src/test/java/eu/europa/ted/efx/EfxExpressionTranslatorTest.java b/src/test/java/eu/europa/ted/efx/EfxExpressionTranslatorTest.java index 6cd2ae9d..6a95ef79 100644 --- a/src/test/java/eu/europa/ted/efx/EfxExpressionTranslatorTest.java +++ b/src/test/java/eu/europa/ted/efx/EfxExpressionTranslatorTest.java @@ -4,10 +4,14 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import org.antlr.v4.runtime.misc.ParseCancellationException; import org.junit.jupiter.api.Test; + +import eu.europa.ted.efx.interfaces.TranslatorOptions; import eu.europa.ted.efx.mock.DependencyFactoryMock; +import eu.europa.ted.efx.model.DecimalFormat; class EfxExpressionTranslatorTest { final private String SDK_VERSION = "eforms-sdk-1.0"; + final private TranslatorOptions TRANSLATOR_OPTIONS = new EfxTranslatorOptions(DecimalFormat.EFX_DEFAULT); private String test(final String context, final String expression) { return test1(String.format("{%s} ${%s}", context, expression)); @@ -16,7 +20,7 @@ private String test(final String context, final String expression) { private String test1(final String expression, final String... params) { try { return EfxTranslator.translateExpression(DependencyFactoryMock.INSTANCE, SDK_VERSION, - expression, params); + expression, TRANSLATOR_OPTIONS, params); } catch (InstantiationException e) { throw new RuntimeException(e); } @@ -93,7 +97,7 @@ void testLikePatternCondition_WithNot() { @Test void testFieldValueComparison_UsingTextFields() { assertEquals( - "PathNode/TextField/normalize-space(text()) = PathNode/TextMultilingualField/normalize-space(text())", + "PathNode/TextField/normalize-space(text()) = efx:preferred-language-text(PathNode/TextMultilingualField)", test("ND-Root", "BT-00-Text == BT-00-Text-Multilingual")); } @@ -299,7 +303,7 @@ void testStringQuantifiedExpression_UsingLiterals() { @Test void testStringQuantifiedExpression_UsingFieldReference() { - assertEquals("every $x in PathNode/TextField satisfies $x <= 'a'", + assertEquals("every $x in PathNode/TextField/normalize-space(text()) satisfies $x <= 'a'", test("ND-Root", "every text:$x in BT-00-Text satisfies $x <= 'a'")); } @@ -323,7 +327,7 @@ void testNumericQuantifiedExpression_UsingLiterals() { @Test void testNumericQuantifiedExpression_UsingFieldReference() { - assertEquals("every $x in PathNode/NumberField satisfies $x <= 1", + assertEquals("every $x in PathNode/NumberField/number() satisfies $x <= 1", test("ND-Root", "every number:$x in BT-00-Number satisfies $x <= 1")); } @@ -337,13 +341,13 @@ void testDateQuantifiedExpression_UsingLiterals() { @Test void testDateQuantifiedExpression_UsingFieldReference() { - assertEquals("every $x in PathNode/StartDateField satisfies $x <= xs:date('2012-01-01Z')", + assertEquals("every $x in PathNode/StartDateField/xs:date(text()) satisfies $x <= xs:date('2012-01-01Z')", test("ND-Root", "every date:$x in BT-00-StartDate satisfies $x <= 2012-01-01Z")); } @Test void testDateQuantifiedExpression_UsingMultipleIterators() { - assertEquals("every $x in PathNode/StartDateField, $y in ($x,xs:date('2022-02-02Z')), $i in (true(),true()) satisfies $x <= xs:date('2012-01-01Z')", + assertEquals("every $x in PathNode/StartDateField/xs:date(text()), $y in ($x,xs:date('2022-02-02Z')), $i in (true(),true()) satisfies $x <= xs:date('2012-01-01Z')", test("ND-Root", "every date:$x in BT-00-StartDate, date:$y in ($x, 2022-02-02Z), indicator:$i in (ALWAYS, TRUE) satisfies $x <= 2012-01-01Z")); } @@ -357,7 +361,7 @@ void testTimeQuantifiedExpression_UsingLiterals() { @Test void testTimeQuantifiedExpression_UsingFieldReference() { - assertEquals("every $x in PathNode/StartTimeField satisfies $x <= xs:time('00:00:00Z')", + assertEquals("every $x in PathNode/StartTimeField/xs:time(text()) satisfies $x <= xs:time('00:00:00Z')", test("ND-Root", "every time:$x in BT-00-StartTime satisfies $x <= 00:00:00Z")); } @@ -371,7 +375,7 @@ void testDurationQuantifiedExpression_UsingLiterals() { @Test void testDurationQuantifiedExpression_UsingFieldReference() { assertEquals( - "every $x in PathNode/MeasureField satisfies boolean(for $T in (current-date()) return ($T + $x <= $T + xs:dayTimeDuration('P1D')))", + "every $x in (for $F in PathNode/MeasureField return (if ($F/@unitCode='WEEK') then xs:dayTimeDuration(concat('P', $F/number() * 7, 'D')) else if ($F/@unitCode='DAY') then xs:dayTimeDuration(concat('P', $F/number(), 'D')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) satisfies boolean(for $T in (current-date()) return ($T + $x <= $T + xs:dayTimeDuration('P1D')))", test("ND-Root", "every measure:$x in BT-00-Measure satisfies $x <= P1D")); } @@ -464,13 +468,13 @@ void testStringsFromStringIteration_UsingLiterals() { @Test void testStringsSequenceFromIteration_UsingMultipleIterators() { - assertEquals("'a' = (for $x in ('a','b','c'), $y in (1,2), $z in PathNode/IndicatorField return concat($x, format-number($y, '0.##########'), 'text'))", + assertEquals("'a' = (for $x in ('a','b','c'), $y in (1,2), $z in PathNode/IndicatorField return concat($x, format-number($y, '0,##########'), 'text'))", test("ND-Root", "'a' in (for text:$x in ('a', 'b', 'c'), number:$y in (1, 2), indicator:$z in BT-00-Indicator return concat($x, string($y), 'text'))")); } @Test void testStringsSequenceFromIteration_UsingObjectVariable() { - assertEquals("for $n in PathNode/TextField[../NumberField], $d in $n/../StartDateField return 'text'", + assertEquals("for $n in PathNode/TextField[../NumberField], $d in $n/../StartDateField/xs:date(text()) return 'text'", test("ND-Root", "for context:$n in BT-00-Text[BT-00-Number is present], date:$d in $n::BT-00-StartDate return 'text'")); } @@ -482,7 +486,7 @@ void testStringsSequenceFromIteration_UsingNodeContextVariable() { @Test void testStringsFromStringIteration_UsingFieldReference() { - assertEquals("'a' = (for $x in PathNode/TextField return concat($x, 'text'))", + assertEquals("'a' = (for $x in PathNode/TextField/normalize-space(text()) return concat($x, 'text'))", test("ND-Root", "'a' in (for text:$x in BT-00-Text return concat($x, 'text'))")); } @@ -508,7 +512,7 @@ void testStringsFromNumericIteration_UsingLiterals() { @Test void testStringsFromNumericIteration_UsingFieldReference() { - assertEquals("'a' = (for $x in PathNode/NumberField return 'y')", + assertEquals("'a' = (for $x in PathNode/NumberField/number() return 'y')", test("ND-Root", "'a' in (for number:$x in BT-00-Number return 'y')")); } @@ -521,7 +525,7 @@ void testStringsFromDateIteration_UsingLiterals() { @Test void testStringsFromDateIteration_UsingFieldReference() { - assertEquals("'a' = (for $x in PathNode/StartDateField return 'y')", + assertEquals("'a' = (for $x in PathNode/StartDateField/xs:date(text()) return 'y')", test("ND-Root", "'a' in (for date:$x in BT-00-StartDate return 'y')")); } @@ -534,7 +538,7 @@ void testStringsFromTimeIteration_UsingLiterals() { @Test void testStringsFromTimeIteration_UsingFieldReference() { - assertEquals("'a' = (for $x in PathNode/StartTimeField return 'y')", + assertEquals("'a' = (for $x in PathNode/StartTimeField/xs:time(text()) return 'y')", test("ND-Root", "'a' in (for time:$x in BT-00-StartTime return 'y')")); } @@ -548,7 +552,7 @@ void testStringsFromDurationIteration_UsingLiterals() { @Test void testStringsFromDurationIteration_UsingFieldReference() { - assertEquals("'a' = (for $x in PathNode/MeasureField return 'y')", + assertEquals("'a' = (for $x in (for $F in PathNode/MeasureField return (if ($F/@unitCode='WEEK') then xs:dayTimeDuration(concat('P', $F/number() * 7, 'D')) else if ($F/@unitCode='DAY') then xs:dayTimeDuration(concat('P', $F/number(), 'D')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) return 'y')", test("ND-Root", "'a' in (for measure:$x in BT-00-Measure return 'y')")); } @@ -562,7 +566,7 @@ void testNumbersFromStringIteration_UsingLiterals() { @Test void testNumbersFromStringIteration_UsingFieldReference() { - assertEquals("123 = (for $x in PathNode/TextField return number($x))", + assertEquals("123 = (for $x in PathNode/TextField/normalize-space(text()) return number($x))", test("ND-Root", "123 in (for text:$x in BT-00-Text return number($x))")); } @@ -588,7 +592,7 @@ void testNumbersFromNumericIteration_UsingLiterals() { @Test void testNumbersFromNumericIteration_UsingFieldReference() { - assertEquals("123 = (for $x in PathNode/NumberField return 0)", + assertEquals("123 = (for $x in PathNode/NumberField/number() return 0)", test("ND-Root", "123 in (for number:$x in BT-00-Number return 0)")); } @@ -601,7 +605,7 @@ void testNumbersFromDateIteration_UsingLiterals() { @Test void testNumbersFromDateIteration_UsingFieldReference() { - assertEquals("123 = (for $x in PathNode/StartDateField return 0)", + assertEquals("123 = (for $x in PathNode/StartDateField/xs:date(text()) return 0)", test("ND-Root", "123 in (for date:$x in BT-00-StartDate return 0)")); } @@ -614,7 +618,7 @@ void testNumbersFromTimeIteration_UsingLiterals() { @Test void testNumbersFromTimeIteration_UsingFieldReference() { - assertEquals("123 = (for $x in PathNode/StartTimeField return 0)", + assertEquals("123 = (for $x in PathNode/StartTimeField/xs:time(text()) return 0)", test("ND-Root", "123 in (for time:$x in BT-00-StartTime return 0)")); } @@ -628,7 +632,7 @@ void testNumbersFromDurationIteration_UsingLiterals() { @Test void testNumbersFromDurationIteration_UsingFieldReference() { - assertEquals("123 = (for $x in PathNode/MeasureField return 0)", + assertEquals("123 = (for $x in (for $F in PathNode/MeasureField return (if ($F/@unitCode='WEEK') then xs:dayTimeDuration(concat('P', $F/number() * 7, 'D')) else if ($F/@unitCode='DAY') then xs:dayTimeDuration(concat('P', $F/number(), 'D')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) return 0)", test("ND-Root", "123 in (for measure:$x in BT-00-Measure return 0)")); } @@ -642,7 +646,7 @@ void testDatesFromStringIteration_UsingLiterals() { @Test void testDatesFromStringIteration_UsingFieldReference() { - assertEquals("xs:date('2022-01-01Z') = (for $x in PathNode/TextField return xs:date($x))", + assertEquals("xs:date('2022-01-01Z') = (for $x in PathNode/TextField/normalize-space(text()) return xs:date($x))", test("ND-Root", "2022-01-01Z in (for text:$x in BT-00-Text return date($x))")); } @@ -671,7 +675,7 @@ void testDatesFromNumericIteration_UsingLiterals() { @Test void testDatesFromNumericIteration_UsingFieldReference() { assertEquals( - "xs:date('2022-01-01Z') = (for $x in PathNode/NumberField return xs:date('2022-01-01Z'))", + "xs:date('2022-01-01Z') = (for $x in PathNode/NumberField/number() return xs:date('2022-01-01Z'))", test("ND-Root", "2022-01-01Z in (for number:$x in BT-00-Number return 2022-01-01Z)")); } @@ -686,7 +690,7 @@ void testDatesFromDateIteration_UsingLiterals() { @Test void testDatesFromDateIteration_UsingFieldReference() { assertEquals( - "xs:date('2022-01-01Z') = (for $x in PathNode/StartDateField return xs:date('2022-01-01Z'))", + "xs:date('2022-01-01Z') = (for $x in PathNode/StartDateField/xs:date(text()) return xs:date('2022-01-01Z'))", test("ND-Root", "2022-01-01Z in (for date:$x in BT-00-StartDate return 2022-01-01Z)")); } @@ -701,7 +705,7 @@ void testDatesFromTimeIteration_UsingLiterals() { @Test void testDatesFromTimeIteration_UsingFieldReference() { assertEquals( - "xs:date('2022-01-01Z') = (for $x in PathNode/StartTimeField return xs:date('2022-01-01Z'))", + "xs:date('2022-01-01Z') = (for $x in PathNode/StartTimeField/xs:time(text()) return xs:date('2022-01-01Z'))", test("ND-Root", "2022-01-01Z in (for time:$x in BT-00-StartTime return 2022-01-01Z)")); } @@ -716,7 +720,7 @@ void testDatesFromDurationIteration_UsingLiterals() { @Test void testDatesFromDurationIteration_UsingFieldReference() { assertEquals( - "xs:date('2022-01-01Z') = (for $x in PathNode/MeasureField return xs:date('2022-01-01Z'))", + "xs:date('2022-01-01Z') = (for $x in (for $F in PathNode/MeasureField return (if ($F/@unitCode='WEEK') then xs:dayTimeDuration(concat('P', $F/number() * 7, 'D')) else if ($F/@unitCode='DAY') then xs:dayTimeDuration(concat('P', $F/number(), 'D')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) return xs:date('2022-01-01Z'))", test("ND-Root", "2022-01-01Z in (for measure:$x in BT-00-Measure return 2022-01-01Z)")); } @@ -730,7 +734,7 @@ void testTimesFromStringIteration_UsingLiterals() { @Test void testTimesFromStringIteration_UsingFieldReference() { - assertEquals("xs:time('12:00:00Z') = (for $x in PathNode/TextField return xs:time($x))", + assertEquals("xs:time('12:00:00Z') = (for $x in PathNode/TextField/normalize-space(text()) return xs:time($x))", test("ND-Root", "12:00:00Z in (for text:$x in BT-00-Text return time($x))")); } @@ -758,7 +762,7 @@ void testTimesFromNumericIteration_UsingLiterals() { @Test void testTimesFromNumericIteration_UsingFieldReference() { assertEquals( - "xs:time('12:00:00Z') = (for $x in PathNode/NumberField return xs:time('12:00:00Z'))", + "xs:time('12:00:00Z') = (for $x in PathNode/NumberField/number() return xs:time('12:00:00Z'))", test("ND-Root", "12:00:00Z in (for number:$x in BT-00-Number return 12:00:00Z)")); } @@ -773,7 +777,7 @@ void testTimesFromDateIteration_UsingLiterals() { @Test void testTimesFromDateIteration_UsingFieldReference() { assertEquals( - "xs:time('12:00:00Z') = (for $x in PathNode/StartDateField return xs:time('12:00:00Z'))", + "xs:time('12:00:00Z') = (for $x in PathNode/StartDateField/xs:date(text()) return xs:time('12:00:00Z'))", test("ND-Root", "12:00:00Z in (for date:$x in BT-00-StartDate return 12:00:00Z)")); } @@ -788,7 +792,7 @@ void testTimesFromTimeIteration_UsingLiterals() { @Test void testTimesFromTimeIteration_UsingFieldReference() { assertEquals( - "xs:time('12:00:00Z') = (for $x in PathNode/StartTimeField return xs:time('12:00:00Z'))", + "xs:time('12:00:00Z') = (for $x in PathNode/StartTimeField/xs:time(text()) return xs:time('12:00:00Z'))", test("ND-Root", "12:00:00Z in (for time:$x in BT-00-StartTime return 12:00:00Z)")); } @@ -803,7 +807,7 @@ void testTimesFromDurationIteration_UsingLiterals() { @Test void testTimesFromDurationIteration_UsingFieldReference() { assertEquals( - "xs:time('12:00:00Z') = (for $x in PathNode/MeasureField return xs:time('12:00:00Z'))", + "xs:time('12:00:00Z') = (for $x in (for $F in PathNode/MeasureField return (if ($F/@unitCode='WEEK') then xs:dayTimeDuration(concat('P', $F/number() * 7, 'D')) else if ($F/@unitCode='DAY') then xs:dayTimeDuration(concat('P', $F/number(), 'D')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) return xs:time('12:00:00Z'))", test("ND-Root", "12:00:00Z in (for measure:$x in BT-00-Measure return 12:00:00Z)")); } @@ -819,7 +823,7 @@ void testDurationsFromStringIteration_UsingLiterals() { @Test void testDurationsFromStringIteration_UsingFieldReference() { assertEquals( - "xs:dayTimeDuration('P1D') = (for $x in PathNode/TextField return xs:dayTimeDuration($x))", + "xs:dayTimeDuration('P1D') = (for $x in PathNode/TextField/normalize-space(text()) return xs:dayTimeDuration($x))", test("ND-Root", "P1D in (for text:$x in BT-00-Text return day-time-duration($x))")); } @@ -848,7 +852,7 @@ void testDurationsFromNumericIteration_UsingLiterals() { @Test void testDurationsFromNumericIteration_UsingFieldReference() { assertEquals( - "xs:dayTimeDuration('P1D') = (for $x in PathNode/NumberField return xs:dayTimeDuration('P1D'))", + "xs:dayTimeDuration('P1D') = (for $x in PathNode/NumberField/number() return xs:dayTimeDuration('P1D'))", test("ND-Root", "P1D in (for number:$x in BT-00-Number return P1D)")); } @@ -862,7 +866,7 @@ void testDurationsFromDateIteration_UsingLiterals() { @Test void testDurationsFromDateIteration_UsingFieldReference() { assertEquals( - "xs:dayTimeDuration('P1D') = (for $x in PathNode/StartDateField return xs:dayTimeDuration('P1D'))", + "xs:dayTimeDuration('P1D') = (for $x in PathNode/StartDateField/xs:date(text()) return xs:dayTimeDuration('P1D'))", test("ND-Root", "P1D in (for date:$x in BT-00-StartDate return P1D)")); } @@ -876,7 +880,7 @@ void testDurationsFromTimeIteration_UsingLiterals() { @Test void testDurationsFromTimeIteration_UsingFieldReference() { assertEquals( - "xs:dayTimeDuration('P1D') = (for $x in PathNode/StartTimeField return xs:dayTimeDuration('P1D'))", + "xs:dayTimeDuration('P1D') = (for $x in PathNode/StartTimeField/xs:time(text()) return xs:dayTimeDuration('P1D'))", test("ND-Root", "P1D in (for time:$x in BT-00-StartTime return P1D)")); } @@ -890,7 +894,7 @@ void testDurationsFromDurationIteration_UsingLiterals() { @Test void testDurationsFromDurationIteration_UsingFieldReference() { assertEquals( - "xs:dayTimeDuration('P1D') = (for $x in PathNode/MeasureField return xs:dayTimeDuration('P1D'))", + "xs:dayTimeDuration('P1D') = (for $x in (for $F in PathNode/MeasureField return (if ($F/@unitCode='WEEK') then xs:dayTimeDuration(concat('P', $F/number() * 7, 'D')) else if ($F/@unitCode='DAY') then xs:dayTimeDuration(concat('P', $F/number(), 'D')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) return xs:dayTimeDuration('P1D'))", test("ND-Root", "P1D in (for measure:$x in BT-00-Measure return P1D)")); } @@ -1072,6 +1076,18 @@ void testFieldReference_WithAxis() { test("ND-Root", "ND-Root::preceding::BT-00-Integer")); } + @Test + void testMultilingualTextFieldReference() { + assertEquals("efx:preferred-language-text(PathNode/TextMultilingualField)", + test("ND-Root", "BT-00-Text-Multilingual")); + } + + @Test + void testMultilingualTextFieldReference_WithLanguagePredicate() { + assertEquals("PathNode/TextMultilingualField[./@languageID = 'eng']/normalize-space(text())", + test("ND-Root", "BT-00-Text-Multilingual[BT-00-Text-Multilingual/@languageID == 'eng']")); + } + /*** Boolean functions ***/ @Test @@ -1103,12 +1119,12 @@ void testEndsWithFunction() { @Test void testCountFunction_UsingFieldReference() { - assertEquals("count(PathNode/TextField)", test("ND-Root", "count(BT-00-Text)")); + assertEquals("count(PathNode/TextField/normalize-space(text()))", test("ND-Root", "count(BT-00-Text)")); } @Test void testCountFunction_UsingSequenceFromIteration() { - assertEquals("count(for $x in PathNode/TextField return concat($x, '-xyz'))", + assertEquals("count(for $x in PathNode/TextField/normalize-space(text()) return concat($x, '-xyz'))", test("ND-Root", "count(for text:$x in BT-00-Text return concat($x, '-xyz'))")); } @@ -1120,12 +1136,12 @@ void testNumberFunction() { @Test void testSumFunction_UsingFieldReference() { - assertEquals("sum(PathNode/NumberField)", test("ND-Root", "sum(BT-00-Number)")); + assertEquals("sum(PathNode/NumberField/number())", test("ND-Root", "sum(BT-00-Number)")); } @Test void testSumFunction_UsingNumericSequenceFromIteration() { - assertEquals("sum(for $v in PathNode/NumberField return $v + 1)", + assertEquals("sum(for $v in PathNode/NumberField/number() return $v + 1)", test("ND-Root", "sum(for number:$v in BT-00-Number return $v +1)")); } @@ -1147,7 +1163,7 @@ void testSubstringFunction() { @Test void testToStringFunction() { - assertEquals("format-number(123, '0.##########')", test("ND-Root", "string(123)")); + assertEquals("format-number(123, '0,##########')", test("ND-Root", "string(123)")); } @Test @@ -1157,7 +1173,7 @@ void testConcatFunction() { @Test void testFormatNumberFunction() { - assertEquals("format-number(PathNode/NumberField/number(), '#,##0.00')", + assertEquals("format-number(PathNode/NumberField/number(), '# ##0,00')", test("ND-Root", "format-number(BT-00-Number, '#,##0.00')")); } @@ -1170,6 +1186,18 @@ void testDateFromStringFunction() { test("ND-Root", "date(BT-00-Text)")); } + @Test + void testDatePlusMeasureFunction() { + assertEquals("(PathNode/StartDateField/xs:date(text()) + (for $F in PathNode/MeasureField return (if ($F/@unitCode='WEEK') then xs:dayTimeDuration(concat('P', $F/number() * 7, 'D')) else if ($F/@unitCode='DAY') then xs:dayTimeDuration(concat('P', $F/number(), 'D')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())))", + test("ND-Root", "add-measure(BT-00-StartDate, BT-00-Measure)")); + } + + @Test + void testDateMinusMeasureFunction() { + assertEquals("(PathNode/StartDateField/xs:date(text()) - (for $F in PathNode/MeasureField return (if ($F/@unitCode='WEEK') then xs:dayTimeDuration(concat('P', $F/number() * 7, 'D')) else if ($F/@unitCode='DAY') then xs:dayTimeDuration(concat('P', $F/number(), 'D')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())))", + test("ND-Root", "subtract-measure(BT-00-StartDate, BT-00-Measure)")); + } + /*** Time functions ***/ @Test @@ -1178,6 +1206,20 @@ void testTimeFromStringFunction() { test("ND-Root", "time(BT-00-Text)")); } + /*** Duration functions ***/ + + @Test + void testDayTimeDurationFromStringFunction() { + assertEquals("xs:yearMonthDuration(PathNode/TextField/normalize-space(text()))", + test("ND-Root", "year-month-duration(BT-00-Text)")); + } + + @Test + void testYearMonthDurationFromStringFunction() { + assertEquals("xs:dayTimeDuration(PathNode/TextField/normalize-space(text()))", + test("ND-Root", "day-time-duration(BT-00-Text)")); + } + /*** Sequence Functions ***/ @Test @@ -1204,6 +1246,12 @@ void testDistinctValuesFunction_WithTimeSequences() { test("ND-Root", "distinct-values((12:00:00Z, 13:00:00Z, 12:00:00Z, 14:00:00Z))")); } + @Test + void testDistinctValuesFunction_WithDurationSequences() { + assertEquals("distinct-values((xs:dayTimeDuration('P7D'),xs:dayTimeDuration('P2D'),xs:dayTimeDuration('P2D'),xs:dayTimeDuration('P5D')))", + test("ND-Root", "distinct-values((P1W, P2D, P2D, P5D))")); + } + @Test void testDistinctValuesFunction_WithBooleanSequences() { assertEquals("distinct-values((true(),false(),false(),false()))", @@ -1212,7 +1260,7 @@ void testDistinctValuesFunction_WithBooleanSequences() { @Test void testDistinctValuesFunction_WithFieldReferences() { - assertEquals("distinct-values(PathNode/TextField)", + assertEquals("distinct-values(PathNode/TextField/normalize-space(text()))", test("ND-Root", "distinct-values(BT-00-Text)")); } @@ -1242,6 +1290,12 @@ void testUnionFunction_WithTimeSequences() { test("ND-Root", "value-union((12:00:00Z, 13:00:00Z), (12:00:00Z, 14:00:00Z))")); } + @Test + void testUnionFunction_WithDurationSequences() { + assertEquals("distinct-values(((xs:dayTimeDuration('P7D'),xs:dayTimeDuration('P2D')), (xs:dayTimeDuration('P2D'),xs:dayTimeDuration('P5D'))))", + test("ND-Root", "value-union((P1W, P2D), (P2D, P5D))")); + } + @Test void testUnionFunction_WithBooleanSequences() { assertEquals("distinct-values(((true(),false()), (false(),false())))", @@ -1250,7 +1304,7 @@ void testUnionFunction_WithBooleanSequences() { @Test void testUnionFunction_WithFieldReferences() { - assertEquals("distinct-values((PathNode/TextField, PathNode/TextField))", + assertEquals("distinct-values((PathNode/TextField/normalize-space(text()), PathNode/TextField/normalize-space(text())))", test("ND-Root", "value-union(BT-00-Text, BT-00-Text)")); } @@ -1264,37 +1318,43 @@ void testUnionFunction_WithTypeMismatch() { @Test void testIntersectFunction_WithStringSequences() { - assertEquals("distinct-values(('one','two')[.= ('two','three','four')])", + assertEquals("distinct-values(for $L1 in ('one','two') return if (some $L2 in ('two','three','four') satisfies $L1 = $L2) then $L1 else ())", test("ND-Root", "value-intersect(('one', 'two'), ('two', 'three', 'four'))")); } @Test void testIntersectFunction_WithNumberSequences() { - assertEquals("distinct-values((1,2,3)[.= (2,3,4)])", + assertEquals("distinct-values(for $L1 in (1,2,3) return if (some $L2 in (2,3,4) satisfies $L1 = $L2) then $L1 else ())", test("ND-Root", "value-intersect((1, 2, 3), (2, 3, 4))")); } @Test void testIntersectFunction_WithDateSequences() { - assertEquals("distinct-values((xs:date('2018-01-01Z'),xs:date('2020-01-01Z'))[.= (xs:date('2018-01-01Z'),xs:date('2022-01-02Z'))])", + assertEquals("distinct-values(for $L1 in (xs:date('2018-01-01Z'),xs:date('2020-01-01Z')) return if (some $L2 in (xs:date('2018-01-01Z'),xs:date('2022-01-02Z')) satisfies $L1 = $L2) then $L1 else ())", test("ND-Root", "value-intersect((2018-01-01Z, 2020-01-01Z), (2018-01-01Z, 2022-01-02Z))")); } @Test void testIntersectFunction_WithTimeSequences() { - assertEquals("distinct-values((xs:time('12:00:00Z'),xs:time('13:00:00Z'))[.= (xs:time('12:00:00Z'),xs:time('14:00:00Z'))])", + assertEquals("distinct-values(for $L1 in (xs:time('12:00:00Z'),xs:time('13:00:00Z')) return if (some $L2 in (xs:time('12:00:00Z'),xs:time('14:00:00Z')) satisfies $L1 = $L2) then $L1 else ())", test("ND-Root", "value-intersect((12:00:00Z, 13:00:00Z), (12:00:00Z, 14:00:00Z))")); } + @Test + void testIntersectFunction_WithDurationSequences() { + assertEquals("distinct-values(for $L1 in (xs:dayTimeDuration('P7D'),xs:dayTimeDuration('P2D')) return if (some $L2 in (xs:dayTimeDuration('P2D'),xs:dayTimeDuration('P5D')) satisfies $L1 = $L2) then $L1 else ())", + test("ND-Root", "value-intersect((P1W, P2D), (P2D, P5D))")); + } + @Test void testIntersectFunction_WithBooleanSequences() { - assertEquals("distinct-values((true(),false())[.= (false(),false())])", + assertEquals("distinct-values(for $L1 in (true(),false()) return if (some $L2 in (false(),false()) satisfies $L1 = $L2) then $L1 else ())", test("ND-Root", "value-intersect((TRUE, FALSE), (FALSE, NEVER))")); } @Test void testIntersectFunction_WithFieldReferences() { - assertEquals("distinct-values(PathNode/TextField[.= PathNode/TextField])", + assertEquals("distinct-values(for $L1 in PathNode/TextField/normalize-space(text()) return if (some $L2 in PathNode/TextField/normalize-space(text()) satisfies $L1 = $L2) then $L1 else ())", test("ND-Root", "value-intersect(BT-00-Text, BT-00-Text)")); } @@ -1308,40 +1368,76 @@ void testIntersectFunction_WithTypeMismatch() { @Test void testExceptFunction_WithStringSequences() { - assertEquals("distinct-values(('one','two')[not(. = ('two','three','four'))])", + assertEquals("distinct-values(for $L1 in ('one','two') return if (every $L2 in ('two','three','four') satisfies $L1 != $L2) then $L1 else ())", test("ND-Root", "value-except(('one', 'two'), ('two', 'three', 'four'))")); } @Test void testExceptFunction_WithNumberSequences() { - assertEquals("distinct-values((1,2,3)[not(. = (2,3,4))])", + assertEquals("distinct-values(for $L1 in (1,2,3) return if (every $L2 in (2,3,4) satisfies $L1 != $L2) then $L1 else ())", test("ND-Root", "value-except((1, 2, 3), (2, 3, 4))")); } @Test void testExceptFunction_WithDateSequences() { - assertEquals("distinct-values((xs:date('2018-01-01Z'),xs:date('2020-01-01Z'))[not(. = (xs:date('2018-01-01Z'),xs:date('2022-01-02Z')))])", + assertEquals("distinct-values(for $L1 in (xs:date('2018-01-01Z'),xs:date('2020-01-01Z')) return if (every $L2 in (xs:date('2018-01-01Z'),xs:date('2022-01-02Z')) satisfies $L1 != $L2) then $L1 else ())", test("ND-Root", "value-except((2018-01-01Z, 2020-01-01Z), (2018-01-01Z, 2022-01-02Z))")); } @Test void testExceptFunction_WithTimeSequences() { - assertEquals("distinct-values((xs:time('12:00:00Z'),xs:time('13:00:00Z'))[not(. = (xs:time('12:00:00Z'),xs:time('14:00:00Z')))])", + assertEquals("distinct-values(for $L1 in (xs:time('12:00:00Z'),xs:time('13:00:00Z')) return if (every $L2 in (xs:time('12:00:00Z'),xs:time('14:00:00Z')) satisfies $L1 != $L2) then $L1 else ())", test("ND-Root", "value-except((12:00:00Z, 13:00:00Z), (12:00:00Z, 14:00:00Z))")); } + @Test + void testExceptFunction_WithDurationSequences() { + assertEquals("distinct-values(for $L1 in (xs:dayTimeDuration('P7D'),xs:dayTimeDuration('P2D')) return if (every $L2 in (xs:dayTimeDuration('P2D'),xs:dayTimeDuration('P5D')) satisfies $L1 != $L2) then $L1 else ())", + test("ND-Root", "value-except((P1W, P2D), (P2D, P5D))")); + } + @Test void testExceptFunction_WithBooleanSequences() { - assertEquals("distinct-values((true(),false())[not(. = (false(),false()))])", + assertEquals("distinct-values(for $L1 in (true(),false()) return if (every $L2 in (false(),false()) satisfies $L1 != $L2) then $L1 else ())", test("ND-Root", "value-except((TRUE, FALSE), (FALSE, NEVER))")); } @Test - void testExceptFunction_WithFieldReferences() { - assertEquals("distinct-values(PathNode/TextField[not(. = PathNode/TextField)])", + void testExceptFunction_WithTextFieldReferences() { + assertEquals("distinct-values(for $L1 in PathNode/TextField/normalize-space(text()) return if (every $L2 in PathNode/TextField/normalize-space(text()) satisfies $L1 != $L2) then $L1 else ())", test("ND-Root", "value-except(BT-00-Text, BT-00-Text)")); } + @Test + void testExceptFunction_WithNumberFieldReferences() { + assertEquals("distinct-values(for $L1 in PathNode/IntegerField/number() return if (every $L2 in PathNode/IntegerField/number() satisfies $L1 != $L2) then $L1 else ())", + test("ND-Root", "value-except(BT-00-Integer, BT-00-Integer)")); + } + + @Test + void testExceptFunction_WithBooleanFieldReferences() { + assertEquals("distinct-values(for $L1 in PathNode/IndicatorField return if (every $L2 in PathNode/IndicatorField satisfies $L1 != $L2) then $L1 else ())", + test("ND-Root", "value-except(BT-00-Indicator, BT-00-Indicator)")); + } + + @Test + void testExceptFunction_WithDateFieldReferences() { + assertEquals("distinct-values(for $L1 in PathNode/StartDateField/xs:date(text()) return if (every $L2 in PathNode/StartDateField/xs:date(text()) satisfies $L1 != $L2) then $L1 else ())", + test("ND-Root", "value-except(BT-00-StartDate, BT-00-StartDate)")); + } + + @Test + void testExceptFunction_WithTimeFieldReferences() { + assertEquals("distinct-values(for $L1 in PathNode/StartTimeField/xs:time(text()) return if (every $L2 in PathNode/StartTimeField/xs:time(text()) satisfies $L1 != $L2) then $L1 else ())", + test("ND-Root", "value-except(BT-00-StartTime, BT-00-StartTime)")); + } + + @Test + void testExceptFunction_WithDurationFieldReferences() { + assertEquals("distinct-values(for $L1 in (for $F in PathNode/MeasureField return (if ($F/@unitCode='WEEK') then xs:dayTimeDuration(concat('P', $F/number() * 7, 'D')) else if ($F/@unitCode='DAY') then xs:dayTimeDuration(concat('P', $F/number(), 'D')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) return if (every $L2 in (for $F in PathNode/MeasureField return (if ($F/@unitCode='WEEK') then xs:dayTimeDuration(concat('P', $F/number() * 7, 'D')) else if ($F/@unitCode='DAY') then xs:dayTimeDuration(concat('P', $F/number(), 'D')) else if ($F/@unitCode='YEAR') then xs:yearMonthDuration(concat('P', $F/number(), 'Y')) else if ($F/@unitCode='MONTH') then xs:yearMonthDuration(concat('P', $F/number(), 'M')) else ())) satisfies $L1 != $L2) then $L1 else ())", + test("ND-Root", "value-except(BT-00-Measure, BT-00-Measure)")); + } + @Test void testExceptFunction_WithTypeMismatch() { assertThrows(ParseCancellationException.class, @@ -1388,7 +1484,7 @@ void testSequenceEqualFunction_WithDurationSequences() { @Test void testSequenceEqualFunction_WithFieldReferences() { - assertEquals("deep-equal(sort(PathNode/TextField), sort(PathNode/TextField))", + assertEquals("deep-equal(sort(PathNode/TextField/normalize-space(text())), sort(PathNode/TextField/normalize-space(text())))", test("ND-Root", "sequence-equal(BT-00-Text, BT-00-Text)")); } diff --git a/src/test/java/eu/europa/ted/efx/EfxTemplateTranslatorTest.java b/src/test/java/eu/europa/ted/efx/EfxTemplateTranslatorTest.java index 9e4d2653..8f23e902 100644 --- a/src/test/java/eu/europa/ted/efx/EfxTemplateTranslatorTest.java +++ b/src/test/java/eu/europa/ted/efx/EfxTemplateTranslatorTest.java @@ -4,15 +4,19 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import org.antlr.v4.runtime.misc.ParseCancellationException; import org.junit.jupiter.api.Test; + +import eu.europa.ted.efx.interfaces.TranslatorOptions; import eu.europa.ted.efx.mock.DependencyFactoryMock; +import eu.europa.ted.efx.model.DecimalFormat; class EfxTemplateTranslatorTest { final private String SDK_VERSION = "eforms-sdk-1.0"; + final private TranslatorOptions TRANSLATOR_OPTIONS = new EfxTranslatorOptions(DecimalFormat.EFX_DEFAULT); private String translate(final String template) { try { return EfxTranslator.translateTemplate(DependencyFactoryMock.INSTANCE, - SDK_VERSION, template + "\n"); + SDK_VERSION, template + "\n", TRANSLATOR_OPTIONS); } catch (InstantiationException e) { throw new RuntimeException(e); } @@ -26,7 +30,7 @@ private String lines(String... lines) { @Test void testTemplateLineNoIdent() { - assertEquals("declare block01 = { text('foo') }\nfor-each(/*/PathNode/TextField).call(block01)", + assertEquals("let block01() -> { text('foo') }\nfor-each(/*/PathNode/TextField).call(block01())", translate("{BT-00-Text} foo")); } @@ -35,12 +39,12 @@ void testTemplateLineNoIdent() { */ @Test void testTemplateLineOutline_Autogenerated() { - assertEquals(lines("declare block01 = { outline('1') text('foo')", - "for-each(../..).call(block0101) }", - "declare block0101 = { outline('1.1') text('bar')", - "for-each(PathNode/NumberField).call(block010101) }", - "declare block010101 = { text('foo') }", - "for-each(/*/PathNode/TextField).call(block01)"), // + assertEquals(lines("let block01() -> { #1: text('foo')", + "for-each(../..).call(block0101()) }", + "let block0101() -> { #1.1: text('bar')", + "for-each(PathNode/NumberField).call(block010101()) }", + "let block010101() -> { text('foo') }", + "for-each(/*/PathNode/TextField).call(block01())"), // translate(lines("{BT-00-Text} foo", "\t{ND-Root} bar", "\t\t{BT-00-Number} foo"))); } @@ -50,12 +54,12 @@ void testTemplateLineOutline_Autogenerated() { */ @Test void testTemplateLineOutline_Explicit() { - assertEquals(lines("declare block01 = { outline('2') text('foo')", - "for-each(../..).call(block0101) }", - "declare block0101 = { outline('2.3') text('bar')", - "for-each(PathNode/NumberField).call(block010101) }", - "declare block010101 = { text('foo') }", - "for-each(/*/PathNode/TextField).call(block01)"), // + assertEquals(lines("let block01() -> { #2: text('foo')", + "for-each(../..).call(block0101()) }", + "let block0101() -> { #2.3: text('bar')", + "for-each(PathNode/NumberField).call(block010101()) }", + "let block010101() -> { text('foo') }", + "for-each(/*/PathNode/TextField).call(block01())"), // translate(lines("2{BT-00-Text} foo", "\t3{ND-Root} bar", "\t\t{BT-00-Number} foo"))); } @@ -66,12 +70,12 @@ void testTemplateLineOutline_Explicit() { */ @Test void testTemplateLineOutline_Mixed() { - assertEquals(lines("declare block01 = { outline('2') text('foo')", - "for-each(../..).call(block0101) }", - "declare block0101 = { outline('2.1') text('bar')", - "for-each(PathNode/NumberField).call(block010101) }", - "declare block010101 = { text('foo') }", - "for-each(/*/PathNode/TextField).call(block01)"), // + assertEquals(lines("let block01() -> { #2: text('foo')", + "for-each(../..).call(block0101()) }", + "let block0101() -> { #2.1: text('bar')", + "for-each(PathNode/NumberField).call(block010101()) }", + "let block010101() -> { text('foo') }", + "for-each(/*/PathNode/TextField).call(block01())"), // translate(lines("2{BT-00-Text} foo", "\t{ND-Root} bar", "\t\t{BT-00-Number} foo"))); } @@ -81,12 +85,12 @@ void testTemplateLineOutline_Mixed() { */ @Test void testTemplateLineOutline_Suppressed() { - assertEquals(lines("declare block01 = { outline('2') text('foo')", - "for-each(../..).call(block0101) }", - "declare block0101 = { text('bar')", - "for-each(PathNode/NumberField).call(block010101) }", - "declare block010101 = { text('foo') }", - "for-each(/*/PathNode/TextField).call(block01)"), // + assertEquals(lines("let block01() -> { #2: text('foo')", + "for-each(../..).call(block0101()) }", + "let block0101() -> { text('bar')", + "for-each(PathNode/NumberField).call(block010101()) }", + "let block010101() -> { text('foo') }", + "for-each(/*/PathNode/TextField).call(block01())"), // translate(lines("2{BT-00-Text} foo", "\t0{ND-Root} bar", "\t\t{BT-00-Number} foo"))); } @@ -97,12 +101,12 @@ void testTemplateLineOutline_Suppressed() { @Test void testTemplateLineOutline_SuppressedAtParent() { // Outline is ignored if the line has no children - assertEquals(lines("declare block01 = { text('foo')", - "for-each(../..).call(block0101) }", - "declare block0101 = { outline('1') text('bar')", - "for-each(PathNode/NumberField).call(block010101) }", - "declare block010101 = { text('foo') }", - "for-each(/*/PathNode/TextField).call(block01)"), // + assertEquals(lines("let block01() -> { text('foo')", + "for-each(../..).call(block0101()) }", + "let block0101() -> { #1: text('bar')", + "for-each(PathNode/NumberField).call(block010101()) }", + "let block010101() -> { text('foo') }", + "for-each(/*/PathNode/TextField).call(block01())"), // translate(lines("0{BT-00-Text} foo", "\t{ND-Root} bar", "\t\t{BT-00-Number} foo"))); } @@ -114,18 +118,18 @@ void testTemplateLineFirstIndented() { @Test void testTemplateLineIdentTab() { assertEquals( - lines("declare block01 = { outline('1') text('foo')", "for-each(.).call(block0101) }", // - "declare block0101 = { text('bar') }", // - "for-each(/*/PathNode/TextField).call(block01)"),// + lines("let block01() -> { #1: text('foo')", "for-each(.).call(block0101()) }", // + "let block0101() -> { text('bar') }", // + "for-each(/*/PathNode/TextField).call(block01())"),// translate(lines("{BT-00-Text} foo", "\t{BT-00-Text} bar"))); } @Test void testTemplateLineIdentSpaces() { assertEquals( - lines("declare block01 = { outline('1') text('foo')", "for-each(.).call(block0101) }", // - "declare block0101 = { text('bar') }", // - "for-each(/*/PathNode/TextField).call(block01)"),// + lines("let block01() -> { #1: text('foo')", "for-each(.).call(block0101()) }", // + "let block0101() -> { text('bar') }", // + "for-each(/*/PathNode/TextField).call(block01())"),// translate(lines("{BT-00-Text} foo", " {BT-00-Text} bar"))); } @@ -144,11 +148,11 @@ void testTemplateLineIdentMixedSpaceThenTab() { @Test void testTemplateLineIdentLower() { assertEquals( - lines("declare block01 = { outline('1') text('foo')", "for-each(.).call(block0101) }", - "declare block0101 = { text('bar') }", - "declare block02 = { text('code') }", - "for-each(/*/PathNode/TextField).call(block01)", - "for-each(/*/PathNode/CodeField).call(block02)"), + lines("let block01() -> { #1: text('foo')", "for-each(.).call(block0101()) }", + "let block0101() -> { text('bar') }", + "let block02() -> { text('code') }", + "for-each(/*/PathNode/TextField).call(block01())", + "for-each(/*/PathNode/CodeField).call(block02())"), translate(lines("{BT-00-Text} foo", "\t{BT-00-Text} bar", "{BT-00-Code} code"))); } @@ -161,10 +165,10 @@ void testTemplateLineIdentUnexpected() { @Test void testTemplateLine_VariableScope() { assertEquals( - lines("declare block01 = { outline('1') eval(for $x in . return $x)", // - "for-each(.).call(block0101) }", // - "declare block0101 = { eval(for $x in . return $x) }", // - "for-each(/*/PathNode/TextField).call(block01)"),// + lines("let block01() -> { #1: eval(for $x in ./normalize-space(text()) return $x)", // + "for-each(.).call(block0101()) }", // + "let block0101() -> { eval(for $x in ./normalize-space(text()) return $x) }", // + "for-each(/*/PathNode/TextField).call(block01())"),// translate(lines("{BT-00-Text} ${for text:$x in BT-00-Text return $x}", " {BT-00-Text} ${for text:$x in BT-00-Text return $x}"))); } @@ -175,28 +179,28 @@ void testTemplateLine_VariableScope() { @Test void testStandardLabelReference() { assertEquals( - "declare block01 = { label(concat('field', '|', 'name', '|', 'BT-00-Text')) }\nfor-each(/*/PathNode/TextField).call(block01)", + "let block01() -> { label(concat('field', '|', 'name', '|', 'BT-00-Text')) }\nfor-each(/*/PathNode/TextField).call(block01())", translate("{BT-00-Text} #{field|name|BT-00-Text}")); } @Test void testStandardLabelReference_UsingLabelTypeAsAssetId() { assertEquals( - "declare block01 = { label(concat('auxiliary', '|', 'text', '|', 'value')) }\nfor-each(/*/PathNode/TextField).call(block01)", + "let block01() -> { label(concat('auxiliary', '|', 'text', '|', 'value')) }\nfor-each(/*/PathNode/TextField).call(block01())", translate("{BT-00-Text} #{auxiliary|text|value}")); } @Test void testShorthandBtLabelReference() { assertEquals( - "declare block01 = { label(concat('business-term', '|', 'name', '|', 'BT-00')) }\nfor-each(/*/PathNode/TextField).call(block01)", + "let block01() -> { label(concat('business-term', '|', 'name', '|', 'BT-00')) }\nfor-each(/*/PathNode/TextField).call(block01())", translate("{BT-00-Text} #{name|BT-00}")); } @Test void testShorthandFieldLabelReference() { assertEquals( - "declare block01 = { label(concat('field', '|', 'name', '|', 'BT-00-Text')) }\nfor-each(/*/PathNode/TextField).call(block01)", + "let block01() -> { label(concat('field', '|', 'name', '|', 'BT-00-Text')) }\nfor-each(/*/PathNode/TextField).call(block01())", translate("{BT-00-Text} #{name|BT-00-Text}")); } @@ -208,42 +212,42 @@ void testShorthandBtLabelReference_MissingLabelType() { @Test void testShorthandIndirectLabelReferenceForIndicator() { assertEquals( - "declare block01 = { label(for $item in ../IndicatorField return concat('indicator', '|', 'when', '-', $item, '|', 'BT-00-Indicator')) }\nfor-each(/*/PathNode/TextField).call(block01)", + "let block01() -> { label(for $item in ../IndicatorField return concat('indicator', '|', 'when', '-', $item, '|', 'BT-00-Indicator')) }\nfor-each(/*/PathNode/TextField).call(block01())", translate("{BT-00-Text} #{BT-00-Indicator}")); } @Test void testShorthandIndirectLabelReferenceForCode() { assertEquals( - "declare block01 = { label(for $item in ../CodeField return concat('code', '|', 'name', '|', 'main-activity', '.', $item)) }\nfor-each(/*/PathNode/TextField).call(block01)", + "let block01() -> { label(for $item in ../CodeField return concat('code', '|', 'name', '|', 'main-activity', '.', $item)) }\nfor-each(/*/PathNode/TextField).call(block01())", translate("{BT-00-Text} #{BT-00-Code}")); } @Test void testShorthandIndirectLabelReferenceForInternalCode() { assertEquals( - "declare block01 = { label(for $item in ../InternalCodeField return concat('code', '|', 'name', '|', 'main-activity', '.', $item)) }\nfor-each(/*/PathNode/TextField).call(block01)", + "let block01() -> { label(for $item in ../InternalCodeField return concat('code', '|', 'name', '|', 'main-activity', '.', $item)) }\nfor-each(/*/PathNode/TextField).call(block01())", translate("{BT-00-Text} #{BT-00-Internal-Code}")); } @Test void testShorthandIndirectLabelReferenceForCodeAttribute() { assertEquals( - "declare block01 = { label(for $item in ../CodeField/@attribute return concat('code', '|', 'name', '|', 'main-activity', '.', $item)) }\nfor-each(/*/PathNode/TextField).call(block01)", + "let block01() -> { label(for $item in ../CodeField/@attribute return concat('code', '|', 'name', '|', 'main-activity', '.', $item)) }\nfor-each(/*/PathNode/TextField).call(block01())", translate("{BT-00-Text} #{BT-00-CodeAttribute}")); } @Test void testShorthandIndirectLabelReferenceForCodeAttribute_WithSameAttributeInContext() { assertEquals( - "declare block01 = { label(for $item in ../@attribute return concat('code', '|', 'name', '|', 'main-activity', '.', $item)) }\nfor-each(/*/PathNode/CodeField/@attribute).call(block01)", + "let block01() -> { label(for $item in ../@attribute return concat('code', '|', 'name', '|', 'main-activity', '.', $item)) }\nfor-each(/*/PathNode/CodeField/@attribute).call(block01())", translate("{BT-00-CodeAttribute} #{BT-00-CodeAttribute}")); } @Test void testShorthandIndirectLabelReferenceForCodeAttribute_WithSameElementInContext() { assertEquals( - "declare block01 = { label(for $item in ./@attribute return concat('code', '|', 'name', '|', 'main-activity', '.', $item)) }\nfor-each(/*/PathNode/CodeField).call(block01)", + "let block01() -> { label(for $item in ./@attribute return concat('code', '|', 'name', '|', 'main-activity', '.', $item)) }\nfor-each(/*/PathNode/CodeField).call(block01())", translate("{BT-00-Code} #{BT-00-CodeAttribute}")); } @@ -260,14 +264,14 @@ void testShorthandIndirectLabelReferenceForAttribute() { @Test void testShorthandLabelReferenceFromContext_WithValueLabelTypeAndIndicatorField() { assertEquals( - "declare block01 = { label(concat('field', '|', 'name', '|', 'BT-00-Indicator')) }\nfor-each(/*/PathNode/IndicatorField).call(block01)", + "let block01() -> { label(concat('field', '|', 'name', '|', 'BT-00-Indicator')) }\nfor-each(/*/PathNode/IndicatorField).call(block01())", translate("{BT-00-Indicator} #{name}")); } @Test void testShorthandLabelReferenceFromContext_WithValueLabelTypeAndCodeField() { assertEquals( - "declare block01 = { label(concat('field', '|', 'name', '|', 'BT-00-Code')) }\nfor-each(/*/PathNode/CodeField).call(block01)", + "let block01() -> { label(concat('field', '|', 'name', '|', 'BT-00-Code')) }\nfor-each(/*/PathNode/CodeField).call(block01())", translate("{BT-00-Code} #{name}")); } @@ -279,7 +283,7 @@ void testShorthandLabelReferenceFromContext_WithValueLabelTypeAndTextField() { @Test void testShorthandLabelReferenceFromContext_WithOtherLabelType() { assertEquals( - "declare block01 = { label(concat('field', '|', 'name', '|', 'BT-00-Text')) }\nfor-each(/*/PathNode/TextField).call(block01)", + "let block01() -> { label(concat('field', '|', 'name', '|', 'BT-00-Text')) }\nfor-each(/*/PathNode/TextField).call(block01())", translate("{BT-00-Text} #{name}")); } @@ -291,14 +295,14 @@ void testShorthandLabelReferenceFromContext_WithUnknownLabelType() { @Test void testShorthandLabelReferenceFromContext_WithNodeContext() { assertEquals( - "declare block01 = { label(concat('node', '|', 'name', '|', 'ND-Root')) }\nfor-each(/*).call(block01)", + "let block01() -> { label(concat('node', '|', 'name', '|', 'ND-Root')) }\nfor-each(/*).call(block01())", translate("{ND-Root} #{name}")); } @Test void testShorthandIndirectLabelReferenceFromContextField() { assertEquals( - "declare block01 = { label(for $item in . return concat('code', '|', 'name', '|', 'main-activity', '.', $item)) }\nfor-each(/*/PathNode/CodeField).call(block01)", + "let block01() -> { label(for $item in . return concat('code', '|', 'name', '|', 'main-activity', '.', $item)) }\nfor-each(/*/PathNode/CodeField).call(block01())", translate("{BT-00-Code} #value")); } @@ -312,14 +316,14 @@ void testShorthandIndirectLabelReferenceFromContextField_WithNodeContext() { @Test void testShorthandFieldValueReferenceFromContextField() { - assertEquals("declare block01 = { eval(.) }\nfor-each(/*/PathNode/CodeField).call(block01)", + assertEquals("let block01() -> { eval(.) }\nfor-each(/*/PathNode/CodeField).call(block01())", translate("{BT-00-Code} $value")); } @Test void testShorthandFieldValueReferenceFromContextField_WithText() { assertEquals( - "declare block01 = { text('blah ')label(for $item in . return concat('code', '|', 'name', '|', 'main-activity', '.', $item))text(' ')text('blah ')eval(.)text(' ')text('blah') }\nfor-each(/*/PathNode/CodeField).call(block01)", + "let block01() -> { text('blah ')label(for $item in . return concat('code', '|', 'name', '|', 'main-activity', '.', $item))text(' ')text('blah ')eval(.)text(' ')text('blah') }\nfor-each(/*/PathNode/CodeField).call(block01())", translate("{BT-00-Code} blah #value blah $value blah")); } @@ -334,14 +338,14 @@ void testShorthandFieldValueReferenceFromContextField_WithNodeContext() { @Test void testNestedExpression() { assertEquals( - "declare block01 = { label(concat('field', '|', 'name', '|', ./normalize-space(text()))) }\nfor-each(/*/PathNode/TextField).call(block01)", + "let block01() -> { label(concat('field', '|', 'name', '|', ./normalize-space(text()))) }\nfor-each(/*/PathNode/TextField).call(block01())", translate("{BT-00-Text} #{field|name|${BT-00-Text}}")); } @Test void testEndOfLineComments() { assertEquals( - "declare block01 = { label(concat('field', '|', 'name', '|', 'BT-00-Text'))text(' ')text('blah blah') }\nfor-each(/*).call(block01)", + "let block01() -> { label(concat('field', '|', 'name', '|', 'BT-00-Text'))text(' ')text('blah blah') }\nfor-each(/*).call(block01())", translate("{ND-Root} #{name|BT-00-Text} blah blah // comment blah blah")); } } diff --git a/src/test/java/eu/europa/ted/efx/mock/DependencyFactoryMock.java b/src/test/java/eu/europa/ted/efx/mock/DependencyFactoryMock.java index 036ac8b2..48bb43dc 100644 --- a/src/test/java/eu/europa/ted/efx/mock/DependencyFactoryMock.java +++ b/src/test/java/eu/europa/ted/efx/mock/DependencyFactoryMock.java @@ -1,11 +1,14 @@ package eu.europa.ted.efx.mock; import org.antlr.v4.runtime.BaseErrorListener; + +import eu.europa.ted.efx.EfxTranslatorOptions; import eu.europa.ted.efx.exceptions.ThrowingErrorListener; import eu.europa.ted.efx.interfaces.MarkupGenerator; import eu.europa.ted.efx.interfaces.ScriptGenerator; import eu.europa.ted.efx.interfaces.SymbolResolver; import eu.europa.ted.efx.interfaces.TranslatorDependencyFactory; +import eu.europa.ted.efx.interfaces.TranslatorOptions; import eu.europa.ted.efx.xpath.XPathScriptGenerator; /** @@ -27,14 +30,24 @@ public SymbolResolver createSymbolResolver(String sdkVersion) { @Override public ScriptGenerator createScriptGenerator(String sdkVersion) { + return this.createScriptGenerator(sdkVersion, EfxTranslatorOptions.DEFAULT); + } + + @Override + public ScriptGenerator createScriptGenerator(String sdkVersion, TranslatorOptions options) { if (scriptGenerator == null) { - this.scriptGenerator = new XPathScriptGenerator(); + this.scriptGenerator = new XPathScriptGenerator(options); } return this.scriptGenerator; } @Override public MarkupGenerator createMarkupGenerator(String sdkVersion) { + return this.createMarkupGenerator(sdkVersion, EfxTranslatorOptions.DEFAULT); + } + + @Override + public MarkupGenerator createMarkupGenerator(String sdkVersion, TranslatorOptions options) { if (this.markupGenerator == null) { this.markupGenerator = new MarkupGeneratorMock(); } diff --git a/src/test/java/eu/europa/ted/efx/mock/MarkupGeneratorMock.java b/src/test/java/eu/europa/ted/efx/mock/MarkupGeneratorMock.java index c79a3fc5..830e906e 100644 --- a/src/test/java/eu/europa/ted/efx/mock/MarkupGeneratorMock.java +++ b/src/test/java/eu/europa/ted/efx/mock/MarkupGeneratorMock.java @@ -1,8 +1,13 @@ package eu.europa.ted.efx.mock; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; + import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; + import eu.europa.ted.efx.interfaces.MarkupGenerator; import eu.europa.ted.efx.model.Expression; import eu.europa.ted.efx.model.Expression.PathExpression; @@ -33,15 +38,27 @@ public Markup renderFreeText(String freeText) { @Override public Markup composeFragmentDefinition(String name, String number, Markup content) { + return this.composeFragmentDefinition(name, number, content, new LinkedHashSet<>()); + } + + public Markup composeFragmentDefinition(String name, String number, Markup content, Set parameters) { if (StringUtils.isBlank(number)) { - return new Markup(String.format("declare %s = { %s }", name, content.script)); + return new Markup(String.format("let %s(%s) -> { %s }", name, + parameters.stream().collect(Collectors.joining(", ")), content.script)); } - return new Markup(String.format("declare %s = { outline('%s') %s }", name, number, content.script)); + return new Markup(String.format("let %s(%s) -> { #%s: %s }", name, + parameters.stream().collect(Collectors.joining(", ")), number, content.script)); } @Override public Markup renderFragmentInvocation(String name, PathExpression context) { - return new Markup(String.format("for-each(%s).call(%s)", context.script, name)); + return this.renderFragmentInvocation(name, context, new LinkedHashSet<>()); + } + + public Markup renderFragmentInvocation(String name, PathExpression context, + Set> variables) { + return new Markup(String.format("for-each(%s).call(%s(%s))", context.script, name, variables.stream() + .map(v -> String.format("%s:%s", v.getLeft(), v.getRight())).collect(Collectors.joining(", ")))); } @Override