Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
<version.jackson>2.18.3</version.jackson>
<version.jackson-databind>2.18.3</version.jackson-databind>
<version.junit-jupiter>5.7.2</version.junit-jupiter>
<version.saxon-he>11.3</version.saxon-he>
<version.slf4j>2.0.13</version.slf4j>

<!-- Versions - Plugins -->
Expand Down Expand Up @@ -127,6 +128,11 @@
<artifactId>freemarker</artifactId>
<version>${version.freemarker}</version>
</dependency>
<dependency>
<groupId>net.sf.saxon</groupId>
<artifactId>Saxon-HE</artifactId>
<version>${version.saxon-he}</version>
</dependency>
</dependencies>
</dependencyManagement>

Expand All @@ -147,6 +153,11 @@
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.sf.saxon</groupId>
<artifactId>Saxon-HE</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.antlr</groupId>
<artifactId>antlr4-runtime</artifactId>
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/eu/europa/ted/eforms/sdk/SdkSymbolResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> contextAncestry = context != null
? context.getParentNode().getAncestry()
Expand Down Expand Up @@ -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<String> contextAncestry = context != null
? context.getAncestry()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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].";
Expand Down Expand Up @@ -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);
}
Expand Down
101 changes: 54 additions & 47 deletions src/main/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2.java
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -1956,16 +1970,16 @@ 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;
case RESOLVE_SEQUENCE:
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);
Expand All @@ -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()
Expand All @@ -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}.
Expand Down Expand Up @@ -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));
}


Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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
Expand All @@ -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));
Expand Down Expand Up @@ -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),
"==",
Expand Down Expand Up @@ -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) {
Expand Down
20 changes: 13 additions & 7 deletions src/main/java/eu/europa/ted/efx/sdk2/EfxTemplateTranslatorV2.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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.*;

/**
Expand Down Expand Up @@ -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 -------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ public BooleanExpression composeAnySatisfies(
public <T extends TypedExpression> T composeConditionalExpression(BooleanExpression condition,
T whenTrue, T whenFalse, Class<T> type) {
return Expression.instantiate(
"(if " + condition.getScript() + " then " + whenTrue.getScript() + " else " + whenFalse.getScript() + ")",
"(if (" + condition.getScript() + ") then " + whenTrue.getScript() + " else " + whenFalse.getScript() + ")",
type);
}

Expand Down
Loading
Loading