Skip to content
Open
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
5 changes: 3 additions & 2 deletions src/main/java/eu/europa/ted/eforms/sdk/SdkSymbolResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -414,9 +414,10 @@ public boolean isFieldRepeatableFromContext(final String fieldId, final String c
}

private boolean isFieldRepeatableFromContext(final SdkField sdkField, final SdkField context) {
// If the field itself is repeatable, it returns multiple values
// If the field itself is repeatable, it returns multiple values UNLESS it IS the context
// (e.g., inside a predicate on this field: BT-Repeatable[BT-Repeatable != ''])
if (sdkField.isRepeatable()) {
return true;
return !sdkField.equals(context);
}

// Use cached ancestry from node
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ public class TypeMismatchException extends ParseCancellationException {
public enum ErrorCode {
CANNOT_CONVERT,
CANNOT_COMPARE,
EXPECTED_SEQUENCE,
EXPECTED_SCALAR,
EXPECTED_FIELD_CONTEXT
}

private static final String CANNOT_CONVERT = "Type mismatch. Expected %s instead of %s.";
private static final String CANNOT_COMPARE = "Type mismatch. Cannot compare values of different types: %s and %s";
private static final String EXPECTED_SEQUENCE = "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 EXPECTED_SCALAR = "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 EXPECTED_FIELD_CONTEXT = "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 final ErrorCode errorCode;
Expand Down Expand Up @@ -71,7 +71,7 @@ public static TypeMismatchException cannotCompare(Expression left, Expression ri
}

public static TypeMismatchException fieldMayRepeat(String fieldId, String contextSymbol) {
return new TypeMismatchException(ErrorCode.EXPECTED_SEQUENCE, String.format(EXPECTED_SEQUENCE, fieldId,
return new TypeMismatchException(ErrorCode.EXPECTED_SCALAR, String.format(EXPECTED_SCALAR, fieldId,
contextSymbol != null ? contextSymbol : "root"));
}

Expand Down
37 changes: 37 additions & 0 deletions src/main/java/eu/europa/ted/efx/interfaces/ScriptGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,13 @@
import eu.europa.ted.efx.model.expressions.scalar.ScalarExpression;
import eu.europa.ted.efx.model.expressions.scalar.StringExpression;
import eu.europa.ted.efx.model.expressions.scalar.TimeExpression;
import eu.europa.ted.efx.model.expressions.sequence.BooleanSequenceExpression;
import eu.europa.ted.efx.model.expressions.sequence.DateSequenceExpression;
import eu.europa.ted.efx.model.expressions.sequence.DurationSequenceExpression;
import eu.europa.ted.efx.model.expressions.sequence.NumericSequenceExpression;
import eu.europa.ted.efx.model.expressions.sequence.SequenceExpression;
import eu.europa.ted.efx.model.expressions.sequence.StringSequenceExpression;
import eu.europa.ted.efx.model.expressions.sequence.TimeSequenceExpression;

/**
* A ScriptGenerator is used by the EFX expression translator to translate specific computations to
Expand Down Expand Up @@ -412,9 +416,42 @@ public StringExpression composeSubstringExtraction(StringExpression text, Numeri

public BooleanExpression composeExistsCondition(PathExpression reference);

/**
* Uniqueness check for EFX 1 syntax.
* <p>
* This method supports the limited uniqueness syntax available in EFX 1.
* It is used exclusively by the EFX 1 translator and is kept for backward
* compatibility with EFX 1.
* <p>
* <b>EFX 2 does not use this method.</b> EFX 2's stricter type checking enables
* more powerful uniqueness syntax, supported by the typed overloads below.
*
* @param needle The value to check for uniqueness
* @param haystack The collection to search within
* @return A boolean expression evaluating to true if needle appears exactly once in haystack
*/
public BooleanExpression composeUniqueValueCondition(PathExpression needle,
PathExpression haystack);

// Typed uniqueness conditions (EFX 2)
public BooleanExpression composeUniqueValueCondition(StringExpression needle,
StringSequenceExpression haystack);

public BooleanExpression composeUniqueValueCondition(NumericExpression needle,
NumericSequenceExpression haystack);

public BooleanExpression composeUniqueValueCondition(BooleanExpression needle,
BooleanSequenceExpression haystack);

public BooleanExpression composeUniqueValueCondition(DateExpression needle,
DateSequenceExpression haystack);

public BooleanExpression composeUniqueValueCondition(TimeExpression needle,
TimeSequenceExpression haystack);

public BooleanExpression composeUniqueValueCondition(DurationExpression needle,
DurationSequenceExpression haystack);

public BooleanExpression composeSequenceEqualFunction(SequenceExpression one,
SequenceExpression two);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -430,9 +430,74 @@ public void exitPresenceCondition(EfxParser.PresenceConditionContext ctx) {
}

@Override
public void exitUniqueValueCondition(EfxParser.UniqueValueConditionContext ctx) {
PathExpression haystack = this.stack.pop(PathExpression.class);
PathExpression needle = this.stack.pop(haystack.getClass());
public void exitStringUniqueValueCondition(EfxParser.StringUniqueValueConditionContext ctx) {
StringSequenceExpression haystack = this.stack.pop(StringSequenceExpression.class);
StringExpression needle = this.stack.pop(StringExpression.class);

if (ctx.modifier != null && ctx.modifier.getText().equals(NOT_MODIFIER)) {
this.stack.push(
this.script.composeLogicalNot(this.script.composeUniqueValueCondition(needle, haystack)));
} else {
this.stack.push(this.script.composeUniqueValueCondition(needle, haystack));
}
}

@Override
public void exitNumericUniqueValueCondition(EfxParser.NumericUniqueValueConditionContext ctx) {
NumericSequenceExpression haystack = this.stack.pop(NumericSequenceExpression.class);
NumericExpression needle = this.stack.pop(NumericExpression.class);

if (ctx.modifier != null && ctx.modifier.getText().equals(NOT_MODIFIER)) {
this.stack.push(
this.script.composeLogicalNot(this.script.composeUniqueValueCondition(needle, haystack)));
} else {
this.stack.push(this.script.composeUniqueValueCondition(needle, haystack));
}
}

@Override
public void exitBooleanUniqueValueCondition(EfxParser.BooleanUniqueValueConditionContext ctx) {
BooleanSequenceExpression haystack = this.stack.pop(BooleanSequenceExpression.class);
BooleanExpression needle = this.stack.pop(BooleanExpression.class);

if (ctx.modifier != null && ctx.modifier.getText().equals(NOT_MODIFIER)) {
this.stack.push(
this.script.composeLogicalNot(this.script.composeUniqueValueCondition(needle, haystack)));
} else {
this.stack.push(this.script.composeUniqueValueCondition(needle, haystack));
}
}

@Override
public void exitDateUniqueValueCondition(EfxParser.DateUniqueValueConditionContext ctx) {
DateSequenceExpression haystack = this.stack.pop(DateSequenceExpression.class);
DateExpression needle = this.stack.pop(DateExpression.class);

if (ctx.modifier != null && ctx.modifier.getText().equals(NOT_MODIFIER)) {
this.stack.push(
this.script.composeLogicalNot(this.script.composeUniqueValueCondition(needle, haystack)));
} else {
this.stack.push(this.script.composeUniqueValueCondition(needle, haystack));
}
}

@Override
public void exitTimeUniqueValueCondition(EfxParser.TimeUniqueValueConditionContext ctx) {
TimeSequenceExpression haystack = this.stack.pop(TimeSequenceExpression.class);
TimeExpression needle = this.stack.pop(TimeExpression.class);

if (ctx.modifier != null && ctx.modifier.getText().equals(NOT_MODIFIER)) {
this.stack.push(
this.script.composeLogicalNot(this.script.composeUniqueValueCondition(needle, haystack)));
} else {
this.stack.push(this.script.composeUniqueValueCondition(needle, haystack));
}
}

@Override
public void exitDurationUniqueValueCondition(EfxParser.DurationUniqueValueConditionContext ctx) {
DurationSequenceExpression haystack = this.stack.pop(DurationSequenceExpression.class);
DurationExpression needle = this.stack.pop(DurationExpression.class);

if (ctx.modifier != null && ctx.modifier.getText().equals(NOT_MODIFIER)) {
this.stack.push(
Expand Down
44 changes: 44 additions & 0 deletions src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,13 @@
import eu.europa.ted.efx.model.expressions.scalar.StringLiteral;
import eu.europa.ted.efx.model.expressions.scalar.TimeExpression;
import eu.europa.ted.efx.model.expressions.scalar.TimeLiteral;
import eu.europa.ted.efx.model.expressions.sequence.BooleanSequenceExpression;
import eu.europa.ted.efx.model.expressions.sequence.DateSequenceExpression;
import eu.europa.ted.efx.model.expressions.sequence.DurationSequenceExpression;
import eu.europa.ted.efx.model.expressions.sequence.NumericSequenceExpression;
import eu.europa.ted.efx.model.expressions.sequence.SequenceExpression;
import eu.europa.ted.efx.model.expressions.sequence.StringSequenceExpression;
import eu.europa.ted.efx.model.expressions.sequence.TimeSequenceExpression;
import eu.europa.ted.efx.model.types.EfxDataType;

@SdkComponent(versions = {"2"},
Expand Down Expand Up @@ -343,13 +347,53 @@ public BooleanExpression composeExistsCondition(PathExpression reference) {
return new BooleanExpression(reference.getScript());
}

/**
* EFX 1 uniqueness check - kept for backward compatibility.
* EFX 2 uses the typed overloads below.
*/
@Override
public BooleanExpression composeUniqueValueCondition(PathExpression needle,
PathExpression haystack) {
return new BooleanExpression("count(for $x in " + needle.getScript() + ", $y in " + haystack.getScript()
+ "[. = $x] return $y) = 1");
}

@Override
public BooleanExpression composeUniqueValueCondition(StringExpression needle,
StringSequenceExpression haystack) {
return new BooleanExpression("count(" + haystack.getScript() + "[. = " + needle.getScript() + "]) = 1");
}

@Override
public BooleanExpression composeUniqueValueCondition(NumericExpression needle,
NumericSequenceExpression haystack) {
return new BooleanExpression("count(" + haystack.getScript() + "[. = " + needle.getScript() + "]) = 1");
}

@Override
public BooleanExpression composeUniqueValueCondition(BooleanExpression needle,
BooleanSequenceExpression haystack) {
return new BooleanExpression("count(" + haystack.getScript() + "[. = " + needle.getScript() + "]) = 1");
}

@Override
public BooleanExpression composeUniqueValueCondition(DateExpression needle,
DateSequenceExpression haystack) {
return new BooleanExpression("count(" + haystack.getScript() + "[. = " + needle.getScript() + "]) = 1");
}

@Override
public BooleanExpression composeUniqueValueCondition(TimeExpression needle,
TimeSequenceExpression haystack) {
return new BooleanExpression("count(" + haystack.getScript() + "[. = " + needle.getScript() + "]) = 1");
}

@Override
public BooleanExpression composeUniqueValueCondition(DurationExpression needle,
DurationSequenceExpression haystack) {
return new BooleanExpression("count(" + haystack.getScript() + "[. = " + needle.getScript() + "]) = 1");
}

//#endregion Boolean Expressions ------------------------------------------

//#region Boolean functions -----------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,17 +78,66 @@ void testPresenceCondition_WithNot() {
@Test
void testUniqueValueCondition() {
testExpressionTranslationWithContext(
"count(for $x in PathNode/TextField, $y in /*/PathNode/TextField[. = $x] return $y) = 1",
"count(/*/PathNode/TextField/normalize-space(text())[. = PathNode/TextField/normalize-space(text())]) = 1",
"ND-Root", "BT-00-Text is unique in /BT-00-Text");
}

@Test
void testUniqueValueCondition_WithNot() {
testExpressionTranslationWithContext(
"not(count(for $x in PathNode/TextField, $y in /*/PathNode/TextField[. = $x] return $y) = 1)",
"not(count(/*/PathNode/TextField/normalize-space(text())[. = PathNode/TextField/normalize-space(text())]) = 1)",
"ND-Root", "BT-00-Text is not unique in /BT-00-Text");
}

@Test
void testStringUniqueValueCondition_WithLiteralSequence() {
testExpressionTranslationWithContext(
"count(('a','b','c','b')[. = 'b']) = 1",
"BT-00-Text", "'b' is unique in ('a', 'b', 'c', 'b')");
}

@Test
void testNumericUniqueValueCondition_WithLiteralSequence() {
testExpressionTranslationWithContext(
"count((1,2,3,2)[. = 2]) = 1",
"BT-00-Number", "2 is unique in (1, 2, 3, 2)");
}

@Test
void testStringUniqueValueCondition_WithRepeatableField() {
testExpressionTranslationWithContext(
"count(/*/PathNode/RepeatableTextField/normalize-space(text())[. = PathNode/TextField/normalize-space(text())]) = 1",
"ND-Root", "BT-00-Text is unique in /BT-00-Repeatable-Text");
}

@Test
void testStringUniqueValueCondition_WithNot() {
testExpressionTranslationWithContext(
"not(count(('a','b','c')[. = 'x']) = 1)",
"BT-00-Text", "'x' is not unique in ('a', 'b', 'c')");
}

@Test
void testStringUniqueValueCondition_WithRelativeFieldReference() {
testExpressionTranslationWithContext(
"count(PathNode/RepeatableTextField/normalize-space(text())[. = PathNode/TextField/normalize-space(text())]) = 1",
"ND-Root", "BT-00-Text is unique in BT-00-Repeatable-Text");
}

@Test
void testStringUniqueValueCondition_WithFieldReferencePredicate() {
testExpressionTranslationWithContext(
"count(/*/PathNode/RepeatableTextField[./normalize-space(text()) != '']/normalize-space(text())[. = PathNode/TextField/normalize-space(text())]) = 1",
"ND-Root", "BT-00-Text is unique in /BT-00-Repeatable-Text[BT-00-Repeatable-Text != '']");
}

@Test
void testStringUniqueValueCondition_WithFieldInRepeatableNodePredicate() {
testExpressionTranslationWithContext(
"count(/*/RepeatableNode/TextField[./normalize-space(text()) != '']/normalize-space(text())[. = PathNode/TextField/normalize-space(text())]) = 1",
"ND-Root", "BT-00-Text is unique in /BT-00-Text-In-Repeatable-Node[BT-00-Text-In-Repeatable-Node != '']");
}


@Test
void testLikePatternCondition() {
Expand Down Expand Up @@ -1791,15 +1840,15 @@ void testScalarFromRepeatableField_ThrowsError() {
// A repeatable field used as scalar should throw TypeMismatchException.fieldMayRepeat()
TypeMismatchException ex = assertThrows(TypeMismatchException.class,
() -> translateExpressionWithContext("ND-Root", "BT-00-Repeatable-Text == 'test'"));
assertEquals(TypeMismatchException.ErrorCode.EXPECTED_SEQUENCE, ex.getErrorCode());
assertEquals(TypeMismatchException.ErrorCode.EXPECTED_SCALAR, ex.getErrorCode());
}

@Test
void testScalarFromFieldInRepeatableNode_ThrowsErrorFromRootContext() {
// Field in ND-RepeatableNode (repeatable) used as scalar from ND-Root should throw
TypeMismatchException ex = assertThrows(TypeMismatchException.class,
() -> translateExpressionWithContext("ND-Root", "BT-00-Text-In-Repeatable-Node == 'test'"));
assertEquals(TypeMismatchException.ErrorCode.EXPECTED_SEQUENCE, ex.getErrorCode());
assertEquals(TypeMismatchException.ErrorCode.EXPECTED_SCALAR, ex.getErrorCode());
}

@Test
Expand All @@ -1814,15 +1863,15 @@ void testScalarFromFieldInNestedRepeatableNode_ThrowsErrorFromRootContext() {
// Field in ND-RepeatableSubSubNode (inside ND-NonRepeatableSubNode inside ND-RepeatableNode) used from root should throw
TypeMismatchException ex = assertThrows(TypeMismatchException.class,
() -> translateExpressionWithContext("ND-Root", "BT-00-Text-In-RepeatableSubSubNode == 'test'"));
assertEquals(TypeMismatchException.ErrorCode.EXPECTED_SEQUENCE, ex.getErrorCode());
assertEquals(TypeMismatchException.ErrorCode.EXPECTED_SCALAR, ex.getErrorCode());
}

@Test
void testScalarFromFieldInNestedRepeatableNode_ThrowsErrorFromRepeatableNodeContext() {
// Field in ND-RepeatableSubSubNode used from ND-RepeatableNode should still throw (ND-RepeatableSubSubNode is also repeatable)
TypeMismatchException ex = assertThrows(TypeMismatchException.class,
() -> translateExpressionWithContext("ND-RepeatableNode", "BT-00-Text-In-RepeatableSubSubNode == 'test'"));
assertEquals(TypeMismatchException.ErrorCode.EXPECTED_SEQUENCE, ex.getErrorCode());
assertEquals(TypeMismatchException.ErrorCode.EXPECTED_SCALAR, ex.getErrorCode());
}

@Test
Expand All @@ -1837,7 +1886,7 @@ void testScalarFromFieldInNonRepeatableNestedInRepeatable_ThrowsErrorFromRootCon
// Field in ND-NonRepeatableSubNode (non-repeatable) inside ND-RepeatableNode (repeatable) used from root should throw
TypeMismatchException ex = assertThrows(TypeMismatchException.class,
() -> translateExpressionWithContext("ND-Root", "BT-00-Text-In-NonRepeatableSubNode == 'test'"));
assertEquals(TypeMismatchException.ErrorCode.EXPECTED_SEQUENCE, ex.getErrorCode());
assertEquals(TypeMismatchException.ErrorCode.EXPECTED_SCALAR, ex.getErrorCode());
}

@Test
Expand All @@ -1847,6 +1896,14 @@ void testScalarFromFieldInNonRepeatableNestedInRepeatable_OkFromRepeatableNodeCo
"ND-RepeatableNode", "BT-00-Text-In-NonRepeatableSubNode == 'test'");
}

@Test
void testRepeatableFieldInUniqueCondition_ThrowsError() {
// A repeatable field used as needle (left side) in uniqueness condition should throw
TypeMismatchException ex = assertThrows(TypeMismatchException.class,
() -> translateExpressionWithContext("ND-Root", "BT-00-Repeatable-Text is unique in /BT-00-Text"));
assertEquals(TypeMismatchException.ErrorCode.EXPECTED_SCALAR, ex.getErrorCode());
}

// #endregion: Scalar/Sequence Validation

// #region: InvalidIdentifierException Tests --------------------------------
Expand Down Expand Up @@ -1906,7 +1963,7 @@ void testScalarFromFieldContextVariable_Repeatable_ThrowsFieldMayRepeat() {
// A repeatable field context variable used as scalar should throw
TypeMismatchException ex = assertThrows(TypeMismatchException.class,
() -> translateExpressionWithContext("ND-Root", "for context:$f in BT-00-Repeatable-Text return $f == 'test'"));
assertEquals(TypeMismatchException.ErrorCode.EXPECTED_SEQUENCE, ex.getErrorCode());
assertEquals(TypeMismatchException.ErrorCode.EXPECTED_SCALAR, ex.getErrorCode());
}

// #endregion: TypeMismatchException - fieldMayRepeat (Context Variables)
Expand Down Expand Up @@ -1963,7 +2020,7 @@ void testPredicateComparison_RepeatableFieldAsScalar_ThrowsError() {
// Pattern: FIELD[REPEATABLE_FIELD == $var] - the repeatable field is used as scalar
TypeMismatchException ex = assertThrows(TypeMismatchException.class,
() -> translateExpressionWithContext("ND-Root", "BT-00-Text[BT-00-Repeatable-Text == 'test']"));
assertEquals(TypeMismatchException.ErrorCode.EXPECTED_SEQUENCE, ex.getErrorCode());
assertEquals(TypeMismatchException.ErrorCode.EXPECTED_SCALAR, ex.getErrorCode());
}

@Test
Expand Down
Loading
Loading