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 1c94c6ef20aa CAMEL-25954: camel-core-languages - Simple: a predicate
inside ${ } is split at an operator outside brackets and parentheses only
(#28271)
1c94c6ef20aa is described below
commit 1c94c6ef20aa100fef0eb017de5d37e6e521e8d4
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Oct 25 12:58:06 2026 +0100
CAMEL-25954: camel-core-languages - Simple: a predicate inside ${ } is
split at an operator outside brackets and parentheses only (#28271)
SimpleSyntaxHints.wrapFunctions split a predicate written inside ${ }, and
the condition of a ternary there, at its first operator outside quotes, also
when the operator was in an index or an argument, such as ${body.?[qty <
1].size() > 0 ? 'a' : 'b'} or ${iif(header.a > 0 && header.b > 0, 'x', 'y')}.
The error then quoted text that was never written. The comparison operators and
&& and || now count only outside quotes, brackets and parentheses.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
.../camel/language/simple/SimpleSyntaxHints.java | 25 ++++++++++++++++----
.../language/simple/SimpleSyntaxHintsTest.java | 27 ++++++++++++++++++++++
2 files changed, 47 insertions(+), 5 deletions(-)
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 1e8a344a38fc..cfed1a9a57ef 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
@@ -194,8 +194,9 @@ public final class SimpleSyntaxHints {
* {@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.
+ * CAMEL-24921). An operator counts only when whitespace surrounds it
outside quotes, brackets and parentheses,
+ * which is what keeps {@code ${header.Content-Length}} and {@code
${date:now:yyyy-MM-dd}} a plain function, and an
+ * operator in an index or an argument, as in {@code ${iif(header.a > 0,
'x', 'y')}}, where it is.
*/
public static String wrapFunctions(String text) {
StringBuilder answer = new StringBuilder();
@@ -218,13 +219,18 @@ public final class SimpleSyntaxHints {
private static int logicalOperator(String text, int from) {
boolean single = false;
boolean dubble = false;
+ int depth = 0;
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 == ' ') {
+ } else if (!single && !dubble && (c == '(' || c == '[')) {
+ depth++;
+ } else if (!single && !dubble && (c == ')' || c == ']')) {
+ depth--;
+ } else if (!single && !dubble && depth == 0 && c == ' ') {
for (String op : new String[] { "&& ", "|| " }) {
if (text.startsWith(op, i + 1)) {
return i + 1;
@@ -278,17 +284,26 @@ public final class SimpleSyntaxHints {
return left + op + right;
}
- /** The index of the text outside single and double quotes, or -1. */
+ /**
+ * The index of the text outside single and double quotes, brackets and
parentheses, or -1: an operator inside them
+ * belongs to an index or an argument, as in {@code body.?[qty < 1].size()
> 0}, which was split at the {@code <}
+ * and reported as {@code ${body.?[qty} < 1].size() > 0}, text that was
never written.
+ */
private static int indexOutsideQuotes(String text, String find) {
boolean single = false;
boolean dubble = false;
+ int depth = 0;
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
if (c == '\'' && !dubble) {
single = !single;
} else if (c == '"' && !single) {
dubble = !dubble;
- } else if (!single && !dubble && text.startsWith(find, i)) {
+ } else if (!single && !dubble && (c == '(' || c == '[')) {
+ depth++;
+ } else if (!single && !dubble && (c == ')' || c == ']')) {
+ depth--;
+ } else if (!single && !dubble && depth == 0 &&
text.startsWith(find, i)) {
return i;
}
}
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 9e9946b61c4e..c0f5ffa9d3a6 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
@@ -20,9 +20,11 @@ import java.io.ByteArrayInputStream;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.List;
+import java.util.Map;
import org.apache.camel.Exchange;
import org.apache.camel.ExchangeTestSupport;
+import org.apache.camel.Expression;
import org.apache.camel.component.file.GenericFile;
import org.apache.camel.converter.stream.ByteArrayInputStreamCache;
import org.apache.camel.language.simple.types.SimpleIllegalSyntaxException;
@@ -166,6 +168,31 @@ public class SimpleSyntaxHintsTest extends
ExchangeTestSupport {
assertEquals(false,
context.resolveLanguage("simple").createPredicate("${body.length() >
3}").matches(exchange));
}
+ /**
+ * An operator inside brackets or parentheses belongs to an index or an
argument: a predicate inside ${ } is split
+ * at an operator outside them only, so the error quotes the text as
written (a local model wrote ${body.?[qty <
+ * 1].size() > 0 ? ...} and was told about ${body.?[qty} < 1], text it
never wrote).
+ */
+ @Test
+ public void testAnOperatorInsideBracketsOrParenthesesIsNotSplit() {
+ assertEquals("${body.?[qty < 1].size()} > 0",
SimpleSyntaxHints.wrapFunctions("body.?[qty < 1].size() > 0"));
+ assertEquals("iif(header.a > 0 && header.b > 0, 'x', 'y')",
+ SimpleSyntaxHints.wrapFunctions("iif(header.a > 0 && header.b
> 0, 'x', 'y')"));
+ assertEquals("${header.x} > 0 && ${body.size()} > 1",
+ SimpleSyntaxHints.wrapFunctions("header.x > 0 && body.size() >
1"));
+ assertEquals("${header.list[0]} == 'a'",
SimpleSyntaxHints.wrapFunctions("header.list[0] == 'a'"));
+
+ // the expression parses as written now; what the index means is the
runtime's to say, about the text as written
+ Expression expression = context.resolveLanguage("simple")
+ .createExpression("${body.?[qty < 1].size() > 0 ? 'not enough'
: 'reserved'}");
+ exchange.getIn().setBody(List.of(Map.of("qty", 0)));
+ try {
+ expression.evaluate(exchange, String.class);
+ } catch (Exception e) {
+ assertThat(e.getMessage()).doesNotContain("[qty}");
+ }
+ }
+
@Test
public void testOgnlRuntimeMessages() {
exchange.getIn().setBody("hello");