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();
+    }
+}

Reply via email to