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 edb3a1406c60 CAMEL-24921: simple - read a predicate inside ${ }
(#26767)
edb3a1406c60 is described below
commit edb3a1406c60394b7873ccb60c362f5f4016cf79
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Sep 23 14:20:52 2026 +0200
CAMEL-24921: simple - read a predicate inside ${ } (#26767)
${body != null && body.size() > 0} was refused with Operators go outside
the function. The braces now answer the predicate when they hold one, the way
the braces do in Jakarta EL, Groovy and a JavaScript template. The form with
the operators outside means the same and is unchanged, and the ternary keeps
working. An operator counts only when whitespace surrounds it outside quotes,
so ${header.Content-Length} is still a name and ${date:now:yyyy-MM-dd} still a
pattern. All 804 distinct s [...]
---
.../camel/catalog/docs/simple-operators.adoc | 18 ++++
.../modules/languages/pages/simple-operators.adoc | 18 ++++
.../camel/language/simple/SimpleSyntaxHints.java | 87 +++++++++++++++++++
.../simple/ast/SimpleFunctionExpression.java | 45 +++++++++-
.../language/simple/ast/SimpleFunctionStart.java | 83 +-----------------
.../simple/SimplePredicateInBracesTest.java | 98 ++++++++++++++++++++++
.../language/simple/SimpleSyntaxHintsTest.java | 16 +++-
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 13 +++
.../ai/PredicateInBracesValidatorTest.java | 57 +++++++++++++
9 files changed, 349 insertions(+), 86 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 7ce0e007b06b..ff9580d8f3f8 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
@@ -24,6 +24,24 @@ Camel will automatically type convert the rightValue type to
the
leftValue type, so it is able to e.g., convert a string into a numeric, so
you can use `>` comparison for numeric values.
+=== Where the operators go
+
+An operator can be written inside `${ }` or outside it:
+
+[source,text]
+----
+${body != null && body.size() > 0} <1>
+${body} != null && ${body.size()} > 0 <2>
+----
+<1> the braces hold a predicate, and answer whether it matches
+<2> the operators are between the functions, and the whole text is the
predicate
+
+Both mean the same thing. Inside the braces the answer is a value, so it can
be used in a template as
+well: `Over three: ${header.n > 3}` gives `Over three: true`.
+
+An operator is only an operator when whitespace surrounds it, which is what
keeps a name such as
+`${header.Content-Length}` and a pattern such as `${date:now:yyyy-MM-dd}` a
plain function.
+
=== Comparison Operators
The following comparison operators are supported:
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 7ce0e007b06b..ff9580d8f3f8 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
@@ -24,6 +24,24 @@ Camel will automatically type convert the rightValue type to
the
leftValue type, so it is able to e.g., convert a string into a numeric, so
you can use `>` comparison for numeric values.
+=== Where the operators go
+
+An operator can be written inside `${ }` or outside it:
+
+[source,text]
+----
+${body != null && body.size() > 0} <1>
+${body} != null && ${body.size()} > 0 <2>
+----
+<1> the braces hold a predicate, and answer whether it matches
+<2> the operators are between the functions, and the whole text is the
predicate
+
+Both mean the same thing. Inside the braces the answer is a value, so it can
be used in a template as
+well: `Over three: ${header.n > 3}` gives `Over three: true`.
+
+An operator is only an operator when whitespace surrounds it, which is what
keeps a name such as
+`${header.Content-Length}` and a pattern such as `${date:now:yyyy-MM-dd}` a
plain function.
+
=== Comparison Operators
The following comparison operators are supported:
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 f56601ce9c2d..171a3258aa09 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
@@ -149,6 +149,93 @@ public final class SimpleSyntaxHints {
+ VALUE_FORMS + (isKnownFunction(functionName(word)) ? ", e.g.
${" + word + "}" : ", e.g. '" + word + "'");
}
+ /** The comparison operators, with the spaces they must be surrounded by.
*/
+ private static final String[] SPACED_OPERATORS = {
+ " >= ", " <= ", " > ", " < ", " == ", " != ", " =~ ", " !=~ ",
+ " contains ", " !contains ", " ~~ ", " !~~ ", " regex ", " !regex
",
+ " in ", " !in ", " is ", " !is ", " range ", " !range ",
+ " startsWith ", " !startsWith ", " endsWith ", " !endsWith " };
+
+ /**
+ * Wraps the function references of a predicate written inside {@code ${
}} so that it can be parsed as one:
+ * {@code body != null && body.size() > 0} becomes {@code ${body} != null
&& ${body.size()} > 0}.
+ * <p/>
+ * Each comparison is wrapped, not only the first, so that a compound
condition reads the way it looks (CAMEL-24920,
+ * CAMEL-24921). An operator counts only when whitespace surrounds it
outside quotes, which is what keeps
+ * {@code ${header.Content-Length}} and {@code ${date:now:yyyy-MM-dd}} a
plain function.
+ */
+ public static String wrapFunctions(String text) {
+ StringBuilder answer = new StringBuilder();
+ int from = 0;
+ for (int at = logicalOperator(text, 0); at >= 0; at =
logicalOperator(text, from)) {
+ // the operator matched with its trailing space, so the next space
is at most two characters away
+ int end = text.indexOf(' ', at + 1);
+ answer.append(wrapComparison(text.substring(from, at).trim()));
+ answer.append(' ').append(text, at, end).append(' ');
+ from = end + 1;
+ }
+ answer.append(wrapComparison(text.substring(from).trim()));
+ return answer.toString();
+ }
+
+ /**
+ * The index of the next logical operator ({@code &&} or {@code ||})
outside quotes, or -1. Simple has no word
+ * forms: {@code and} and {@code or} are refused by the parser with a
message that says so.
+ */
+ private static int logicalOperator(String text, int from) {
+ boolean single = false;
+ boolean dubble = false;
+ for (int i = from; i < text.length(); i++) {
+ char c = text.charAt(i);
+ if (c == '\'' && !dubble) {
+ single = !single;
+ } else if (c == '"' && !single) {
+ dubble = !dubble;
+ } else if (!single && !dubble && c == ' ') {
+ for (String op : new String[] { "&& ", "|| " }) {
+ if (text.startsWith(op, i + 1)) {
+ return i + 1;
+ }
+ }
+ }
+ }
+ return -1;
+ }
+
+ /** Wraps the left hand side of one comparison with {@code ${ }} when it
is a function reference. */
+ private static String wrapComparison(String text) {
+ for (String op : SPACED_OPERATORS) {
+ int at = text.indexOf(op);
+ if (at < 0 && text.endsWith(op.stripTrailing())) {
+ // the operator ends the text: wrap what is there, so the
parser says what is missing after it
+ at = text.length() - op.stripTrailing().length();
+ }
+ if (at > 0) {
+ String left = text.substring(0, at).trim();
+ String right = at + op.length() <= text.length() ?
text.substring(at + op.length()).trim() : "";
+ if (!left.startsWith("${") && !left.startsWith("'") &&
!left.startsWith("\"")
+ && !isNumeric(left) && !"true".equalsIgnoreCase(left)
+ && !"false".equalsIgnoreCase(left) &&
!"null".equalsIgnoreCase(left)) {
+ left = "${" + left + "}";
+ }
+ return left + op + right;
+ }
+ }
+ return text;
+ }
+
+ private static boolean isNumeric(String text) {
+ if (text == null || text.isEmpty()) {
+ return false;
+ }
+ try {
+ Double.parseDouble(text);
+ return true;
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+
/**
* When an operator is written inside the function (${body == 'x'}), the
rewrite with the operator outside, else
* null.
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 c120eabf3082..0b8f61958605 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
@@ -19,10 +19,13 @@ package org.apache.camel.language.simple.ast;
import java.util.Map;
import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
import org.apache.camel.Expression;
+import org.apache.camel.Predicate;
import org.apache.camel.language.simple.FileExpressionBuilder;
import org.apache.camel.language.simple.SimpleFunctionDispatcher;
import org.apache.camel.language.simple.SimpleFunctionHelper;
+import org.apache.camel.language.simple.SimplePredicateParser;
import org.apache.camel.language.simple.SimpleSyntaxHints;
import org.apache.camel.language.simple.functions.DirectFunctionFactory;
import org.apache.camel.language.simple.types.SimpleParserException;
@@ -98,12 +101,46 @@ public class SimpleFunctionExpression extends
LiteralExpression {
private static final DirectFunctionFactory DIRECT_FACTORY = new
DirectFunctionFactory();
+ /**
+ * A predicate written inside the braces, as an expression that answers
whether it matches; null when the text is
+ * not a predicate but a plain function (CAMEL-24921).
+ * <p/>
+ * An operator counts only when whitespace surrounds it outside quotes, so
{@code ${header.Content-Length}} and
+ * {@code ${date:now:yyyy-MM-dd}} are names, not arithmetic.
+ */
+ private Expression createPredicateExpression(CamelContext camelContext,
String function) {
+ if (SimpleSyntaxHints.operatorsOutside(function) == null) {
+ return null;
+ }
+ String text = SimpleSyntaxHints.wrapFunctions(function);
+ final Predicate predicate;
+ try {
+ predicate = new SimplePredicateParser(camelContext, text, true,
skipFileFunctions, null).parsePredicate();
+ } catch (SimpleParserException e) {
+ // not a predicate after all: say what is wrong with it, at the
place it went wrong
+ throw new SimpleParserException(e.getMessage(), token.getIndex());
+ }
+ return new Expression() {
+ @Override
+ public <T> T evaluate(Exchange exchange, Class<T> type) {
+ boolean matches = predicate.matches(exchange);
+ return
exchange.getContext().getTypeConverter().convertTo(type, exchange, matches);
+ }
+
+ @Override
+ public String toString() {
+ return text;
+ }
+ };
+ }
+
private Expression doCreateSimpleExpression(CamelContext camelContext,
String function, boolean strict) {
if (strict) {
- // ${body == 'x'}: the operator belongs outside the function
(CAMEL-24703)
- String rewrite = SimpleSyntaxHints.operatorsOutside(function);
- if (rewrite != null) {
- throw new SimpleParserException("Operators go outside the
function: " + rewrite, token.getIndex());
+ // ${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)
+ Expression predicate = createPredicateExpression(camelContext,
function);
+ if (predicate != null) {
+ return predicate;
}
}
// return the function directly if we can create function without
analyzing the prefix
diff --git
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/SimpleFunctionStart.java
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/SimpleFunctionStart.java
index f1052ca34d4c..54a320073a09 100644
---
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/SimpleFunctionStart.java
+++
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/SimpleFunctionStart.java
@@ -25,6 +25,7 @@ import org.apache.camel.Expression;
import org.apache.camel.Predicate;
import org.apache.camel.language.simple.SimpleExpressionParser;
import org.apache.camel.language.simple.SimplePredicateParser;
+import org.apache.camel.language.simple.SimpleSyntaxHints;
import org.apache.camel.language.simple.types.SimpleIllegalSyntaxException;
import org.apache.camel.language.simple.types.SimpleParserException;
import org.apache.camel.language.simple.types.SimpleToken;
@@ -332,86 +333,8 @@ public class SimpleFunctionStart extends BaseSimpleNode
implements BlockStart {
if (conditionText.contains("${")) {
return conditionText;
}
-
- // the condition may be several comparisons joined by a logical
operator, and each of them needs its
- // functions wrapped, not only the first (CAMEL-24920)
- StringBuilder answer = new StringBuilder();
- int from = 0;
- for (int at = logicalOperator(conditionText, 0); at >= 0; at =
logicalOperator(conditionText, from)) {
- // the operator matched with its trailing space, so the next space
is at most two characters away
- int end = conditionText.indexOf(' ', at + 1);
- answer.append(wrapComparison(conditionText.substring(from,
at).trim()));
- answer.append(' ').append(conditionText, at, end).append(' ');
- from = end + 1;
- }
- answer.append(wrapComparison(conditionText.substring(from).trim()));
- return answer.toString();
- }
-
- /**
- * The index of the next logical operator ({@code &&} or {@code ||})
outside quotes, or -1. Simple has no word
- * forms: {@code and} and {@code or} are refused by the parser with a
message that says so.
- */
- private static int logicalOperator(String text, int from) {
- boolean single = false;
- boolean dubble = false;
- for (int i = from; i < text.length(); i++) {
- char c = text.charAt(i);
- if (c == '\'' && !dubble) {
- single = !single;
- } else if (c == '"' && !single) {
- dubble = !dubble;
- } else if (!single && !dubble && c == ' ') {
- for (String op : new String[] { "&& ", "|| " }) {
- if (text.startsWith(op, i + 1)) {
- return i + 1;
- }
- }
- }
- }
- return -1;
- }
-
- /** Wraps the left hand side of one comparison with ${} when it is a
function reference. */
- private String wrapComparison(String conditionText) {
- // Find the operator in the condition
- String[] operators = {
- " >= ", " <= ", " > ", " < ", " == ", " != ", " =~ ", " !=~ ",
- " contains ", " !contains ", " ~~ ", " !~~ ", " regex ", "
!regex ",
- " in ", " !in ", " is ", " !is ", " range ", " !range ",
- " startsWith ", " !startsWith ", " endsWith ", " !endsWith " };
-
- for (String op : operators) {
- int opIdx = conditionText.indexOf(op);
- if (opIdx > 0) {
- String leftSide = conditionText.substring(0, opIdx).trim();
- String rightSide = conditionText.substring(opIdx +
op.length()).trim();
-
- // Wrap the left side with ${} if it looks like a function
reference
- if (!leftSide.startsWith("${") && !leftSide.startsWith("'") &&
!leftSide.startsWith("\"")
- && !isNumeric(leftSide) &&
!"true".equalsIgnoreCase(leftSide)
- && !"false".equalsIgnoreCase(leftSide) &&
!"null".equalsIgnoreCase(leftSide)) {
- leftSide = "${" + leftSide + "}";
- }
-
- return leftSide + op + rightSide;
- }
- }
-
- // No operator found, return as-is
- return conditionText;
- }
-
- private boolean isNumeric(String str) {
- if (str == null || str.isEmpty()) {
- return false;
- }
- try {
- Double.parseDouble(str);
- return true;
- } catch (NumberFormatException e) {
- return false;
- }
+ // the same wrapping a predicate written inside ${ } gets, so the two
cannot drift (CAMEL-24920)
+ return SimpleSyntaxHints.wrapFunctions(conditionText);
}
/**
diff --git
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimplePredicateInBracesTest.java
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimplePredicateInBracesTest.java
new file mode 100644
index 000000000000..bdd7d9d67491
--- /dev/null
+++
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimplePredicateInBracesTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.List;
+import java.util.Map;
+
+import org.apache.camel.LanguageTestSupport;
+import org.junit.jupiter.api.Test;
+
+/**
+ * CAMEL-24921: {@code ${ }} may hold a predicate, the way the braces do in
Jakarta EL, Groovy and a JavaScript
+ * template, instead of refusing the shape.
+ */
+public class SimplePredicateInBracesTest extends LanguageTestSupport {
+
+ @Override
+ protected String getLanguageName() {
+ return "simple";
+ }
+
+ @Test
+ public void testPredicateInsideTheBraces() {
+ exchange.getIn().setBody(List.of(Map.of("sku", "CAMEL-MUG")));
+ assertPredicate("${body != null && body.size() > 0}", true);
+ assertExpression("${body != null && body.size() > 0}", "true");
+
+ exchange.getIn().setBody(List.of());
+ assertPredicate("${body != null && body.size() > 0}", false);
+ }
+
+ @Test
+ public void testOneComparison() {
+ exchange.getIn().setHeader("n", 5);
+ assertPredicate("${header.n > 0}", true);
+ assertPredicate("${header.n > 10}", false);
+ assertPredicate("${header.n == 5}", true);
+ exchange.getIn().setBody("Hello");
+ assertPredicate("${body contains 'ell'}", true);
+ assertPredicate("${body.length() > 3}", true);
+ }
+
+ @Test
+ public void testBothFormsAgree() {
+ exchange.getIn().setHeader("n", 5);
+ assertPredicate("${header.n > 0 && header.n < 10}", true);
+ assertPredicate("${header.n} > 0 && ${header.n} < 10", true);
+ assertPredicate("${header.n > 0 || header.n > 100}", true);
+ }
+
+ @Test
+ public void testAValueInTextIsStillATemplate() {
+ exchange.getIn().setHeader("n", 5);
+ // outside the braces the expression is a template, and that has not
changed
+ assertExpression("${header.n} > 0", "5 > 0");
+ // inside the braces it is the answer of the predicate, which a
template can now hold
+ assertExpression("Count ${header.n} is over three: ${header.n > 3}",
"Count 5 is over three: true");
+ }
+
+ @Test
+ public void testANameIsNotArithmetic() {
+ // an operator counts only when whitespace surrounds it, so these are
names and patterns as before
+ exchange.getIn().setHeader("Content-Length", 42);
+ assertExpression("${header.Content-Length}", "42");
+ exchange.getIn().setHeader("aws-s3-bucket", "orders");
+ assertExpression("${header.aws-s3-bucket}", "orders");
+ assertExpression("${date:now:yyyy-MM-dd}".substring(0, 10) + "}",
null, true);
+ }
+
+ @Test
+ public void testTheTernaryStillWins() {
+ exchange.getIn().setHeader("n", 5);
+ assertExpression("${header.n > 0 ? 'positive' : 'negative'}",
"positive");
+ assertExpression("${header.n > 0 && header.n < 10 ? 'in' : 'out'}",
"in");
+ }
+
+ private void assertExpression(String expression, Object expected, boolean
onlyParse) {
+ if (onlyParse) {
+ context.resolveLanguage("simple").createExpression(expression);
+ return;
+ }
+ assertExpression(expression, expected);
+ }
+}
diff --git
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleSyntaxHintsTest.java
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleSyntaxHintsTest.java
index 38dbdbc25faa..b67842929599 100644
---
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleSyntaxHintsTest.java
+++
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleSyntaxHintsTest.java
@@ -50,7 +50,15 @@ public class SimpleSyntaxHintsTest extends
ExchangeTestSupport {
@Test
public void testOperatorInsideFunction() {
- assertThat(predicateError("${body == 'x'}")).contains("Operators go
outside the function: ${body} == 'x'");
+ // CAMEL-24921: the braces may hold a predicate, which is then what
they answer
+ exchange.getIn().setBody("x");
+ assertEquals(true,
context.resolveLanguage("simple").createPredicate("${body ==
'x'}").matches(exchange));
+ exchange.getIn().setBody("y");
+ assertEquals(false,
context.resolveLanguage("simple").createPredicate("${body ==
'x'}").matches(exchange));
+ // and what is inside must still be a predicate the parser
understands, reported against the wrapped text
+ assertThat(predicateError("${body == }"))
+ .contains("Unexpected token ==")
+ .contains("${body} ==");
}
@Test
@@ -130,7 +138,11 @@ public class SimpleSyntaxHintsTest extends
ExchangeTestSupport {
@Test
public void testOperatorAfterOgnlMethod() {
- assertThat(predicateError("${body.length() > 3}")).contains("Operators
go outside the function: ${body.length()} > 3");
+ // CAMEL-24921: an OGNL call on the left of the operator is wrapped as
the function it is
+ exchange.getIn().setBody("hello");
+ assertEquals(true,
context.resolveLanguage("simple").createPredicate("${body.length() >
3}").matches(exchange));
+ exchange.getIn().setBody("hi");
+ assertEquals(false,
context.resolveLanguage("simple").createPredicate("${body.length() >
3}").matches(exchange));
}
@Test
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 7d66a04dd688..1c5679137d41 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -19,6 +19,19 @@ OAuth client credentials token caching now distinguishes
profiles by client secr
=== Simple language
+`${ }` may now hold a predicate, as the braces do in Jakarta EL, Groovy and a
JavaScript template:
+`${body != null && body.size() > 0}` answers whether it matches, instead of
being refused with
+_Operators go outside the function_. The form with the operators outside the
braces
+(`${body} != null && ${body.size()} > 0`) means the same and is unchanged, and
the ternary keeps
+working as before.
+
+An operator counts only when whitespace surrounds it outside quotes, so a name
such as
+`${header.Content-Length}` and a pattern such as `${date:now:yyyy-MM-dd}` are
read as they were.
+Only expressions that used to throw can now return a value: every one of the
804 distinct simple
+expressions in camel, camel-kamelets, camel-examples and camel-jbang-examples
parses to what it
+parsed to before.
+
+
The simple language reads a `Map` with a dot as well as with a key:
`${body.sku}` answers the `sku` entry of a map
body when the map has no `sku()` method, the same value `${body[sku]}` gives.
A method of the map still wins, so
`${body.size}` calls `size()` as before, and a name that is neither a method
nor a key still fails. Only expressions
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/PredicateInBracesValidatorTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/PredicateInBracesValidatorTest.java
new file mode 100644
index 000000000000..dae26c73370c
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/PredicateInBracesValidatorTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.dsl.jbang.core.commands.ai;
+
+import java.util.List;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-24921: what the runtime accepts, the tools that write files must
accept too - a route with a predicate inside
+ * the braces is not refused before it ever runs.
+ */
+class PredicateInBracesValidatorTest {
+
+ private static final String ROUTE = """
+ - route:
+ from:
+ uri: direct:start
+ steps:
+ - choice:
+ when:
+ - expression:
+ simple: "${body != null && body.size() > 0}"
+ steps:
+ - log:
+ message: "some"
+ otherwise:
+ steps:
+ - log:
+ message: "none"
+ """;
+
+ @Test
+ void aPredicateInsideTheBracesValidates() {
+ CamelCatalog catalog = new DefaultCamelCatalog();
+ List<String> problems = SourceValidator.validateCamelYaml(ROUTE,
catalog);
+ assertThat(problems).as("the runtime accepts this route, so the
validator must too").isEmpty();
+ }
+}