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.