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