From 0d62981743da13b175f5c2e344415c450bf76c75 Mon Sep 17 00:00:00 2001 From: Ioannis Rosuochatzakis Date: Wed, 4 Feb 2026 04:36:11 +0100 Subject: [PATCH] TEDEFO-1854 Align toolkit to improved uniqueness testing - Implement typed uniqueness conditions (any value in any collection) - Fix: repeatable fields used as own context treated as scalar - Rename error code EXPECTED_SEQUENCE to EXPECTED_SCALAR --- .../ted/eforms/sdk/SdkSymbolResolver.java | 5 +- .../efx/exceptions/TypeMismatchException.java | 6 +- .../ted/efx/interfaces/ScriptGenerator.java | 37 +++++++++ .../efx/sdk2/EfxExpressionTranslatorV2.java | 71 +++++++++++++++++- .../ted/efx/xpath/XPathScriptGenerator.java | 44 +++++++++++ .../sdk2/EfxExpressionTranslatorV2Test.java | 75 ++++++++++++++++--- .../ted/efx/sdk2/SdkSymbolResolverTest.java | 14 ++++ 7 files changed, 235 insertions(+), 17 deletions(-) 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 faefcaf..08df970 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/SdkSymbolResolver.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/SdkSymbolResolver.java @@ -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 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 1eeb6e1..381a0b9 100644 --- a/src/main/java/eu/europa/ted/efx/exceptions/TypeMismatchException.java +++ b/src/main/java/eu/europa/ted/efx/exceptions/TypeMismatchException.java @@ -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; @@ -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")); } diff --git a/src/main/java/eu/europa/ted/efx/interfaces/ScriptGenerator.java b/src/main/java/eu/europa/ted/efx/interfaces/ScriptGenerator.java index 3b0c29c..8e67e89 100644 --- a/src/main/java/eu/europa/ted/efx/interfaces/ScriptGenerator.java +++ b/src/main/java/eu/europa/ted/efx/interfaces/ScriptGenerator.java @@ -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 @@ -412,9 +416,42 @@ public StringExpression composeSubstringExtraction(StringExpression text, Numeri public BooleanExpression composeExistsCondition(PathExpression reference); + /** + * Uniqueness check for EFX 1 syntax. + *

+ * 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. + *

+ * EFX 2 does not use this method. 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); 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 e439e85..c2e7fc4 100644 --- a/src/main/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2.java +++ b/src/main/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2.java @@ -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( 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 3bd4a92..a3da967 100644 --- a/src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java +++ b/src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java @@ -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"}, @@ -343,6 +347,10 @@ 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) { @@ -350,6 +358,42 @@ public BooleanExpression composeUniqueValueCondition(PathExpression needle, + "[. = $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 ----------------------------------------------- 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 3f1f83b..532f022 100644 --- a/src/test/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2Test.java +++ b/src/test/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2Test.java @@ -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() { @@ -1791,7 +1840,7 @@ 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 @@ -1799,7 +1848,7 @@ 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 @@ -1814,7 +1863,7 @@ 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 @@ -1822,7 +1871,7 @@ void testScalarFromFieldInNestedRepeatableNode_ThrowsErrorFromRepeatableNodeCont // 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 @@ -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 @@ -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 -------------------------------- @@ -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) @@ -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 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 70cbbb1..5698e35 100644 --- a/src/test/java/eu/europa/ted/efx/sdk2/SdkSymbolResolverTest.java +++ b/src/test/java/eu/europa/ted/efx/sdk2/SdkSymbolResolverTest.java @@ -183,6 +183,20 @@ void isFieldRepeatableFromContext_attributeInRepNode_fromRepNode_returnsFalse() assertFalse(resolver.isFieldRepeatableFromContext("BT-00-Attribute-In-Repeatable-Node", "ND-RepeatableNode"), "Attribute field should not be repeatable when context is its parent repeatable node"); } + + @Test + @DisplayName("Repeatable field from root returns true") + void isFieldRepeatableFromContext_repeatableField_fromRoot_returnsTrue() { + assertTrue(resolver.isFieldRepeatableFromContext("BT-00-Repeatable-Text", null), + "Repeatable field should return true from root context"); + } + + @Test + @DisplayName("Repeatable node from root returns true") + void isNodeRepeatableFromContext_repeatableNode_fromRoot_returnsTrue() { + assertTrue(resolver.isNodeRepeatableFromContext("ND-RepeatableNode", null), + "Repeatable node should return true from root context"); + } } // =========================================================================