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 b265af249c98 CAMEL-24984: simple - ! negates a function in a predicate
(#26824)
b265af249c98 is described below
commit b265af249c98b868e234a0a5fe9d99c7e2e5b12b
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Sep 24 12:36:28 2026 +0200
CAMEL-24984: simple - ! negates a function in a predicate (#26824)
* CAMEL-24984: simple - ! negates a function in a predicate
Every language a Camel user comes from has !, and it is one of the forms
a local model writes unprompted. Simple had none, so !${body.isEmpty()}
had to be written as ${body.isEmpty()} == false.
!${body.isEmpty()}
!${body.isEmpty()} && ${header.foo} == 'bar'
${body} == null || !${body.isEmpty()}
An expression never has operators - a ! there is text, as in
"Hello ${body}!" - so the tokenizer only offers the operator to a
predicate parser, which is the one that turns it on. Within a predicate
the ! is a token only when a function follows it directly (!${) and what
precedes it is nothing, a space or an opening parenthesis, so a ! that
belongs to text stays text. The eleven operators that start with one
(!=, !=~, !is, !contains, !~~, !regex, !in, !range, !startsWith,
!endsWith, !equals) are matched before it, and are unaffected.
Unlike ++ and --, ! is written in front of what it works upon, so the
node takes the one that follows it and LogicalExpression accepts a
negated function as an operand. A value that is not true or false says
so: "Cannot negate ${body} as it is not true or false but: Hello".
The message for a ! the parser cannot use now points at the new form
rather than only at the negated operators.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj
* CAMEL-24984: simple - ! also negates a function written inside the braces
${!body.isEmpty()} is the form a model writes as often as !${...}, so the
function parser reads a leading ! and answers the opposite. It is read
after the predicate inside the braces, so the ! in ${!a && b} negates a
and not the whole predicate, which a test asserts with a case where the
two readings differ.
wrapFunctions gives a bare negated function the form the predicate parser
knows, so ${body != null && !body.isEmpty()} works as well as the nested
${body != null && !${body.isEmpty()}}.
The negated expression passes init on, as the function it wraps needs it
before it is evaluated.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj
* Regen
* CAMEL-24984: simple - ! answers the opposite of the predicate, and is in
the catalog
Three things found by trying the edges rather than the happy path:
* !${body} == 'x' answered true whichever body it was given, which is
neither of the two things it could mean. A comparison now refuses a
negated function and says to negate the operator instead.
* The negation was stricter than the language: !${header.missing} threw,
while ${header.missing} as a predicate on its own is false. It now uses
the same rule, so !${header.missing} is true and !${body} is false.
* The operator was missing from the catalog metadata, which is what
tooling reads. SimpleOperatorConstants carries it now, and the
generated language json, the docs copy and the simple validator
javascript in the catalog have it.
The documentation gains the rule, a table of what it answers, and the
cases that do not work: a space after the !, negating a comparison, a
negation on the right of one, and double negation outside the braces.
The message for a ! the parser cannot use points at the form that works
(!x is written as !${x}) instead of at the comparison, which is what
CAMEL-24983 said before this operator existed.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj
* Regen
* CAMEL-24984: forward init on the operator path, and escape ${ } in the
prose
The expression returned for !${...} did not pass init on to the function
it negates, unlike the one for ${!...}. It is the same mistake in both
places; only the ${!...} one showed it, where the function is built
outside the node tree and threw an NPE in OgnlExpressionBuilder without
init. The operator path evaluates correctly either way with the functions
in the tests, so this is a latent inconsistency rather than a fault, and
the two now match.
The documentation escapes ${ } in the prose of the new section: outside a
source block Asciidoctor reads ${body} as an attribute reference and the
documentation build warns "skipping reference to missing attribute: body".
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj
* Regen
* CAMEL-24984: escape ${ } the way the documentation does
The previous commit wrote \${body}, which leaves {body} live: asciidoctor
reads the attribute reference from the brace, so the backslash has to sit
in front of it. The rest of the Camel documentation writes $\{body}, 32
times, and the documentation build warned "skipping reference to missing
attribute: body" until this matched it.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj
* CAMEL-24984: name the operator that says the opposite, and only when
there is one
The message built it with operator.toString().replace("=", "!="), which
replaces each = on its own, so == became !=!= and the advice for
!${body} == 'x' named an operator that does not exist. negatedOperator()
was already there, one line below, and is used for it now.
That helper had the same fault in its fallback: "!" + text gave !> for a
comparison, which is also not an operator. Only the eleven that have a
negated form answer one; the comparisons answer null and the message says
to compare the other way round instead.
== write ${...} != value
> compare the other way round
contains write ${...} !contains value
in write ${...} !in value
The test asserted only that the message started with "! cannot be
compared", which is why it passed over this; it now asserts the operator
it names, and that !=!= is not in it.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj
---------
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
Co-authored-by: Guillaume Nodet <[email protected]>
---
.../camel/catalog/docs/simple-operators.adoc | 39 ++++++
.../org/apache/camel/catalog/languages/simple.json | 17 +--
.../camel/catalog/simple/camel-simple-validator.js | 1 +
.../org/apache/camel/language/simple/simple.json | 17 +--
.../modules/languages/pages/simple-operators.adoc | 39 ++++++
.../camel/language/simple/BaseSimpleParser.java | 18 +++
.../language/simple/SimpleOperatorConstants.java | 11 ++
.../language/simple/SimplePredicateParser.java | 15 +++
.../camel/language/simple/SimpleSyntaxHints.java | 33 ++++-
.../camel/language/simple/SimpleTokenizer.java | 33 ++++-
.../language/simple/ast/BinaryExpression.java | 28 ++++
.../language/simple/ast/LogicalExpression.java | 5 +-
.../simple/ast/SimpleFunctionExpression.java | 30 +++++
.../camel/language/simple/ast/UnaryExpression.java | 32 +++++
.../language/simple/types/UnaryOperatorType.java | 8 +-
.../language/simple/SimpleNotOperatorTest.java | 149 +++++++++++++++++++++
16 files changed, 455 insertions(+), 20 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 ff9580d8f3f8..e797fc2ebadb 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
@@ -252,8 +252,47 @@ And the following boolean operators can be used to group
expressions:
|Operator |Description
|`&&` | The `and` operator is used to group two expressions if both operands
evaluates to `true`.
|`\|\|` | The `or` operator is used to group two expressions if any operand
evaluates to `true`.
+|`!` | The `not` operator negates the function that follows it, answering the
opposite of what that function answers as a predicate.
|====
+The `!` operator is written directly in front of the function it negates, and
only in a predicate:
+
+[source,java]
+----
+// true when the body is not empty
+simple("!${body.isEmpty()}")
+
+// the same written inside the braces
+simple("${!body.isEmpty()}")
+
+// it groups with && and || like any other predicate, and negates only its own
function
+simple("!${body.isEmpty()} && ${header.foo} == 'bar'")
+simple("${!body.isEmpty() && header.foo == 'bar'}")
+----
+
+In an expression a `!` is ordinary text, so a message such as `Hello
$\{body}!` is unaffected.
+
+`!` answers the opposite of what the function answers as a predicate on its
own, which is the rule the language
+already uses: a value that is not null and not `false` is true, and null is
false.
+
+[width="100%",cols="40%,20%,40%",options="header"]
+|====
+|Expression |Answer |Why
+|`!$\{body.isEmpty()}` |`true` |the body is not empty
+|`!$\{header.missing}` |`true` |a missing header is false, so its opposite is
true
+|`!$\{body}` |`false` |a body that is set is true
+|====
+
+==== Where it does not work
+
+* *A `!` must be directly in front of the function.* `! $\{body.isEmpty()}`
with a space is not the operator.
+* *`!` negates a function, not a comparison.* `!$\{body} == 'x'` is refused:
negate the operator instead and write
+ `$\{body} != 'x'`, or one of `!contains`, `!startsWith`, `!endsWith`, `!in`,
`!is`, `!range`, `!regex`, which are
+ unchanged.
+* *It cannot be the right hand side of a comparison.* `$\{body} ==
!$\{header.foo}` is refused.
+* *Double negation only works inside the braces.* `$\{!!body.isEmpty()}`
answers what you would expect, while
+ `!!$\{body.isEmpty()}` is not valid.
+
The syntax for AND is:
[source,text]
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 dfc7242ff7f6..d3c1a3ef82db 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
@@ -210,13 +210,14 @@
"!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} ?: $ [...]
- ":=": { "index": 34, "kind": "operator", "displayName": "Init variable",
"label": "init", "required": false, "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "Assigns a local variable
in the init block at the top of an expression ($init{ ... }init$), computed
once and used in the expression as ${name}. Each statement ends with a
semicolon and a new line.", "operatorKind": "init", "operatorSyntax": "$name :=
expr;", "precedence": 1, "exam [...]
- "~:=": { "index": 35, "kind": "operator", "displayName": "Init function",
"label": "init", "required": false, "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "Declares a local custom
function in the init block at the top of an expression ($init{ ... }init$),
usually as a chain of functions on the input, called as ${name()} (the message
body as input), ${name(exp)} (an explicit input) or from another function as
${function(name)}. Each s [...]
+ "!": { "index": 28, "kind": "operator", "displayName": "Not", "label":
"logical", "required": false, "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "Logical NOT. Negates the
function that follows it, answering the opposite of what that function answers
as a predicate: a value that is not null and not false is true. Written
directly in front of the function, in a predicate only: in an expression a ! is
text, as in Hello ${body}!", "oper [...]
+ "&&": { "index": 29, "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": 30, "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": 31, "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": 32, "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": 33, "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": 34, "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} ?: $ [...]
+ ":=": { "index": 35, "kind": "operator", "displayName": "Init variable",
"label": "init", "required": false, "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "Assigns a local variable
in the init block at the top of an expression ($init{ ... }init$), computed
once and used in the expression as ${name}. Each statement ends with a
semicolon and a new line.", "operatorKind": "init", "operatorSyntax": "$name :=
expr;", "precedence": 1, "exam [...]
+ "~:=": { "index": 36, "kind": "operator", "displayName": "Init function",
"label": "init", "required": false, "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "Declares a local custom
function in the init block at the top of an expression ($init{ ... }init$),
usually as a chain of functions on the input, called as ${name()} (the message
body as input), ${name(exp)} (an explicit input) or from another function as
${function(name)}. Each s [...]
}
}
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 afe2dc088311..4ee093a5f0f8 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
@@ -189,6 +189,7 @@ const OPERATORS = {
'!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 NOT. Negates the function that
follows it, answering the opposite of what that function answers as a
predicate: a value that is not null and not false is true. Written directly in
front of the function, in a predicate only: in an expression a ! is text, as in
Hello ${body}!' },
'&&': { kind: 'logical', description: 'Logical AND. Both left and right
predicates must evaluate to true.' },
'||': { kind: 'logical', description: 'Logical OR. At least one of the left
or right predicates must evaluate to true.' },
'? :': { kind: 'ternary', description: 'Ternary conditional operator.
Evaluates the predicate and returns trueValue if true, falseValue if false.
Requires spaces around both ? and : tokens.' },
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 dfc7242ff7f6..d3c1a3ef82db 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
@@ -210,13 +210,14 @@
"!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} ?: $ [...]
- ":=": { "index": 34, "kind": "operator", "displayName": "Init variable",
"label": "init", "required": false, "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "Assigns a local variable
in the init block at the top of an expression ($init{ ... }init$), computed
once and used in the expression as ${name}. Each statement ends with a
semicolon and a new line.", "operatorKind": "init", "operatorSyntax": "$name :=
expr;", "precedence": 1, "exam [...]
- "~:=": { "index": 35, "kind": "operator", "displayName": "Init function",
"label": "init", "required": false, "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "Declares a local custom
function in the init block at the top of an expression ($init{ ... }init$),
usually as a chain of functions on the input, called as ${name()} (the message
body as input), ${name(exp)} (an explicit input) or from another function as
${function(name)}. Each s [...]
+ "!": { "index": 28, "kind": "operator", "displayName": "Not", "label":
"logical", "required": false, "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "Logical NOT. Negates the
function that follows it, answering the opposite of what that function answers
as a predicate: a value that is not null and not false is true. Written
directly in front of the function, in a predicate only: in an expression a ! is
text, as in Hello ${body}!", "oper [...]
+ "&&": { "index": 29, "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": 30, "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": 31, "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": 32, "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": 33, "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": 34, "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} ?: $ [...]
+ ":=": { "index": 35, "kind": "operator", "displayName": "Init variable",
"label": "init", "required": false, "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "Assigns a local variable
in the init block at the top of an expression ($init{ ... }init$), computed
once and used in the expression as ${name}. Each statement ends with a
semicolon and a new line.", "operatorKind": "init", "operatorSyntax": "$name :=
expr;", "precedence": 1, "exam [...]
+ "~:=": { "index": 36, "kind": "operator", "displayName": "Init function",
"label": "init", "required": false, "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "Declares a local custom
function in the init block at the top of an expression ($init{ ... }init$),
usually as a chain of functions on the input, called as ${name()} (the message
body as input), ${name(exp)} (an explicit input) or from another function as
${function(name)}. Each s [...]
}
}
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 ff9580d8f3f8..e797fc2ebadb 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
@@ -252,8 +252,47 @@ And the following boolean operators can be used to group
expressions:
|Operator |Description
|`&&` | The `and` operator is used to group two expressions if both operands
evaluates to `true`.
|`\|\|` | The `or` operator is used to group two expressions if any operand
evaluates to `true`.
+|`!` | The `not` operator negates the function that follows it, answering the
opposite of what that function answers as a predicate.
|====
+The `!` operator is written directly in front of the function it negates, and
only in a predicate:
+
+[source,java]
+----
+// true when the body is not empty
+simple("!${body.isEmpty()}")
+
+// the same written inside the braces
+simple("${!body.isEmpty()}")
+
+// it groups with && and || like any other predicate, and negates only its own
function
+simple("!${body.isEmpty()} && ${header.foo} == 'bar'")
+simple("${!body.isEmpty() && header.foo == 'bar'}")
+----
+
+In an expression a `!` is ordinary text, so a message such as `Hello
$\{body}!` is unaffected.
+
+`!` answers the opposite of what the function answers as a predicate on its
own, which is the rule the language
+already uses: a value that is not null and not `false` is true, and null is
false.
+
+[width="100%",cols="40%,20%,40%",options="header"]
+|====
+|Expression |Answer |Why
+|`!$\{body.isEmpty()}` |`true` |the body is not empty
+|`!$\{header.missing}` |`true` |a missing header is false, so its opposite is
true
+|`!$\{body}` |`false` |a body that is set is true
+|====
+
+==== Where it does not work
+
+* *A `!` must be directly in front of the function.* `! $\{body.isEmpty()}`
with a space is not the operator.
+* *`!` negates a function, not a comparison.* `!$\{body} == 'x'` is refused:
negate the operator instead and write
+ `$\{body} != 'x'`, or one of `!contains`, `!startsWith`, `!endsWith`, `!in`,
`!is`, `!range`, `!regex`, which are
+ unchanged.
+* *It cannot be the right hand side of a comparison.* `$\{body} ==
!$\{header.foo}` is refused.
+* *Double negation only works inside the braces.* `$\{!!body.isEmpty()}`
answers what you would expect, while
+ `!!$\{body.isEmpty()}` is not valid.
+
The syntax for AND is:
[source,text]
diff --git
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/BaseSimpleParser.java
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/BaseSimpleParser.java
index 2b4bc0c726fc..5a7493afc6c9 100644
---
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/BaseSimpleParser.java
+++
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/BaseSimpleParser.java
@@ -37,6 +37,7 @@ import
org.apache.camel.language.simple.types.SimpleParserException;
import org.apache.camel.language.simple.types.SimpleToken;
import org.apache.camel.language.simple.types.SimpleTokenType;
import org.apache.camel.language.simple.types.TokenType;
+import org.apache.camel.language.simple.types.UnaryOperatorType;
/**
* Base class for Simple language parser.
@@ -198,11 +199,24 @@ public abstract class BaseSimpleParser {
protected void prepareUnaryExpressions(List<SimpleNode> nodes) {
Deque<SimpleNode> stack = new ArrayDeque<>();
+ UnaryExpression pending = null;
for (SimpleNode node : nodes) {
+ if (pending != null) {
+ // a prefix operator works on the node that follows it
(CAMEL-24984)
+ pending.acceptRight(node);
+ pending = null;
+ continue;
+ }
if (node instanceof UnaryExpression token) {
// remember the logical operator
String operator = token.getOperator().toString();
+ if (token.getOperator() == UnaryOperatorType.NOT) {
+ pending = token;
+ stack.push(node);
+ continue;
+ }
+
SimpleNode previous = stack.isEmpty() ? null : stack.pop();
if (previous == null) {
throw new SimpleParserException(
@@ -213,6 +227,10 @@ public abstract class BaseSimpleParser {
}
stack.push(node);
}
+ if (pending != null) {
+ throw new SimpleParserException(
+ "Unary operator ! has no token to negate on its right hand
side", pending.getToken().getIndex());
+ }
// replace nodes from the stack
nodes.clear();
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 6b1c8318dbf7..c4056a536a68 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
@@ -195,6 +195,17 @@ public final class SimpleOperatorConstants {
// --- Logical operators (precedence 30) ---
+ @Metadata(description = "Logical NOT. Negates the function that follows
it, answering the opposite of what that"
+ + " function answers as a predicate: a value that
is not null and not false is true."
+ + " Written directly in front of the function, in
a predicate only: in an expression a !"
+ + " is text, as in Hello ${body}!",
+ label = "logical",
+ examples = {
+ "!${body.isEmpty()}", "${!body.isEmpty()}",
+ "!${body.isEmpty()} && ${header.foo} == 'bar'" },
+ annotations = { "kind=logical", "syntax=!${fn}", "precedence=10"
})
+ public static final String NOT = "!";
+
@Metadata(description = "Logical AND. Both left and right predicates must
evaluate to true.",
label = "logical",
examples = { "${header.title} contains 'Camel' && ${header.type}
== 'gold'" },
diff --git
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimplePredicateParser.java
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimplePredicateParser.java
index 3e28d2b9184c..c0077b362185 100644
---
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimplePredicateParser.java
+++
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimplePredicateParser.java
@@ -63,6 +63,11 @@ import org.apache.camel.support.builder.PredicateBuilder;
*/
public class SimplePredicateParser extends BaseSimpleParser {
+ {
+ // a ! in front of a function negates it, in a predicate only
(CAMEL-24984)
+ tokenizer.setNotOperator(true);
+ }
+
// use caches to avoid re-parsing the same expressions over and over again
private final Map<String, Expression> cacheExpression;
private boolean skipFileFunctions;
@@ -658,6 +663,11 @@ public class SimplePredicateParser extends
BaseSimpleParser {
protected boolean unaryOperator() {
if (accept(TokenType.unaryOperator)) {
+ if ("!".equals(token.getText())) {
+ // ! is written in front of what it negates, and the tokenizer
only makes it an operator when a
+ // function follows it, so leave that function to the next
round of the grammar
+ return true;
+ }
nextToken();
// there should be a whitespace after the operator
expect(TokenType.whiteSpace);
@@ -839,6 +849,11 @@ public class SimplePredicateParser extends
BaseSimpleParser {
// there should be at least one whitespace after the operator
expectAndAcceptMore(TokenType.whiteSpace);
+ // the right hand side may be negated, and the function it negates
follows it (CAMEL-24984)
+ if (accept(TokenType.unaryOperator) &&
"!".equals(token.getText())) {
+ nextToken();
+ }
+
// then we expect either some quoted text, another function, or a
numeric, boolean or null value
if (singleQuotedLiteralWithFunctionsText()
|| doubleQuotedLiteralWithFunctionsText()
diff --git
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleSyntaxHints.java
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleSyntaxHints.java
index 9b80f36a0e8b..5003ac2f52d2 100644
---
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleSyntaxHints.java
+++
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleSyntaxHints.java
@@ -103,6 +103,27 @@ public final class SimpleSyntaxHints {
return expression.substring(start, end);
}
+ /**
+ * The same negation with its function in braces, or null when the text is
not a negated name: {@code !x} is
+ * {@code !${x}}. The parser otherwise reports it as a missing predicate
next to the operator (CAMEL-24984).
+ */
+ private static String negatedFunction(String word) {
+ if (word.length() < 2 || word.charAt(0) != '!' ||
word.startsWith("!${")) {
+ return null;
+ }
+ String rest = word.substring(1);
+ // !=, !contains and the other negated operators are words of their
own, not a negated function
+ if (rest.charAt(0) == '=' || rest.charAt(0) == '$') {
+ return null;
+ }
+ for (String op : new String[] { "contains", "endsWith", "equals",
"in", "is", "range", "regex", "startsWith", "~~" }) {
+ if (rest.equals(op)) {
+ return null;
+ }
+ }
+ return "!${" + rest + "}";
+ }
+
/** The message for a token the grammar does not know at the given index.
*/
public static String unexpectedToken(String expression, int index) {
String word = wordAt(expression, index);
@@ -129,7 +150,8 @@ public final class SimpleSyntaxHints {
return "Unknown operator " + word + ": use || for or, and &&
for and";
case "not":
case "!":
- return "Unknown operator " + word + ": negate the operator
instead, e.g. != or !contains";
+ return "Unknown operator " + word + ": ! negates a function
and is written directly in front of it, "
+ + "e.g. !${body.isEmpty()}; a comparison negates its
operator instead, e.g. != or !contains";
default:
}
String name = functionName(word);
@@ -145,6 +167,10 @@ public final class SimpleSyntaxHints {
/** The message when an operator has no usable value next to it. */
public static String unsupportedOperand(String kind, Object operator,
String expression, int index) {
String word = wordAt(expression, index);
+ String negated = negatedFunction(word);
+ if (negated != null) {
+ return "! negates a function, which is written as ${ }: " + word +
" is written as " + negated;
+ }
if ("Logical".equals(kind)) {
return kind + " operator " + operator + " needs a predicate on the
right hand side, e.g. ${header.foo} == 'bar'"
+ (word.isEmpty() ? "" : "; was: " + word);
@@ -233,6 +259,11 @@ public final class SimpleSyntaxHints {
}
}
}
+ if (text.length() > 1 && text.charAt(0) == '!' &&
!text.startsWith("!${")) {
+ // a negated function on its own, such as !body.isEmpty(): the
predicate parser reads a ! in front of
+ // a ${ }, so give it that form (CAMEL-24984)
+ return "!${" + text.substring(1).trim() + "}";
+ }
return text;
}
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 65ece116c6eb..19d5432d9ccf 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
@@ -26,8 +26,12 @@ import org.apache.camel.util.ObjectHelper;
*/
public class SimpleTokenizer {
+ private static final String NOT_OPERATOR = "!";
+
+ private boolean notOperator;
+
// keep this number in sync with tokens list
- private static final int NUMBER_OF_TOKENS = 56;
+ private static final int NUMBER_OF_TOKENS = 57;
private static final SimpleTokenType[] KNOWN_TOKENS = new
SimpleTokenType[NUMBER_OF_TOKENS];
@@ -110,6 +114,10 @@ public class SimpleTokenizer {
// 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[55] = new SimpleTokenType(TokenType.minusValue, "-");
+
+ // the ! that negates a predicate, last so every operator that starts
with one (!=, !contains, ...)
+ // is matched first, and only offered to a predicate (CAMEL-24984)
+ KNOWN_TOKENS[56] = new SimpleTokenType(TokenType.unaryOperator, "!");
}
/**
@@ -148,6 +156,14 @@ public class SimpleTokenizer {
* @param filter defines the accepted token types to be returned
(character is always used as fallback)
* @return the created token, will always return a token
*/
+ /**
+ * Whether a {@code !} in front of a function negates it. Only a predicate
parser turns this on: in an expression a
+ * {@code !} is ordinary text, as in {@code Hello ${body}!}, and must
never become an operator (CAMEL-24984).
+ */
+ public void setNotOperator(boolean notOperator) {
+ this.notOperator = notOperator;
+ }
+
public SimpleToken nextToken(String expression, int index, boolean
allowEscape, TokenType... filter) {
return doNextToken(expression, index, allowEscape, filter);
}
@@ -188,6 +204,10 @@ public class SimpleTokenizer {
String text = expression.substring(index);
for (int i = 0; i < NUMBER_OF_TOKENS; i++) {
SimpleTokenType token = KNOWN_TOKENS[i];
+ if (NOT_OPERATOR.equals(token.getValue()) && !notOperator) {
+ // a predicate parser enables it; in an expression a ! is
text, as in "Hello World!"
+ continue;
+ }
if (acceptType(token.getType(), filters)
&& acceptToken(token, text, expression, index)) {
onToken(token, index);
@@ -328,6 +348,17 @@ public class SimpleTokenizer {
private static boolean evalUnary(SimpleTokenType token, String text,
String expression, int index) {
int endLen = 1;
+ if (NOT_OPERATOR.equals(token.getValue())) {
+ // ! is written before what it negates, so the rule is the mirror
of ++ and --: the next must be a
+ // function, and the previous must be nothing, a space or an
opening parenthesis, which keeps a ! that
+ // belongs to text ("Hello!", "Warning!${body}") out of it
+ if (!text.startsWith("!${")) {
+ return false;
+ }
+ String before = index > 0 ? expression.substring(index - 1, index)
: "";
+ return before.isEmpty() || " ".equals(before) ||
"(".equals(before);
+ }
+
// special check for unary as the previous must be a function end, and
the next a whitespace
// to ensure unary operators is only applied on functions as intended
int len = token.getValue().length();
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 39f4c8886e52..a3ce8afdfd75 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
@@ -19,6 +19,7 @@ package org.apache.camel.language.simple.ast;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
+import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -30,6 +31,7 @@ import
org.apache.camel.language.simple.types.BinaryOperatorType;
import org.apache.camel.language.simple.types.SimpleIllegalSyntaxException;
import org.apache.camel.language.simple.types.SimpleParserException;
import org.apache.camel.language.simple.types.SimpleToken;
+import org.apache.camel.language.simple.types.UnaryOperatorType;
import org.apache.camel.support.ObjectHelper;
import org.apache.camel.support.builder.ExpressionBuilder;
import org.apache.camel.support.builder.PredicateBuilder;
@@ -58,10 +60,36 @@ public class BinaryExpression extends BaseSimpleNode {
}
public boolean acceptLeftNode(SimpleNode lef) {
+ if (lef instanceof UnaryExpression unary && unary.getOperator() ==
UnaryOperatorType.NOT) {
+ // ! negates a function, not a comparison: !${body} == 'x' would
read as neither of the two things it
+ // could mean, so it is refused and the negated operator is
offered instead (CAMEL-24984)
+ throw new SimpleParserException(
+ "! cannot be compared: it negates a function, not a
comparison"
+ + (negatedOperator() != null
+ ? ": write ${...} " +
negatedOperator() + " value"
+ : ": compare the other way
round"),
+ getToken().getIndex());
+ }
this.left = lef;
return true;
}
+ /** The eleven operators that have one saying the opposite; the
comparisons do not, and answer null. */
+ private static final Set<String> NEGATED = Set.of(
+ "==", "=~", "~~", "contains", "endsWith", "equals", "in", "is",
"range", "regex", "startsWith");
+
+ /**
+ * The operator that says the opposite of this one, or null when it has
none: {@code >} and the other comparisons
+ * are negated by using the opposite comparison, not by putting a ! in
front of them.
+ */
+ private String negatedOperator() {
+ String text = operator.toString();
+ if (!NEGATED.contains(text)) {
+ return null;
+ }
+ return "==".equals(text) ? "!=" : "!" + text;
+ }
+
public boolean acceptRightNode(SimpleNode right) {
this.right = right;
return true;
diff --git
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/LogicalExpression.java
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/LogicalExpression.java
index f4e0c37ca1be..ec7f1710c878 100644
---
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/LogicalExpression.java
+++
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/LogicalExpression.java
@@ -23,6 +23,7 @@ import org.apache.camel.Predicate;
import org.apache.camel.language.simple.types.LogicalOperatorType;
import org.apache.camel.language.simple.types.SimpleParserException;
import org.apache.camel.language.simple.types.SimpleToken;
+import org.apache.camel.language.simple.types.UnaryOperatorType;
import org.apache.camel.support.ExpressionToPredicateAdapter;
import org.apache.camel.support.builder.PredicateBuilder;
import org.apache.camel.util.ObjectHelper;
@@ -70,7 +71,9 @@ public class LogicalExpression extends BaseSimpleNode {
private static boolean isValidPredicateOperand(SimpleNode node) {
return node instanceof BinaryExpression
|| node instanceof LogicalExpression
- || node instanceof SimpleFunctionStart;
+ || node instanceof SimpleFunctionStart
+ // a negated function is a predicate too; ++ and -- are
numeric (CAMEL-24984)
+ || node instanceof UnaryExpression unary &&
unary.getOperator() == UnaryOperatorType.NOT;
}
public LogicalOperatorType getOperator() {
diff --git
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/SimpleFunctionExpression.java
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/SimpleFunctionExpression.java
index 00a839c12a32..ba0c801ed081 100644
---
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/SimpleFunctionExpression.java
+++
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/SimpleFunctionExpression.java
@@ -136,6 +136,31 @@ public class SimpleFunctionExpression extends
LiteralExpression {
};
}
+ /** The function without its leading {@code !}, answering the opposite of
what it answers (CAMEL-24984). */
+ private Expression createNegatedExpression(CamelContext camelContext,
String function) {
+ final String name = function.substring(1).trim();
+ final Expression exp = doCreateSimpleExpression(camelContext, name);
+ return new Expression() {
+ @Override
+ public void init(CamelContext context) {
+ exp.init(context);
+ }
+
+ @Override
+ public <T> T evaluate(Exchange exchange, Class<T> type) {
+ Object value = exp.evaluate(exchange, Object.class);
+ // the same rule the language uses for a predicate on its own
(CAMEL-24984)
+ boolean matches = ObjectHelper.evaluateValuePredicate(value);
+ return
exchange.getContext().getTypeConverter().convertTo(type, exchange, !matches);
+ }
+
+ @Override
+ public String toString() {
+ return "!${" + name + "}";
+ }
+ };
+ }
+
private Expression doCreateSimpleExpression(CamelContext camelContext,
String function) {
// ${body != null && body.size() > 0}: the braces hold a predicate,
which is what they hold in EL,
// Groovy and a JavaScript template, so read it as one (CAMEL-24921)
@@ -143,6 +168,11 @@ public class SimpleFunctionExpression extends
LiteralExpression {
if (predicate != null) {
return predicate;
}
+ // ${!body.isEmpty()}: a ! in front of a single function negates what
it answers. It is read after the
+ // predicate above, so a ! in ${!a && b} negates a and not the whole
predicate (CAMEL-24984)
+ if (function.length() > 1 && function.charAt(0) == '!') {
+ return createNegatedExpression(camelContext, function);
+ }
// return the function directly if we can create function without
analyzing the prefix
Expression answer = DIRECT_FACTORY.createFunction(camelContext,
function, token.getIndex());
if (answer != null) {
diff --git
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/UnaryExpression.java
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/UnaryExpression.java
index 07fcd0fb1c60..dd5d39158715 100644
---
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/UnaryExpression.java
+++
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/UnaryExpression.java
@@ -61,6 +61,13 @@ public class UnaryExpression extends BaseSimpleNode {
this.left = left;
}
+ /**
+ * NOT is written before what it negates, so the node it works upon comes
from the right (CAMEL-24984).
+ */
+ public void acceptRight(SimpleNode right) {
+ this.left = right;
+ }
+
public UnaryOperatorType getOperator() {
return operator;
}
@@ -79,11 +86,36 @@ public class UnaryExpression extends BaseSimpleNode {
return createIncDecExpression(camelContext, leftExp, 1);
} else if (operator == UnaryOperatorType.DEC) {
return createIncDecExpression(camelContext, leftExp, -1);
+ } else if (operator == UnaryOperatorType.NOT) {
+ return createNotExpression(camelContext, leftExp);
}
throw new SimpleParserException("Unknown unary operator " + operator,
token.getIndex());
}
+ private Expression createNotExpression(CamelContext camelContext, final
Expression exp) {
+ return new Expression() {
+ @Override
+ public void init(CamelContext context) {
+ exp.init(context);
+ }
+
+ @Override
+ public <T> T evaluate(Exchange exchange, Class<T> type) {
+ Object value = exp.evaluate(exchange, Object.class);
+ // the same rule the language uses for a predicate on its own,
where ${body} is true and a missing
+ // header is false, so !${body} and !${header.foo} answer the
opposite of those (CAMEL-24984)
+ boolean matches = ObjectHelper.evaluateValuePredicate(value);
+ return camelContext.getTypeConverter().convertTo(type,
exchange, !matches);
+ }
+
+ @Override
+ public String toString() {
+ return "!" + left;
+ }
+ };
+ }
+
private Expression createIncDecExpression(CamelContext camelContext, final
Expression leftExp, final int delta) {
return new Expression() {
@Override
diff --git
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/types/UnaryOperatorType.java
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/types/UnaryOperatorType.java
index fed35954cb68..c6d0b6443a4a 100644
---
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/types/UnaryOperatorType.java
+++
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/types/UnaryOperatorType.java
@@ -25,13 +25,17 @@ package org.apache.camel.language.simple.types;
public enum UnaryOperatorType {
INC,
- DEC;
+ DEC,
+ /** Negates a predicate, and unlike INC and DEC it is written before what
it works upon (CAMEL-24984). */
+ NOT;
public static UnaryOperatorType asOperator(String text) {
if ("++".equals(text)) {
return INC;
} else if ("--".equals(text)) {
return DEC;
+ } else if ("!".equals(text)) {
+ return NOT;
}
throw new IllegalArgumentException("Operator not supported: " + text);
}
@@ -41,6 +45,8 @@ public enum UnaryOperatorType {
return "++";
} else if (operator == DEC) {
return "--";
+ } else if (operator == NOT) {
+ return "!";
}
return "";
}
diff --git
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleNotOperatorTest.java
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleNotOperatorTest.java
new file mode 100644
index 000000000000..d8dc33a9c39d
--- /dev/null
+++
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleNotOperatorTest.java
@@ -0,0 +1,149 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.language.simple;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.camel.ExchangeTestSupport;
+import org.apache.camel.Expression;
+import org.apache.camel.Predicate;
+import org.apache.camel.language.simple.types.SimpleIllegalSyntaxException;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * {@code !} negates a function in a predicate, the way every other language
writes it (CAMEL-24984). It is a predicate
+ * operator only: in an expression a {@code !} is text, as in {@code Hello
${body}!}.
+ */
+public class SimpleNotOperatorTest extends ExchangeTestSupport {
+
+ private boolean predicate(String text) {
+ Predicate p = context.resolveLanguage("simple").createPredicate(text);
+ p.init(context);
+ return p.matches(exchange);
+ }
+
+ private Object expression(String text) {
+ Expression e =
context.resolveLanguage("simple").createExpression(text);
+ e.init(context);
+ return e.evaluate(exchange, Object.class);
+ }
+
+ @Test
+ public void testNegatesAFunction() {
+ exchange.getMessage().setBody(new LinkedHashMap<>(Map.of("a", 1)));
+ assertTrue(predicate("!${body.isEmpty()}"));
+ assertFalse(predicate("!${body.containsKey('a')}"));
+ }
+
+ @Test
+ public void testWithLogicalOperators() {
+ exchange.getMessage().setBody(new LinkedHashMap<>(Map.of("a", 1)));
+ // on the left, on the right, and the form that reaches it through the
operators inside ${ } (CAMEL-24921)
+ assertTrue(predicate("!${body.isEmpty()} && ${body} != null"));
+ assertTrue(predicate("${body} == null || !${body.isEmpty()}"));
+ assertTrue(predicate("${body != null && !${body.isEmpty()}}"));
+ }
+
+ @Test
+ public void testInsideTheBraces() {
+ exchange.getMessage().setBody(new LinkedHashMap<>(Map.of("a", 1)));
+ // a ! in front of the function inside the braces, with and without a
nested ${ }
+ assertTrue(predicate("${!body.isEmpty()}"));
+ assertFalse(predicate("${!body.containsKey('a')}"));
+ assertTrue(predicate("${body != null && !body.isEmpty()}"));
+ assertTrue(predicate("${!body.isEmpty() && body != null}"));
+ }
+
+ @Test
+ public void testNegatesOnlyItsOwnFunction() {
+ exchange.getMessage().setBody(new LinkedHashMap<>(Map.of("a", 1)));
+ // (!isEmpty) && (body == null) is true && false; negating the whole
predicate would answer true
+ assertFalse(predicate("${!body.isEmpty() && body == null}"));
+ assertFalse(predicate("!${body.isEmpty()} && ${body} == null"));
+ }
+
+ @Test
+ public void testAnswersTheOppositeOfThePredicate() {
+ // the same rule the language uses for a predicate on its own
+ exchange.getMessage().setBody(new LinkedHashMap<>(Map.of("a", 1)));
+ assertFalse(predicate("!${body}"));
+ assertTrue(predicate("!${header.missing}"));
+ }
+
+ @Test
+ public void testCannotNegateAComparison() {
+ exchange.getMessage().setBody("y");
+ // it would read as neither of the two things it could mean, so it is
refused rather than answered
+ SimpleIllegalSyntaxException e =
assertThrows(SimpleIllegalSyntaxException.class,
+ () -> predicate("!${body} == 'x'"));
+ assertTrue(e.getMessage().contains("! cannot be compared"),
e.getMessage());
+ // the operator that says the opposite, not a mangled one
+ assertTrue(e.getMessage().contains("${...} != value"), e.getMessage());
+ assertFalse(e.getMessage().contains("!=!="), e.getMessage());
+ assertThrows(SimpleIllegalSyntaxException.class, () ->
predicate("${body} == !${body}"));
+ // the negated operator is the way to say it
+ assertTrue(predicate("${body} != 'x'"));
+ }
+
+ @Test
+ public void testSaysHowToWriteANegationItCannotRead() {
+ exchange.getMessage().setBody("x");
+ // ! before something that is not a ${ } function: say the form that
works
+ SimpleIllegalSyntaxException e =
assertThrows(SimpleIllegalSyntaxException.class,
+ () -> predicate("${body} != null && !someFlag"));
+ assertTrue(e.getMessage().contains("! negates a function, which is
written as ${ }"), e.getMessage());
+ assertTrue(e.getMessage().contains("!${someFlag}"), e.getMessage());
+ // a negated operator is not a negated function
+ assertTrue(predicate("${body} !contains 'zz'"));
+ }
+
+ @Test
+ public void testEmptyBodyIsNegatedToFalse() {
+ exchange.getMessage().setBody(new LinkedHashMap<>());
+ assertFalse(predicate("!${body.isEmpty()}"));
+ }
+
+ @Test
+ public void testTheNegatedOperatorsAreUnaffected() {
+ exchange.getMessage().setBody("Hello");
+ assertTrue(predicate("${body} != 'x'"));
+ assertTrue(predicate("${body} !contains 'zz'"));
+ assertTrue(predicate("${body} !startsWith 'zz'"));
+ assertTrue(predicate("${body} !endsWith 'zz'"));
+ assertTrue(predicate("${body} !in 'a,b'"));
+ }
+
+ @Test
+ public void testAnExclamationMarkInTextIsNotAnOperator() {
+ exchange.getMessage().setBody("World");
+ // an expression never has operators, so every one of these is text
+ assertEquals("Hello World! how are you", expression("Hello ${body}!
how are you"));
+ assertEquals("!aaa! is a weird text", expression("!aaa! is a weird
text"));
+ assertEquals("Alert: !World", expression("Alert: !${body}"));
+ assertEquals("Order World!!!", expression("Order ${body}!!!"));
+ // and in a predicate a quoted one is text too
+ exchange.getMessage().setBody("Hello!");
+ assertTrue(predicate("${body} == 'Hello!'"));
+ assertTrue(predicate("${body} contains '!'"));
+ }
+}