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 5337aee73a02 CAMEL-26886: camel-core - Simple: the other functions 
with arguments keep an empty argument, and a comma in a nested function does 
not split (#29790)
5337aee73a02 is described below

commit 5337aee73a02404c6ceffab9bdbb22c895b4e45f
Author: allthingssecurity <[email protected]>
AuthorDate: Fri Nov 13 20:23:25 2026 +0530

    CAMEL-26886: camel-core - Simple: the other functions with arguments keep 
an empty argument, and a comma in a nested function does not split (#29790)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../camel/catalog/docs/simple-functions.adoc       |  13 +-
 .../modules/languages/pages/simple-functions.adoc  |  13 +-
 .../language/simple/SimpleFunctionHelper.java      |  15 +++
 .../functions/CollectionFunctionFactory.java       |  19 +--
 .../simple/functions/CustomFunctionFactory.java    |  10 +-
 .../simple/functions/MathFunctionFactory.java      |   8 +-
 .../simple/functions/MiscFunctionFactory.java      |  56 ++++----
 .../simple/functions/RandomFunctionFactory.java    |  18 +--
 .../simple/functions/StringFunctionFactory.java    |  28 ++--
 .../functions/OtherFunctionArgumentsTest.java      | 146 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  13 ++
 11 files changed, 268 insertions(+), 71 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-functions.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-functions.adoc
index b19b30092c7f..0a8b7e182582 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-functions.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-functions.adoc
@@ -8,13 +8,14 @@ xref:simple-language.adoc[Back to Simple Language]
 The Simple language has many built-in functions which allows access to various 
part of Camel and the current `Exchange`, the message payload such as body and 
headers, and much more.
 
 NOTE: Some functions take 1 or more arguments enclosed in parentheses, and 
arguments separated by comma (ie `${replace('custID','customerId')}`.
-In the arguments of `replace`, `substring`, `substringBefore`, 
`substringAfter`, `substringBetween`, `contains`,
-`join`, `split`, `sort`, `setHeader`, `setVariable`, `forEach`, `filter`, 
`listAdd`, `listRemove`, `mapAdd` and
-`mapRemove`, a comma inside quotes or inside a nested function such as 
`$\{header.name.substring(1,3)}` does not
-separate arguments. An argument left out, as in
+In the arguments of a function, a comma inside quotes or inside a nested 
function such as
+`$\{header.name.substring(1,3)}` does not separate arguments. An argument left 
out, as in
 `${replace(a,,$\{header.name})}`, stays in its place: it means the default 
where the function has one (the message
-body for an expression, a comma for a separator, the empty text for the `to` 
of `replace`) and is refused otherwise;
-`''` is the empty text.
+body where the function otherwise reads the body, a comma for the separator of 
`join` and `split`, the empty text for
+the `to` of `replace`, 0 for the min of `range` and `random`, SHA-256 for the 
algorithm of `hash`, a space for the
+separator of `pad`) and is refused otherwise; `''` is the empty text. In the
+values of `sum`, `max`, `min`, `average`, `distinct`, `reverse`, `shuffle`, 
`list` and `map` an empty value is no
+value.
 
 === Inlined Result Type Function
 
diff --git 
a/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-functions.adoc
 
b/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-functions.adoc
index b19b30092c7f..0a8b7e182582 100644
--- 
a/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-functions.adoc
+++ 
b/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-functions.adoc
@@ -8,13 +8,14 @@ xref:simple-language.adoc[Back to Simple Language]
 The Simple language has many built-in functions which allows access to various 
part of Camel and the current `Exchange`, the message payload such as body and 
headers, and much more.
 
 NOTE: Some functions take 1 or more arguments enclosed in parentheses, and 
arguments separated by comma (ie `${replace('custID','customerId')}`.
-In the arguments of `replace`, `substring`, `substringBefore`, 
`substringAfter`, `substringBetween`, `contains`,
-`join`, `split`, `sort`, `setHeader`, `setVariable`, `forEach`, `filter`, 
`listAdd`, `listRemove`, `mapAdd` and
-`mapRemove`, a comma inside quotes or inside a nested function such as 
`$\{header.name.substring(1,3)}` does not
-separate arguments. An argument left out, as in
+In the arguments of a function, a comma inside quotes or inside a nested 
function such as
+`$\{header.name.substring(1,3)}` does not separate arguments. An argument left 
out, as in
 `${replace(a,,$\{header.name})}`, stays in its place: it means the default 
where the function has one (the message
-body for an expression, a comma for a separator, the empty text for the `to` 
of `replace`) and is refused otherwise;
-`''` is the empty text.
+body where the function otherwise reads the body, a comma for the separator of 
`join` and `split`, the empty text for
+the `to` of `replace`, 0 for the min of `range` and `random`, SHA-256 for the 
algorithm of `hash`, a space for the
+separator of `pad`) and is refused otherwise; `''` is the empty text. In the
+values of `sum`, `max`, `min`, `average`, `distinct`, `reverse`, `shuffle`, 
`list` and `map` an empty value is no
+value.
 
 === Inlined Result Type Function
 
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleFunctionHelper.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleFunctionHelper.java
index 8689f1696911..5e5a4974fd08 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleFunctionHelper.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleFunctionHelper.java
@@ -158,6 +158,21 @@ public final class SimpleFunctionHelper {
         return answer.toArray(new String[0]);
     }
 
+    /**
+     * Splits the arguments of a function as {@link 
#splitArgumentsAsWritten(String)} does (an empty argument is kept in
+     * its place, a comma inside a nested function does not split), with the 
white space around each argument removed
+     * and its quotes kept, for a function that removes the quotes of some of 
its arguments only. An argument left out
+     * (nothing, or only white space, between the commas) is {@code null}.
+     */
+    public static String[] splitArgumentsKeepQuotes(String values) {
+        String[] tokens = splitArgumentsAsWritten(values);
+        for (int i = 0; i < tokens.length; i++) {
+            String token = tokens[i].trim();
+            tokens[i] = token.isEmpty() ? null : token;
+        }
+        return tokens;
+    }
+
     /**
      * The expression argument of a function as {@link 
#splitArguments(String)} gives it, or {@code ${body}} when it is
      * left out, such as in {@code ${replace(a,b,)}}.
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/CollectionFunctionFactory.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/CollectionFunctionFactory.java
index 3860b2af96ec..d76cbbe393f7 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/CollectionFunctionFactory.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/CollectionFunctionFactory.java
@@ -29,6 +29,7 @@ import static 
org.apache.camel.language.simple.SimpleFunctionHelper.expressionOr
 import static 
org.apache.camel.language.simple.SimpleFunctionHelper.ifStartsWithReturnRemainder;
 import static 
org.apache.camel.language.simple.SimpleFunctionHelper.joinArguments;
 import static 
org.apache.camel.language.simple.SimpleFunctionHelper.splitArguments;
+import static 
org.apache.camel.language.simple.SimpleFunctionHelper.splitArgumentsKeepQuotes;
 
 /**
  * Built-in Simple collection functions: {@code ${setHeader}}, {@code 
${setVariable}}, {@code ${range}},
@@ -117,16 +118,16 @@ public final class CollectionFunctionFactory implements 
SimpleLanguageFunctionFa
                 throw new SimpleParserException(
                         "Valid syntax: ${range(min,max)} or ${range(max)} was: 
" + function, index);
             }
-            if (values.contains(",")) {
-                String[] tokens = values.split(",", 3);
-                if (tokens.length > 2) {
-                    throw new SimpleParserException(
-                            "Valid syntax: ${range(min,max)} or ${range(max)} 
was: " + function, index);
-                }
-                return 
CollectionExpressionBuilder.rangeExpression(tokens[0].trim(), tokens[1].trim());
-            } else {
-                return CollectionExpressionBuilder.rangeExpression("0", 
values.trim());
+            // a comma inside a nested function is part of the argument; a min 
left out is 0
+            String[] tokens = splitArgumentsKeepQuotes(values);
+            if (tokens.length > 2 || tokens[tokens.length - 1] == null) {
+                throw new SimpleParserException(
+                        "Valid syntax: ${range(min,max)} or ${range(max)} was: 
" + function, index);
+            }
+            if (tokens.length == 2) {
+                return CollectionExpressionBuilder.rangeExpression(tokens[0] 
!= null ? tokens[0] : "0", tokens[1]);
             }
+            return CollectionExpressionBuilder.rangeExpression("0", tokens[0]);
         }
 
         remainder = ifStartsWithReturnRemainder("distinct(", function);
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/CustomFunctionFactory.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/CustomFunctionFactory.java
index ff05c5d05a6f..676f20a6e0d6 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/CustomFunctionFactory.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/CustomFunctionFactory.java
@@ -23,9 +23,9 @@ import 
org.apache.camel.language.simple.types.SimpleParserException;
 import org.apache.camel.spi.SimpleLanguageFunctionFactory;
 import org.apache.camel.util.ObjectHelper;
 import org.apache.camel.util.StringHelper;
-import org.apache.camel.util.StringQuoteHelper;
 
 import static 
org.apache.camel.language.simple.SimpleFunctionHelper.ifStartsWithReturnRemainder;
+import static 
org.apache.camel.language.simple.SimpleFunctionHelper.splitArgumentsKeepQuotes;
 
 /**
  * Built-in Simple function for user-defined custom functions: {@code 
${function(name)}} and
@@ -47,14 +47,16 @@ public final class CustomFunctionFactory implements 
SimpleLanguageFunctionFactor
             throw new SimpleParserException(
                     "Valid syntax: ${function(name)} or ${function(name,exp)} 
was: " + function, index);
         }
-        String[] tokens = StringQuoteHelper.splitSafeQuote(values, ',', true, 
true);
-        if (tokens.length < 1 || tokens.length > 2) {
+        // a comma inside quotes or a nested function is part of the expression
+        String[] tokens = splitArgumentsKeepQuotes(values);
+        if (tokens.length > 2 || tokens[0] == null) {
             throw new SimpleParserException(
                     "Valid syntax: ${function(name)} or ${function(name,exp)} 
was: " + function, index);
         }
         key = StringHelper.removeQuotes(tokens[0]);
         key = key.trim();
-        if (tokens.length == 2) {
+        // an expression left out is the message body
+        if (tokens.length == 2 && tokens[1] != null) {
             param = tokens[1];
             param = StringHelper.removeLeadingAndEndingQuotes(param.trim());
         }
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/MathFunctionFactory.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/MathFunctionFactory.java
index ff4c848cfa31..c7c901528b1d 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/MathFunctionFactory.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/MathFunctionFactory.java
@@ -16,15 +16,17 @@
  */
 package org.apache.camel.language.simple.functions;
 
+import java.util.Arrays;
+
 import org.apache.camel.CamelContext;
 import org.apache.camel.Expression;
 import org.apache.camel.language.simple.MathExpressionBuilder;
 import org.apache.camel.spi.SimpleLanguageFunctionFactory;
 import org.apache.camel.util.ObjectHelper;
 import org.apache.camel.util.StringHelper;
-import org.apache.camel.util.StringQuoteHelper;
 
 import static 
org.apache.camel.language.simple.SimpleFunctionHelper.ifStartsWithReturnRemainder;
+import static 
org.apache.camel.language.simple.SimpleFunctionHelper.splitArguments;
 
 /**
  * Built-in Simple math functions: {@code ${abs}}, {@code ${floor}}, {@code 
${ceil}}, {@code ${sum}}, {@code ${max}},
@@ -77,7 +79,9 @@ public final class MathFunctionFactory implements 
SimpleLanguageFunctionFactory
     private static String[] parseVariadicTokens(String remainder) {
         String values = StringHelper.beforeLast(remainder, ")");
         if (ObjectHelper.isNotEmpty(values)) {
-            return StringQuoteHelper.splitSafeQuote(values, ',', true, false);
+            // a comma inside quotes or a nested function is part of the 
value; as before, an empty value is no value
+            return Arrays.stream(splitArguments(values)).filter(v -> v != null 
&& !v.isBlank())
+                    .toArray(String[]::new);
         }
         // no values, such as ${sum()}, then use the message body
         return new String[] { "${body}" };
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/MiscFunctionFactory.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/MiscFunctionFactory.java
index 2e26d45bd1fe..48734aa54206 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/MiscFunctionFactory.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/MiscFunctionFactory.java
@@ -25,9 +25,9 @@ import org.apache.camel.spi.SimpleLanguageFunctionFactory;
 import org.apache.camel.util.ObjectHelper;
 import org.apache.camel.util.OgnlHelper;
 import org.apache.camel.util.StringHelper;
-import org.apache.camel.util.StringQuoteHelper;
 
 import static 
org.apache.camel.language.simple.SimpleFunctionHelper.ifStartsWithReturnRemainder;
+import static 
org.apache.camel.language.simple.SimpleFunctionHelper.splitArgumentsKeepQuotes;
 
 /**
  * Built-in Simple miscellaneous functions: {@code ${isEmpty}}, {@code 
${isAlpha}}, {@code ${isAlphaNumeric}},
@@ -110,14 +110,15 @@ public final class MiscFunctionFactory implements 
SimpleLanguageFunctionFactory
                 throw new SimpleParserException(
                         "Valid syntax: ${throwException(msg)} or 
${throwException(msg,type)} was: " + function, index);
             }
-            String[] tokens = StringQuoteHelper.splitSafeQuote(values, ',', 
true, true);
-            if (tokens.length > 2) {
+            // a comma inside a quoted message or a nested function is part of 
the message
+            String[] tokens = splitArgumentsKeepQuotes(values);
+            if (tokens.length > 2 || tokens[0] == null) {
                 throw new SimpleParserException(
                         "Valid syntax: ${throwException(msg)} or 
${throwException(msg,type)} was: " + function, index);
             }
-            // a comma inside a quoted message is part of the message
             msg = StringHelper.removeLeadingAndEndingQuotes(tokens[0]);
-            if (tokens.length == 2) {
+            if (tokens.length == 2 && tokens[1] != null) {
+                // a type left out is the default type
                 type = StringHelper.removeLeadingAndEndingQuotes(tokens[1]);
             }
             return MiscExpressionBuilder.throwExceptionExpression(msg, type);
@@ -129,8 +130,8 @@ public final class MiscFunctionFactory implements 
SimpleLanguageFunctionFactory
             if (values == null || ObjectHelper.isEmpty(values)) {
                 throw new SimpleParserException("Valid syntax: 
${assert(exp,msg)} was: " + function, index);
             }
-            String[] tokens = StringQuoteHelper.splitSafeQuote(values, ',', 
true, true);
-            if (tokens.length != 2) {
+            String[] tokens = splitArgumentsKeepQuotes(values);
+            if (tokens.length != 2 || tokens[0] == null || tokens[1] == null) {
                 throw new SimpleParserException("Valid syntax: 
${assert(exp,msg)} was: " + function, index);
             }
             return MiscExpressionBuilder.assertExpression(tokens[0], 
StringHelper.removeLeadingAndEndingQuotes(tokens[1]));
@@ -146,16 +147,19 @@ public final class MiscFunctionFactory implements 
SimpleLanguageFunctionFactory
                 throw new SimpleParserException(
                         "Valid syntax: ${convertTo(type)} or 
${convertTo(exp,type)} was: " + function, index);
             }
-            if (values.contains(",")) {
-                String[] tokens = StringQuoteHelper.splitSafeQuote(values, 
',', true, true);
-                if (tokens.length > 2) {
-                    throw new SimpleParserException(
-                            "Valid syntax: ${convertTo(type)} or 
${convertTo(exp,type)} was: " + function, index);
+            String[] tokens = splitArgumentsKeepQuotes(values);
+            if (tokens.length > 2 || tokens[tokens.length - 1] == null) {
+                throw new SimpleParserException(
+                        "Valid syntax: ${convertTo(type)} or 
${convertTo(exp,type)} was: " + function, index);
+            }
+            if (tokens.length == 2) {
+                // an expression left out is the message body
+                if (tokens[0] != null) {
+                    exp = StringHelper.removeLeadingAndEndingQuotes(tokens[0]);
                 }
-                exp = StringHelper.removeLeadingAndEndingQuotes(tokens[0]);
                 type = StringHelper.removeLeadingAndEndingQuotes(tokens[1]);
             } else {
-                type = 
StringHelper.removeLeadingAndEndingQuotes(values.trim());
+                type = StringHelper.removeLeadingAndEndingQuotes(tokens[0]);
             }
             remainder = remainder.substring(end + 1);
             if (ObjectHelper.isNotEmpty(remainder)) {
@@ -196,19 +200,21 @@ public final class MiscFunctionFactory implements 
SimpleLanguageFunctionFactory
         remainder = ifStartsWithReturnRemainder("hash(", function);
         if (remainder != null) {
             String values = StringHelper.beforeLast(remainder, ")");
-            if (values == null || ObjectHelper.isEmpty(values)) {
+            if (values == null) {
                 throw new SimpleParserException(
                         "Valid syntax: ${hash(value,algorithm)} or 
${hash(value)} was: " + function, index);
             }
-            // a comma inside quotes is part of the value
-            String[] tokens = StringQuoteHelper.splitSafeQuote(values, ',', 
true, true);
+            // ${hash()}: the message body with SHA-256, as documented; a 
comma inside quotes or a nested function is
+            // part of the value
+            String[] tokens = ObjectHelper.isEmpty(values) ? new String[] { 
null } : splitArgumentsKeepQuotes(values);
             if (tokens.length > 2) {
                 throw new SimpleParserException(
                         "Valid syntax: ${hash(value,algorithm)} or 
${hash(value)} was: " + function, index);
             }
-            String value = 
StringHelper.removeLeadingAndEndingQuotes(tokens[0].trim());
-            if (tokens.length == 2) {
-                return MiscExpressionBuilder.hashExpression(value, 
StringHelper.removeLeadingAndEndingQuotes(tokens[1].trim()));
+            // a value left out is the message body, an algorithm left out is 
SHA-256
+            String value = tokens[0] != null ? 
StringHelper.removeLeadingAndEndingQuotes(tokens[0]) : "${body}";
+            if (tokens.length == 2 && tokens[1] != null) {
+                return MiscExpressionBuilder.hashExpression(value, 
StringHelper.removeLeadingAndEndingQuotes(tokens[1]));
             } else {
                 return MiscExpressionBuilder.hashExpression(value, "SHA-256");
             }
@@ -239,17 +245,17 @@ public final class MiscFunctionFactory implements 
SimpleLanguageFunctionFactory
                 throw new SimpleParserException(
                         "Valid syntax: 
${iif(predicate,trueExpression,falseExpression)} was: " + function, index);
             }
-            String[] tokens = StringQuoteHelper.splitSafeQuote(values, ',', 
true, true);
-            if (tokens.length != 3) {
+            String[] tokens = splitArgumentsKeepQuotes(values);
+            if (tokens.length != 3 || tokens[0] == null || tokens[1] == null 
|| tokens[2] == null) {
                 throw new SimpleParserException(
                         "Valid syntax: 
${iif(predicate,trueExpression,falseExpression)} was: " + function, index);
             }
             // the predicate keeps its quotes (CAMEL-22204); a quoted value 
loses its quotes, as with the ternary
             // operator, but unlike the ternary the text inside the quotes is 
still evaluated as Simple
             // ('Hello ${body}' gives Hello World)
-            return MiscExpressionBuilder.iifExpression(tokens[0].trim(),
-                    
StringHelper.removeLeadingAndEndingQuotes(tokens[1].trim()),
-                    
StringHelper.removeLeadingAndEndingQuotes(tokens[2].trim()));
+            return MiscExpressionBuilder.iifExpression(tokens[0],
+                    StringHelper.removeLeadingAndEndingQuotes(tokens[1]),
+                    StringHelper.removeLeadingAndEndingQuotes(tokens[2]));
         }
 
         remainder = ifStartsWithReturnRemainder("load(", function);
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/RandomFunctionFactory.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/RandomFunctionFactory.java
index 1361729fa3d5..4f65771065b9 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/RandomFunctionFactory.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/RandomFunctionFactory.java
@@ -25,6 +25,7 @@ import org.apache.camel.util.ObjectHelper;
 import org.apache.camel.util.StringHelper;
 
 import static 
org.apache.camel.language.simple.SimpleFunctionHelper.ifStartsWithReturnRemainder;
+import static 
org.apache.camel.language.simple.SimpleFunctionHelper.splitArgumentsKeepQuotes;
 
 /**
  * Built-in Simple function: {@code ${random(max)}} / {@code 
${random(min,max)}}.
@@ -42,14 +43,15 @@ public final class RandomFunctionFactory implements 
SimpleLanguageFunctionFactor
             throw new SimpleParserException(
                     "Valid syntax: ${random(min,max)} or ${random(max)} was: " 
+ function, index);
         }
-        if (values.contains(",")) {
-            String[] tokens = values.split(",", 3);
-            if (tokens.length > 2) {
-                throw new SimpleParserException(
-                        "Valid syntax: ${random(min,max)} or ${random(max)} 
was: " + function, index);
-            }
-            return MiscExpressionBuilder.randomExpression(tokens[0].trim(), 
tokens[1].trim());
+        // a comma inside a nested function is part of the argument; a min 
left out is 0
+        String[] tokens = splitArgumentsKeepQuotes(values);
+        if (tokens.length > 2 || tokens[tokens.length - 1] == null) {
+            throw new SimpleParserException(
+                    "Valid syntax: ${random(min,max)} or ${random(max)} was: " 
+ function, index);
+        }
+        if (tokens.length == 2) {
+            return MiscExpressionBuilder.randomExpression(tokens[0] != null ? 
tokens[0] : "0", tokens[1]);
         }
-        return MiscExpressionBuilder.randomExpression("0", values.trim());
+        return MiscExpressionBuilder.randomExpression("0", tokens[0]);
     }
 }
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/StringFunctionFactory.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/StringFunctionFactory.java
index 6d1daa522907..8e37ccc6d96c 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/StringFunctionFactory.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/StringFunctionFactory.java
@@ -25,11 +25,11 @@ import org.apache.camel.support.builder.ExpressionBuilder;
 import org.apache.camel.util.EscapeHelper;
 import org.apache.camel.util.ObjectHelper;
 import org.apache.camel.util.StringHelper;
-import org.apache.camel.util.StringQuoteHelper;
 
 import static 
org.apache.camel.language.simple.SimpleFunctionHelper.expressionOrBody;
 import static 
org.apache.camel.language.simple.SimpleFunctionHelper.ifStartsWithReturnRemainder;
 import static 
org.apache.camel.language.simple.SimpleFunctionHelper.splitArguments;
+import static 
org.apache.camel.language.simple.SimpleFunctionHelper.splitArgumentsKeepQuotes;
 
 /**
  * Built-in Simple string functions: {@code ${replace}}, {@code ${substring}}, 
{@code ${substringBefore}},
@@ -259,17 +259,18 @@ public final class StringFunctionFactory implements 
SimpleLanguageFunctionFactor
             String values = StringHelper.beforeLast(remainder, ")");
             if (values == null || ObjectHelper.isEmpty(values)) {
                 throw new SimpleParserException(
-                        "Valid syntax: ${pad(len)} or ${pad(exp,len)} or 
${pad(exp,len,separator)} was: " + function,
-                        index);
+                        "Valid syntax: ${pad(exp,len)} or 
${pad(exp,len,separator)} was: " + function, index);
             }
-            String[] tokens = StringQuoteHelper.splitSafeQuote(values, ',', 
true, true);
-            if (tokens.length < 2 || tokens.length > 3) {
+            // a comma inside quotes or a nested function is part of the 
argument
+            String[] tokens = splitArgumentsKeepQuotes(values);
+            if (tokens.length < 2 || tokens.length > 3 || tokens[0] == null || 
tokens[1] == null) {
                 throw new SimpleParserException(
                         "Valid syntax: ${pad(exp,len)} or 
${pad(exp,len,separator)} was: " + function, index);
             }
             exp = StringHelper.removeLeadingAndEndingQuotes(tokens[0]);
             len = StringHelper.removeLeadingAndEndingQuotes(tokens[1]);
-            if (tokens.length == 3) {
+            // a separator left out is a space
+            if (tokens.length == 3 && tokens[2] != null) {
                 separator = 
StringHelper.removeLeadingAndEndingQuotes(tokens[2]);
             }
             return StringExpressionBuilder.padExpression(exp, len, separator);
@@ -287,18 +288,23 @@ public final class StringFunctionFactory implements 
SimpleLanguageFunctionFactor
                                                 + function,
                         index);
             }
-            // a comma inside quotes is part of the value, such as 
${concat('Hello, ')}
-            String[] tokens = StringQuoteHelper.splitSafeQuote(values, ',', 
true, true);
-            if (tokens.length > 3) {
+            // a comma inside quotes or a nested function is part of the 
value, such as ${concat('Hello, ')}
+            String[] tokens = splitArgumentsKeepQuotes(values);
+            // the expression that must be given: the only one, or the second 
one when there are more
+            int expIdx = tokens.length == 1 ? 0 : 1;
+            if (tokens.length > 3 || tokens[expIdx] == null) {
                 throw new SimpleParserException(
                         "Valid syntax: ${concat(exp)} or ${concat(exp,exp)} or 
${concat(exp,exp,separator)} was: "
                                                 + function,
                         index);
             }
             if (tokens.length >= 2) {
-                exp1 = StringHelper.removeLeadingAndEndingQuotes(tokens[0]);
+                // a first expression left out is the message body, a 
separator left out is none
+                if (tokens[0] != null) {
+                    exp1 = 
StringHelper.removeLeadingAndEndingQuotes(tokens[0]);
+                }
                 exp2 = StringHelper.removeLeadingAndEndingQuotes(tokens[1]);
-                if (tokens.length == 3) {
+                if (tokens.length == 3 && tokens[2] != null) {
                     separator = 
StringHelper.removeLeadingAndEndingQuotes(tokens[2]);
                 }
             } else {
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/language/simple/functions/OtherFunctionArgumentsTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/language/simple/functions/OtherFunctionArgumentsTest.java
new file mode 100644
index 000000000000..d4364fa7fbc3
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/language/simple/functions/OtherFunctionArgumentsTest.java
@@ -0,0 +1,146 @@
+/*
+ * 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.functions;
+
+import java.util.List;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.language.simple.types.SimpleParserException;
+import org.apache.camel.spi.SimpleFunction;
+import org.apache.camel.spi.SimpleLanguageFunctionFactory;
+import org.apache.camel.support.PluginHelper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The arguments of the functions that did not split their arguments with the 
rules of CAMEL-26465: an argument left out
+ * keeps its place, and a comma inside a nested function does not split an 
argument.
+ */
+public class OtherFunctionArgumentsTest extends 
AbstractSimpleFunctionFactoryTestSupport {
+
+    @BeforeEach
+    public void setUpExchange() {
+        exchange.getIn().setBody("abc");
+        exchange.getIn().setHeader("s", "abcdef");
+        exchange.getIn().setHeader("n", "1234");
+        PluginHelper.getSimpleFunctionRegistry(context).addFunction(new 
SimpleFunction() {
+            @Override
+            public String getName() {
+                return "shout";
+            }
+
+            @Override
+            public Object apply(Exchange exchange, Object input) {
+                return input + "!";
+            }
+        });
+    }
+
+    @Override
+    protected SimpleLanguageFunctionFactory createFactory() {
+        return new MiscFunctionFactory();
+    }
+
+    @Test
+    public void testCommaInsideANestedFunction() {
+        assertEquals("bc-de", 
evaluate("concat(${header.s.substring(1,3)},${header.s.substring(3,5)},-)", 
String.class));
+        assertEquals("bc   ", evaluate("pad(${header.s.substring(1,3)},5)", 
String.class));
+        assertEquals(23, evaluate("convertTo(${header.n.substring(1,3)},int)", 
Integer.class));
+        assertEquals("bc", evaluate("iif(${header.s.substring(0,1)} == 
'a',${header.s.substring(1,3)},no)", String.class));
+        assertEquals("bc!", 
evaluate("function(shout,${header.s.substring(1,3)})", String.class));
+        assertEquals(evaluate("hash(bc)", String.class), 
evaluate("hash(${header.s.substring(1,3)})", String.class));
+        assertEquals(35L, evaluate("sum(${header.n.substring(1,3)},12)", 
Long.class));
+        assertEquals(23L, evaluate("max(${header.n.substring(1,3)},12)", 
Long.class));
+        assertEquals("[23, 24]", 
evaluate("range(${header.n.substring(1,3)},25)", List.class).toString());
+        int random = evaluate("random(${header.n.substring(1,3)},24)", 
Integer.class);
+        assertEquals(23, random);
+        IllegalStateException e = assertThrows(IllegalStateException.class,
+                () -> 
evaluate("throwException(${header.s.substring(1,3)},java.lang.IllegalStateException)"));
+        assertTrue(e.getMessage().contains("bc"), e.getMessage());
+    }
+
+    @Test
+    public void testArgumentLeftOutWithADefault() {
+        // the first expression of concat, and the expression of convertTo, 
function and hash, is the message body
+        assertEquals("abcx", evaluate("concat(,x)", String.class));
+        assertEquals("abc-x", evaluate("concat(,x,-)", String.class));
+        exchange.getIn().setBody("42");
+        assertEquals(42, evaluate("convertTo(,int)", Integer.class));
+        assertEquals("42!", evaluate("function(shout,)", String.class));
+        assertEquals(evaluate("hash(${body})", String.class), 
evaluate("hash(,SHA-256)", String.class));
+        assertEquals(evaluate("hash(${body})", String.class), 
evaluate("hash()", String.class));
+        // the algorithm of hash is SHA-256, the separator of pad a space, of 
concat none
+        assertEquals(evaluate("hash(${body})", String.class), 
evaluate("hash(${body},)", String.class));
+        assertEquals("42   ", evaluate("pad(${body},5,)", String.class));
+        assertEquals("42x", evaluate("concat(${body},x,)", String.class));
+        // the min of range and random is 0
+        assertEquals("[0, 1]", evaluate("range(,2)", List.class).toString());
+        assertEquals(0, evaluate("random(,1)", Integer.class));
+        // the type of throwException is IllegalArgumentException
+        IllegalArgumentException e = 
assertThrows(IllegalArgumentException.class, () -> 
evaluate("throwException(Oops,)"));
+        assertEquals("Oops", e.getMessage());
+    }
+
+    @Test
+    public void testArgumentLeftOutWithoutADefaultIsRefused() {
+        // before, the next argument took its place, or the argument was 
dropped
+        String[][] refused = {
+                { "misc", "throwException(,java.lang.IllegalStateException)" },
+                { "misc", "assert(,Must be Hello)" },
+                { "misc", "assert(${body} == 'abc',)" },
+                { "misc", "convertTo(${body},)" },
+                { "misc", "iif(${body} == 'abc',,no)" },
+                { "misc", "iif(${body} == 'abc',yes,)" },
+                { "misc", "iif(,yes,no)" },
+                { "string", "pad(,5)" },
+                { "string", "pad(${body},)" },
+                { "string", "concat(${body},)" },
+                { "string", "concat(${body},,-)" },
+                { "custom", "function(,${body})" },
+                { "collection", "range(1,)" },
+                { "random", "random(1,)" } };
+        for (String[] r : refused) {
+            SimpleLanguageFunctionFactory factory = switch (r[0]) {
+                case "misc" -> new MiscFunctionFactory();
+                case "string" -> new StringFunctionFactory();
+                case "custom" -> new CustomFunctionFactory();
+                case "collection" -> new CollectionFunctionFactory();
+                default -> new RandomFunctionFactory();
+            };
+            assertThrows(SimpleParserException.class, () -> 
factory.createFunction(context, r[1], 0), r[1]);
+        }
+    }
+
+    @Test
+    public void testEmptyValueOfAMathFunctionIsNoValue() {
+        // the values of sum, max, min and average are a list: an empty value 
is no value, as before
+        assertEquals(3L, evaluate("sum(1,,2)", Long.class));
+        assertEquals(2L, evaluate("max(1,,2)", Long.class));
+    }
+
+    @Test
+    public void testQuotesAndSpacesAsBefore() {
+        assertEquals("Hello, abc", evaluate("concat('Hello, ',${body})", 
String.class));
+        assertEquals("@@abc", evaluate("pad(${body}, -5, '@')", String.class));
+        assertEquals("yes", evaluate("iif(${body} == 'abc', 'yes' , 'no')", 
String.class));
+        assertEquals(42, evaluate("convertTo( '42' , int )", Integer.class));
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index eb1094feafd4..1f74fee69eee 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -841,6 +841,19 @@ longer separates two arguments; a parenthesis outside a 
nested function is text,
 after an unquoted `(` or `{` no longer separates two values: quote a value 
with a lone parenthesis. A lone quote at the
 end of an argument is now kept: `+${replace(b,x')}+` replaces with `x'`.
 
+=== camel-core - Simple: the other functions with arguments keep an empty 
argument too
+
+The rules of the entry above now also apply to `concat`, `pad`, `convertTo`, 
`iif`, `hash`, `assert`, `throwException`,
+`function`, `range`, `random`, `sum`, `max`, `min` and `average`: a comma 
inside a nested function, such as
+`+${header.s.substring(1,3)}+`, no longer separates two arguments, and an 
argument left out stays in its place. Where
+the function has a default for it, that default is used: `+${concat(,x,-)}+` 
gives the body, `-` and `x` (it gave `x-`),
+`+${hash(,SHA3-256)}+` hashes the body with SHA3-256 (it hashed the text 
`SHA3-256`). Where it has none it is refused,
+such as `+${throwException(,java.io.IOException)}+`, `+${concat(x,)}+` or 
`+${function(,f)}+`, and a stray comma after
+the last argument that was ignored is now an error, such as `+${iif(p,a,b,)}+` 
or `+${hash(v,SHA-1,)}+`: remove the
+comma (which gives the result of before for each of these examples). A lone 
quote at the end of an argument is now kept,
+as in the entry above. An empty value of `sum`, `max`, `min` or `average` is 
still no value. `+${hash()}+` now hashes
+the body with SHA-256, as documented (it was refused).
+
 === camel-core - a route called from a doTry uses the error handler of its EIPs
 
 A route that a `doTry` calls from its steps, or from its `doCatch` or 
`doFinally` blocks (with `direct`, `seda`,

Reply via email to