diff --git a/pom.xml b/pom.xml
index 5364068..6c69a6e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -58,6 +58,7 @@
2.18.3
2.18.3
5.7.2
+ 11.3
2.0.13
@@ -127,6 +128,11 @@
freemarker
${version.freemarker}
+
+ net.sf.saxon
+ Saxon-HE
+ ${version.saxon-he}
+
@@ -147,6 +153,11 @@
junit-jupiter-params
test
+
+ net.sf.saxon
+ Saxon-HE
+ test
+
org.antlr
antlr4-runtime
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 430831d..8978d8a 100644
--- a/src/main/java/eu/europa/ted/eforms/sdk/SdkSymbolResolver.java
+++ b/src/main/java/eu/europa/ted/eforms/sdk/SdkSymbolResolver.java
@@ -442,6 +442,11 @@ private boolean isFieldRepeatableFromContext(final SdkField sdkField, final SdkF
return !sdkField.equals(context);
}
+ // Multilingual fields have multiple XML elements (one per language)
+ if (FieldTypes.TEXT_MULTILINGUAL.getName().equals(sdkField.getType())) {
+ return true;
+ }
+
// Use cached ancestry from node
List contextAncestry = context != null
? context.getParentNode().getAncestry()
@@ -480,6 +485,11 @@ private boolean isFieldRepeatableFromContext(final SdkField sdkField, final SdkN
return true;
}
+ // Multilingual fields have multiple XML elements (one per language)
+ if (FieldTypes.TEXT_MULTILINGUAL.getName().equals(sdkField.getType())) {
+ return true;
+ }
+
// Use cached ancestry from node
List contextAncestry = context != null
? context.getAncestry()
diff --git a/src/main/java/eu/europa/ted/efx/autocomplete/EfxLinkedProperty.java b/src/main/java/eu/europa/ted/efx/autocomplete/EfxLinkedProperty.java
index 63d9fd0..cb39ca8 100644
--- a/src/main/java/eu/europa/ted/efx/autocomplete/EfxLinkedProperty.java
+++ b/src/main/java/eu/europa/ted/efx/autocomplete/EfxLinkedProperty.java
@@ -24,7 +24,11 @@ public enum EfxLinkedProperty {
IS_MASKED(":isMasked", INDICATOR),
// Raw value (textExpression rule)
- RAW_VALUE(":rawValue", TEXT);
+ RAW_VALUE(":rawValue", TEXT),
+
+ // Preferred language properties (stringExpression rule, template-only)
+ PREFERRED_LANGUAGE(":preferredLanguage", TEXT),
+ PREFERRED_LANGUAGE_TEXT(":preferredLanguageText", TEXT);
private final String label;
private final EfxDataType dataType;
diff --git a/src/main/java/eu/europa/ted/efx/exceptions/TypeMismatchException.java b/src/main/java/eu/europa/ted/efx/exceptions/TypeMismatchException.java
index 38680fc..b9b8069 100644
--- a/src/main/java/eu/europa/ted/efx/exceptions/TypeMismatchException.java
+++ b/src/main/java/eu/europa/ted/efx/exceptions/TypeMismatchException.java
@@ -29,6 +29,7 @@ public enum ErrorCode {
CANNOT_COMPARE,
INCOMPATIBLE_OPERANDS,
FIELD_MAY_REPEAT,
+ FIELD_IS_MULTILINGUAL,
NODE_CONTEXT_AS_VALUE,
IDENTIFIER_IS_SEQUENCE,
IDENTIFIER_IS_SCALAR,
@@ -39,6 +40,7 @@ public enum ErrorCode {
private static final String CANNOT_COMPARE = "Type mismatch. Cannot compare values of different types: %s and %s.";
private static final String INCOMPATIBLE_OPERANDS = "Type mismatch. Operator '%s' cannot be applied to %s and %s.";
private static final String FIELD_MAY_REPEAT = "Type mismatch. Field '%s' may return multiple values from context '%s', but is used as a scalar. Use a sequence expression or change the context.";
+ private static final String FIELD_IS_MULTILINGUAL = "Type mismatch. Field '%s' is multilingual and has multiple values (one per language), but is used as a scalar. Use :preferredLanguageText to select the preferred language, or an indexer [n] to select a specific element.";
private static final String NODE_CONTEXT_AS_VALUE = "Type mismatch. Context variable '$%s' refers to node '%s', but is used as a value. Only field context variables can be used in value expressions.";
private static final String IDENTIFIER_IS_SEQUENCE = "Type mismatch. Variable '$%s' is declared as a sequence, but is used as a scalar.";
private static final String IDENTIFIER_IS_SCALAR = "Type mismatch. Variable '$%s' is declared as a scalar, but is used where a sequence is expected. To use it as a single-element sequence, wrap it in square brackets: [$%s].";
@@ -90,6 +92,10 @@ public static TypeMismatchException fieldMayRepeat(ParserRuleContext ctx, String
contextSymbol != null ? contextSymbol : "root");
}
+ public static TypeMismatchException fieldIsMultilingual(ParserRuleContext ctx, String fieldId) {
+ return new TypeMismatchException(ErrorCode.FIELD_IS_MULTILINGUAL, ctx, FIELD_IS_MULTILINGUAL, fieldId);
+ }
+
public static TypeMismatchException nodeContextUsedAsValue(ParserRuleContext ctx, String variableName, String nodeId) {
return new TypeMismatchException(ErrorCode.NODE_CONTEXT_AS_VALUE, ctx, NODE_CONTEXT_AS_VALUE, variableName, nodeId);
}
diff --git a/src/main/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2.java b/src/main/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2.java
index db8510b..d6d504b 100644
--- a/src/main/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2.java
+++ b/src/main/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2.java
@@ -1435,7 +1435,7 @@ public void exitContextIteratorExpression(ContextIteratorExpressionContext ctx)
final String contextFieldId = getFieldId(ctx.fieldContext());
this.efxContext.declareContextVariable(variable.name,
new FieldContext(contextFieldId, this.symbols.getAbsolutePathOfField(contextFieldId),
- this.symbols.getRelativePathOfField(contextFieldId, this.efxContext.symbol())));
+ path));
} else if (ctx.nodeContext() != null) {
final String contextNodeId =
getNodeId(ctx.nodeContext());
@@ -1793,6 +1793,20 @@ public void exitAbsoluteFieldReference(AbsoluteFieldReferenceContext ctx) {
}
}
+ @Override
+ public void exitFieldContext(FieldContextContext ctx) {
+ if (ctx.indexer() != null) {
+ NumericExpression index = this.stack.pop(NumericExpression.class);
+ final TypedExpression top = this.stack.peekType();
+ if (!(top instanceof PathExpression)) {
+ throw TypeMismatchException.cannotConvert(PathExpression.class, top.getClass());
+ }
+ PathExpression fieldPath = (PathExpression) this.stack.pop(top.getClass());
+ this.stack.push(this.script.composeIndexer(fieldPath.asSequence(), index,
+ ScalarPath.fromEfxDataType.get(EfxTypeLattice.toPrimitive(fieldPath.getDataType()))));
+ }
+ }
+
@Override
public void enterAbsoluteNodeReference(AbsoluteNodeReferenceContext ctx) {
if (ctx.Slash() != null) {
@@ -1956,8 +1970,8 @@ private void resolveAndPushFieldReference(ParserRuleContext ctx, PathExpression
this.stack.push(result);
break;
case RESOLVE_SCALAR:
- if (this.isFieldRepeatableFromCurrentContext(fieldId)) {
- throw TypeMismatchException.fieldMayRepeat(ctx, fieldId, this.efxContext.symbol());
+ if (this.fieldMayReturnMultipleValues(fieldId)) {
+ throw this.fieldMayReturnMultipleValuesException(ctx, fieldId);
}
this.stack.push(result);
break;
@@ -1965,7 +1979,7 @@ private void resolveAndPushFieldReference(ParserRuleContext ctx, PathExpression
this.stack.push(result.asSequence());
break;
case RESOLVE_EITHER:
- if (this.isFieldRepeatableFromCurrentContext(fieldId)) {
+ if (this.fieldMayReturnMultipleValues(fieldId)) {
this.stack.push(result.asSequence());
} else {
this.stack.push(result);
@@ -1975,11 +1989,13 @@ private void resolveAndPushFieldReference(ParserRuleContext ctx, PathExpression
}
/**
- * Returns true if a field is repeatable from the current EFX context.
+ * Returns true if a field may return multiple values from the current EFX context.
+ * This includes fields that are repeatable (from SDK metadata or node hierarchy)
+ * and multilingual fields (which have multiple XML elements, one per language).
* A field is not considered repeatable if it IS the current context (e.g., inside a WITH block
* on this field, we're referencing the current element being iterated, not the whole sequence).
*/
- private boolean isFieldRepeatableFromCurrentContext(String fieldId) {
+ private boolean fieldMayReturnMultipleValues(String fieldId) {
if (!this.efxContext.isEmpty()
&& this.efxContext.peek() != null
&& this.efxContext.peek().isFieldContext()
@@ -1989,6 +2005,18 @@ private boolean isFieldRepeatableFromCurrentContext(String fieldId) {
return this.symbols.isFieldRepeatableFromContext(fieldId, this.getContextNodeId());
}
+ /**
+ * Creates the appropriate exception for a field that may return multiple values but is used
+ * as a scalar. Returns {@link TypeMismatchException#fieldIsMultilingual} for multilingual fields,
+ * or {@link TypeMismatchException#fieldMayRepeat} for structurally repeatable fields.
+ */
+ private TypeMismatchException fieldMayReturnMultipleValuesException(ParserRuleContext ctx, String fieldId) {
+ if (FieldTypes.TEXT_MULTILINGUAL.getName().equals(this.symbols.getTypeOfField(fieldId))) {
+ return TypeMismatchException.fieldIsMultilingual(ctx, fieldId);
+ }
+ return TypeMismatchException.fieldMayRepeat(ctx, fieldId, this.efxContext.symbol());
+ }
+
/**
* Gets the node ID of the current context for use with
* {@link SymbolResolver#isFieldRepeatableFromContext}.
@@ -2018,12 +2046,11 @@ private String getContextNodeId() {
*/
@Override
public void exitContextFieldSpecifier(ContextFieldSpecifierContext ctx) {
- this.stack.pop(PathExpression.class); // Discard the PathExpression placed in the stack for
- // the context field.
+ final PathExpression contextFieldPath = this.stack.pop(PathExpression.class);
final String contextFieldId = getFieldId(ctx.fieldContext());
this.efxContext
.push(new FieldContext(contextFieldId, this.symbols.getAbsolutePathOfField(contextFieldId),
- this.symbols.getRelativePathOfField(contextFieldId, this.efxContext.symbol())));
+ contextFieldPath));
}
@@ -2518,10 +2545,6 @@ public void exitEndsWithFunction(EndsWithFunctionContext ctx) {
@Override
public void exitFieldWasWithheldProperty(FieldWasWithheldPropertyContext ctx) {
final String fieldId = getFieldId(ctx.fieldMention());
- if (this.isFieldRepeatableFromCurrentContext(fieldId)) {
- throw TypeMismatchException.fieldMayRepeat(ctx, fieldId, this.efxContext.symbol());
- }
-
final String privacyCode = this.symbols.getPrivacyCodeOfField(fieldId);
if (privacyCode == null || privacyCode.isEmpty()) {
throw InvalidUsageException.fieldNotWithholdable(ctx, fieldId);
@@ -2533,10 +2556,6 @@ public void exitFieldWasWithheldProperty(FieldWasWithheldPropertyContext ctx) {
@Override
public void exitFieldIsWithheldProperty(FieldIsWithheldPropertyContext ctx) {
final String fieldId = getFieldId(ctx.fieldMention());
- if (this.isFieldRepeatableFromCurrentContext(fieldId)) {
- throw TypeMismatchException.fieldMayRepeat(ctx, fieldId, this.efxContext.symbol());
- }
-
final String privacyCode = this.symbols.getPrivacyCodeOfField(fieldId);
if (privacyCode == null || privacyCode.isEmpty()) {
throw InvalidUsageException.fieldNotWithholdable(ctx, fieldId);
@@ -2558,10 +2577,6 @@ public void exitFieldIsWithholdableProperty(FieldIsWithholdablePropertyContext c
@Override
public void exitFieldIsDisclosedProperty(FieldIsDisclosedPropertyContext ctx) {
final String fieldId = getFieldId(ctx.fieldMention());
- if (this.isFieldRepeatableFromCurrentContext(fieldId)) {
- throw TypeMismatchException.fieldMayRepeat(ctx, fieldId, this.efxContext.symbol());
- }
-
final String privacyCode = this.symbols.getPrivacyCodeOfField(fieldId);
if (privacyCode == null || privacyCode.isEmpty()) {
throw InvalidUsageException.fieldNotWithholdable(ctx, fieldId);
@@ -2572,16 +2587,12 @@ public void exitFieldIsDisclosedProperty(FieldIsDisclosedPropertyContext ctx) {
this.script.composeLogicalAnd(
this.composeWasWithheldCondition(fieldId, privacyCode),
this.script.composeLogicalNot(this.composeStillWithheldCondition(fieldId))),
- this.script.composeLogicalNot(this.composeIsMaskedCondition(ctx, fieldId))));
+ this.script.composeLogicalNot(this.composeIsMaskedCondition(fieldId))));
}
@Override
public void exitFieldIsMaskedProperty(FieldIsMaskedPropertyContext ctx) {
final String fieldId = getFieldId(ctx.fieldMention());
- if (this.isFieldRepeatableFromCurrentContext(fieldId)) {
- throw TypeMismatchException.fieldMayRepeat(ctx, fieldId, this.efxContext.symbol());
- }
-
final String privacyCode = this.symbols.getPrivacyCodeOfField(fieldId);
if (privacyCode == null || privacyCode.isEmpty()) {
throw InvalidUsageException.fieldNotWithholdable(ctx, fieldId);
@@ -2590,7 +2601,7 @@ public void exitFieldIsMaskedProperty(FieldIsMaskedPropertyContext ctx) {
// "isMasked" = was withheld AND field value equals the privacy mask
this.stack.push(this.script.composeLogicalAnd(
this.composeWasWithheldCondition(fieldId, privacyCode),
- this.composeIsMaskedCondition(ctx, fieldId)));
+ this.composeIsMaskedCondition(fieldId)));
}
@Override
@@ -2604,25 +2615,15 @@ public void exitFieldPrivacyCodeProperty(FieldPrivacyCodePropertyContext ctx) {
}
@Override
- public void exitFieldRawValueProperty(FieldRawValuePropertyContext ctx) {
+ public void exitRawValueReference(RawValueReferenceContext ctx) {
final TypedExpression top = this.stack.peekType();
if (!(top instanceof PathExpression)) {
throw TypeMismatchException.cannotConvert(PathExpression.class, top.getClass());
}
- if (top instanceof SequenceExpression) {
- final String fieldId = getFieldId(ctx.fieldReferenceWithVariableContextOverride()
- .reference.reference.reference);
- throw TypeMismatchException.fieldMayRepeat(ctx, fieldId, this.efxContext.symbol());
- }
- final PathExpression fieldPath = (PathExpression) this.stack.pop(top.getClass());
- this.stack.push(this.script.composeFieldRawValueReference(fieldPath));
- }
-
- @Override
- public void exitFieldRawValuePropertySequence(FieldRawValuePropertySequenceContext ctx) {
- final TypedExpression top = this.stack.peekType();
- if (!(top instanceof PathExpression)) {
- throw TypeMismatchException.cannotConvert(PathExpression.class, top.getClass());
+ if (top instanceof SequenceExpression
+ && this.currentCardinalityResolutionContext() == CardinalityResolutionContext.RESOLVE_SCALAR) {
+ final String fieldId = getFieldId(ctx.fieldContext());
+ throw this.fieldMayReturnMultipleValuesException(ctx, fieldId);
}
final PathExpression fieldPath = (PathExpression) this.stack.pop(top.getClass());
this.stack.push(this.script.composeFieldRawValueReference(fieldPath));
@@ -2660,14 +2661,10 @@ private BooleanExpression composeStillWithheldCondition(String fieldId) {
BooleanExpression.class);
}
- private BooleanExpression composeIsMaskedCondition(ParserRuleContext ctx, String fieldId) {
+ private BooleanExpression composeIsMaskedCondition(String fieldId) {
final String maskingValue = this.symbols.getPrivacyMask(fieldId);
final PathExpression fieldValue = this.script.composeFieldValueReference(this.symbols.getRelativePathOfField(fieldId, this.efxContext.symbol()));
- if (!(fieldValue instanceof ScalarExpression)) {
- throw TypeMismatchException.fieldMayRepeat(ctx, fieldId, this.efxContext.symbol());
- }
-
return this.script.composeComparisonOperation(
TypedExpression.from(fieldValue, ScalarExpression.class),
"==",
@@ -3165,6 +3162,16 @@ public void exitPreferredLanguageTextFunction(PreferredLanguageTextFunctionConte
throw InvalidUsageException.templateOnlyFunction(ctx, "preferred-language-text");
}
+ @Override
+ public void exitFieldPreferredLanguageProperty(FieldPreferredLanguagePropertyContext ctx) {
+ throw InvalidUsageException.templateOnlyFunction(ctx, ":preferredLanguage");
+ }
+
+ @Override
+ public void exitFieldPreferredLanguageTextProperty(FieldPreferredLanguageTextPropertyContext ctx) {
+ throw InvalidUsageException.templateOnlyFunction(ctx, ":preferredLanguageText");
+ }
+
@Override
public void exitDictionaryLookup(DictionaryLookupContext ctx) {
if (this.currentCardinalityResolutionContext() == CardinalityResolutionContext.RESOLVE_SCALAR) {
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 5dd4d45..2ac9d0d 100644
--- a/src/main/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2.java
+++ b/src/main/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2.java
@@ -42,7 +42,6 @@
import eu.europa.ted.efx.exceptions.InvalidIndentationException;
import eu.europa.ted.efx.interfaces.Argument;
import eu.europa.ted.efx.interfaces.EfxTemplateTranslator;
-import eu.europa.ted.efx.interfaces.IncludedFileResolver;
import eu.europa.ted.efx.interfaces.MarkupGenerator;
import eu.europa.ted.efx.interfaces.ScriptGenerator;
import eu.europa.ted.efx.interfaces.SymbolResolver;
@@ -60,10 +59,8 @@
import eu.europa.ted.efx.model.expressions.scalar.BooleanExpression;
import eu.europa.ted.efx.model.expressions.scalar.DateExpression;
import eu.europa.ted.efx.model.expressions.scalar.DurationExpression;
-import eu.europa.ted.efx.model.expressions.scalar.NodePath;
import eu.europa.ted.efx.model.expressions.scalar.NumericExpression;
import eu.europa.ted.efx.model.expressions.scalar.ScalarExpression;
-import eu.europa.ted.efx.model.expressions.scalar.ScalarPath;
import eu.europa.ted.efx.model.expressions.scalar.StringExpression;
import eu.europa.ted.efx.model.expressions.scalar.StringPath;
import eu.europa.ted.efx.model.expressions.scalar.TimeExpression;
@@ -82,7 +79,6 @@
import eu.europa.ted.efx.model.templates.TemplateInvocation;
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.Dictionary;
import eu.europa.ted.efx.model.variables.Function;
import eu.europa.ted.efx.model.variables.StrictArguments;
@@ -92,7 +88,7 @@
import eu.europa.ted.efx.model.variables.Template;
import eu.europa.ted.efx.model.variables.Variable;
import eu.europa.ted.efx.model.variables.Variables;
-import eu.europa.ted.efx.model.expressions.scalar.MultilingualStringPath;
+import eu.europa.ted.efx.model.expressions.sequence.MultilingualStringSequencePath;
import eu.europa.ted.efx.sdk2.EfxParser.*;
/**
@@ -978,12 +974,22 @@ public void exitFormatLongDateTimeFunction(FormatLongDateTimeFunctionContext ctx
@Override
public void exitPreferredLanguageFunction(PreferredLanguageFunctionContext ctx) {
- this.stack.push(this.script.getPreferredLanguage(this.stack.pop(MultilingualStringPath.class)));
+ this.stack.push(this.script.getPreferredLanguage(this.stack.pop(MultilingualStringSequencePath.class)));
}
@Override
public void exitPreferredLanguageTextFunction(PreferredLanguageTextFunctionContext ctx) {
- this.stack.push(this.script.getTextInPreferredLanguage(this.stack.pop(MultilingualStringPath.class)));
+ this.stack.push(this.script.getTextInPreferredLanguage(this.stack.pop(MultilingualStringSequencePath.class)));
+ }
+
+ @Override
+ public void exitFieldPreferredLanguageProperty(FieldPreferredLanguagePropertyContext ctx) {
+ this.stack.push(this.script.getPreferredLanguage(this.stack.pop(MultilingualStringSequencePath.class)));
+ }
+
+ @Override
+ public void exitFieldPreferredLanguageTextProperty(FieldPreferredLanguageTextPropertyContext ctx) {
+ this.stack.push(this.script.getTextInPreferredLanguage(this.stack.pop(MultilingualStringSequencePath.class)));
}
// #endregion Preferred language functions -------------------------------------
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 2fb7e01..e8a7afb 100644
--- a/src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java
+++ b/src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java
@@ -257,7 +257,7 @@ public BooleanExpression composeAnySatisfies(
public T composeConditionalExpression(BooleanExpression condition,
T whenTrue, T whenFalse, Class type) {
return Expression.instantiate(
- "(if " + condition.getScript() + " then " + whenTrue.getScript() + " else " + whenFalse.getScript() + ")",
+ "(if (" + condition.getScript() + ") then " + whenTrue.getScript() + " else " + whenFalse.getScript() + ")",
type);
}
diff --git a/src/test/java/eu/europa/ted/efx/EfxTestsBase.java b/src/test/java/eu/europa/ted/efx/EfxTestsBase.java
index f8bf9af..d9dcebd 100644
--- a/src/test/java/eu/europa/ted/efx/EfxTestsBase.java
+++ b/src/test/java/eu/europa/ted/efx/EfxTestsBase.java
@@ -5,12 +5,72 @@
import eu.europa.ted.efx.interfaces.TranslatorOptions;
import eu.europa.ted.efx.mock.DependencyFactoryMock;
import eu.europa.ted.efx.model.DecimalFormat;
+import net.sf.saxon.s9api.ExtensionFunction;
+import net.sf.saxon.s9api.ItemType;
+import net.sf.saxon.s9api.OccurrenceIndicator;
+import net.sf.saxon.s9api.Processor;
+import net.sf.saxon.s9api.QName;
+import net.sf.saxon.s9api.SaxonApiException;
+import net.sf.saxon.s9api.SequenceType;
+import net.sf.saxon.s9api.XPathCompiler;
+import net.sf.saxon.s9api.XdmValue;
public abstract class EfxTestsBase {
protected static final TranslatorOptions DEFAULT_OPTIONS = new EfxTranslatorOptions("udf",
DecimalFormat.EFX_DEFAULT);
-
+
+ private static final String EFX_NAMESPACE = "http://ted.europa.eu/efx";
+ private static final XPathCompiler XPATH_COMPILER;
+
+ static {
+ Processor processor = new Processor(false);
+
+ // Register custom EFX extension functions so Saxon can validate XPath syntax.
+ // These are only needed for V1 expression tests, where multilingual field references
+ // are implicitly wrapped in efx:preferred-language-text() by XPathScriptGeneratorV1.
+ // V2 bans these functions in expression context (template-only).
+ processor.registerExtensionFunction(efxFunction("preferred-language"));
+ processor.registerExtensionFunction(efxFunction("preferred-language-text"));
+
+ XPATH_COMPILER = processor.newXPathCompiler();
+ XPATH_COMPILER.setLanguageVersion("3.1");
+ XPATH_COMPILER.declareNamespace("fn", "http://www.w3.org/2005/xpath-functions");
+ XPATH_COMPILER.declareNamespace("xs", "http://www.w3.org/2001/XMLSchema");
+ XPATH_COMPILER.declareNamespace("efx", EFX_NAMESPACE);
+ XPATH_COMPILER.declareVariable(new QName("urlPrefix"));
+ }
+
+ /**
+ * Creates a dummy extension function stub for XPath syntax validation.
+ * Accepts one argument (node) and returns a string.
+ */
+ private static ExtensionFunction efxFunction(String localName) {
+ return new ExtensionFunction() {
+ @Override
+ public QName getName() {
+ return new QName(EFX_NAMESPACE, localName);
+ }
+
+ @Override
+ public SequenceType getResultType() {
+ return SequenceType.makeSequenceType(ItemType.STRING, OccurrenceIndicator.ONE);
+ }
+
+ @Override
+ public SequenceType[] getArgumentTypes() {
+ return new SequenceType[] {
+ SequenceType.makeSequenceType(ItemType.ANY_ITEM, OccurrenceIndicator.ONE_OR_MORE)
+ };
+ }
+
+ @Override
+ public XdmValue call(XdmValue[] arguments) {
+ throw new UnsupportedOperationException("Stub for XPath validation only");
+ }
+ };
+ }
+
protected abstract String getSdkVersion();
protected void testExpressionTranslationWithContext(final String expectedTranslation,
@@ -29,8 +89,10 @@ protected String translateExpressionWithContext(final String context, final Stri
protected String translateExpression(final String expression, final String... params) {
try {
- return EfxTranslator.translateExpression(DependencyFactoryMock.INSTANCE, getSdkVersion(),
- expression, DEFAULT_OPTIONS, params);
+ String result = EfxTranslator.translateExpression(DependencyFactoryMock.INSTANCE,
+ getSdkVersion(), expression, DEFAULT_OPTIONS, params);
+ assertValidXPath(result);
+ return result;
} catch (InstantiationException e) {
throw new RuntimeException(e);
}
@@ -45,6 +107,15 @@ protected String translateTemplate(final String template) {
}
}
+ protected static void assertValidXPath(final String xpath) {
+ try {
+ XPATH_COMPILER.compile(xpath);
+ } catch (SaxonApiException e) {
+ throw new AssertionError(
+ "Generated XPath is not valid: " + xpath + "\n" + e.getMessage(), e);
+ }
+ }
+
protected String lines(String... lines) {
return String.join("\n", lines);
}
diff --git a/src/test/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1Test.java b/src/test/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1Test.java
index 8854d32..233adbf 100644
--- a/src/test/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1Test.java
+++ b/src/test/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1Test.java
@@ -392,42 +392,42 @@ void testDurationQuantifiedExpression_UsingFieldReference() {
@Test
void testConditionalExpression() {
- testExpressionTranslationWithContext("(if 1 > 2 then 'a' else 'b')", "ND-Root",
+ testExpressionTranslationWithContext("(if (1 > 2) then 'a' else 'b')", "ND-Root",
"if 1 > 2 then 'a' else 'b'");
}
@Test
void testConditionalStringExpression_UsingLiterals() {
- testExpressionTranslationWithContext("(if 'a' > 'b' then 'a' else 'b')", "ND-Root",
+ testExpressionTranslationWithContext("(if ('a' > 'b') then 'a' else 'b')", "ND-Root",
"if 'a' > 'b' then 'a' else 'b'");
}
@Test
void testConditionalStringExpression_UsingFieldReferenceInCondition() {
testExpressionTranslationWithContext(
- "(if 'a' > PathNode/TextField/normalize-space(text()) then 'a' else 'b')", "ND-Root",
+ "(if ('a' > PathNode/TextField/normalize-space(text())) then 'a' else 'b')", "ND-Root",
"if 'a' > BT-00-Text then 'a' else 'b'");
testExpressionTranslationWithContext(
- "(if PathNode/TextField/normalize-space(text()) >= 'a' then 'a' else 'b')", "ND-Root",
+ "(if (PathNode/TextField/normalize-space(text()) >= 'a') then 'a' else 'b')", "ND-Root",
"if BT-00-Text >= 'a' then 'a' else 'b'");
testExpressionTranslationWithContext(
- "(if PathNode/TextField/normalize-space(text()) >= PathNode/TextField/normalize-space(text()) then 'a' else 'b')",
+ "(if (PathNode/TextField/normalize-space(text()) >= PathNode/TextField/normalize-space(text())) then 'a' else 'b')",
"ND-Root", "if BT-00-Text >= BT-00-Text then 'a' else 'b'");
testExpressionTranslationWithContext(
- "(if PathNode/StartDateField/xs:date(text()) >= PathNode/EndDateField/xs:date(text()) then 'a' else 'b')",
+ "(if (PathNode/StartDateField/xs:date(text()) >= PathNode/EndDateField/xs:date(text())) then 'a' else 'b')",
"ND-Root", "if BT-00-StartDate >= BT-00-EndDate then 'a' else 'b'");
}
@Test
void testConditionalStringExpression_UsingFieldReference() {
testExpressionTranslationWithContext(
- "(if 'a' > 'b' then PathNode/TextField/normalize-space(text()) else 'b')", "ND-Root",
+ "(if ('a' > 'b') then PathNode/TextField/normalize-space(text()) else 'b')", "ND-Root",
"if 'a' > 'b' then BT-00-Text else 'b'");
testExpressionTranslationWithContext(
- "(if 'a' > 'b' then 'a' else PathNode/TextField/normalize-space(text()))", "ND-Root",
+ "(if ('a' > 'b') then 'a' else PathNode/TextField/normalize-space(text()))", "ND-Root",
"if 'a' > 'b' then 'a' else BT-00-Text");
testExpressionTranslationWithContext(
- "(if 'a' > 'b' then PathNode/TextField/normalize-space(text()) else PathNode/TextField/normalize-space(text()))",
+ "(if ('a' > 'b') then PathNode/TextField/normalize-space(text()) else PathNode/TextField/normalize-space(text()))",
"ND-Root", "if 'a' > 'b' then BT-00-Text else BT-00-Text");
}
@@ -439,34 +439,34 @@ void testConditionalStringExpression_UsingFieldReferences_TypeMismatch() {
@Test
void testConditionalBooleanExpression() {
- testExpressionTranslationWithContext("(if PathNode/IndicatorField then true() else false())",
+ testExpressionTranslationWithContext("(if (PathNode/IndicatorField) then true() else false())",
"ND-Root", "if BT-00-Indicator then TRUE else FALSE");
}
@Test
void testConditionalNumericExpression() {
- testExpressionTranslationWithContext("(if 1 > 2 then 1 else PathNode/NumberField/number())",
+ testExpressionTranslationWithContext("(if (1 > 2) then 1 else PathNode/NumberField/number())",
"ND-Root", "if 1 > 2 then 1 else BT-00-Number");
}
@Test
void testConditionalDateExpression() {
testExpressionTranslationWithContext(
- "(if xs:date('2012-01-01Z') > PathNode/EndDateField/xs:date(text()) then PathNode/StartDateField/xs:date(text()) else xs:date('2012-01-02Z'))",
+ "(if (xs:date('2012-01-01Z') > PathNode/EndDateField/xs:date(text())) then PathNode/StartDateField/xs:date(text()) else xs:date('2012-01-02Z'))",
"ND-Root", "if 2012-01-01Z > BT-00-EndDate then BT-00-StartDate else 2012-01-02Z");
}
@Test
void testConditionalTimeExpression() {
testExpressionTranslationWithContext(
- "(if PathNode/EndTimeField/xs:time(text()) > xs:time('00:00:01Z') then PathNode/StartTimeField/xs:time(text()) else xs:time('00:00:01Z'))",
+ "(if (PathNode/EndTimeField/xs:time(text()) > xs:time('00:00:01Z')) then PathNode/StartTimeField/xs:time(text()) else xs:time('00:00:01Z'))",
"ND-Root", "if BT-00-EndTime > 00:00:01Z then BT-00-StartTime else 00:00:01Z");
}
@Test
void testConditionalDurationExpression() {
assertEquals(
- "(if boolean(for $T in (current-date()) return ($T + xs:dayTimeDuration('P1D') > $T + (for $F in PathNode/DurationField 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 ())))) then xs:dayTimeDuration('P1D') else xs:dayTimeDuration('P2D'))",
+ "(if (boolean(for $T in (current-date()) return ($T + xs:dayTimeDuration('P1D') > $T + (for $F in PathNode/DurationField 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 ()))))) then xs:dayTimeDuration('P1D') else xs:dayTimeDuration('P2D'))",
translateExpressionWithContext("ND-Root", "if P1D > BT-00-Duration then P1D else P2D"));
}
diff --git a/src/test/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2Test.java b/src/test/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2Test.java
index 9bd5066..6ec00e8 100644
--- a/src/test/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2Test.java
+++ b/src/test/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2Test.java
@@ -166,11 +166,6 @@ void testLikePatternCondition_WithTextField() {
"{ND-Root} ${BT-00-Text like '[0-9]*'}");
}
- @Test
- void testLikePatternCondition_WithTextMultilingualField() {
- testExpressionTranslation("every $lang in PathNode/TextMultilingualField/@languageID satisfies fn:matches(normalize-space(PathNode/TextMultilingualField[./@languageID = $lang]/normalize-space(text())), '[0-9]*')",
- "{ND-Root} ${every text:$lang in BT-00-Text-Multilingual/@languageID satisfies BT-00-Text-Multilingual[BT-00-Text-Multilingual/@languageID == $lang] like '[0-9]*'}");
- }
@Test
void testPreferredLanguage_ThrowsInExpressionContext() {
@@ -188,10 +183,24 @@ void testPreferredLanguageText_ThrowsInExpressionContext() {
}
@Test
- void testFieldValueComparison_UsingTextFields() {
- testExpressionTranslationWithContext(
- "PathNode/TextField/normalize-space(text()) = PathNode/TextMultilingualField/normalize-space(text())",
- "Root", "textField == textMultilingualField");
+ void testPreferredLanguageProperty_ThrowsInExpressionContext() {
+ InvalidUsageException exception = assertThrows(InvalidUsageException.class,
+ () -> translateExpressionWithContext("ND-Root", "BT-00-Text-Multilingual:preferredLanguage"));
+ assertEquals(InvalidUsageException.ErrorCode.TEMPLATE_ONLY_FUNCTION, exception.getErrorCode());
+ }
+
+ @Test
+ void testPreferredLanguageTextProperty_ThrowsInExpressionContext() {
+ InvalidUsageException exception = assertThrows(InvalidUsageException.class,
+ () -> translateExpressionWithContext("ND-Root", "BT-00-Text-Multilingual:preferredLanguageText"));
+ assertEquals(InvalidUsageException.ErrorCode.TEMPLATE_ONLY_FUNCTION, exception.getErrorCode());
+ }
+
+ @Test
+ void testFieldValueComparison_UsingTextFields_ThrowsBecauseMultilingual() {
+ TypeMismatchException exception = assertThrows(TypeMismatchException.class,
+ () -> translateExpressionWithContext("Root", "textField == textMultilingualField"));
+ assertEquals(TypeMismatchException.ErrorCode.FIELD_IS_MULTILINGUAL, exception.getErrorCode());
}
@Test
@@ -504,42 +513,42 @@ void testDurationQuantifiedExpression_UsingFieldReference() {
@Test
void testConditionalExpression() {
- testExpressionTranslationWithContext("(if 1 > 2 then 'a' else 'b')", "ND-Root",
+ testExpressionTranslationWithContext("(if (1 > 2) then 'a' else 'b')", "ND-Root",
"if 1 > 2 then 'a' else 'b'");
}
@Test
void testConditionalStringExpression_UsingLiterals() {
- testExpressionTranslationWithContext("(if 'a' > 'b' then 'a' else 'b')", "ND-Root",
+ testExpressionTranslationWithContext("(if ('a' > 'b') then 'a' else 'b')", "ND-Root",
"if 'a' > 'b' then 'a' else 'b'");
}
@Test
void testConditionalStringExpression_UsingFieldReferenceInCondition() {
testExpressionTranslationWithContext(
- "(if 'a' > PathNode/TextField/normalize-space(text()) then 'a' else 'b')", "ND-Root",
+ "(if ('a' > PathNode/TextField/normalize-space(text())) then 'a' else 'b')", "ND-Root",
"if 'a' > BT-00-Text then 'a' else 'b'");
testExpressionTranslationWithContext(
- "(if PathNode/TextField/normalize-space(text()) >= 'a' then 'a' else 'b')", "ND-Root",
+ "(if (PathNode/TextField/normalize-space(text()) >= 'a') then 'a' else 'b')", "ND-Root",
"if BT-00-Text >= 'a' then 'a' else 'b'");
testExpressionTranslationWithContext(
- "(if PathNode/TextField/normalize-space(text()) >= PathNode/TextField/normalize-space(text()) then 'a' else 'b')",
+ "(if (PathNode/TextField/normalize-space(text()) >= PathNode/TextField/normalize-space(text())) then 'a' else 'b')",
"ND-Root", "if BT-00-Text >= BT-00-Text then 'a' else 'b'");
testExpressionTranslationWithContext(
- "(if PathNode/StartDateField/xs:date(text()) >= PathNode/EndDateField/xs:date(text()) then 'a' else 'b')",
+ "(if (PathNode/StartDateField/xs:date(text()) >= PathNode/EndDateField/xs:date(text())) then 'a' else 'b')",
"ND-Root", "if BT-00-StartDate >= BT-00-EndDate then 'a' else 'b'");
}
@Test
void testConditionalStringExpression_UsingFieldReference() {
testExpressionTranslationWithContext(
- "(if 'a' > 'b' then PathNode/TextField/normalize-space(text()) else 'b')", "ND-Root",
+ "(if ('a' > 'b') then PathNode/TextField/normalize-space(text()) else 'b')", "ND-Root",
"if 'a' > 'b' then BT-00-Text else 'b'");
testExpressionTranslationWithContext(
- "(if 'a' > 'b' then 'a' else PathNode/TextField/normalize-space(text()))", "ND-Root",
+ "(if ('a' > 'b') then 'a' else PathNode/TextField/normalize-space(text()))", "ND-Root",
"if 'a' > 'b' then 'a' else BT-00-Text");
testExpressionTranslationWithContext(
- "(if 'a' > 'b' then PathNode/TextField/normalize-space(text()) else PathNode/TextField/normalize-space(text()))",
+ "(if ('a' > 'b') then PathNode/TextField/normalize-space(text()) else PathNode/TextField/normalize-space(text()))",
"ND-Root", "if 'a' > 'b' then BT-00-Text else BT-00-Text");
}
@@ -551,34 +560,34 @@ void testConditionalStringExpression_UsingFieldReferences_TypeMismatch() {
@Test
void testConditionalBooleanExpression() {
- testExpressionTranslationWithContext("(if PathNode/IndicatorField then true() else false())",
+ testExpressionTranslationWithContext("(if (PathNode/IndicatorField) then true() else false())",
"ND-Root", "if BT-00-Indicator then TRUE else FALSE");
}
@Test
void testConditionalNumericExpression() {
- testExpressionTranslationWithContext("(if 1 > 2 then 1 else PathNode/NumberField/number())",
+ testExpressionTranslationWithContext("(if (1 > 2) then 1 else PathNode/NumberField/number())",
"ND-Root", "if 1 > 2 then 1 else BT-00-Number");
}
@Test
void testConditionalDateExpression() {
testExpressionTranslationWithContext(
- "(if xs:date('2012-01-01Z') > PathNode/EndDateField/xs:date(text()) then PathNode/StartDateField/xs:date(text()) else xs:date('2012-01-02Z'))",
+ "(if (xs:date('2012-01-01Z') > PathNode/EndDateField/xs:date(text())) then PathNode/StartDateField/xs:date(text()) else xs:date('2012-01-02Z'))",
"ND-Root", "if 2012-01-01Z > BT-00-EndDate then BT-00-StartDate else 2012-01-02Z");
}
@Test
void testConditionalTimeExpression() {
testExpressionTranslationWithContext(
- "(if PathNode/EndTimeField/xs:time(text()) > xs:time('00:00:01Z') then PathNode/StartTimeField/xs:time(text()) else xs:time('00:00:01Z'))",
+ "(if (PathNode/EndTimeField/xs:time(text()) > xs:time('00:00:01Z')) then PathNode/StartTimeField/xs:time(text()) else xs:time('00:00:01Z'))",
"ND-Root", "if BT-00-EndTime > 00:00:01Z then BT-00-StartTime else 00:00:01Z");
}
@Test
void testConditionalDurationExpression() {
assertEquals(
- "(if boolean(for $T in (current-date()) return ($T + xs:dayTimeDuration('P1D') > $T + (for $F in PathNode/DurationField 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 ())))) then xs:dayTimeDuration('P1D') else xs:dayTimeDuration('P2D'))",
+ "(if (boolean(for $T in (current-date()) return ($T + xs:dayTimeDuration('P1D') > $T + (for $F in PathNode/DurationField 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 ()))))) then xs:dayTimeDuration('P1D') else xs:dayTimeDuration('P2D'))",
translateExpressionWithContext("ND-Root", "if P1D > BT-00-Duration then P1D else P2D"));
}
@@ -1305,6 +1314,21 @@ void testMultilingualTextFieldReference_WithLanguagePredicate() {
"ND-Root", "BT-00-Text-Multilingual[BT-00-Text-Multilingual/@languageID == 'eng']");
}
+ @Test
+ void testMultilingualTextFieldReference_InScalarContext_Throws() {
+ TypeMismatchException ex = assertThrows(TypeMismatchException.class,
+ () -> translateExpressionWithContext("ND-Root",
+ "BT-00-Text-Multilingual == 'test'"));
+ assertEquals(TypeMismatchException.ErrorCode.FIELD_IS_MULTILINGUAL, ex.getErrorCode());
+ }
+
+ @Test
+ void testMultilingualTextFieldReference_InSequenceContext() {
+ testExpressionTranslationWithContext(
+ "'test' = PathNode/TextMultilingualField/normalize-space(text())",
+ "ND-Root",
+ "'test' in BT-00-Text-Multilingual");
+ }
// #endregion: References
@@ -1507,9 +1531,13 @@ void testComputedProperty_isMasked_numericField() {
@Test
void testComputedProperty_isMasked_repeatingFieldFromContext() {
- assertThrows(TypeMismatchException.class,
- () -> translateExpressionWithContext("ND-Root",
- "BT-00-Text-In-Repeatable-Node:isMasked"));
+ final String privacyPath = "RepeatableNode/FieldsPrivacy[FieldIdentifierCode/text()='test-priv']";
+ testExpressionTranslationWithContext(
+ privacyPath + "/FieldIdentifierCode/normalize-space(text()) = 'test-priv'"
+ + " and "
+ + "RepeatableNode/TextField/normalize-space(text()) = 'unpublished'",
+ "ND-Root",
+ "BT-00-Text-In-Repeatable-Node:isMasked");
}
@Test
@@ -1716,6 +1744,22 @@ void testFieldRawValue_RepeatableFieldInScalarContext_Throws() {
assertEquals(TypeMismatchException.ErrorCode.FIELD_MAY_REPEAT, ex.getErrorCode());
}
+ @Test
+ void testFieldRawValue_MultilingualFieldInScalarContext_Throws() {
+ TypeMismatchException ex = assertThrows(TypeMismatchException.class,
+ () -> translateExpressionWithContext("ND-Root",
+ "BT-00-Text-Multilingual:rawValue == 'test'"));
+ assertEquals(TypeMismatchException.ErrorCode.FIELD_IS_MULTILINGUAL, ex.getErrorCode());
+ }
+
+ @Test
+ void testFieldRawValue_MultilingualFieldInSequenceContext() {
+ testExpressionTranslationWithContext(
+ "'test' = PathNode/TextMultilingualField/text()",
+ "ND-Root",
+ "'test' in BT-00-Text-Multilingual:rawValue");
+ }
+
// #endregion: Boolean functions
// #region: Numeric functions -----------------------------------------------
@@ -3549,4 +3593,933 @@ void testStringJoin_ParenthesizedFieldReference() {
}
// #endregion: B1 Grammar Issue
+
+ // #region: Field Reference Structural Combinations -------------------------
+ // Tests covering fieldReference grammar structural forms
+ // (predicates, absolute refs, context overrides, indexers)
+ // in different consumer contexts (scalar, sequence, presence).
+
+ // #region: Absolute and predicate combinations
+
+ @Test
+ void testAbsoluteFieldReference_InSequenceContext() {
+ testExpressionTranslationWithContext(
+ "'test' = /*/PathNode/TextField/normalize-space(text())",
+ "BT-00-Text",
+ "'test' in /BT-00-Text");
+ }
+
+ @Test
+ void testAbsoluteFieldReference_InPresenceCondition() {
+ testExpressionTranslationWithContext(
+ "/*/PathNode/TextField",
+ "BT-00-Text",
+ "/BT-00-Text is present");
+ }
+
+ @Test
+ void testAbsoluteFieldReference_WithPredicate() {
+ testExpressionTranslationWithContext(
+ "/*/PathNode/IndicatorField['a' = 'a']",
+ "BT-00-Text",
+ "/BT-00-Indicator['a' == 'a']");
+ }
+
+ @Test
+ void testFieldReferenceWithPredicate_InSequenceContext() {
+ testExpressionTranslationWithContext(
+ "'test' = PathNode/TextField['a' = 'a']/normalize-space(text())",
+ "ND-Root",
+ "'test' in BT-00-Text['a' == 'a']");
+ }
+
+ @Test
+ void testFieldReferenceWithPredicate_InPresenceCondition() {
+ testExpressionTranslationWithContext(
+ "PathNode/TextField['a' = 'a']",
+ "ND-Root",
+ "BT-00-Text['a' == 'a'] is present");
+ }
+
+ @Test
+ void testLinkedProperty_InScalarComparison() {
+ final String privacyPath = "../FieldsPrivacy[FieldIdentifierCode/text()='test-priv']";
+ testExpressionTranslationWithContext(
+ privacyPath + "/PublicationDate/xs:date(text()) = xs:date('2025-01-01Z')",
+ "BT-00-Text-In-Repeatable-Node",
+ "BT-00-Text-In-Repeatable-Node:publicationDate == 2025-01-01Z");
+ }
+
+ // Row 2 ATTR
+ @Test
+ void testFieldReferenceWithPredicate_InAttributeContext() {
+ testExpressionTranslationWithContext(
+ "PathNode/CodeField['a' = 'a']/@listName",
+ "ND-Root",
+ "BT-00-Code['a' == 'a']/@listName");
+ }
+
+ // Row 3 ATTR
+ @Test
+ void testAbsoluteFieldReference_InAttributeContext() {
+ testExpressionTranslationWithContext(
+ "/*/PathNode/CodeField/@listName",
+ "BT-00-Text",
+ "/BT-00-Code/@listName");
+ }
+
+ // Row 4 SC
+ @Test
+ void testAbsoluteFieldReferenceWithPredicate_InScalarContext() {
+ testExpressionTranslationWithContext(
+ "/*/PathNode/TextField['a' = 'a']/normalize-space(text()) = 'test'",
+ "BT-00-Text",
+ "/BT-00-Text['a' == 'a'] == 'test'");
+ }
+
+ // Row 4 SEQ
+ @Test
+ void testAbsoluteFieldReferenceWithPredicate_InSequenceContext() {
+ testExpressionTranslationWithContext(
+ "'test' = /*/PathNode/TextField['a' = 'a']/normalize-space(text())",
+ "BT-00-Text",
+ "'test' in /BT-00-Text['a' == 'a']");
+ }
+
+ // Row 4 ATTR
+ @Test
+ void testAbsoluteFieldReferenceWithPredicate_InAttributeContext() {
+ testExpressionTranslationWithContext(
+ "/*/PathNode/CodeField['a' = 'a']/@listName",
+ "BT-00-Text",
+ "/BT-00-Code['a' == 'a']/@listName");
+ }
+
+ // Row 5 SEQ
+ @Test
+ void testLinkedProperty_InSequenceContext() {
+ final String privacyPath = "RepeatableNode/FieldsPrivacy[FieldIdentifierCode/text()='test-priv']";
+ testExpressionTranslationWithContext(
+ "xs:date('2025-01-01Z') = " + privacyPath + "/PublicationDate/xs:date(text())",
+ "ND-Root",
+ "2025-01-01Z in BT-00-Text-In-Repeatable-Node:publicationDate");
+ }
+
+ // #endregion: Absolute and predicate combinations
+
+ // #region: Context override combinations (field::, node::, $var::, stacked)
+
+ @Test
+ void testFieldContextOverride_WithPredicateOnContext() {
+ // joinPaths simplifies: ../ChildNode/SubLevelTextField['a'='a']/../../TextField → ../TextField
+ // because navigating UP from a predicated node reaches the parent regardless
+ testExpressionTranslationWithContext(
+ "../TextField/normalize-space(text())",
+ "BT-00-Code",
+ "BT-01-SubLevel-Text['a' == 'a']::BT-00-Text");
+ }
+
+ @Test
+ void testFieldContextOverride_WithIndexerOnContext() {
+ // joinPaths simplifies the indexed context path the same way as predicates
+ testExpressionTranslationWithContext(
+ "../TextField/normalize-space(text())",
+ "BT-00-Code",
+ "BT-01-SubLevel-Text[1]::BT-00-Text");
+ }
+
+ @Test
+ void testFieldContextOverride_InSequenceContext() {
+ testExpressionTranslationWithContext(
+ "'test' = ../TextField/normalize-space(text())",
+ "BT-00-Code",
+ "'test' in BT-01-SubLevel-Text::BT-00-Text");
+ }
+
+ @Test
+ void testNodeContextOverride_InSequenceContext() {
+ testExpressionTranslationWithContext(
+ "'test' = ../../PathNode/TextField/normalize-space(text())",
+ "BT-00-Text",
+ "'test' in ND-Root::BT-00-Text");
+ }
+
+ @Test
+ void testStackedOverride_NodeAndField() {
+ // joinPaths simplifies: .../ChildNode/SubLevelTextField/../../TextField → .../TextField
+ testExpressionTranslationWithContext(
+ "../../PathNode/TextField/normalize-space(text())",
+ "BT-00-Text",
+ "ND-Root::BT-01-SubLevel-Text::BT-00-Text");
+ }
+
+ // Row 6 PRES
+ @Test
+ void testFieldContextOverride_InPresenceCondition() {
+ testExpressionTranslationWithContext(
+ "../TextField",
+ "BT-00-Code",
+ "BT-01-SubLevel-Text::BT-00-Text is present");
+ }
+
+ // Row 6 ATTR
+ @Test
+ void testFieldContextOverride_InAttributeContext() {
+ testExpressionTranslationWithContext(
+ "../CodeField/@listName",
+ "BT-00-Code",
+ "BT-01-SubLevel-Text::BT-00-Code/@listName");
+ }
+
+ // Row 7 SEQ
+ @Test
+ void testFieldContextOverride_WithPredicate_InSequenceContext() {
+ testExpressionTranslationWithContext(
+ "'test' = ../TextField/normalize-space(text())",
+ "BT-00-Code",
+ "'test' in BT-01-SubLevel-Text['a' == 'a']::BT-00-Text");
+ }
+
+ // Row 7 PRES
+ @Test
+ void testFieldContextOverride_WithPredicate_InPresenceCondition() {
+ testExpressionTranslationWithContext(
+ "../TextField",
+ "BT-00-Code",
+ "BT-01-SubLevel-Text['a' == 'a']::BT-00-Text is present");
+ }
+
+ // Row 7 ATTR
+ @Test
+ void testFieldContextOverride_WithPredicate_InAttributeContext() {
+ testExpressionTranslationWithContext(
+ "../CodeField/@listName",
+ "BT-00-Code",
+ "BT-01-SubLevel-Text['a' == 'a']::BT-00-Code/@listName");
+ }
+
+ // Row 8 SEQ
+ @Test
+ void testFieldContextOverride_WithIndexer_InSequenceContext() {
+ testExpressionTranslationWithContext(
+ "'test' = ../TextField/normalize-space(text())",
+ "BT-00-Code",
+ "'test' in BT-01-SubLevel-Text[1]::BT-00-Text");
+ }
+
+ // Row 8 PRES
+ @Test
+ void testFieldContextOverride_WithIndexer_InPresenceCondition() {
+ testExpressionTranslationWithContext(
+ "../TextField",
+ "BT-00-Code",
+ "BT-01-SubLevel-Text[1]::BT-00-Text is present");
+ }
+
+ // Row 8 ATTR
+ @Test
+ void testFieldContextOverride_WithIndexer_InAttributeContext() {
+ testExpressionTranslationWithContext(
+ "../CodeField/@listName",
+ "BT-00-Code",
+ "BT-01-SubLevel-Text[1]::BT-00-Code/@listName");
+ }
+
+ // Row 9 PRES
+ @Test
+ void testNodeContextOverride_InPresenceCondition() {
+ testExpressionTranslationWithContext(
+ "../../PathNode/TextField",
+ "BT-00-Text",
+ "ND-Root::BT-00-Text is present");
+ }
+
+ // Row 9 ATTR
+ @Test
+ void testNodeContextOverride_InAttributeContext() {
+ testExpressionTranslationWithContext(
+ "../../PathNode/CodeField/@listName",
+ "BT-00-Text",
+ "ND-Root::BT-00-Code/@listName");
+ }
+
+ // Row 10 SEQ
+ @Test
+ void testNodeContextOverride_WithPredicate_InSequenceContext() {
+ testExpressionTranslationWithContext(
+ "'test' = ../../PathNode/TextField/normalize-space(text())",
+ "BT-00-Text",
+ "'test' in ND-Root[BT-00-Indicator == TRUE]::BT-00-Text");
+ }
+
+ // Row 10 PRES
+ @Test
+ void testNodeContextOverride_WithPredicate_InPresenceCondition() {
+ testExpressionTranslationWithContext(
+ "../../PathNode/TextField",
+ "BT-00-Text",
+ "ND-Root[BT-00-Indicator == TRUE]::BT-00-Text is present");
+ }
+
+ // Row 10 ATTR
+ @Test
+ void testNodeContextOverride_WithPredicate_InAttributeContext() {
+ testExpressionTranslationWithContext(
+ "../../PathNode/CodeField/@listName",
+ "BT-00-Text",
+ "ND-Root[BT-00-Indicator == TRUE]::BT-00-Code/@listName");
+ }
+
+ // Row 11 SEQ
+ @Test
+ void testVariableContextOverride_InSequenceContext() {
+ testExpressionTranslationWithContext(
+ "for $n in SubNode return 'test' = $n/SubTextField/normalize-space(text())",
+ "ND-Root",
+ "for context:$n in ND-SubNode return 'test' in $n::BT-01-SubNode-Text");
+ }
+
+ // Row 11 PRES
+ @Test
+ void testVariableContextOverride_InPresenceCondition() {
+ testExpressionTranslationWithContext(
+ "for $n in SubNode return $n/SubTextField",
+ "ND-Root",
+ "for context:$n in ND-SubNode return $n::BT-01-SubNode-Text is present");
+ }
+
+ // Row 11 ATTR
+ @Test
+ void testVariableContextOverride_InAttributeContext() {
+ testExpressionTranslationWithContext(
+ "for $n in . return $n/PathNode/CodeField/@listName",
+ "ND-Root",
+ "for context:$n in ND-Root return $n::BT-00-Code/@listName");
+ }
+
+ // Row 12 SEQ
+ @Test
+ void testFieldReferenceInOtherNotice_InSequenceContext() {
+ testExpressionTranslationWithContext(
+ "'test' = fn:doc(concat($urlPrefix, 'da4d46e9-490b-41ff-a2ae-8166d356a619'))/*/PathNode/TextField/normalize-space(text())",
+ "ND-Root",
+ "'test' in notice('da4d46e9-490b-41ff-a2ae-8166d356a619')/BT-00-Text");
+ }
+
+ // Row 12 PRES
+ @Test
+ void testFieldReferenceInOtherNotice_InPresenceCondition() {
+ testExpressionTranslationWithContext(
+ "fn:doc(concat($urlPrefix, 'da4d46e9-490b-41ff-a2ae-8166d356a619'))/*/PathNode/TextField",
+ "ND-Root",
+ "notice('da4d46e9-490b-41ff-a2ae-8166d356a619')/BT-00-Text is present");
+ }
+
+ // Row 12 ATTR
+ @Test
+ void testFieldReferenceInOtherNotice_InAttributeContext() {
+ testExpressionTranslationWithContext(
+ "fn:doc(concat($urlPrefix, 'da4d46e9-490b-41ff-a2ae-8166d356a619'))/*/PathNode/CodeField/@listName",
+ "ND-Root",
+ "notice('da4d46e9-490b-41ff-a2ae-8166d356a619')/BT-00-Code/@listName");
+ }
+
+ // Row 13 SEQ
+ @Test
+ void testStackedOverride_NodeAndField_InSequenceContext() {
+ testExpressionTranslationWithContext(
+ "'test' = ../../PathNode/TextField/normalize-space(text())",
+ "BT-00-Text",
+ "'test' in ND-Root::BT-01-SubLevel-Text::BT-00-Text");
+ }
+
+ // Row 13 PRES
+ @Test
+ void testStackedOverride_NodeAndField_InPresenceCondition() {
+ testExpressionTranslationWithContext(
+ "../../PathNode/TextField",
+ "BT-00-Text",
+ "ND-Root::BT-01-SubLevel-Text::BT-00-Text is present");
+ }
+
+ // Row 13 ATTR
+ @Test
+ void testStackedOverride_NodeAndField_InAttributeContext() {
+ testExpressionTranslationWithContext(
+ "../../PathNode/CodeField/@listName",
+ "BT-00-Text",
+ "ND-Root::BT-01-SubLevel-Text::BT-00-Code/@listName");
+ }
+
+ // Row 14 SC
+ @Test
+ void testVariableAndNodeContextOverride_InScalarContext() {
+ testExpressionTranslationWithContext(
+ "for $n in SubNode return $n/../PathNode/IntegerField/number()",
+ "ND-Root",
+ "for context:$n in ND-SubNode return $n::ND-Root::integerField");
+ }
+
+ // Row 14 SEQ
+ @Test
+ void testVariableAndNodeContextOverride_InSequenceContext() {
+ testExpressionTranslationWithContext(
+ "for $n in SubNode return 'test' = $n/../PathNode/TextField/normalize-space(text())",
+ "ND-Root",
+ "for context:$n in ND-SubNode return 'test' in $n::ND-Root::BT-00-Text");
+ }
+
+ // Row 14 PRES
+ @Test
+ void testVariableAndNodeContextOverride_InPresenceCondition() {
+ testExpressionTranslationWithContext(
+ "for $n in SubNode return $n/../PathNode/TextField",
+ "ND-Root",
+ "for context:$n in ND-SubNode return $n::ND-Root::BT-00-Text is present");
+ }
+
+ // Row 14 ATTR
+ @Test
+ void testVariableAndNodeContextOverride_InAttributeContext() {
+ testExpressionTranslationWithContext(
+ "for $n in SubNode return $n/../PathNode/CodeField/@listName",
+ "ND-Root",
+ "for context:$n in ND-SubNode return $n::ND-Root::BT-00-Code/@listName");
+ }
+
+ // #endregion: Context override combinations
+
+ // #region: Indexer interactions
+
+ @Test
+ void testIndexer_MultilingualField_InScalarContext() {
+ // Indexer narrows multilingual field to a single element, allowing scalar use
+ testExpressionTranslationWithContext(
+ "(PathNode/TextMultilingualField/normalize-space(text()))[1] = 'test'",
+ "ND-Root",
+ "BT-00-Text-Multilingual[1] == 'test'");
+ }
+
+ // Note: testIndexer_MultilingualField_InSequenceContext is not possible because
+ // the 'in' operator uses fieldReference (not fieldContext), and [indexer] is only
+ // available on fieldContext. This is a grammar-level limitation.
+
+ @Test
+ void testIndexer_MultilingualFieldWithPredicate_InScalarContext() {
+ testExpressionTranslationWithContext(
+ "(PathNode/TextMultilingualField[./@languageID = 'eng']/normalize-space(text()))[1] = 'test'",
+ "ND-Root",
+ "BT-00-Text-Multilingual[BT-00-Text-Multilingual/@languageID == 'eng'][1] == 'test'");
+ }
+
+ // #endregion: Indexer interactions
+
+ // #endregion: Field Reference Structural Combinations
+
+ // #region: fieldContext Consumer Coverage -----------------------------------
+ // Tests covering fieldContext forms (predicate, indexer, absolute)
+ // in rawValue, context iterator, and template context declaration contexts.
+
+ // #region: rawValue with structural variations
+
+ @Test
+ void testFieldRawValue_WithIndexer() {
+ // Indexer applies at node level, then rawValue appends /text()
+ testExpressionTranslationWithContext(
+ "PathNode/RepeatableTextField[1]/text() = 'test'",
+ "ND-Root",
+ "BT-00-Repeatable-Text[1]:rawValue == 'test'");
+ }
+
+ @Test
+ // @Disabled("BUG: composeIndexer maps MultilingualStringPath back to MultilingualStringPath instead of narrowing to StringPath")
+ void testFieldRawValue_MultilingualWithIndexer() {
+ // Indexer should narrow multilingual to scalar, then rawValue appends /text()
+ testExpressionTranslationWithContext(
+ "PathNode/TextMultilingualField[1]/text() = 'test'",
+ "ND-Root",
+ "BT-00-Text-Multilingual[1]:rawValue == 'test'");
+ }
+
+ @Test
+ void testFieldRawValue_AbsoluteReference() {
+ testExpressionTranslationWithContext(
+ "/*/PathNode/TextField/text() = 'test'",
+ "BT-00-Text",
+ "/BT-00-Text:rawValue == 'test'");
+ }
+
+ @Test
+ void testFieldRawValue_FieldInRepeatableNode_WithIndexer() {
+ // Indexer resolves the repeatability, rawValue appends /text()
+ testExpressionTranslationWithContext(
+ "RepeatableNode/TextField[1]/text() = 'test'",
+ "ND-Root",
+ "BT-00-Text-In-Repeatable-Node[1]:rawValue == 'test'");
+ }
+
+ @Test
+ void testFieldRawValue_WithPredicateAndIndexer() {
+ // Predicate + indexer at node level, then rawValue appends /text()
+ testExpressionTranslationWithContext(
+ "PathNode/RepeatableTextField['a' = 'a'][1]/text() = 'test'",
+ "ND-Root",
+ "BT-00-Repeatable-Text['a' == 'a'][1]:rawValue == 'test'");
+ }
+
+ @Test
+ void testFieldRawValue_WithIndexer_InSequenceContext() {
+ testExpressionTranslationWithContext(
+ "'test' = PathNode/RepeatableTextField[1]/text()",
+ "ND-Root",
+ "'test' in BT-00-Repeatable-Text[1]:rawValue");
+ }
+
+ @Test
+ void testFieldRawValue_WithPredicateAndIndexer_InSequenceContext() {
+ testExpressionTranslationWithContext(
+ "'test' = PathNode/RepeatableTextField['a' = 'a'][1]/text()",
+ "ND-Root",
+ "'test' in BT-00-Repeatable-Text['a' == 'a'][1]:rawValue");
+ }
+
+ @Test
+ void testFieldRawValue_AbsoluteReference_InSequenceContext() {
+ testExpressionTranslationWithContext(
+ "'test' = /*/PathNode/TextField/text()",
+ "BT-00-Text",
+ "'test' in /BT-00-Text:rawValue");
+ }
+
+ @Test
+ void testFieldRawValue_AbsoluteReferenceWithPredicate() {
+ testExpressionTranslationWithContext(
+ "/*/PathNode/TextField['a' = 'a']/text() = 'test'",
+ "BT-00-Text",
+ "/BT-00-Text['a' == 'a']:rawValue == 'test'");
+ }
+
+ @Test
+ void testFieldRawValue_AbsoluteReferenceWithPredicate_InSequenceContext() {
+ testExpressionTranslationWithContext(
+ "'test' = /*/PathNode/TextField['a' = 'a']/text()",
+ "BT-00-Text",
+ "'test' in /BT-00-Text['a' == 'a']:rawValue");
+ }
+
+ @Test
+ void testFieldRawValue_AbsoluteReferenceWithIndexer() {
+ testExpressionTranslationWithContext(
+ "/*/PathNode/RepeatableTextField[1]/text() = 'test'",
+ "BT-00-Repeatable-Text",
+ "/BT-00-Repeatable-Text[1]:rawValue == 'test'");
+ }
+
+ @Test
+ void testFieldRawValue_AbsoluteReferenceWithIndexer_InSequenceContext() {
+ testExpressionTranslationWithContext(
+ "'test' = /*/PathNode/RepeatableTextField[1]/text()",
+ "BT-00-Repeatable-Text",
+ "'test' in /BT-00-Repeatable-Text[1]:rawValue");
+ }
+
+ // #endregion: rawValue with structural variations
+
+ // #region: Context iterator with structural variations
+
+ @Test
+ void testContextIterator_WithIndexer() {
+ // Indexer applies at node level in context iterator
+ testExpressionTranslationWithContext(
+ "for $x in PathNode/RepeatableTextField[1] return $x = 'test'",
+ "ND-Root",
+ "for context:$x in BT-00-Repeatable-Text[1] return $x == 'test'");
+ }
+
+ @Test
+ void testContextIterator_WithPredicateAndIndexer() {
+ testExpressionTranslationWithContext(
+ "for $x in PathNode/TextField['a' = 'a'][1] return $x = 'test'",
+ "ND-Root",
+ "for context:$x in BT-00-Text['a' == 'a'][1] return $x == 'test'");
+ }
+
+ @Test
+ void testContextIterator_AbsoluteReference() {
+ testExpressionTranslationWithContext(
+ "for $x in /*/PathNode/TextField return $x = 'test'",
+ "BT-00-Text",
+ "for context:$x in /BT-00-Text return $x == 'test'");
+ }
+
+ @Test
+ void testContextIterator_AbsoluteReferenceWithPredicate() {
+ testExpressionTranslationWithContext(
+ "for $x in /*/PathNode/TextField['a' = 'a'] return $x = 'test'",
+ "BT-00-Text",
+ "for context:$x in /BT-00-Text['a' == 'a'] return $x == 'test'");
+ }
+
+ @Test
+ void testContextIterator_AbsoluteReferenceWithIndexer() {
+ testExpressionTranslationWithContext(
+ "for $x in /*/PathNode/RepeatableTextField[1] return $x = 'test'",
+ "BT-00-Repeatable-Text",
+ "for context:$x in /BT-00-Repeatable-Text[1] return $x == 'test'");
+ }
+
+ // #endregion: Context iterator with structural variations
+
+ // #endregion: fieldContext Consumer Coverage
+
+ // #region: Cardinality x Consumer Gaps -------------------------------------
+ // Tests covering cardinality (repeatable, in-rep-node, multilingual)
+ // crossed with consumer contexts where coverage was missing.
+
+ // #region: rawValue cardinality
+
+ @Test
+ void testFieldRawValue_FieldInRepeatableNode_ScalarContext_Throws() {
+ // Field in repeatable node used as rawValue scalar from root should throw
+ TypeMismatchException ex = assertThrows(TypeMismatchException.class,
+ () -> translateExpressionWithContext("ND-Root",
+ "BT-00-Text-In-Repeatable-Node:rawValue == 'test'"));
+ assertEquals(TypeMismatchException.ErrorCode.FIELD_MAY_REPEAT, ex.getErrorCode());
+ }
+
+ @Test
+ void testFieldRawValue_FieldInRepeatableNode_SequenceContext() {
+ testExpressionTranslationWithContext(
+ "'test' = RepeatableNode/TextField/text()",
+ "ND-Root",
+ "'test' in BT-00-Text-In-Repeatable-Node:rawValue");
+ }
+
+ @Test
+ void testFieldRawValue_FieldInRepeatableNode_FromOwnContext() {
+ testExpressionTranslationWithContext(
+ "TextField/text() = 'test'",
+ "ND-RepeatableNode",
+ "BT-00-Text-In-Repeatable-Node:rawValue == 'test'");
+ }
+
+ @Test
+ void testFieldRawValue_RepeatableField_FromOwnContext_InSequenceContext() {
+ testExpressionTranslationWithContext(
+ "'test' = ./text()",
+ "BT-00-Repeatable-Text",
+ "'test' in BT-00-Repeatable-Text:rawValue");
+ }
+
+ // Matrix 3, Row 3 SEQ: Repeatable field from own context in sequence context
+ @Test
+ void testSequence_RepeatableFieldFromOwnContext() {
+ testExpressionTranslationWithContext(
+ "'test' = ./normalize-space(text())",
+ "BT-00-Repeatable-Text",
+ "'test' in BT-00-Repeatable-Text");
+ }
+
+ // Matrix 3, Row 4 SEQ: Field in repeatable node from root in sequence context
+ @Test
+ void testSequence_FieldInRepeatableNode_FromRoot() {
+ testExpressionTranslationWithContext(
+ "'test' = RepeatableNode/TextField/normalize-space(text())",
+ "ND-Root",
+ "'test' in BT-00-Text-In-Repeatable-Node");
+ }
+
+ // Matrix 3, Row 5 SEQ: Field in repeatable node from own node in sequence context
+ @Test
+ void testSequence_FieldInRepeatableNode_FromOwnNode() {
+ testExpressionTranslationWithContext(
+ "'test' = TextField/normalize-space(text())",
+ "ND-RepeatableNode",
+ "'test' in BT-00-Text-In-Repeatable-Node");
+ }
+
+ // Matrix 3, Row 5 RV-SEQ: Field in repeatable node from own node rawValue in sequence context
+ @Test
+ void testFieldRawValue_FieldInRepeatableNode_FromOwnNode_InSequenceContext() {
+ testExpressionTranslationWithContext(
+ "'test' = TextField/text()",
+ "ND-RepeatableNode",
+ "'test' in BT-00-Text-In-Repeatable-Node:rawValue");
+ }
+
+ // Matrix 3, Row 7 SEQ: Multilingual[pred] in sequence context
+ @Test
+ void testSequence_MultilingualWithPredicate() {
+ testExpressionTranslationWithContext(
+ "'test' = PathNode/TextMultilingualField['a' = 'a']/normalize-space(text())",
+ "ND-Root",
+ "'test' in BT-00-Text-Multilingual['a' == 'a']");
+ }
+
+ // Matrix 3, Row 7 RV-SC: Multilingual[pred]:rawValue in scalar context — should throw FIELD_IS_MULTILINGUAL
+ @Test
+ void testFieldRawValue_MultilingualWithPredicate_InScalarContext_Throws() {
+ TypeMismatchException ex = assertThrows(TypeMismatchException.class,
+ () -> translateExpressionWithContext("ND-Root",
+ "BT-00-Text-Multilingual['a' == 'a']:rawValue == 'test'"));
+ assertEquals(TypeMismatchException.ErrorCode.FIELD_IS_MULTILINGUAL, ex.getErrorCode());
+ }
+
+ // Matrix 3, Row 7 RV-SEQ: Multilingual[pred]:rawValue in sequence context
+ @Test
+ void testFieldRawValue_MultilingualWithPredicate_InSequenceContext() {
+ testExpressionTranslationWithContext(
+ "'test' = PathNode/TextMultilingualField['a' = 'a']/text()",
+ "ND-Root",
+ "'test' in BT-00-Text-Multilingual['a' == 'a']:rawValue");
+ }
+
+ // Matrix 3, Row 8 RV-SEQ: Multilingual[idx]:rawValue in sequence context
+ @Test
+ void testFieldRawValue_MultilingualWithIndexer_InSequenceContext() {
+ testExpressionTranslationWithContext(
+ "'test' = PathNode/TextMultilingualField[1]/text()",
+ "ND-Root",
+ "'test' in BT-00-Text-Multilingual[1]:rawValue");
+ }
+
+ // #endregion: rawValue cardinality
+
+ // #region: Privacy property cardinality
+
+ @Test
+ void testComputedProperty_wasWithheld_FieldInRepeatableNode_FromRoot() {
+ testExpressionTranslationWithContext(
+ "RepeatableNode/FieldsPrivacy[FieldIdentifierCode/text()='test-priv']/FieldIdentifierCode/normalize-space(text()) = 'test-priv'",
+ "ND-Root",
+ "BT-00-Text-In-Repeatable-Node:wasWithheld");
+ }
+
+ @Test
+ void testComputedProperty_isWithheld_FieldInRepeatableNode_FromRoot() {
+ final String privacyPath = "RepeatableNode/FieldsPrivacy[FieldIdentifierCode/text()='test-priv']";
+ testExpressionTranslationWithContext(
+ privacyPath + "/FieldIdentifierCode/normalize-space(text()) = 'test-priv'"
+ + " and "
+ + "(not(" + privacyPath + "/PublicationDate)"
+ + " or "
+ + privacyPath + "/PublicationDate/xs:date(text()) > current-date())",
+ "ND-Root",
+ "BT-00-Text-In-Repeatable-Node:isWithheld");
+ }
+
+ @Test
+ void testComputedProperty_isDisclosed_FieldInRepeatableNode_FromRoot() {
+ final String privacyPath = "RepeatableNode/FieldsPrivacy[FieldIdentifierCode/text()='test-priv']";
+ testExpressionTranslationWithContext(
+ privacyPath + "/FieldIdentifierCode/normalize-space(text()) = 'test-priv'"
+ + " and "
+ + "not((not(" + privacyPath + "/PublicationDate)"
+ + " or "
+ + privacyPath + "/PublicationDate/xs:date(text()) > current-date()))"
+ + " and "
+ + "not(RepeatableNode/TextField/normalize-space(text()) = 'unpublished')",
+ "ND-Root",
+ "BT-00-Text-In-Repeatable-Node:isDisclosed");
+ }
+
+ @Test
+ void testComputedProperty_isMasked_FieldInRepeatableNode_FromRoot() {
+ final String privacyPath = "RepeatableNode/FieldsPrivacy[FieldIdentifierCode/text()='test-priv']";
+ testExpressionTranslationWithContext(
+ privacyPath + "/FieldIdentifierCode/normalize-space(text()) = 'test-priv'"
+ + " and "
+ + "RepeatableNode/TextField/normalize-space(text()) = 'unpublished'",
+ "ND-Root",
+ "BT-00-Text-In-Repeatable-Node:isMasked");
+ }
+
+ // Matrix 4, Row 1 PC: privacyCode on withholdable scalar field
+ @Test
+ void testComputedProperty_privacyCode_WithholdableField() {
+ testExpressionTranslationWithContext(
+ "'test-priv'",
+ "BT-00-Text-In-Repeatable-Node",
+ "BT-00-Text-In-Repeatable-Node:privacyCode");
+ }
+
+ // Matrix 4, Row 2 PC: privacyCode on non-withholdable field — should throw FIELD_NOT_WITHHOLDABLE
+ @Test
+ void testComputedProperty_privacyCode_NonWithholdableField_Throws() {
+ InvalidUsageException ex = assertThrows(InvalidUsageException.class,
+ () -> translateExpressionWithContext("BT-00-Text",
+ "BT-00-Text:privacyCode"));
+ assertEquals(InvalidUsageException.ErrorCode.FIELD_NOT_WITHHOLDABLE, ex.getErrorCode());
+ }
+
+ // Matrix 4, Row 3 IH: isWithholdable on repeatable field from root
+ @Test
+ void testComputedProperty_isWithholdable_RepeatableFromRoot() {
+ testExpressionTranslationWithContext(
+ "true()",
+ "ND-Root",
+ "BT-00-Repeatable-Text:isWithholdable");
+ }
+
+ // Matrix 4, Row 3 ID: isDisclosed on repeatable field from root
+ @Test
+ void testComputedProperty_isDisclosed_RepeatableFromRoot() {
+ final String privacyPath = "PathNode/FieldsPrivacy[FieldIdentifierCode/text()='rep-text-priv']";
+ testExpressionTranslationWithContext(
+ privacyPath + "/FieldIdentifierCode/normalize-space(text()) = 'rep-text-priv'"
+ + " and "
+ + "not((not(" + privacyPath + "/PublicationDate)"
+ + " or "
+ + privacyPath + "/PublicationDate/xs:date(text()) > current-date()))"
+ + " and "
+ + "not(PathNode/RepeatableTextField/normalize-space(text()) = 'unpublished')",
+ "ND-Root",
+ "BT-00-Repeatable-Text:isDisclosed");
+ }
+
+ // Matrix 4, Row 4 ID: isDisclosed on repeatable field from own context
+ @Test
+ void testComputedProperty_isDisclosed_RepeatableFromOwnContext() {
+ final String privacyPath = "../FieldsPrivacy[FieldIdentifierCode/text()='rep-text-priv']";
+ testExpressionTranslationWithContext(
+ privacyPath + "/FieldIdentifierCode/normalize-space(text()) = 'rep-text-priv'"
+ + " and "
+ + "not((not(" + privacyPath + "/PublicationDate)"
+ + " or "
+ + privacyPath + "/PublicationDate/xs:date(text()) > current-date()))"
+ + " and "
+ + "not(./normalize-space(text()) = 'unpublished')",
+ "BT-00-Repeatable-Text",
+ "BT-00-Repeatable-Text:isDisclosed");
+ }
+
+ // Matrix 4, Row 4 IM: isMasked on repeatable field from own context
+ @Test
+ void testComputedProperty_isMasked_RepeatableFromOwnContext() {
+ final String privacyPath = "../FieldsPrivacy[FieldIdentifierCode/text()='rep-text-priv']";
+ testExpressionTranslationWithContext(
+ privacyPath + "/FieldIdentifierCode/normalize-space(text()) = 'rep-text-priv'"
+ + " and "
+ + "./normalize-space(text()) = 'unpublished'",
+ "BT-00-Repeatable-Text",
+ "BT-00-Repeatable-Text:isMasked");
+ }
+
+ // Matrix 4, Row 5 IH: isWithholdable on field in repeatable node from root
+ @Test
+ void testComputedProperty_isWithholdable_FieldInRepeatableNode_FromRoot() {
+ testExpressionTranslationWithContext(
+ "true()",
+ "ND-Root",
+ "BT-00-Text-In-Repeatable-Node:isWithholdable");
+ }
+
+ // Matrix 4, Row 6 WW: wasWithheld on multilingual field
+ @Test
+ void testComputedProperty_wasWithheld_Multilingual() {
+ testExpressionTranslationWithContext(
+ "PathNode/FieldsPrivacy[FieldIdentifierCode/text()='ml-text-priv']/FieldIdentifierCode/normalize-space(text()) = 'ml-text-priv'",
+ "ND-Root",
+ "BT-00-Text-Multilingual:wasWithheld");
+ }
+
+ // Matrix 4, Row 6 IW: isWithheld on multilingual field
+ @Test
+ void testComputedProperty_isWithheld_Multilingual() {
+ final String privacyPath = "PathNode/FieldsPrivacy[FieldIdentifierCode/text()='ml-text-priv']";
+ testExpressionTranslationWithContext(
+ privacyPath + "/FieldIdentifierCode/normalize-space(text()) = 'ml-text-priv'"
+ + " and "
+ + "(not(" + privacyPath + "/PublicationDate)"
+ + " or "
+ + privacyPath + "/PublicationDate/xs:date(text()) > current-date())",
+ "ND-Root",
+ "BT-00-Text-Multilingual:isWithheld");
+ }
+
+ // Matrix 4, Row 6 ID: isDisclosed on multilingual field
+ @Test
+ void testComputedProperty_isDisclosed_Multilingual() {
+ final String privacyPath = "PathNode/FieldsPrivacy[FieldIdentifierCode/text()='ml-text-priv']";
+ testExpressionTranslationWithContext(
+ privacyPath + "/FieldIdentifierCode/normalize-space(text()) = 'ml-text-priv'"
+ + " and "
+ + "not((not(" + privacyPath + "/PublicationDate)"
+ + " or "
+ + privacyPath + "/PublicationDate/xs:date(text()) > current-date()))"
+ + " and "
+ + "not(PathNode/TextMultilingualField/normalize-space(text()) = 'unpublished')",
+ "ND-Root",
+ "BT-00-Text-Multilingual:isDisclosed");
+ }
+
+ // Matrix 4, Row 6 IM: isMasked on multilingual field
+ @Test
+ void testComputedProperty_isMasked_Multilingual() {
+ final String privacyPath = "PathNode/FieldsPrivacy[FieldIdentifierCode/text()='ml-text-priv']";
+ testExpressionTranslationWithContext(
+ privacyPath + "/FieldIdentifierCode/normalize-space(text()) = 'ml-text-priv'"
+ + " and "
+ + "PathNode/TextMultilingualField/normalize-space(text()) = 'unpublished'",
+ "ND-Root",
+ "BT-00-Text-Multilingual:isMasked");
+ }
+
+ // Matrix 4, Row 3 PC: privacyCode on repeatable field from root
+ @Test
+ void testComputedProperty_privacyCode_RepeatableFromRoot() {
+ testExpressionTranslationWithContext(
+ "'rep-text-priv'",
+ "ND-Root",
+ "BT-00-Repeatable-Text:privacyCode");
+ }
+
+ // Matrix 4, Row 4 IH: isWithholdable on repeatable field from own context
+ @Test
+ void testComputedProperty_isWithholdable_RepeatableFromOwnContext() {
+ testExpressionTranslationWithContext(
+ "true()",
+ "BT-00-Repeatable-Text",
+ "BT-00-Repeatable-Text:isWithholdable");
+ }
+
+ // Matrix 4, Row 4 PC: privacyCode on repeatable field from own context
+ @Test
+ void testComputedProperty_privacyCode_RepeatableFromOwnContext() {
+ testExpressionTranslationWithContext(
+ "'rep-text-priv'",
+ "BT-00-Repeatable-Text",
+ "BT-00-Repeatable-Text:privacyCode");
+ }
+
+ // Matrix 4, Row 5 PC: privacyCode on field in repeatable node from root
+ @Test
+ void testComputedProperty_privacyCode_FieldInRepeatableNode_FromRoot() {
+ testExpressionTranslationWithContext(
+ "'test-priv'",
+ "ND-Root",
+ "BT-00-Text-In-Repeatable-Node:privacyCode");
+ }
+
+ // Matrix 4, Row 6 IH: isWithholdable on multilingual field
+ @Test
+ void testComputedProperty_isWithholdable_Multilingual() {
+ testExpressionTranslationWithContext(
+ "true()",
+ "ND-Root",
+ "BT-00-Text-Multilingual:isWithholdable");
+ }
+
+ // Matrix 4, Row 6 PC: privacyCode on multilingual field — succeeds (compile-time constant)
+ @Test
+ void testComputedProperty_privacyCode_Multilingual() {
+ testExpressionTranslationWithContext(
+ "'ml-text-priv'",
+ "ND-Root",
+ "BT-00-Text-Multilingual:privacyCode");
+ }
+
+ // #endregion: Privacy property cardinality
+
+ // #endregion: Cardinality x Consumer Gaps
}
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 c84698f..39b0373 100644
--- a/src/test/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2Test.java
+++ b/src/test/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2Test.java
@@ -1558,6 +1558,176 @@ void testPreferredLanguageTextFunction() {
translateTemplate("{/} ${preferred-language-text(BT-00-Text-Multilingual)}"));
}
+ @Test
+ void testPreferredLanguageFunction_WithPredicate() {
+ assertEquals(
+ lines("TEMPLATES:",
+ "let body01() -> { eval(efx:preferred-language(PathNode/TextMultilingualField['a' = 'a'])) }",
+ "MAIN:", "for-each(/*).call(body01())"),
+ translateTemplate("{/} ${preferred-language(BT-00-Text-Multilingual['a' == 'a'])}"));
+ }
+
+ @Test
+ void testPreferredLanguageTextFunction_WithPredicate() {
+ assertEquals(
+ lines("TEMPLATES:",
+ "let body01() -> { eval(efx:preferred-language-text(PathNode/TextMultilingualField['a' = 'a'])) }",
+ "MAIN:", "for-each(/*).call(body01())"),
+ translateTemplate("{/} ${preferred-language-text(BT-00-Text-Multilingual['a' == 'a'])}"));
+ }
+
+ @Test
+ void testPreferredLanguageFunction_RepeatableField() {
+ assertEquals(
+ lines("TEMPLATES:",
+ "let body01() -> { eval(efx:preferred-language(SubNode/RepeatableInSubNode/TextMultilingual)) }",
+ "MAIN:", "for-each(/*).call(body01())"),
+ translateTemplate("{/} ${preferred-language(BT-13-TextMultilingual)}"));
+ }
+
+ @Test
+ void testPreferredLanguageTextFunction_RepeatableField() {
+ assertEquals(
+ lines("TEMPLATES:",
+ "let body01() -> { eval(efx:preferred-language-text(SubNode/RepeatableInSubNode/TextMultilingual)) }",
+ "MAIN:", "for-each(/*).call(body01())"),
+ translateTemplate("{/} ${preferred-language-text(BT-13-TextMultilingual)}"));
+ }
+
+ @Test
+ void testPreferredLanguageFunction_RepeatableFieldWithIndexer() {
+ assertEquals(
+ lines("TEMPLATES:",
+ "let body01() -> { eval(efx:preferred-language(SubNode/RepeatableInSubNode/TextMultilingual[1])) }",
+ "MAIN:", "for-each(/*).call(body01())"),
+ translateTemplate("{/} ${preferred-language(BT-13-TextMultilingual[1])}"));
+ }
+
+ @Test
+ void testPreferredLanguageTextFunction_RepeatableFieldWithIndexer() {
+ assertEquals(
+ lines("TEMPLATES:",
+ "let body01() -> { eval(efx:preferred-language-text(SubNode/RepeatableInSubNode/TextMultilingual[1])) }",
+ "MAIN:", "for-each(/*).call(body01())"),
+ translateTemplate("{/} ${preferred-language-text(BT-13-TextMultilingual[1])}"));
+ }
+
+ @Test
+ void testPreferredLanguageTextFunction_RepeatableFieldWithPredicateAndIndexer() {
+ assertEquals(
+ lines("TEMPLATES:",
+ "let body01() -> { eval(efx:preferred-language-text(SubNode/RepeatableInSubNode/TextMultilingual['a' = 'a'][1])) }",
+ "MAIN:", "for-each(/*).call(body01())"),
+ translateTemplate("{/} ${preferred-language-text(BT-13-TextMultilingual['a' == 'a'][1])}"));
+ }
+
+ @Test
+ void testPreferredLanguageFunction_NonMultilingualField_Throws() {
+ assertThrows(TypeMismatchException.class,
+ () -> translateTemplate("{/} ${preferred-language(BT-00-Text)}"));
+ }
+
+ @Test
+ void testPreferredLanguageTextFunction_NonMultilingualField_Throws() {
+ assertThrows(TypeMismatchException.class,
+ () -> translateTemplate("{/} ${preferred-language-text(BT-00-Text)}"));
+ }
+
+ // Property syntax: fieldContext:preferredLanguage / fieldContext:preferredLanguageText
+
+ @Test
+ void testPreferredLanguageProperty() {
+ assertEquals(
+ lines("TEMPLATES:",
+ "let body01() -> { eval(efx:preferred-language(PathNode/TextMultilingualField)) }",
+ "MAIN:", "for-each(/*).call(body01())"),
+ translateTemplate("{/} ${BT-00-Text-Multilingual:preferredLanguage}"));
+ }
+
+ @Test
+ void testPreferredLanguageTextProperty() {
+ assertEquals(
+ lines("TEMPLATES:",
+ "let body01() -> { eval(efx:preferred-language-text(PathNode/TextMultilingualField)) }",
+ "MAIN:", "for-each(/*).call(body01())"),
+ translateTemplate("{/} ${BT-00-Text-Multilingual:preferredLanguageText}"));
+ }
+
+ @Test
+ void testPreferredLanguageProperty_WithPredicate() {
+ assertEquals(
+ lines("TEMPLATES:",
+ "let body01() -> { eval(efx:preferred-language(PathNode/TextMultilingualField['a' = 'a'])) }",
+ "MAIN:", "for-each(/*).call(body01())"),
+ translateTemplate("{/} ${BT-00-Text-Multilingual['a' == 'a']:preferredLanguage}"));
+ }
+
+ @Test
+ void testPreferredLanguageTextProperty_WithPredicate() {
+ assertEquals(
+ lines("TEMPLATES:",
+ "let body01() -> { eval(efx:preferred-language-text(PathNode/TextMultilingualField['a' = 'a'])) }",
+ "MAIN:", "for-each(/*).call(body01())"),
+ translateTemplate("{/} ${BT-00-Text-Multilingual['a' == 'a']:preferredLanguageText}"));
+ }
+
+ @Test
+ void testPreferredLanguageProperty_RepeatableField() {
+ assertEquals(
+ lines("TEMPLATES:",
+ "let body01() -> { eval(efx:preferred-language(SubNode/RepeatableInSubNode/TextMultilingual)) }",
+ "MAIN:", "for-each(/*).call(body01())"),
+ translateTemplate("{/} ${BT-13-TextMultilingual:preferredLanguage}"));
+ }
+
+ @Test
+ void testPreferredLanguageTextProperty_RepeatableField() {
+ assertEquals(
+ lines("TEMPLATES:",
+ "let body01() -> { eval(efx:preferred-language-text(SubNode/RepeatableInSubNode/TextMultilingual)) }",
+ "MAIN:", "for-each(/*).call(body01())"),
+ translateTemplate("{/} ${BT-13-TextMultilingual:preferredLanguageText}"));
+ }
+
+ @Test
+ void testPreferredLanguageProperty_RepeatableFieldWithIndexer() {
+ assertEquals(
+ lines("TEMPLATES:",
+ "let body01() -> { eval(efx:preferred-language(SubNode/RepeatableInSubNode/TextMultilingual[1])) }",
+ "MAIN:", "for-each(/*).call(body01())"),
+ translateTemplate("{/} ${BT-13-TextMultilingual[1]:preferredLanguage}"));
+ }
+
+ @Test
+ void testPreferredLanguageTextProperty_RepeatableFieldWithIndexer() {
+ assertEquals(
+ lines("TEMPLATES:",
+ "let body01() -> { eval(efx:preferred-language-text(SubNode/RepeatableInSubNode/TextMultilingual[1])) }",
+ "MAIN:", "for-each(/*).call(body01())"),
+ translateTemplate("{/} ${BT-13-TextMultilingual[1]:preferredLanguageText}"));
+ }
+
+ @Test
+ void testPreferredLanguageTextProperty_RepeatableFieldWithPredicateAndIndexer() {
+ assertEquals(
+ lines("TEMPLATES:",
+ "let body01() -> { eval(efx:preferred-language-text(SubNode/RepeatableInSubNode/TextMultilingual['a' = 'a'][1])) }",
+ "MAIN:", "for-each(/*).call(body01())"),
+ translateTemplate("{/} ${BT-13-TextMultilingual['a' == 'a'][1]:preferredLanguageText}"));
+ }
+
+ @Test
+ void testPreferredLanguageProperty_NonMultilingualField_Throws() {
+ assertThrows(TypeMismatchException.class,
+ () -> translateTemplate("{/} ${BT-00-Text:preferredLanguage}"));
+ }
+
+ @Test
+ void testPreferredLanguageTextProperty_NonMultilingualField_Throws() {
+ assertThrows(TypeMismatchException.class,
+ () -> translateTemplate("{/} ${BT-00-Text:preferredLanguageText}"));
+ }
+
// #endregion Preferred language functions -------------------------------------
// #region contextDeclarationBlock -------------------------------------------
@@ -2070,4 +2240,63 @@ void testInclude_SingleFile_SameOutputAsInlined() throws Exception {
}
// #endregion Include directive ------------------------------------------------
+
+ // #region: fieldContext structural variations in template context declarations
+
+ @Test
+ void testContextDeclaration_WithFieldContextIndexer() {
+ assertEquals(
+ lines(
+ "TEMPLATES:",
+ "let body01() -> { eval(./normalize-space(text())) }",
+ "MAIN:",
+ "for-each(/*/PathNode/RepeatableTextField[1]).call(body01())"),
+ translateTemplate("{BT-00-Repeatable-Text[1]} ${BT-00-Repeatable-Text}"));
+ }
+
+ @Test
+ void testContextDeclaration_WithFieldContextPredicate() {
+ assertEquals(
+ lines(
+ "TEMPLATES:",
+ "let body01() -> { eval(./normalize-space(text())) }",
+ "MAIN:",
+ "for-each(/*/PathNode/TextField['a' = 'a']).call(body01())"),
+ translateTemplate("{BT-00-Text['a' == 'a']} ${BT-00-Text}"));
+ }
+
+ @Test
+ void testContextDeclaration_WithFieldContextPredicateAndIndexer() {
+ assertEquals(
+ lines(
+ "TEMPLATES:",
+ "let body01() -> { eval(./normalize-space(text())) }",
+ "MAIN:",
+ "for-each(/*/PathNode/RepeatableTextField['a' = 'a'][1]).call(body01())"),
+ translateTemplate("{BT-00-Repeatable-Text['a' == 'a'][1]} ${BT-00-Repeatable-Text}"));
+ }
+
+ @Test
+ void testContextDeclaration_AbsoluteReferenceWithPredicate() {
+ assertEquals(
+ lines(
+ "TEMPLATES:",
+ "let body01() -> { eval(./normalize-space(text())) }",
+ "MAIN:",
+ "for-each(/*/PathNode/TextField['a' = 'a']).call(body01())"),
+ translateTemplate("{/BT-00-Text['a' == 'a']} ${BT-00-Text}"));
+ }
+
+ @Test
+ void testContextDeclaration_AbsoluteReferenceWithIndexer() {
+ assertEquals(
+ lines(
+ "TEMPLATES:",
+ "let body01() -> { eval(./normalize-space(text())) }",
+ "MAIN:",
+ "for-each(/*/PathNode/RepeatableTextField[1]).call(body01())"),
+ translateTemplate("{/BT-00-Repeatable-Text[1]} ${BT-00-Repeatable-Text}"));
+ }
+
+ // #endregion: fieldContext structural variations in template context declarations
}
\ No newline at end of file
diff --git a/src/test/java/eu/europa/ted/efx/sdk2/SdkSymbolResolverTest.java b/src/test/java/eu/europa/ted/efx/sdk2/SdkSymbolResolverTest.java
index b2cf014..2dfe055 100644
--- a/src/test/java/eu/europa/ted/efx/sdk2/SdkSymbolResolverTest.java
+++ b/src/test/java/eu/europa/ted/efx/sdk2/SdkSymbolResolverTest.java
@@ -35,7 +35,7 @@
import eu.europa.ted.efx.model.expressions.scalar.BooleanPath;
import eu.europa.ted.efx.model.expressions.scalar.DatePath;
import eu.europa.ted.efx.model.expressions.scalar.DurationPath;
-import eu.europa.ted.efx.model.expressions.scalar.MultilingualStringPath;
+import eu.europa.ted.efx.model.expressions.sequence.MultilingualStringSequencePath;
import eu.europa.ted.efx.model.expressions.scalar.NodePath;
import eu.europa.ted.efx.model.expressions.scalar.NumericPath;
import eu.europa.ted.efx.model.expressions.scalar.ScalarPath;
@@ -365,11 +365,11 @@ void codeField_shouldReturnStringPath() {
}
@Test
- @DisplayName("Multilingual field returns MultilingualStringPath")
- void multilingualField_shouldReturnMultilingualStringPath() {
+ @DisplayName("Multilingual field returns MultilingualStringSequencePath")
+ void multilingualField_shouldReturnMultilingualStringSequencePath() {
PathExpression path = resolver.getAbsolutePathOfField("BT-00-Text-Multilingual");
- assertEquals(MultilingualStringPath.class, path.getClass(),
- "Multilingual field should return MultilingualStringPath");
+ assertEquals(MultilingualStringSequencePath.class, path.getClass(),
+ "Multilingual field should return MultilingualStringSequencePath");
}
@Test
diff --git a/src/test/resources/json/sdk2-fields.json b/src/test/resources/json/sdk2-fields.json
index e43e391..a8e6904 100644
--- a/src/test/resources/json/sdk2-fields.json
+++ b/src/test/resources/json/sdk2-fields.json
@@ -103,6 +103,13 @@
"xpathRelative": "PathNode/FieldsPrivacy[FieldIdentifierCode/text()='rep-text-priv']",
"repeatable": false
},
+ {
+ "id": "ND-PrivacyForMultilingualText",
+ "parentId": "ND-Root",
+ "xpathAbsolute": "/*/PathNode/FieldsPrivacy[FieldIdentifierCode/text()='ml-text-priv']",
+ "xpathRelative": "PathNode/FieldsPrivacy[FieldIdentifierCode/text()='ml-text-priv']",
+ "repeatable": false
+ },
{
"id": "ND-PrivacyInRepeatableNode",
"parentId": "ND-RepeatableNode",
@@ -194,7 +201,14 @@
"type": "text-multilingual",
"parentNodeId": "ND-Root",
"xpathAbsolute": "/*/PathNode/TextMultilingualField",
- "xpathRelative": "PathNode/TextMultilingualField"
+ "xpathRelative": "PathNode/TextMultilingualField",
+ "privacy": {
+ "code": "ml-text-priv",
+ "unpublishedFieldId": "BT-195(BT-00)-Text-Multilingual",
+ "reasonCodeFieldId": "BT-197(BT-00)-Text-Multilingual",
+ "reasonDescriptionFieldId": "BT-196(BT-00)-Text-Multilingual",
+ "publicationDateFieldId": "BT-198(BT-00)-Text-Multilingual"
+ }
},
{
"id": "BT-00-StartDate",
@@ -1085,6 +1099,35 @@
"parentNodeId": "ND-PrivacyForRepeatableText",
"xpathAbsolute": "/*/PathNode/FieldsPrivacy[FieldIdentifierCode/text()='rep-text-priv']/PublicationDate",
"xpathRelative": "PublicationDate"
+ },
+ {
+ "id": "BT-195(BT-00)-Text-Multilingual",
+ "type": "code",
+ "parentNodeId": "ND-PrivacyForMultilingualText",
+ "xpathAbsolute": "/*/PathNode/FieldsPrivacy[FieldIdentifierCode/text()='ml-text-priv']/FieldIdentifierCode",
+ "xpathRelative": "FieldIdentifierCode",
+ "presetValue": "ml-text-priv"
+ },
+ {
+ "id": "BT-196(BT-00)-Text-Multilingual",
+ "type": "text-multilingual",
+ "parentNodeId": "ND-PrivacyForMultilingualText",
+ "xpathAbsolute": "/*/PathNode/FieldsPrivacy[FieldIdentifierCode/text()='ml-text-priv']/ReasonDescription",
+ "xpathRelative": "ReasonDescription"
+ },
+ {
+ "id": "BT-197(BT-00)-Text-Multilingual",
+ "type": "code",
+ "parentNodeId": "ND-PrivacyForMultilingualText",
+ "xpathAbsolute": "/*/PathNode/FieldsPrivacy[FieldIdentifierCode/text()='ml-text-priv']/ReasonCode",
+ "xpathRelative": "ReasonCode"
+ },
+ {
+ "id": "BT-198(BT-00)-Text-Multilingual",
+ "type": "date",
+ "parentNodeId": "ND-PrivacyForMultilingualText",
+ "xpathAbsolute": "/*/PathNode/FieldsPrivacy[FieldIdentifierCode/text()='ml-text-priv']/PublicationDate",
+ "xpathRelative": "PublicationDate"
}
]
}