This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 5c3f5bdea595 CAMEL-24582: simple language - add equals/!equals 
operators
5c3f5bdea595 is described below

commit 5c3f5bdea595cb44881af577da577af2e40c004c
Author: Karol Krawczyk <[email protected]>
AuthorDate: Tue Sep 1 22:17:35 2026 +0200

    CAMEL-24582: simple language - add equals/!equals operators
    
    Adds `equals` and `!equals` operators to the simple language, letting a 
comparison always be evaluated as text rather than favoring numeric comparison 
for all-digit strings (as `==` intentionally does). This lets users compare 
identifiers like padded account numbers where leading zeros carry meaning.
    
    `LanguageHelper.equalsString` and `PredicateBuilder.equalsString` mirror 
the existing `startsWith`/`endsWith` pair. Simple's tokenizer, operator 
constants, and generated catalog metadata are updated accordingly, along with 
documentation and tests.
    
    Closes #26022
    
    Co-authored-by: Claude <[email protected]>
---
 .../camel/catalog/docs/simple-operators.adoc       | 14 ++++++++
 .../org/apache/camel/catalog/languages/simple.json | 18 +++++-----
 .../camel/catalog/simple/camel-simple-validator.js |  2 ++
 .../org/apache/camel/language/simple/simple.json   | 18 +++++-----
 .../modules/languages/pages/simple-operators.adoc  | 14 ++++++++
 .../language/simple/SimpleOperatorConstants.java   | 12 +++++++
 .../camel/language/simple/SimpleTokenizer.java     | 24 +++++++------
 .../language/simple/ast/BinaryExpression.java      |  5 +++
 .../language/simple/types/BinaryOperatorType.java  | 14 +++++++-
 .../camel/language/simple/SimpleOperatorTest.java  | 39 ++++++++++++++++++++++
 .../org/apache/camel/support/LanguageHelper.java   | 25 ++++++++++++++
 .../camel/support/builder/PredicateBuilder.java    | 13 ++++++++
 12 files changed, 170 insertions(+), 28 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-operators.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-operators.adoc
index d1b48abe47d9..76eebd96b0d7 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-operators.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-operators.adoc
@@ -45,6 +45,8 @@ The following comparison operators are supported:
 |`!contains` | For testing if it does not contain in a string-based value
 |`endsWith` | For testing if the left-hand side string ends with the 
right-hand string.
 |`!endsWith` | For testing if the left-hand side string does not end with the 
right-hand string.
+|`equals` | For testing if the left-hand side string is equal to the 
right-hand string, compared as text only.
+|`!equals` | For testing if the left-hand side string is not equal to the 
right-hand string, compared as text only.
 |`in` | For matching if in a set of values, each element must be separated by 
comma. If you want to include an empty value, then it must be defined using 
double comma, e.g. `",,bronze,silver,gold"`, which is a set of four values with 
an empty value and then the three medals.
 |`!in` | For matching if not in a set of values, each element must be 
separated by comma. If you want to include an empty value, then it must be 
defined using double comma, e.g. `",,bronze,silver,gold"`, which is a set of 
four values with an empty value and then the three medals.
 |`is` | For matching if the left-hand side type is an instance of the value.
@@ -57,6 +59,18 @@ The following comparison operators are supported:
 |`!startsWith` | For testing if the left-hand side string does not start with 
the right-hand string.
 |====
 
+When both sides of `==` are strings made up only of digits, they are compared 
as numbers. That is what makes
+`0001 == 001` true, which is usually what you want for values that happen to 
be written with padding, such as
+the hour from `${date:now:HH}`. It is not what you want for identifiers like 
account numbers, where the leading
+zeros carry meaning. Use `equals` for those, as it always compares the two 
values as text:
+
+[source,java]
+----
+// Account1 is "0001" and Account2 is "001"
+.filter().simple("${header.Account1} == ${header.Account2}")     // true, 
compared as numbers
+.filter().simple("${header.Account1} equals ${header.Account2}") // false, 
compared as text
+----
+
 === Numeric Operators
 
 The following numeric operators can be used:
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/simple.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/simple.json
index bf5fdfc80090..6da75471d8fb 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/simple.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/simple.json
@@ -203,13 +203,15 @@
     "!startsWith": { "index": 21, "kind": "operator", "displayName": "Not 
starts with", "label": "binary", "required": false, "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "description": 
"Tests whether the left operand string does not start with the right operand 
string.", "operatorKind": "binary", "operatorSyntax": "LHS !startsWith RHS", 
"precedence": 10, "examples": [ "${header.name} !startsWith 'Camel'" ] },
     "endsWith": { "index": 22, "kind": "operator", "displayName": "Ends with", 
"label": "binary", "required": false, "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "Tests whether the left 
operand string ends with the right operand string.", "operatorKind": "binary", 
"operatorSyntax": "LHS endsWith RHS", "precedence": 10, "examples": [ 
"${header.name} endsWith '.xml'" ] },
     "!endsWith": { "index": 23, "kind": "operator", "displayName": "Not ends 
with", "label": "binary", "required": false, "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "description": 
"Tests whether the left operand string does not end with the right operand 
string.", "operatorKind": "binary", "operatorSyntax": "LHS !endsWith RHS", 
"precedence": 10, "examples": [ "${header.name} !endsWith '.xml'" ] },
-    "++": { "index": 24, "kind": "operator", "displayName": "Inc", "label": 
"unary", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Increments the numeric 
value by one. Must immediately follow a function closing brace.", 
"operatorKind": "unary", "operatorSyntax": "${fn}++", "precedence": 1, 
"examples": [ "${header.count}++" ] },
-    "--": { "index": 25, "kind": "operator", "displayName": "Dec", "label": 
"unary", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Decrements the numeric 
value by one. Must immediately follow a function closing brace.", 
"operatorKind": "unary", "operatorSyntax": "${fn}--", "precedence": 1, 
"examples": [ "${header.count}--" ] },
-    "&&": { "index": 26, "kind": "operator", "displayName": "And", "label": 
"logical", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Logical AND. Both left and 
right predicates must evaluate to true.", "operatorKind": "logical", 
"operatorSyntax": "predicate && predicate", "precedence": 30, "examples": [ 
"${header.title} contains 'Camel' && ${header.type} == 'gold'" ] },
-    "||": { "index": 27, "kind": "operator", "displayName": "Or", "label": 
"logical", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Logical OR. At least one 
of the left or right predicates must evaluate to true.", "operatorKind": 
"logical", "operatorSyntax": "predicate || predicate", "precedence": 30, 
"examples": [ "${header.title} contains 'Camel' || ${header.type} == 'gold'" ] 
},
-    "? :": { "index": 28, "kind": "operator", "displayName": "Ternary", 
"label": "ternary", "required": false, "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "Ternary conditional 
operator. Evaluates the predicate and returns trueValue if true, falseValue if 
false. Requires spaces around both ? and : tokens.", "operatorKind": "ternary", 
"operatorSyntax": "predicate ? trueValue : falseValue", "precedence": 25, 
"examples": [ "${header.foo} >  [...]
-    "~>": { "index": 29, "kind": "operator", "displayName": "Chain", "label": 
"chain", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Pipes the result of the 
left expression as input body to the right expression. Use $param in the right 
expression to reference the piped value explicitly.", "operatorKind": "chain", 
"operatorSyntax": "expr ~> expr", "precedence": 5, "examples": [ "${trim()} ~> 
${uppercase()}", "${substrin [...]
-    "?~>": { "index": 30, "kind": "operator", "displayName": "Chain null 
safe", "label": "chain", "required": false, "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "description": 
"Null-safe chain operator. Same as ~> but stops chaining and returns null if 
the left expression evaluates to null.", "operatorKind": "chain", 
"operatorSyntax": "expr ?~> expr", "precedence": 5, "examples": [ 
"${header.name} ?~> ${trim()} ?~> ${uppercase()}" ] },
-    "?:": { "index": 31, "kind": "operator", "displayName": "Elvis", "label": 
"other", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Elvis operator 
(null-coalescing). Returns the left operand if it is not null\/empty, otherwise 
returns the right operand as a fallback value.", "operatorKind": "other", 
"operatorSyntax": "expr ?: defaultValue", "precedence": 20, "examples": [ 
"${header.username} ?: 'Guest'", "${body} ?: $ [...]
+    "equals": { "index": 24, "kind": "operator", "displayName": "Equals", 
"label": "binary", "required": false, "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "Tests whether the left 
operand string equals the right operand string, compared as text without 
numeric coercion.", "operatorKind": "binary", "operatorSyntax": "LHS equals 
RHS", "precedence": 10, "examples": [ "${header.Account1} equals 
${header.Account2}" ] },
+    "!equals": { "index": 25, "kind": "operator", "displayName": "Not equals", 
"label": "binary", "required": false, "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "Tests whether the left 
operand string does not equal the right operand string, compared as text 
without numeric coercion.", "operatorKind": "binary", "operatorSyntax": "LHS 
!equals RHS", "precedence": 10, "examples": [ "${header.Account1} !equals 
${header.Account2}" ] },
+    "++": { "index": 26, "kind": "operator", "displayName": "Inc", "label": 
"unary", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Increments the numeric 
value by one. Must immediately follow a function closing brace.", 
"operatorKind": "unary", "operatorSyntax": "${fn}++", "precedence": 1, 
"examples": [ "${header.count}++" ] },
+    "--": { "index": 27, "kind": "operator", "displayName": "Dec", "label": 
"unary", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Decrements the numeric 
value by one. Must immediately follow a function closing brace.", 
"operatorKind": "unary", "operatorSyntax": "${fn}--", "precedence": 1, 
"examples": [ "${header.count}--" ] },
+    "&&": { "index": 28, "kind": "operator", "displayName": "And", "label": 
"logical", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Logical AND. Both left and 
right predicates must evaluate to true.", "operatorKind": "logical", 
"operatorSyntax": "predicate && predicate", "precedence": 30, "examples": [ 
"${header.title} contains 'Camel' && ${header.type} == 'gold'" ] },
+    "||": { "index": 29, "kind": "operator", "displayName": "Or", "label": 
"logical", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Logical OR. At least one 
of the left or right predicates must evaluate to true.", "operatorKind": 
"logical", "operatorSyntax": "predicate || predicate", "precedence": 30, 
"examples": [ "${header.title} contains 'Camel' || ${header.type} == 'gold'" ] 
},
+    "? :": { "index": 30, "kind": "operator", "displayName": "Ternary", 
"label": "ternary", "required": false, "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "Ternary conditional 
operator. Evaluates the predicate and returns trueValue if true, falseValue if 
false. Requires spaces around both ? and : tokens.", "operatorKind": "ternary", 
"operatorSyntax": "predicate ? trueValue : falseValue", "precedence": 25, 
"examples": [ "${header.foo} >  [...]
+    "~>": { "index": 31, "kind": "operator", "displayName": "Chain", "label": 
"chain", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Pipes the result of the 
left expression as input body to the right expression. Use $param in the right 
expression to reference the piped value explicitly.", "operatorKind": "chain", 
"operatorSyntax": "expr ~> expr", "precedence": 5, "examples": [ "${trim()} ~> 
${uppercase()}", "${substrin [...]
+    "?~>": { "index": 32, "kind": "operator", "displayName": "Chain null 
safe", "label": "chain", "required": false, "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "description": 
"Null-safe chain operator. Same as ~> but stops chaining and returns null if 
the left expression evaluates to null.", "operatorKind": "chain", 
"operatorSyntax": "expr ?~> expr", "precedence": 5, "examples": [ 
"${header.name} ?~> ${trim()} ?~> ${uppercase()}" ] },
+    "?:": { "index": 33, "kind": "operator", "displayName": "Elvis", "label": 
"other", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Elvis operator 
(null-coalescing). Returns the left operand if it is not null\/empty, otherwise 
returns the right operand as a fallback value.", "operatorKind": "other", 
"operatorSyntax": "expr ?: defaultValue", "precedence": 20, "examples": [ 
"${header.username} ?: 'Guest'", "${body} ?: $ [...]
   }
 }
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/simple/camel-simple-validator.js
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/simple/camel-simple-validator.js
index 911aafbc2bd8..1afe01bfc64b 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/simple/camel-simple-validator.js
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/simple/camel-simple-validator.js
@@ -182,6 +182,8 @@ const OPERATORS = {
   '!startsWith': { kind: 'binary', description: 'Tests whether the left 
operand string does not start with the right operand string.' },
   'endsWith': { kind: 'binary', description: 'Tests whether the left operand 
string ends with the right operand string.' },
   '!endsWith': { kind: 'binary', description: 'Tests whether the left operand 
string does not end with the right operand string.' },
+  'equals': { kind: 'binary', description: 'Tests whether the left operand 
string equals the right operand string, compared as text without numeric 
coercion.' },
+  '!equals': { kind: 'binary', description: 'Tests whether the left operand 
string does not equal the right operand string, compared as text without 
numeric coercion.' },
   '++': { kind: 'unary', description: 'Increments the numeric value by one. 
Must immediately follow a function closing brace.' },
   '--': { kind: 'unary', description: 'Decrements the numeric value by one. 
Must immediately follow a function closing brace.' },
   '&&': { kind: 'logical', description: 'Logical AND. Both left and right 
predicates must evaluate to true.' },
diff --git 
a/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json
 
b/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json
index bf5fdfc80090..6da75471d8fb 100644
--- 
a/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json
+++ 
b/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json
@@ -203,13 +203,15 @@
     "!startsWith": { "index": 21, "kind": "operator", "displayName": "Not 
starts with", "label": "binary", "required": false, "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "description": 
"Tests whether the left operand string does not start with the right operand 
string.", "operatorKind": "binary", "operatorSyntax": "LHS !startsWith RHS", 
"precedence": 10, "examples": [ "${header.name} !startsWith 'Camel'" ] },
     "endsWith": { "index": 22, "kind": "operator", "displayName": "Ends with", 
"label": "binary", "required": false, "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "Tests whether the left 
operand string ends with the right operand string.", "operatorKind": "binary", 
"operatorSyntax": "LHS endsWith RHS", "precedence": 10, "examples": [ 
"${header.name} endsWith '.xml'" ] },
     "!endsWith": { "index": 23, "kind": "operator", "displayName": "Not ends 
with", "label": "binary", "required": false, "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "description": 
"Tests whether the left operand string does not end with the right operand 
string.", "operatorKind": "binary", "operatorSyntax": "LHS !endsWith RHS", 
"precedence": 10, "examples": [ "${header.name} !endsWith '.xml'" ] },
-    "++": { "index": 24, "kind": "operator", "displayName": "Inc", "label": 
"unary", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Increments the numeric 
value by one. Must immediately follow a function closing brace.", 
"operatorKind": "unary", "operatorSyntax": "${fn}++", "precedence": 1, 
"examples": [ "${header.count}++" ] },
-    "--": { "index": 25, "kind": "operator", "displayName": "Dec", "label": 
"unary", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Decrements the numeric 
value by one. Must immediately follow a function closing brace.", 
"operatorKind": "unary", "operatorSyntax": "${fn}--", "precedence": 1, 
"examples": [ "${header.count}--" ] },
-    "&&": { "index": 26, "kind": "operator", "displayName": "And", "label": 
"logical", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Logical AND. Both left and 
right predicates must evaluate to true.", "operatorKind": "logical", 
"operatorSyntax": "predicate && predicate", "precedence": 30, "examples": [ 
"${header.title} contains 'Camel' && ${header.type} == 'gold'" ] },
-    "||": { "index": 27, "kind": "operator", "displayName": "Or", "label": 
"logical", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Logical OR. At least one 
of the left or right predicates must evaluate to true.", "operatorKind": 
"logical", "operatorSyntax": "predicate || predicate", "precedence": 30, 
"examples": [ "${header.title} contains 'Camel' || ${header.type} == 'gold'" ] 
},
-    "? :": { "index": 28, "kind": "operator", "displayName": "Ternary", 
"label": "ternary", "required": false, "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "Ternary conditional 
operator. Evaluates the predicate and returns trueValue if true, falseValue if 
false. Requires spaces around both ? and : tokens.", "operatorKind": "ternary", 
"operatorSyntax": "predicate ? trueValue : falseValue", "precedence": 25, 
"examples": [ "${header.foo} >  [...]
-    "~>": { "index": 29, "kind": "operator", "displayName": "Chain", "label": 
"chain", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Pipes the result of the 
left expression as input body to the right expression. Use $param in the right 
expression to reference the piped value explicitly.", "operatorKind": "chain", 
"operatorSyntax": "expr ~> expr", "precedence": 5, "examples": [ "${trim()} ~> 
${uppercase()}", "${substrin [...]
-    "?~>": { "index": 30, "kind": "operator", "displayName": "Chain null 
safe", "label": "chain", "required": false, "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "description": 
"Null-safe chain operator. Same as ~> but stops chaining and returns null if 
the left expression evaluates to null.", "operatorKind": "chain", 
"operatorSyntax": "expr ?~> expr", "precedence": 5, "examples": [ 
"${header.name} ?~> ${trim()} ?~> ${uppercase()}" ] },
-    "?:": { "index": 31, "kind": "operator", "displayName": "Elvis", "label": 
"other", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Elvis operator 
(null-coalescing). Returns the left operand if it is not null\/empty, otherwise 
returns the right operand as a fallback value.", "operatorKind": "other", 
"operatorSyntax": "expr ?: defaultValue", "precedence": 20, "examples": [ 
"${header.username} ?: 'Guest'", "${body} ?: $ [...]
+    "equals": { "index": 24, "kind": "operator", "displayName": "Equals", 
"label": "binary", "required": false, "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "Tests whether the left 
operand string equals the right operand string, compared as text without 
numeric coercion.", "operatorKind": "binary", "operatorSyntax": "LHS equals 
RHS", "precedence": 10, "examples": [ "${header.Account1} equals 
${header.Account2}" ] },
+    "!equals": { "index": 25, "kind": "operator", "displayName": "Not equals", 
"label": "binary", "required": false, "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "Tests whether the left 
operand string does not equal the right operand string, compared as text 
without numeric coercion.", "operatorKind": "binary", "operatorSyntax": "LHS 
!equals RHS", "precedence": 10, "examples": [ "${header.Account1} !equals 
${header.Account2}" ] },
+    "++": { "index": 26, "kind": "operator", "displayName": "Inc", "label": 
"unary", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Increments the numeric 
value by one. Must immediately follow a function closing brace.", 
"operatorKind": "unary", "operatorSyntax": "${fn}++", "precedence": 1, 
"examples": [ "${header.count}++" ] },
+    "--": { "index": 27, "kind": "operator", "displayName": "Dec", "label": 
"unary", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Decrements the numeric 
value by one. Must immediately follow a function closing brace.", 
"operatorKind": "unary", "operatorSyntax": "${fn}--", "precedence": 1, 
"examples": [ "${header.count}--" ] },
+    "&&": { "index": 28, "kind": "operator", "displayName": "And", "label": 
"logical", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Logical AND. Both left and 
right predicates must evaluate to true.", "operatorKind": "logical", 
"operatorSyntax": "predicate && predicate", "precedence": 30, "examples": [ 
"${header.title} contains 'Camel' && ${header.type} == 'gold'" ] },
+    "||": { "index": 29, "kind": "operator", "displayName": "Or", "label": 
"logical", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Logical OR. At least one 
of the left or right predicates must evaluate to true.", "operatorKind": 
"logical", "operatorSyntax": "predicate || predicate", "precedence": 30, 
"examples": [ "${header.title} contains 'Camel' || ${header.type} == 'gold'" ] 
},
+    "? :": { "index": 30, "kind": "operator", "displayName": "Ternary", 
"label": "ternary", "required": false, "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "Ternary conditional 
operator. Evaluates the predicate and returns trueValue if true, falseValue if 
false. Requires spaces around both ? and : tokens.", "operatorKind": "ternary", 
"operatorSyntax": "predicate ? trueValue : falseValue", "precedence": 25, 
"examples": [ "${header.foo} >  [...]
+    "~>": { "index": 31, "kind": "operator", "displayName": "Chain", "label": 
"chain", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Pipes the result of the 
left expression as input body to the right expression. Use $param in the right 
expression to reference the piped value explicitly.", "operatorKind": "chain", 
"operatorSyntax": "expr ~> expr", "precedence": 5, "examples": [ "${trim()} ~> 
${uppercase()}", "${substrin [...]
+    "?~>": { "index": 32, "kind": "operator", "displayName": "Chain null 
safe", "label": "chain", "required": false, "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "description": 
"Null-safe chain operator. Same as ~> but stops chaining and returns null if 
the left expression evaluates to null.", "operatorKind": "chain", 
"operatorSyntax": "expr ?~> expr", "precedence": 5, "examples": [ 
"${header.name} ?~> ${trim()} ?~> ${uppercase()}" ] },
+    "?:": { "index": 33, "kind": "operator", "displayName": "Elvis", "label": 
"other", "required": false, "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Elvis operator 
(null-coalescing). Returns the left operand if it is not null\/empty, otherwise 
returns the right operand as a fallback value.", "operatorKind": "other", 
"operatorSyntax": "expr ?: defaultValue", "precedence": 20, "examples": [ 
"${header.username} ?: 'Guest'", "${body} ?: $ [...]
   }
 }
diff --git 
a/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-operators.adoc
 
b/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-operators.adoc
index d1b48abe47d9..76eebd96b0d7 100644
--- 
a/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-operators.adoc
+++ 
b/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-operators.adoc
@@ -45,6 +45,8 @@ The following comparison operators are supported:
 |`!contains` | For testing if it does not contain in a string-based value
 |`endsWith` | For testing if the left-hand side string ends with the 
right-hand string.
 |`!endsWith` | For testing if the left-hand side string does not end with the 
right-hand string.
+|`equals` | For testing if the left-hand side string is equal to the 
right-hand string, compared as text only.
+|`!equals` | For testing if the left-hand side string is not equal to the 
right-hand string, compared as text only.
 |`in` | For matching if in a set of values, each element must be separated by 
comma. If you want to include an empty value, then it must be defined using 
double comma, e.g. `",,bronze,silver,gold"`, which is a set of four values with 
an empty value and then the three medals.
 |`!in` | For matching if not in a set of values, each element must be 
separated by comma. If you want to include an empty value, then it must be 
defined using double comma, e.g. `",,bronze,silver,gold"`, which is a set of 
four values with an empty value and then the three medals.
 |`is` | For matching if the left-hand side type is an instance of the value.
@@ -57,6 +59,18 @@ The following comparison operators are supported:
 |`!startsWith` | For testing if the left-hand side string does not start with 
the right-hand string.
 |====
 
+When both sides of `==` are strings made up only of digits, they are compared 
as numbers. That is what makes
+`0001 == 001` true, which is usually what you want for values that happen to 
be written with padding, such as
+the hour from `${date:now:HH}`. It is not what you want for identifiers like 
account numbers, where the leading
+zeros carry meaning. Use `equals` for those, as it always compares the two 
values as text:
+
+[source,java]
+----
+// Account1 is "0001" and Account2 is "001"
+.filter().simple("${header.Account1} == ${header.Account2}")     // true, 
compared as numbers
+.filter().simple("${header.Account1} equals ${header.Account2}") // false, 
compared as text
+----
+
 === Numeric Operators
 
 The following numeric operators can be used:
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleOperatorConstants.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleOperatorConstants.java
index dfbca7987d91..658878ea5778 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleOperatorConstants.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleOperatorConstants.java
@@ -167,6 +167,18 @@ public final class SimpleOperatorConstants {
               annotations = { "kind=binary", "syntax=LHS !endsWith RHS", 
"precedence=10" })
     public static final String NOT_ENDS_WITH = "!endsWith";
 
+    @Metadata(description = "Tests whether the left operand string equals the 
right operand string, compared as text without numeric coercion.",
+              label = "binary",
+              examples = { "${header.Account1} equals ${header.Account2}" },
+              annotations = { "kind=binary", "syntax=LHS equals RHS", 
"precedence=10" })
+    public static final String EQUALS = "equals";
+
+    @Metadata(description = "Tests whether the left operand string does not 
equal the right operand string, compared as text without numeric coercion.",
+              label = "binary",
+              examples = { "${header.Account1} !equals ${header.Account2}" },
+              annotations = { "kind=binary", "syntax=LHS !equals RHS", 
"precedence=10" })
+    public static final String NOT_EQUALS = "!equals";
+
     // --- Unary operators (precedence 1) ---
 
     @Metadata(description = "Increments the numeric value by one. Must 
immediately follow a function closing brace.",
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleTokenizer.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleTokenizer.java
index c8b4e149d698..4b4ae0956b67 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleTokenizer.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleTokenizer.java
@@ -27,7 +27,7 @@ import org.apache.camel.util.ObjectHelper;
 public class SimpleTokenizer {
 
     // keep this number in sync with tokens list
-    private static final int NUMBER_OF_TOKENS = 54;
+    private static final int NUMBER_OF_TOKENS = 56;
 
     private static final SimpleTokenType[] KNOWN_TOKENS = new 
SimpleTokenType[NUMBER_OF_TOKENS];
 
@@ -84,30 +84,32 @@ public class SimpleTokenizer {
         KNOWN_TOKENS[41] = new SimpleTokenType(TokenType.binaryOperator, 
"endsWith");
         KNOWN_TOKENS[42] = new SimpleTokenType(TokenType.binaryOperator, "ends 
with");
         KNOWN_TOKENS[43] = new SimpleTokenType(TokenType.binaryOperator, 
"!endsWith");
+        KNOWN_TOKENS[44] = new SimpleTokenType(TokenType.binaryOperator, 
"!equals");
+        KNOWN_TOKENS[45] = new SimpleTokenType(TokenType.binaryOperator, 
"equals");
 
         // other operators
-        KNOWN_TOKENS[44] = new SimpleTokenType(TokenType.otherOperator, "?:");
+        KNOWN_TOKENS[46] = new SimpleTokenType(TokenType.otherOperator, "?:");
 
         // chain operators
-        KNOWN_TOKENS[45] = new SimpleTokenType(TokenType.chainOperator, "~>");
-        KNOWN_TOKENS[46] = new SimpleTokenType(TokenType.chainOperator, "?~>");
+        KNOWN_TOKENS[47] = new SimpleTokenType(TokenType.chainOperator, "~>");
+        KNOWN_TOKENS[48] = new SimpleTokenType(TokenType.chainOperator, "?~>");
 
         // unary operators
-        KNOWN_TOKENS[47] = new SimpleTokenType(TokenType.unaryOperator, "++");
-        KNOWN_TOKENS[48] = new SimpleTokenType(TokenType.unaryOperator, "--");
+        KNOWN_TOKENS[49] = new SimpleTokenType(TokenType.unaryOperator, "++");
+        KNOWN_TOKENS[50] = new SimpleTokenType(TokenType.unaryOperator, "--");
 
         // logical operators
-        KNOWN_TOKENS[49] = new SimpleTokenType(TokenType.logicalOperator, 
"&&");
-        KNOWN_TOKENS[50] = new SimpleTokenType(TokenType.logicalOperator, 
"||");
+        KNOWN_TOKENS[51] = new SimpleTokenType(TokenType.logicalOperator, 
"&&");
+        KNOWN_TOKENS[52] = new SimpleTokenType(TokenType.logicalOperator, 
"||");
 
         // ternary operators
-        KNOWN_TOKENS[51] = new SimpleTokenType(TokenType.ternaryOperator, "?");
-        KNOWN_TOKENS[52] = new SimpleTokenType(TokenType.ternaryOperator, ":");
+        KNOWN_TOKENS[53] = new SimpleTokenType(TokenType.ternaryOperator, "?");
+        KNOWN_TOKENS[54] = new SimpleTokenType(TokenType.ternaryOperator, ":");
 
         //binary operator
         // it is added as the last item because unary -- has the priority
         // if unary not found it is highly possible - operator is run into.
-        KNOWN_TOKENS[53] = new SimpleTokenType(TokenType.minusValue, "-");
+        KNOWN_TOKENS[55] = new SimpleTokenType(TokenType.minusValue, "-");
     }
 
     /**
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/BinaryExpression.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/BinaryExpression.java
index 9c515c6077db..46ce040e1c1f 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/BinaryExpression.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/BinaryExpression.java
@@ -133,6 +133,11 @@ public class BinaryExpression extends BaseSimpleNode {
         } else if (operator == BinaryOperatorType.NOT_ENDS_WITH) {
             return createExpression(camelContext, leftExp, rightExp,
                     PredicateBuilder.not(PredicateBuilder.endsWith(leftExp, 
rightExp)));
+        } else if (operator == BinaryOperatorType.EQUALS) {
+            return createExpression(camelContext, leftExp, rightExp, 
PredicateBuilder.equalsString(leftExp, rightExp));
+        } else if (operator == BinaryOperatorType.NOT_EQUALS) {
+            return createExpression(camelContext, leftExp, rightExp,
+                    
PredicateBuilder.not(PredicateBuilder.equalsString(leftExp, rightExp)));
         }
 
         throw new SimpleParserException("Unknown binary operator " + operator, 
token.getIndex());
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/types/BinaryOperatorType.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/types/BinaryOperatorType.java
index fb82fc584a0e..baa7e36e5c09 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/types/BinaryOperatorType.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/types/BinaryOperatorType.java
@@ -47,7 +47,9 @@ public enum BinaryOperatorType {
     STARTS_WITH,
     NOT_STARTS_WITH,
     ENDS_WITH,
-    NOT_ENDS_WITH;
+    NOT_ENDS_WITH,
+    EQUALS,
+    NOT_EQUALS;
 
     private static final Logger LOG = 
LoggerFactory.getLogger(BinaryOperatorType.class);
 
@@ -121,6 +123,10 @@ public enum BinaryOperatorType {
             return NOT_STARTS_WITH;
         } else if ("!endsWith".equals(text)) {
             return NOT_ENDS_WITH;
+        } else if ("equals".equals(text)) {
+            return EQUALS;
+        } else if ("!equals".equals(text)) {
+            return NOT_EQUALS;
         }
         throw new IllegalArgumentException("Operator not supported: " + text);
     }
@@ -174,6 +180,10 @@ public enum BinaryOperatorType {
             return "endsWith";
         } else if (operator == NOT_ENDS_WITH) {
             return "!endsWith";
+        } else if (operator == EQUALS) {
+            return "equals";
+        } else if (operator == NOT_EQUALS) {
+            return "!equals";
         }
         return "";
     }
@@ -278,6 +288,8 @@ public enum BinaryOperatorType {
             return null;
         } else if (operator == ENDS_WITH || operator == NOT_ENDS_WITH) {
             return null;
+        } else if (operator == EQUALS || operator == NOT_EQUALS) {
+            return null;
         }
         return null;
     }
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleOperatorTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleOperatorTest.java
index dd1539b0beb3..d7272c4928ce 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleOperatorTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleOperatorTest.java
@@ -831,6 +831,45 @@ public class SimpleOperatorTest extends 
LanguageTestSupport {
         assertPredicate("${in.body} !endsWith 'Hi'", true);
     }
 
+    @Test
+    public void testEquals() {
+        exchange.getIn().setHeader("Account1", "0001");
+        exchange.getIn().setHeader("Account2", "001");
+        // == favours numeric comparison when both sides are all digits
+        assertPredicate("${in.header.Account1} == ${in.header.Account2}", 
true);
+        // equals compares as text, so the leading zeros are significant
+        assertPredicate("${in.header.Account1} equals ${in.header.Account2}", 
false);
+        assertPredicate("${in.header.Account1} equals ${in.header.Account1}", 
true);
+        assertPredicate("${in.header.Account1} equals '0001'", true);
+        assertPredicate("${in.header.Account1} equals '001'", false);
+        assertPredicate("${in.header.Account1} equals 1", false);
+
+        exchange.getIn().setBody("Hello there");
+        assertPredicate("${in.body} equals 'Hello there'", true);
+        assertPredicate("${in.body} equals 'hello there'", false);
+        assertPredicate("${in.body} equals 'Hello'", false);
+    }
+
+    @Test
+    public void testNotEquals() {
+        exchange.getIn().setHeader("Account1", "0001");
+        exchange.getIn().setHeader("Account2", "001");
+        assertPredicate("${in.header.Account1} !equals ${in.header.Account2}", 
true);
+        assertPredicate("${in.header.Account1} !equals ${in.header.Account1}", 
false);
+        assertPredicate("${in.header.Account1} !equals '0001'", false);
+
+        exchange.getIn().setBody("Hello there");
+        assertPredicate("${in.body} !equals 'Hello there'", false);
+        assertPredicate("${in.body} !equals 'hello there'", true);
+    }
+
+    @Test
+    public void testEqualsAsLiteralText() {
+        // only parsed as an operator in a predicate, elsewhere it is ordinary 
text
+        assertExpression("the value equals something", "the value equals 
something");
+        assertExpression("Logging equals start", "Logging equals start");
+    }
+
     @Test
     public void testElvis() {
         exchange.getIn().setBody(false);
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/LanguageHelper.java 
b/core/camel-support/src/main/java/org/apache/camel/support/LanguageHelper.java
index f9cc5123e579..9cd4e6706511 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/LanguageHelper.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/LanguageHelper.java
@@ -80,6 +80,31 @@ public final class LanguageHelper {
         return ExceptionHelper.stackTraceToString(exception);
     }
 
+    /**
+     * Tests if two values are equal as text, without the numeric coercion 
that {@code ==} applies to all-digit strings.
+     *
+     * @param  exchange   the exchange to test
+     * @param  leftValue  the value being tested
+     * @param  rightValue the value to test against
+     * @return            true if both convert to the same string, or false 
otherwise
+     */
+    public static boolean equalsString(Exchange exchange, Object leftValue, 
Object rightValue) {
+        if (leftValue == null && rightValue == null) {
+            // they are equal
+            return true;
+        } else if (leftValue == null || rightValue == null) {
+            // only one of them is null so they are not equal
+            return false;
+        }
+        String leftStr = 
exchange.getContext().getTypeConverter().convertTo(String.class, leftValue);
+        String rightStr = 
exchange.getContext().getTypeConverter().convertTo(String.class, rightValue);
+        if (leftStr != null && rightStr != null) {
+            return leftStr.equals(rightStr);
+        } else {
+            return false;
+        }
+    }
+
     /**
      * Tests if the exchange ends with a given value
      *
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/builder/PredicateBuilder.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/builder/PredicateBuilder.java
index f2a8cf3104f0..cac09ae1dea3 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/builder/PredicateBuilder.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/builder/PredicateBuilder.java
@@ -505,6 +505,19 @@ public class PredicateBuilder {
         };
     }
 
+    public static Predicate equalsString(final Expression left, final 
Expression right) {
+        return new BinaryPredicateSupport(left, right) {
+
+            protected boolean matches(Exchange exchange, Object leftValue, 
Object rightValue) {
+                return LanguageHelper.equalsString(exchange, leftValue, 
rightValue);
+            }
+
+            protected String getOperationText() {
+                return "equals";
+            }
+        };
+    }
+
     public static Predicate endsWith(final Expression left, final Expression 
right) {
         return new BinaryPredicateSupport(left, right) {
 

Reply via email to