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 6698a1b01921 CAMEL-25081: camel-bean - a comma inside ${...} in the 
method parameters does not split the parameter (#26977)
6698a1b01921 is described below

commit 6698a1b019213a8bbf5c262c56cabcd22da179a4
Author: allthingssecurity <[email protected]>
AuthorDate: Mon Sep 28 17:57:33 2026 +0530

    CAMEL-25081: camel-bean - a comma inside ${...} in the method parameters 
does not split the parameter (#26977)
    
    The parameter values in the method name were split with
    StringQuoteHelper.splitSafeQuote, which only knows about quotes. A comma
    inside a Simple expression, such as the arguments of an OGNL method call in
    echo(${body.substring(0, 3)}), split the parameter, and every exchange
    failed with "missing } to close the function". For overloaded methods the
    same split in BeanInfo counted the wrong number of parameters, so no method
    matched (AmbiguousMethodCallException).
    
    Add a splitSafeQuote overload that can also ignore the separator inside
    parenthesis and curly brackets (outside quotes), and use it in MethodInfo
    and BeanInfo. A parameter list without a comma inside brackets is split
    exactly as before.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../org/apache/camel/component/bean/BeanInfo.java  |   3 +-
 .../apache/camel/component/bean/MethodInfo.java    |   3 +-
 .../bean/BeanParameterValueWithCommaTest.java      | 114 +++++++++++++++++++++
 .../org/apache/camel/util/StringQuoteHelper.java   |  33 +++++-
 .../apache/camel/util/StringQuoteHelperTest.java   |  50 +++++++++
 .../modules/ROOT/pages/bean-binding.adoc           |   2 +-
 6 files changed, 201 insertions(+), 4 deletions(-)

diff --git 
a/components/camel-bean/src/main/java/org/apache/camel/component/bean/BeanInfo.java
 
b/components/camel-bean/src/main/java/org/apache/camel/component/bean/BeanInfo.java
index ee174f94493d..13a0ae2a22e7 100644
--- 
a/components/camel-bean/src/main/java/org/apache/camel/component/bean/BeanInfo.java
+++ 
b/components/camel-bean/src/main/java/org/apache/camel/component/bean/BeanInfo.java
@@ -1202,7 +1202,8 @@ public class BeanInfo {
         String types = StringHelper.betweenOuterPair(methodName, '(', ')');
         if (org.apache.camel.util.ObjectHelper.isNotEmpty(types)) {
             // we must qualify based on types to match method
-            String[] parameters = StringQuoteHelper.splitSafeQuote(types, ',', 
true, true);
+            // split the same way as the parameter values (see MethodInfo)
+            String[] parameters = StringQuoteHelper.splitSafeQuote(types, ',', 
true, true, true);
             Class<?>[] parameterTypes = null;
             Iterator<?> it = ObjectHelper.createIterator(parameters);
             for (int i = 0; i < method.getParameterCount(); i++) {
diff --git 
a/components/camel-bean/src/main/java/org/apache/camel/component/bean/MethodInfo.java
 
b/components/camel-bean/src/main/java/org/apache/camel/component/bean/MethodInfo.java
index 4f5e31a6731c..0a6f2c7673be 100644
--- 
a/components/camel-bean/src/main/java/org/apache/camel/component/bean/MethodInfo.java
+++ 
b/components/camel-bean/src/main/java/org/apache/camel/component/bean/MethodInfo.java
@@ -609,7 +609,8 @@ public class MethodInfo {
             if (methodParameters != null) {
                 // split the parameters safely separated by comma, but beware 
that we can have
                 // quoted parameters which contains comma as well, so do a 
safe quote split (keep quotes)
-                String[] parameters = 
StringQuoteHelper.splitSafeQuote(methodParameters, ',', true, true);
+                // and a parameter such as ${body.substring(0, 3)} can have 
comma inside parenthesis
+                String[] parameters = 
StringQuoteHelper.splitSafeQuote(methodParameters, ',', true, true, true);
                 it = ObjectHelper.createIterator(parameters, ",", true);
             }
 
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/component/bean/BeanParameterValueWithCommaTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/component/bean/BeanParameterValueWithCommaTest.java
new file mode 100644
index 000000000000..7340f07dc8bd
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/component/bean/BeanParameterValueWithCommaTest.java
@@ -0,0 +1,114 @@
+/*
+ * 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.component.bean;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.builder.RouteBuilder;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * A comma inside a Simple expression in the method name, such as the 
arguments of an OGNL method call in
+ * ${body.substring(0, 3)}, is part of that parameter and does not separate 
the parameters.
+ */
+class BeanParameterValueWithCommaTest extends ContextTestSupport {
+
+    private final MyBean bean = new MyBean();
+    private final MyOverloadedBean overloaded = new MyOverloadedBean();
+
+    @Test
+    void testOgnlMethodCallWithTwoArguments() {
+        assertEquals("[abc]", template.requestBody("direct:substring", 
"abcdef"));
+    }
+
+    @Test
+    void testTwoParametersWithCommaInSecond() {
+        assertEquals("[X|bbb]", template.requestBodyAndHeader("direct:two", 
"X", "v", "aaa"));
+    }
+
+    @Test
+    void testTwoParametersWithCommaInBoth() {
+        assertEquals("[bc|a]", template.requestBody("direct:twoOgnl", 
"abcdef"));
+    }
+
+    @Test
+    void testNestedParenthesis() {
+        assertEquals("[bc]", template.requestBody("direct:chained", "abcdef"));
+        assertEquals("[zzz|X]", 
template.requestBodyAndHeader("direct:nestedFunction", "X", "v", "aaa"));
+    }
+
+    @Test
+    void testOverloadedMethod() {
+        assertEquals("[abc]", template.requestBody("direct:overloaded", 
"abcdef"));
+        assertEquals("[abc|de]", template.requestBody("direct:overloadedTwo", 
"abcdef"));
+        assertEquals("[abc]", template.requestBody("direct:overloadedType", 
"abcdef"));
+    }
+
+    @Test
+    void testExistingForms() {
+        assertEquals("[abcdef]", template.requestBody("direct:body", 
"abcdef"));
+        assertEquals("[X|aaa]", 
template.requestBodyAndHeader("direct:twoPlain", "X", "v", "aaa"));
+        assertEquals("[a,b]", template.requestBody("direct:quotedComma", 
"Hello"));
+        assertEquals("[a,b|(c, d)]", template.requestBody("direct:quotedTwo", 
"Hello"));
+        assertEquals("[Hello|b]", template.requestBody("direct:bodyAndQuoted", 
"Hello"));
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:substring").bean(bean, "echo(${body.substring(0, 
3)})");
+                from("direct:two").bean(bean, "two(${body}, 
${header.v.replace('a', 'b')})");
+                from("direct:twoOgnl").bean(bean, "two(${body.substring(1, 
3)}, ${body.substring(0, 1)})");
+                from("direct:chained").bean(bean, "echo(${body.substring(0, 
4).substring(1, 3)})");
+                from("direct:nestedFunction").bean(bean, 
"two(${replace(a,z,${header.v})}, ${body})");
+                from("direct:overloaded").bean(overloaded, 
"echo(${body.substring(0, 3)})");
+                from("direct:overloadedTwo").bean(overloaded, 
"echo(${body.substring(0, 3)}, ${body.substring(3, 5)})");
+                from("direct:overloadedType").bean(overloaded, 
"echo(String.class ${body.substring(0, 3)})");
+                from("direct:body").bean(bean, "echo(${body})");
+                from("direct:twoPlain").bean(bean, "two(${body}, 
${header.v})");
+                from("direct:quotedComma").bean(bean, "echo('a,b')");
+                from("direct:quotedTwo").bean(bean, "two('a,b', '(c, d)')");
+                from("direct:bodyAndQuoted").bean(bean, "two(${body}, 'b')");
+            }
+        };
+    }
+
+    public static class MyBean {
+
+        public String echo(String s) {
+            return "[" + s + "]";
+        }
+
+        public String two(String a, String b) {
+            return "[" + a + "|" + b + "]";
+        }
+    }
+
+    public static class MyOverloadedBean {
+
+        public String echo(String s) {
+            return "[" + s + "]";
+        }
+
+        public String echo(String a, String b) {
+            return "[" + a + "|" + b + "]";
+        }
+    }
+}
diff --git 
a/core/camel-util/src/main/java/org/apache/camel/util/StringQuoteHelper.java 
b/core/camel-util/src/main/java/org/apache/camel/util/StringQuoteHelper.java
index 2544c8f608fa..b80cbbb2aa34 100644
--- a/core/camel-util/src/main/java/org/apache/camel/util/StringQuoteHelper.java
+++ b/core/camel-util/src/main/java/org/apache/camel/util/StringQuoteHelper.java
@@ -138,6 +138,28 @@ public final class StringQuoteHelper {
      * @return            the input split, or <tt>null</tt> if the input is 
null.
      */
     public static String[] splitSafeQuote(String input, char separator, 
boolean trim, boolean keepQuotes) {
+        return splitSafeQuote(input, separator, trim, keepQuotes, false);
+    }
+
+    /**
+     * Splits the input safely honoring if values is enclosed in quotes, and 
optionally also if values is enclosed in
+     * parenthesis or curly brackets.
+     * <p/>
+     * Though this method does not support double quoting values. A quoted 
value must start with the same start and
+     * ending quote, which is either a single quote or double quote value.
+     * <p/>
+     * When <tt>nested</tt> is enabled then the separator is not used for 
splitting when it is inside <tt>( )</tt> or
+     * <tt>{ }</tt> (outside quotes), such as a comma in a simple expression 
<tt>${body.substring(0, 3)}</tt>. The input
+     * is otherwise split in the same way as when <tt>nested</tt> is disabled.
+     *
+     * @param  input      the input
+     * @param  separator  the separator char to split the input, for example a 
comma.
+     * @param  trim       whether to trim each split value
+     * @param  keepQuotes whether to keep quotes
+     * @param  nested     whether to not split inside parenthesis and curly 
brackets
+     * @return            the input split, or <tt>null</tt> if the input is 
null.
+     */
+    public static String[] splitSafeQuote(String input, char separator, 
boolean trim, boolean keepQuotes, boolean nested) {
         if (input == null) {
             return null;
         }
@@ -165,6 +187,7 @@ public final class StringQuoteHelper {
         boolean singleQuoted = false;
         boolean doubleQuoted = false;
         boolean separating = false;
+        int depth = 0;
 
         for (int i = 0; i < input.length(); i++) {
             char ch = input.charAt(i);
@@ -228,7 +251,7 @@ public final class StringQuoteHelper {
                     sb.append(ch);
                 }
                 continue;
-            } else if (!isQuoting && ch == separator) {
+            } else if (!isQuoting && ch == separator && depth == 0) {
                 separating = true;
                 // add as answer if we are not in a quote
                 if (!sb.isEmpty()) {
@@ -243,6 +266,14 @@ public final class StringQuoteHelper {
                 continue;
             }
 
+            if (nested && !isQuoting) {
+                if (ch == '(' || ch == '{') {
+                    depth++;
+                } else if ((ch == ')' || ch == '}') && depth > 0) {
+                    depth--;
+                }
+            }
+
             if (trim && !isQuoting && separating && separator != ' ' && ch == 
' ') {
                 continue;
             }
diff --git 
a/core/camel-util/src/test/java/org/apache/camel/util/StringQuoteHelperTest.java
 
b/core/camel-util/src/test/java/org/apache/camel/util/StringQuoteHelperTest.java
index 883e2a9e479a..7304a8caf7dd 100644
--- 
a/core/camel-util/src/test/java/org/apache/camel/util/StringQuoteHelperTest.java
+++ 
b/core/camel-util/src/test/java/org/apache/camel/util/StringQuoteHelperTest.java
@@ -54,4 +54,54 @@ public class StringQuoteHelperTest {
         Assertions.assertEquals(" String.class Mars   ", arr[1]);
     }
 
+    @Test
+    public void testSplitNested() {
+        // a comma inside parenthesis or curly brackets is not a separator
+        Assertions.assertArrayEquals(new String[] { "${body.substring(0, 3)}" 
},
+                StringQuoteHelper.splitSafeQuote("${body.substring(0, 3)}", 
',', true, true, true));
+        Assertions.assertArrayEquals(new String[] { "${body}", 
"${header.v.replace('a', 'b')}" },
+                StringQuoteHelper.splitSafeQuote("${body}, 
${header.v.replace('a', 'b')}", ',', true, true, true));
+        Assertions.assertArrayEquals(new String[] { "${body.substring(0, 
4).substring(1, 3)}", "${replace(a,z,${header.v})}" },
+                StringQuoteHelper.splitSafeQuote("${body.substring(0, 
4).substring(1, 3)} , ${replace(a,z,${header.v})}",
+                        ',', true, true, true));
+        Assertions.assertArrayEquals(new String[] { "String.class 
${body.substring(0, 3)}", "5" },
+                StringQuoteHelper.splitSafeQuote("String.class 
${body.substring(0, 3)}, 5", ',', true, true, true));
+        Assertions.assertArrayEquals(new String[] { "{a, b}", "c" },
+                StringQuoteHelper.splitSafeQuote("{a, b}, c", ',', true, true, 
true));
+        // without nested the comma splits
+        Assertions.assertArrayEquals(new String[] { "${body.substring(0", 
"3)}" },
+                StringQuoteHelper.splitSafeQuote("${body.substring(0, 3)}", 
',', true, true, false));
+        // brackets inside quotes are text
+        Assertions.assertArrayEquals(new String[] { "'a(b'", "c" },
+                StringQuoteHelper.splitSafeQuote("'a(b', c", ',', true, true, 
true));
+        Assertions.assertArrayEquals(new String[] { "\"{\"", "c" },
+                StringQuoteHelper.splitSafeQuote("\"{\", c", ',', true, true, 
true));
+        // a closing bracket without an opening bracket does not stop the 
splitting
+        Assertions.assertArrayEquals(new String[] { "a)", "b", "c}", "d" },
+                StringQuoteHelper.splitSafeQuote("a), b, c}, d", ',', true, 
true, true));
+        // no trim and no keep quotes
+        Assertions.assertArrayEquals(new String[] { "f(a, b)", " 'x, y'" },
+                StringQuoteHelper.splitSafeQuote("f(a, b), 'x, y'", ',', 
false, true, true));
+        Assertions.assertArrayEquals(new String[] { "f(a, b)", "x, y" },
+                StringQuoteHelper.splitSafeQuote("f(a, b), 'x, y'", ',', true, 
false, true));
+    }
+
+    @Test
+    public void testSplitNestedSameAsNotNestedWithoutCommaInBrackets() {
+        String[] inputs = {
+                "${body}, ${header.foo}", "'a,b', 5", "${body}, 
${header.foo?['key']}", "*, true", "'World'",
+                "String.class ${body}, String.class Mars", "  String.class 
${body}  , String.class Mars   ",
+                "null, 'a,b'", "'', ${body}", "\"\", 'x'", 
"${body.substring(1)}, ${header.foo.toUpperCase()}", "a,,b",
+                ", a,", "'it''s', b" };
+        for (String input : inputs) {
+            for (boolean trim : new boolean[] { true, false }) {
+                for (boolean keepQuotes : new boolean[] { true, false }) {
+                    
Assertions.assertArrayEquals(StringQuoteHelper.splitSafeQuote(input, ',', trim, 
keepQuotes),
+                            StringQuoteHelper.splitSafeQuote(input, ',', trim, 
keepQuotes, true), input);
+                }
+            }
+        }
+        Assertions.assertNull(StringQuoteHelper.splitSafeQuote(null, ',', 
true, true, true));
+    }
+
 }
diff --git a/docs/user-manual/modules/ROOT/pages/bean-binding.adoc 
b/docs/user-manual/modules/ROOT/pages/bean-binding.adoc
index b69720872de3..783985a33673 100644
--- a/docs/user-manual/modules/ROOT/pages/bean-binding.adoc
+++ b/docs/user-manual/modules/ROOT/pages/bean-binding.adoc
@@ -227,7 +227,7 @@ the method option
 * It can be evaluated using the 
xref:components:languages:simple-language.adoc[Simple] language, which
 means you can use, e.g., `$\{body}`, `${header.foo}` and others
 xref:components:languages:simple-language.adoc[Simple] tokens. Notice the 
tokens must be enclosed with
-`${ }`.
+`${ }`. A comma inside the `${ }`, such as in `${body.substring(0, 3)}`, is 
part of the parameter.
 * The value ends with `.class` then it's a type declaration instead - see the
 next section about specifying types for overloaded methods.
 

Reply via email to