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 1c7bcc6c1ff6 CAMEL-26465: camel-core - Simple: an empty function
argument is kept, and a comma in a nested function does not split an argument
(#29126)
1c7bcc6c1ff6 is described below
commit 1c7bcc6c1ff6ab0dc7a6a1f454651c7b2f8b79b1
Author: allthingssecurity <[email protected]>
AuthorDate: Wed Nov 4 15:04:52 2026 +0530
CAMEL-26465: camel-core - Simple: an empty function argument is kept, and a
comma in a nested function does not split an argument (#29126)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../camel/catalog/docs/simple-functions.adoc | 10 ++-
.../modules/languages/pages/simple-functions.adoc | 10 ++-
.../language/simple/SimpleFunctionHelper.java | 85 +++++++++++++++++++---
.../functions/CollectionFunctionFactory.java | 83 ++++++++++++++++-----
.../simple/functions/JoinFunctionFactory.java | 24 ++++--
.../simple/functions/StringFunctionFactory.java | 46 +++++++++---
.../apache/camel/language/simple/SimpleTest.java | 11 +++
.../functions/CollectionFunctionFactoryTest.java | 62 ++++++++++++++++
.../simple/functions/JoinFunctionFactoryTest.java | 46 ++++++++++++
.../functions/StringFunctionFactoryTest.java | 78 ++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 15 ++++
11 files changed, 423 insertions(+), 47 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 af676991a731..b19b30092c7f 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,6 +8,13 @@ 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
+`${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.
=== Inlined Result Type Function
@@ -565,7 +572,8 @@ For example if the message body contains a list of [A, B,
C] then `${join()}` wo
To use semicolon as separator you would use `${join(;)}`. The `prefix`
argument can be used to prefix each value, so for example
`${join(&,id=)}` would return `id=A&id=B&id=C`. The white space around the
prefix and the expression is not part of
them, so `${join(&, id=)}` returns the same. An unquoted separator is kept as
written, so `${join(; )}` returns
-`A; B; C`; a quoted one is the text inside its quotes.
+`A; B; C`; a quoted one is the text inside its quotes, so `${join('')}`
returns `ABC`. An empty separator is the
+default, so `${join(,id=)}` returns `id=A,id=B,id=C`.
The `escape` function escapes special characters so a value can be safely
embedded in another format,
instead of chaining multiple `replace` calls or using a custom bean. The first
argument is the kind of escaping:
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 af676991a731..b19b30092c7f 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,6 +8,13 @@ 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
+`${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.
=== Inlined Result Type Function
@@ -565,7 +572,8 @@ For example if the message body contains a list of [A, B,
C] then `${join()}` wo
To use semicolon as separator you would use `${join(;)}`. The `prefix`
argument can be used to prefix each value, so for example
`${join(&,id=)}` would return `id=A&id=B&id=C`. The white space around the
prefix and the expression is not part of
them, so `${join(&, id=)}` returns the same. An unquoted separator is kept as
written, so `${join(; )}` returns
-`A; B; C`; a quoted one is the text inside its quotes.
+`A; B; C`; a quoted one is the text inside its quotes, so `${join('')}`
returns `ABC`. An empty separator is the
+default, so `${join(,id=)}` returns `id=A,id=B,id=C`.
The `escape` function escapes special characters so a value can be safely
embedded in another format,
instead of chaining multiple `replace` calls or using a custom bean. The first
argument is the kind of escaping:
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 a93e0835beb3..8689f1696911 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
@@ -16,8 +16,11 @@
*/
package org.apache.camel.language.simple;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.StringJoiner;
+
import org.apache.camel.util.StringHelper;
-import org.apache.camel.util.StringQuoteHelper;
public final class SimpleFunctionHelper {
@@ -101,23 +104,87 @@ public final class SimpleFunctionHelper {
}
/**
- * Splits the arguments of a function at the commas outside quotes. The
white space around an unquoted argument is
- * not part of it, so {@code ${sort(${body}, true)}} and {@code
${replace('o', '0')}} work, while a quoted argument
- * such as {@code ' '} is kept as written (without its quotes). An
argument of only white space is kept as it is, so
- * {@code ${replace( ,-)}} replaces a space.
+ * Splits the arguments of a function at the commas outside quotes and
outside nested functions, so
+ * {@code ${replace(b,${header.s.substring(1,3)})}} has two arguments. The
white space around an unquoted argument
+ * is not part of it, so {@code ${sort(${body}, true)}} and {@code
${replace('o', '0')}} work, while a quoted
+ * argument such as {@code ' '} is kept as written (without its quotes).
An argument of only white space is kept as
+ * it is, so {@code ${replace( ,-)}} replaces a space. An argument left
out (nothing between the commas) is kept in
+ * its place as {@code null}, so {@code ${replace(b,,${header.s})}} has
three arguments, the second {@code null}; a
+ * quoted empty argument ({@code ''}) is the empty text.
*/
public static String[] splitArguments(String values) {
- String[] tokens = StringQuoteHelper.splitSafeQuote(values, ',', false,
true);
+ String[] tokens = splitArgumentsAsWritten(values);
for (int i = 0; i < tokens.length; i++) {
- tokens[i] = trimArgument(tokens[i]);
+ tokens[i] = tokens[i].isEmpty() ? null : trimArgument(tokens[i]);
}
return tokens;
}
+ /**
+ * Splits the arguments of a function as written (with their quotes and
the white space around them) at the commas
+ * outside quotes and outside nested functions: a comma inside a nested
{@code ${...}}, also inside its parentheses,
+ * such as in {@code ${header.s.substring(1,3)}}, does not split. A
parenthesis or brace outside a nested function
+ * is text, as before, so {@code ${substringBetween(${body},(,))}} has the
arguments {@code ${body}}, {@code (} and
+ * {@code )}. Every argument is kept, also an empty one, so {@code a,,b}
gives three arguments and {@code a,} two.
+ */
+ public static String[] splitArgumentsAsWritten(String values) {
+ List<String> answer = new ArrayList<>();
+ boolean single = false;
+ boolean dubble = false;
+ int depth = 0;
+ int start = 0;
+ for (int i = 0; i < values.length(); i++) {
+ char c = values.charAt(i);
+ if (c == '\'' && !dubble) {
+ single = !single;
+ } else if (c == '"' && !single) {
+ dubble = !dubble;
+ } else if (!single && !dubble) {
+ if (c == '$' && i + 1 < values.length() && values.charAt(i +
1) == '{') {
+ // a nested function: its commas, parentheses and braces
belong to it
+ depth++;
+ i++;
+ } else if (depth > 0 && (c == '(' || c == '{')) {
+ depth++;
+ } else if (depth > 0 && (c == ')' || c == '}')) {
+ depth--;
+ } else if (c == ',' && depth == 0) {
+ answer.add(values.substring(start, i));
+ start = i + 1;
+ }
+ }
+ }
+ answer.add(values.substring(start));
+ return answer.toArray(new String[0]);
+ }
+
+ /**
+ * 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,)}}.
+ */
+ public static String expressionOrBody(String token) {
+ return token == null ? "${body}" : token;
+ }
+
+ /**
+ * The arguments from the given index on, joined again with a comma (for a
function whose last argument is an
+ * expression that may contain commas), or {@code null} when one of them
is left out, such as with a trailing comma.
+ */
+ public static String joinArguments(String[] tokens, int from) {
+ StringJoiner joiner = new StringJoiner(",");
+ for (int i = from; i < tokens.length; i++) {
+ if (tokens[i] == null) {
+ return null;
+ }
+ joiner.add(tokens[i]);
+ }
+ return joiner.toString();
+ }
+
/**
* An argument as {@link #splitArguments(String)} gives it, from the
argument as written (split with
- * {@code StringQuoteHelper.splitSafeQuote(values, ',', false, true)}):
without the white space and the quotes
- * around it, unless it is only white space.
+ * {@link #splitArgumentsAsWritten(String)}): without the white space and
the quotes around it, unless it is only
+ * white space.
*/
public static String trimArgument(String token) {
String trimmed = token.trim();
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 ec5508ef64e5..3860b2af96ec 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
@@ -16,9 +16,6 @@
*/
package org.apache.camel.language.simple.functions;
-import java.util.Arrays;
-import java.util.stream.Collectors;
-
import org.apache.camel.CamelContext;
import org.apache.camel.Expression;
import org.apache.camel.language.simple.CollectionExpressionBuilder;
@@ -28,7 +25,9 @@ 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.joinArguments;
import static
org.apache.camel.language.simple.SimpleFunctionHelper.splitArguments;
/**
@@ -64,7 +63,16 @@ public final class CollectionFunctionFactory implements
SimpleLanguageFunctionFa
} else {
exp = tokens[1];
}
+ if (name == null || exp == null) {
+ // an argument left out is refused where it has no default
+ throw new SimpleParserException(
+ "Valid syntax: ${setHeader(name,exp)} or
${setHeader(name,type,exp)} was: " + function, index);
+ }
type = StringHelper.removeQuotes(type);
+ if (type != null && type.isEmpty()) {
+ // an empty type is no type, as when it is left out
+ type = null;
+ }
return CollectionExpressionBuilder.setHeaderExpression(name, type,
exp);
}
@@ -89,7 +97,16 @@ public final class CollectionFunctionFactory implements
SimpleLanguageFunctionFa
} else {
exp = tokens[1];
}
+ if (name == null || exp == null) {
+ // an argument left out is refused where it has no default
+ throw new SimpleParserException(
+ "Valid syntax: ${setVariable(name,exp)} or
${setVariable(name,type,exp)} was: " + function, index);
+ }
type = StringHelper.removeQuotes(type);
+ if (type != null && type.isEmpty()) {
+ // an empty type is no type, as when it is left out
+ type = null;
+ }
return CollectionExpressionBuilder.setVariableExpression(name,
type, exp);
}
@@ -117,7 +134,7 @@ public final class CollectionFunctionFactory implements
SimpleLanguageFunctionFa
String values = StringHelper.beforeLast(remainder, ")");
String[] tokens;
if (ObjectHelper.isNotEmpty(values)) {
- tokens = StringQuoteHelper.splitSafeQuote(values, ',', true,
false);
+ tokens = StringQuoteHelper.splitSafeQuote(values, ',', true,
false, true);
} else {
tokens = new String[] { "${body}" };
}
@@ -129,7 +146,7 @@ public final class CollectionFunctionFactory implements
SimpleLanguageFunctionFa
String values = StringHelper.beforeLast(remainder, ")");
String[] tokens;
if (ObjectHelper.isNotEmpty(values)) {
- tokens = StringQuoteHelper.splitSafeQuote(values, ',', true,
false);
+ tokens = StringQuoteHelper.splitSafeQuote(values, ',', true,
false, true);
} else {
tokens = new String[] { "${body}" };
}
@@ -141,7 +158,7 @@ public final class CollectionFunctionFactory implements
SimpleLanguageFunctionFa
String values = StringHelper.beforeLast(remainder, ")");
String[] tokens;
if (ObjectHelper.isNotEmpty(values)) {
- tokens = StringQuoteHelper.splitSafeQuote(values, ',', true,
false);
+ tokens = StringQuoteHelper.splitSafeQuote(values, ',', true,
false, true);
} else {
tokens = new String[] { "${body}" };
}
@@ -160,11 +177,15 @@ public final class CollectionFunctionFactory implements
SimpleLanguageFunctionFa
"Valid syntax: ${split(separator)} or
${split(exp,separator)} was: " + function, index);
}
if (tokens.length == 2) {
- exp = tokens[0];
+ exp = expressionOrBody(tokens[0]);
separator = tokens[1];
} else {
separator = tokens[0];
}
+ if (separator == null) {
+ // a separator left out is the default
+ separator = ",";
+ }
} else if ("\n".equals(values)) {
separator = values;
}
@@ -183,7 +204,7 @@ public final class CollectionFunctionFactory implements
SimpleLanguageFunctionFa
"Valid syntax: ${sort(reverse)} or
${sort(exp,reverse)} was: " + function, index);
}
if (tokens.length == 2) {
- exp = tokens[0];
+ exp = expressionOrBody(tokens[0]);
reverse = Boolean.parseBoolean(tokens[1]);
} else if ("true".equalsIgnoreCase(tokens[0]) ||
"false".equalsIgnoreCase(tokens[0])) {
reverse = Boolean.parseBoolean(tokens[0]);
@@ -208,7 +229,11 @@ public final class CollectionFunctionFactory implements
SimpleLanguageFunctionFa
"Valid syntax: ${forEach(exp,exp)} was: " + function,
index);
}
String exp1 = tokens[0];
- String exp2 =
Arrays.stream(tokens).skip(1).collect(Collectors.joining(","));
+ String exp2 = joinArguments(tokens, 1);
+ if (exp1 == null || exp2 == null) {
+ throw new SimpleParserException(
+ "Valid syntax: ${forEach(exp,exp)} was: " + function,
index);
+ }
return CollectionExpressionBuilder.forEachExpression(exp1, exp2);
}
@@ -225,7 +250,11 @@ public final class CollectionFunctionFactory implements
SimpleLanguageFunctionFa
"Valid syntax: ${filter(exp,exp)} was: " + function,
index);
}
String exp1 = tokens[0];
- String exp2 =
Arrays.stream(tokens).skip(1).collect(Collectors.joining(","));
+ String exp2 = joinArguments(tokens, 1);
+ if (exp1 == null || exp2 == null) {
+ throw new SimpleParserException(
+ "Valid syntax: ${filter(exp,exp)} was: " + function,
index);
+ }
return CollectionExpressionBuilder.filterExpression(exp1, exp2);
}
@@ -241,9 +270,13 @@ public final class CollectionFunctionFactory implements
SimpleLanguageFunctionFa
String exp1 = "${body}";
if (tokens.length > 1) {
skip = 1;
- exp1 = tokens[0];
+ exp1 = expressionOrBody(tokens[0]);
+ }
+ String exp2 = joinArguments(tokens, skip);
+ if (exp2 == null) {
+ throw new SimpleParserException(
+ "Valid syntax: ${listAdd(exp)} or ${listAdd(exp,exp)}
was: " + function, index);
}
- String exp2 =
Arrays.stream(tokens).skip(skip).collect(Collectors.joining(","));
return CollectionExpressionBuilder.listAddExpression(exp1, exp2);
}
@@ -259,9 +292,13 @@ public final class CollectionFunctionFactory implements
SimpleLanguageFunctionFa
String exp1 = "${body}";
if (tokens.length > 1) {
skip = 1;
- exp1 = tokens[0];
+ exp1 = expressionOrBody(tokens[0]);
+ }
+ String exp2 = joinArguments(tokens, skip);
+ if (exp2 == null) {
+ throw new SimpleParserException(
+ "Valid syntax: ${listRemove(exp)} or
${listRemove(exp,exp)} was: " + function, index);
}
- String exp2 =
Arrays.stream(tokens).skip(skip).collect(Collectors.joining(","));
return CollectionExpressionBuilder.listRemoveExpression(exp1,
exp2);
}
@@ -277,7 +314,7 @@ public final class CollectionFunctionFactory implements
SimpleLanguageFunctionFa
String exp1 = "${body}";
String key;
if (tokens.length > 2) {
- exp1 = tokens[0];
+ exp1 = expressionOrBody(tokens[0]);
key = tokens[1];
skip = 2;
} else if (tokens.length == 2) {
@@ -287,7 +324,11 @@ public final class CollectionFunctionFactory implements
SimpleLanguageFunctionFa
throw new SimpleParserException(
"Valid syntax: ${mapAdd(key,exp)} or
${mapAdd(exp,key,exp)} was: " + function, index);
}
- String exp2 =
Arrays.stream(tokens).skip(skip).collect(Collectors.joining(","));
+ String exp2 = joinArguments(tokens, skip);
+ if (key == null || exp2 == null) {
+ throw new SimpleParserException(
+ "Valid syntax: ${mapAdd(key,exp)} or
${mapAdd(exp,key,exp)} was: " + function, index);
+ }
return CollectionExpressionBuilder.mapAddExpression(exp1, key,
exp2);
}
@@ -306,11 +347,15 @@ public final class CollectionFunctionFactory implements
SimpleLanguageFunctionFa
String key;
String exp = "${body}";
if (tokens.length == 2) {
- exp = tokens[0];
+ exp = expressionOrBody(tokens[0]);
key = tokens[1];
} else {
key = tokens[0];
}
+ if (key == null) {
+ throw new SimpleParserException(
+ "Valid syntax: ${mapRemove(key)} or
${mapRemove(exp,key)} was: " + function, index);
+ }
return CollectionExpressionBuilder.mapRemoveExpression(exp, key);
}
@@ -319,7 +364,7 @@ public final class CollectionFunctionFactory implements
SimpleLanguageFunctionFa
String values = StringHelper.beforeLast(remainder, ")");
String[] tokens = null;
if (ObjectHelper.isNotEmpty(values)) {
- tokens = StringQuoteHelper.splitSafeQuote(values, ',', true,
false);
+ tokens = StringQuoteHelper.splitSafeQuote(values, ',', true,
false, true);
}
return CollectionExpressionBuilder.listExpression(tokens);
}
@@ -329,7 +374,7 @@ public final class CollectionFunctionFactory implements
SimpleLanguageFunctionFa
String values = StringHelper.beforeLast(remainder, ")");
String[] tokens = null;
if (ObjectHelper.isNotEmpty(values)) {
- tokens = StringQuoteHelper.splitSafeQuote(values, ',', true,
false);
+ tokens = StringQuoteHelper.splitSafeQuote(values, ',', true,
false, true);
}
if (tokens != null && tokens.length % 2 == 1) {
throw new SimpleParserException(
diff --git
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/JoinFunctionFactory.java
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/JoinFunctionFactory.java
index 7a9d34337d8d..54ef45de8e6f 100644
---
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/JoinFunctionFactory.java
+++
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/JoinFunctionFactory.java
@@ -21,11 +21,10 @@ import org.apache.camel.Expression;
import org.apache.camel.language.simple.CollectionExpressionBuilder;
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.splitArgumentsAsWritten;
import static
org.apache.camel.language.simple.SimpleFunctionHelper.trimArgument;
/**
@@ -48,20 +47,29 @@ public final class JoinFunctionFactory implements
SimpleLanguageFunctionFactory
String separator = ",";
String prefix = null;
String exp = "${body}";
- if (ObjectHelper.isNotEmpty(values)) {
- // the arguments as written (with their quotes), split at the
commas outside quotes as splitArguments does,
- // so the separator can be kept as written
- String[] tokens = StringQuoteHelper.splitSafeQuote(values, ',',
false, true);
+ // not ObjectHelper.isNotEmpty, which trims: a white space separator,
as in ${join( )}, is kept as written
+ if (values != null && !values.isEmpty()) {
+ // the arguments as written (with their quotes), split as
splitArguments does (an empty argument is kept,
+ // and a comma inside a nested function does not split), so the
separator can be kept as written
+ String[] tokens = splitArgumentsAsWritten(values);
if (tokens.length > 3) {
throw new SimpleParserException(
"Valid syntax: ${join(separator,prefix,expression)}
was: " + function, index);
}
- separator = StringHelper.removeLeadingAndEndingQuotes(tokens[0]);
+ if (!tokens[0].isEmpty()) {
+ // a separator left out is the default; '' is no separator
+ separator =
StringHelper.removeLeadingAndEndingQuotes(tokens[0]);
+ }
if (tokens.length > 1) {
// the prefix and the expression without the white space and
the quotes around them
prefix = trimArgument(tokens[1]);
+ if (prefix.isEmpty()) {
+ // an empty prefix, as in ${join(&,,${header.ids})}, is no
prefix
+ prefix = null;
+ }
if (tokens.length == 3) {
- exp = trimArgument(tokens[2]);
+ // an expression left out is the body
+ exp = tokens[2].isEmpty() ? "${body}" :
trimArgument(tokens[2]);
}
}
}
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 ef44f6227e0f..6d1daa522907 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
@@ -27,6 +27,7 @@ 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;
@@ -51,18 +52,19 @@ public final class StringFunctionFactory implements
SimpleLanguageFunctionFactor
"Valid syntax: ${replace(from,to)} or
${replace(from,to,expression)} was: " + function, index);
}
String[] tokens = splitArguments(values);
- if (tokens.length < 2 || tokens.length > 3) {
+ // the from cannot be left out; a to left out is the empty text
+ if (tokens.length < 2 || tokens.length > 3 || tokens[0] == null) {
throw new SimpleParserException(
"Valid syntax: ${replace(from,to)} or
${replace(from,to,expression)} was: " + function, index);
}
String from = StringHelper.xmlDecode(tokens[0]);
- String to = StringHelper.xmlDecode(tokens[1]);
+ String to = tokens[1] == null ? "" :
StringHelper.xmlDecode(tokens[1]);
if ("∅".equals(to)) {
to = "";
}
String exp = "${body}";
if (tokens.length == 3) {
- exp = tokens[2];
+ exp = expressionOrBody(tokens[2]);
}
return StringExpressionBuilder.replaceExpression(exp, from, to);
}
@@ -81,14 +83,20 @@ public final class StringFunctionFactory implements
SimpleLanguageFunctionFactor
throw new SimpleParserException(
"Valid syntax: ${substring(num,num,expression)} was: "
+ function, index);
}
+ if (tokens[0] == null) {
+ throw new SimpleParserException(
+ "Valid syntax: ${substring(num)},
${substring(num,num)}, or ${substring(num,num,expression)} was: "
+ + function,
+ index);
+ }
String num1 = tokens[0];
String num2 = "0";
- if (tokens.length > 1) {
+ if (tokens.length > 1 && tokens[1] != null) {
num2 = tokens[1];
}
String exp = "${body}";
if (tokens.length == 3) {
- exp = tokens[2];
+ exp = expressionOrBody(tokens[2]);
}
return StringExpressionBuilder.substringExpression(exp, num1,
num2);
}
@@ -108,11 +116,17 @@ public final class StringFunctionFactory implements
SimpleLanguageFunctionFactor
String exp1 = "${body}";
String before;
if (tokens.length == 2) {
- exp1 = tokens[0];
+ exp1 = expressionOrBody(tokens[0]);
before = tokens[1];
} else {
before = tokens[0];
}
+ if (before == null) {
+ // an argument left out is refused where it has no default, so
${substringBefore(x,)} is not read as
+ // x before nothing
+ throw new SimpleParserException(
+ "Valid syntax: ${substringBefore(exp)} or
${substringBefore(exp,exp)} was: " + function, index);
+ }
return StringExpressionBuilder.substringBeforeExpression(exp1,
before);
}
@@ -131,11 +145,15 @@ public final class StringFunctionFactory implements
SimpleLanguageFunctionFactor
String exp1 = "${body}";
String after;
if (tokens.length == 2) {
- exp1 = tokens[0];
+ exp1 = expressionOrBody(tokens[0]);
after = tokens[1];
} else {
after = tokens[0];
}
+ if (after == null) {
+ throw new SimpleParserException(
+ "Valid syntax: ${substringAfter(exp)} or
${substringAfter(exp,exp)} was: " + function, index);
+ }
return StringExpressionBuilder.substringAfterExpression(exp1,
after);
}
@@ -159,13 +177,19 @@ public final class StringFunctionFactory implements
SimpleLanguageFunctionFactor
String after;
String before;
if (tokens.length == 3) {
- exp1 = tokens[0];
+ exp1 = expressionOrBody(tokens[0]);
after = tokens[1];
before = tokens[2];
} else {
after = tokens[0];
before = tokens[1];
}
+ if (after == null || before == null) {
+ throw new SimpleParserException(
+ "Valid syntax: ${substringBetween(after,before)} or
${substringBetween(exp,after,before)} was: "
+ + function,
+ index);
+ }
return StringExpressionBuilder.substringBetweenExpression(exp1,
after, before);
}
@@ -186,9 +210,13 @@ public final class StringFunctionFactory implements
SimpleLanguageFunctionFactor
if (tokens.length == 1) {
pattern = tokens[0];
} else {
- exp = tokens[0];
+ exp = expressionOrBody(tokens[0]);
pattern = tokens[1];
}
+ if (pattern == null) {
+ throw new SimpleParserException(
+ "Valid syntax: ${contains(text)} or
${contains(exp,text)} was: " + function, index);
+ }
return StringExpressionBuilder.containsExpression(exp, pattern);
}
diff --git
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleTest.java
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleTest.java
index c598f0158865..509183607543 100644
---
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleTest.java
+++
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleTest.java
@@ -547,6 +547,17 @@ public class SimpleTest extends LanguageTestSupport {
assertTrue(cause.getMessage().contains("the value is JSON text, not a
Map: unmarshal it first"), cause.getMessage());
}
+ @Test
+ public void testFunctionArgumentLeftOutAndCommaInANestedFunction() {
+ exchange.getIn().setBody("abc");
+ exchange.getIn().setHeader("s", "abcdef");
+ // an argument left out stays in its place, and a comma inside a
nested function does not split an argument
+ assertExpression("${replace(b,,${header.s})}", "acdef");
+ assertExpression("${substring(1,2,${header.s.substring(0,4)})}", "b");
+ // a quoted empty argument is the empty text
+ assertExpression("${replace(b,'')}", "ac");
+ }
+
@Test
public void testOGNLKeyOnAListSaysToPickAnElement() {
exchange.getIn().setBody(List.of(Map.of("sku", "CAMEL-MUG", "qty",
42)));
diff --git
a/core/camel-core/src/test/java/org/apache/camel/language/simple/functions/CollectionFunctionFactoryTest.java
b/core/camel-core/src/test/java/org/apache/camel/language/simple/functions/CollectionFunctionFactoryTest.java
index 7cd75dc80ee7..a230d3f88ae6 100644
---
a/core/camel-core/src/test/java/org/apache/camel/language/simple/functions/CollectionFunctionFactoryTest.java
+++
b/core/camel-core/src/test/java/org/apache/camel/language/simple/functions/CollectionFunctionFactoryTest.java
@@ -21,11 +21,14 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import org.apache.camel.language.simple.types.SimpleParserException;
import org.apache.camel.spi.SimpleLanguageFunctionFactory;
import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class CollectionFunctionFactoryTest extends
AbstractSimpleFunctionFactoryTestSupport {
@@ -263,4 +266,63 @@ public class CollectionFunctionFactoryTest extends
AbstractSimpleFunctionFactory
assertEquals("v", map.get("k"));
}
+ @Test
+ public void testCommaInsideANestedFunction() {
+ exchange.getIn().setBody("abc");
+ exchange.getIn().setHeader("list", List.of("c", "a", "b"));
+ exchange.getIn().setHeader("s", "abcdef");
+
+ // a comma inside a nested function does not split the argument
+ assertEquals(List.of("c", "a"),
evaluate("sort(${header.list.subList(0,2)},true)", List.class));
+ assertEquals(List.of("c", "a", "x"),
evaluate("distinct(${header.list.subList(0,2)},x,c)", List.class));
+ assertEquals(List.of("ab", "z"),
evaluate("list(${header.s.substring(0,2)},z)", List.class));
+ evaluate("setHeader(foo,${header.s.substring(0,2)})");
+ assertEquals("ab", exchange.getIn().getHeader("foo"));
+ }
+
+ @Test
+ public void testEmptyExpressionArgumentIsTheBody() {
+ exchange.getIn().setBody(new ArrayList<>(List.of("b", "a")));
+
+ // an empty expression is the same as leaving it out: the body
+ assertEquals(List.of("b", "a"), evaluate("sort(,true)", List.class));
+ // with a trailing comma the first argument is the expression: the
text true
+ assertEquals("[true]", String.valueOf(evaluate("sort(true,)")));
+ assertEquals(List.of("b", "a", "x"), evaluate("listAdd(,x)",
List.class));
+ }
+
+ @Test
+ public void testEmptyArguments() {
+ exchange.getIn().setBody(new ArrayList<>(List.of("a", "b")));
+ exchange.getIn().setHeader("s", "abc");
+ exchange.getIn().setHeader("csv", "x,y");
+
+ // an empty type is no type, as when it is left out
+ evaluate("setHeader(foo,,${header.s})");
+ assertEquals("abc", exchange.getIn().getHeader("foo"));
+ // a separator of split left out is the default, a comma; a quoted
empty one splits into characters
+ assertArrayEquals(new String[] { "x", "y" },
evaluate("split(${header.csv},)", String[].class));
+ assertArrayEquals(new String[] { "x", ",", "y" },
evaluate("split(${header.csv},'')", String[].class));
+ // a quoted empty value is the empty text
+ evaluate("setHeader(bar,'')");
+ assertEquals("", exchange.getIn().getHeader("bar"));
+ // an empty argument that has no default is refused
+ for (String function : new String[] {
+ "setHeader(,x)", "setVariable(,x)", "setHeader(foo,)",
"listAdd(x,)", "listRemove(x,)",
+ "mapRemove(${body},)", "mapAdd(${body},k,)", "mapAdd(,v)",
"forEach(,x)", "filter(${body},)",
+ "listAdd(${body},x,)", "mapAdd(${body},k,v,)",
"filter(${body},x,)" }) {
+ assertThrows(SimpleParserException.class, () ->
createFactory().createFunction(context, function, 0),
+ function);
+ }
+ }
+
+ @Test
+ public void testCommaInsideANestedFunctionOfAValue() {
+ exchange.getIn().setHeader("s", "abcdef");
+
+ assertEquals(List.of("z", "ab"),
evaluate("reverse(${header.s.substring(0,2)},z)", List.class));
+ assertEquals(2, evaluate("shuffle(${header.s.substring(0,2)},z)",
List.class).size());
+ Map<String, Object> map =
evaluate("map(k,${header.s.substring(0,2)})", Map.class);
+ assertEquals("ab", map.get("k"));
+ }
}
diff --git
a/core/camel-core/src/test/java/org/apache/camel/language/simple/functions/JoinFunctionFactoryTest.java
b/core/camel-core/src/test/java/org/apache/camel/language/simple/functions/JoinFunctionFactoryTest.java
index 71a46a75e97c..1c5b2440561f 100644
---
a/core/camel-core/src/test/java/org/apache/camel/language/simple/functions/JoinFunctionFactoryTest.java
+++
b/core/camel-core/src/test/java/org/apache/camel/language/simple/functions/JoinFunctionFactoryTest.java
@@ -19,10 +19,12 @@ package org.apache.camel.language.simple.functions;
import java.util.ArrayList;
import java.util.List;
+import org.apache.camel.language.simple.types.SimpleParserException;
import org.apache.camel.spi.SimpleLanguageFunctionFactory;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
public class JoinFunctionFactoryTest extends
AbstractSimpleFunctionFactoryTestSupport {
@@ -103,4 +105,48 @@ public class JoinFunctionFactoryTest extends
AbstractSimpleFunctionFactoryTestSu
// a quoted separator is the text inside its quotes
assertEquals("id=A&id=B", evaluate("join('&' , id=)", String.class));
}
+
+ @Test
+ public void testJoinEmptyArgument() {
+ List<Object> data = new ArrayList<>();
+ data.add("A");
+ data.add("B");
+ data.add("C");
+ exchange.getIn().setBody(data);
+ exchange.getIn().setHeader("id", List.of("X", "Y"));
+
+ // an empty argument is the same as leaving it out, so the next
argument does not take its place
+ assertEquals("id=A,id=B,id=C", evaluate("join(,id=)", String.class));
+ assertEquals("X&Y", evaluate("join(&,,${header.id})", String.class));
+ assertEquals("id=A&id=B&id=C", evaluate("join(&,id=,)", String.class));
+ assertEquals("A,B,C", evaluate("join(,,)", String.class));
+ // a quoted empty separator is no separator
+ assertEquals("ABC", evaluate("join('')", String.class));
+ // a space is a separator, as written
+ assertEquals("A B C", evaluate("join( )", String.class));
+ }
+
+ @Test
+ public void testJoinCommaInsideANestedFunction() {
+ exchange.getIn().setHeader("id", List.of("X", "Y", "Z"));
+
+ // a comma inside a nested function does not split the expression
+ assertEquals("id=X&id=Y",
evaluate("join(&,id=,${header.id.subList(0,2)})", String.class));
+ }
+
+ @Test
+ public void testJoinTooManyArguments() {
+ exchange.getIn().setBody(List.of("A"));
+
+ assertThrows(SimpleParserException.class,
+ () -> createFactory().createFunction(context,
"join(&,id=,${body},x)", 0));
+ }
+
+ @Test
+ public void testJoinQuotedParenthesisInThePrefix() {
+ exchange.getIn().setHeader("id", List.of("X", "Y"));
+
+ // a parenthesis in an argument is quoted, so it does not nest
+ assertEquals("(X&(Y", evaluate("join(&,'(',${header.id})",
String.class));
+ }
}
diff --git
a/core/camel-core/src/test/java/org/apache/camel/language/simple/functions/StringFunctionFactoryTest.java
b/core/camel-core/src/test/java/org/apache/camel/language/simple/functions/StringFunctionFactoryTest.java
index fc0ea7ceba5d..c79759f19f21 100644
---
a/core/camel-core/src/test/java/org/apache/camel/language/simple/functions/StringFunctionFactoryTest.java
+++
b/core/camel-core/src/test/java/org/apache/camel/language/simple/functions/StringFunctionFactoryTest.java
@@ -34,6 +34,17 @@ public class StringFunctionFactoryTest extends
AbstractSimpleFunctionFactoryTest
return new StringFunctionFactory();
}
+ @Test
+ public void testParenthesesOutsideANestedFunctionAreText() {
+ // a parenthesis outside a nested ${...} is text, as before: only the
commas of a nested function do not split
+ exchange.getIn().setBody("order (42) done");
+ assertEquals("42", evaluate("substringBetween(${body},(,))",
String.class));
+ exchange.getIn().setHeader("s", "abcdef");
+ assertEquals("bc",
evaluate("substringBetween(${header.s.substring(0,4)},a,d)", String.class));
+ exchange.getIn().setBody("a(b");
+ assertEquals("axb", evaluate("replace((,x)", String.class));
+ }
+
// --- replace ---
@Test
@@ -308,4 +319,71 @@ public class StringFunctionFactoryTest extends
AbstractSimpleFunctionFactoryTest
assertThrows(SimpleParserException.class,
() -> createFactory().createFunction(context, "escape()", 0));
}
+
+ @Test
+ void testEmptyArgumentIsKept() {
+ // an empty argument is kept, so the next argument does not take its
place
+ exchange.getIn().setBody("abc");
+ exchange.getIn().setHeader("s", "abcdef");
+ assertEquals("acdef", evaluate("replace(b,,${header.s})",
String.class));
+ assertEquals("ac", evaluate("replace(b,)", String.class));
+ // an empty expression is the same as leaving it out: the body
+ assertEquals("aXc", evaluate("replace(b,X,)", String.class));
+ assertEquals("bc", evaluate("substring(1,)", String.class));
+ assertEquals("a", evaluate("substringBefore(,b)", String.class));
+ assertEquals("c", evaluate("substringAfter(,b)", String.class));
+ assertEquals("b", evaluate("substringBetween(,a,c)", String.class));
+ assertEquals(true, evaluate("contains(,bc)", Boolean.class));
+ }
+
+ @Test
+ void testEmptyArgumentWithoutDefaultIsRefused() {
+ exchange.getIn().setBody("abc");
+ assertThrows(SimpleParserException.class, () ->
createFactory().createFunction(context, "replace(,X)", 0));
+ assertThrows(SimpleParserException.class, () ->
createFactory().createFunction(context, "substring(,2)", 0));
+ }
+
+ @Test
+ void testCommaInsideANestedFunction() {
+ // a comma inside a nested function does not split the argument
+ exchange.getIn().setBody("abc");
+ exchange.getIn().setHeader("s", "abcdef");
+ assertEquals("b",
evaluate("substring(1,2,${header.s.substring(0,4)})", String.class));
+ assertEquals("bc",
evaluate("substringBefore(${header.s.substring(1,4)},d)", String.class));
+ assertEquals("c",
evaluate("substringAfter(${header.s.substring(1,3)},b)", String.class));
+ assertEquals("cd",
evaluate("substringBetween(${header.s.substring(1,5)},b,e)", String.class));
+ assertEquals(true, evaluate("contains(${header.s.substring(0,2)},ab)",
Boolean.class));
+ // the from and to of replace are text, not expressions
+ assertEquals("a${header.s.substring(1,3)}c",
evaluate("replace(b,${header.s.substring(1,3)})", String.class));
+ assertEquals("aXcdef",
evaluate("replace(b,X,${header.s.substring(0,6)})", String.class));
+ }
+
+ @Test
+ void testEmptyTextArgumentIsRefused() {
+ // an empty argument that has no default is refused, so a trailing
comma is not read as an empty text
+ for (String function : new String[] {
+ "contains(xyz,)", "substringBefore(cd,)",
"substringAfter(cd,)", "substringBetween(x,,c)",
+ "substringBetween(x,a,)", "replace(a,b,${body},)" }) {
+ assertThrows(SimpleParserException.class, () ->
createFactory().createFunction(context, function, 0),
+ function);
+ }
+ }
+
+ @Test
+ void testQuotedEmptyArgumentIsTheEmptyText() {
+ // '' is the empty text, not an argument left out
+ exchange.getIn().setBody("abc");
+ assertEquals(true, evaluate("contains(${body},'')", Boolean.class));
+ assertEquals("abc", evaluate("substringAfter(${body},'')",
String.class));
+ assertEquals("ac", evaluate("replace(b,'')", String.class));
+ // only the quotes around a whole argument are removed, so a lone
quote at the end is kept
+ assertEquals("ax'c", evaluate("replace(b,x')", String.class));
+ }
+
+ @Test
+ void testQuotedParenthesisIsText() {
+ // a parenthesis in an argument is quoted, so it does not nest
+ exchange.getIn().setBody("a(b");
+ assertEquals("axb", evaluate("replace('(',x)", String.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 435bf93c39fe..3e329077add8 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
@@ -415,6 +415,21 @@ inside the quotes to keep it: `+${join('& ',id=)}+`). A
prefix with a space at i
`join` are removed: a quote inside an argument, as in `+${join(&,a'b'c)}+`, is
now kept (it was removed), so an
expression such as `+${join(&,id=,${header.x.split(',')})}+` now works.
+=== camel-core - Simple: an empty function argument is kept, and a comma in a
nested function does not split
+
+In the arguments of `replace`, `substring`, `substringBefore`,
`substringAfter`, `substringBetween`, `contains`, `join`,
+`split`, `sort`, `setHeader`, `setVariable`, `forEach`, `filter`, `listAdd`,
`listRemove`, `mapAdd` and `mapRemove`, an
+argument left out now stays in its place instead of letting the next argument
take it: `+${replace(b,,${header.s})}+`
+replaces `b` with nothing in the header (it replaced `b` with the text
`+${header.s}+`), and `+${join(,id=)}+` joins
+with a comma and the prefix `id=` (it joined with `id=`). Where the function
has no default for it, an argument left out
+is refused, so a stray comma that was ignored is now an error, such as
`+${contains(xyz,)}+`, `+${substring(,2)}+` or
+`+${replace(a,b,${body},)}+`: remove the comma. `+${sort(true,)}+` now sorts
the text `true`, and `+${join( )}+` joins
+with a space (it joined with a comma). A comma inside a nested function, such
as `+${header.s.substring(1,3)}+`, no
+longer separates two arguments; a parenthesis outside a nested function is
text, as before
+(`+${substringBetween(${body},(,))}+`). In the values of `distinct`,
`reverse`, `shuffle`, `list` and `map`, a comma
+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 - 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`,