This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch fix/CAMEL-24962 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 28e3825dc4c55d64c6b158ab5ec3cf304e25da73 Author: Claus Ibsen <[email protected]> AuthorDate: Wed Sep 23 19:09:18 2026 +0200 CAMEL-24967: simple - fix how function names are matched and arguments are split - quotes inside a nested function argument are kept: ${size(${body.split(',')})} - convertTo, headerAs and variableAs find the closing parenthesis of the argument list: ${convertTo(${header.foo.trim()},Integer)} - a comma inside quotes is part of the value in concat, throwException and hash - iif and replace with too few arguments report the valid syntax - ${bean:type:com.foo.MyClass.myMethod} splits the method from the class name at the last dot - a quoted key such as ${header['a.b']} is the name, not OGNL (also variable and exchangeProperty) - a name glued to a function prefix is unknown: ${headerfoo}, ${uuidv7}, ${exceptionInfo} (so custom functions with such names can be called) - fix a SimpleTest typo that asserted ${variableAA(...)} Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../language/simple/SimpleFunctionHelper.java | 45 +++++++- .../simple/functions/BeanFunctionFactory.java | 11 ++ .../simple/functions/ExchangeFunctionFactory.java | 22 +++- .../simple/functions/HeaderFunctionFactory.java | 11 +- .../simple/functions/MathFunctionFactory.java | 6 +- .../simple/functions/MiscFunctionFactory.java | 72 +++++++------ .../simple/functions/StringFunctionFactory.java | 57 +++++----- .../simple/functions/VariableFunctionFactory.java | 11 +- .../simple/SimpleFunctionArgumentsTest.java | 117 +++++++++++++++++++++ .../apache/camel/language/simple/SimpleTest.java | 2 +- 10 files changed, 279 insertions(+), 75 deletions(-) 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 ab78e630b7ce..64f1336abe24 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 @@ -43,7 +43,7 @@ public final class SimpleFunctionHelper { if (remainder == null) { remainder = ifStartsWithReturnRemainder("header", function); } - return remainder; + return keyRemainder(remainder); } public static String parseVariable(String function) { @@ -51,6 +51,49 @@ public final class SimpleFunctionHelper { if (remainder == null) { remainder = ifStartsWithReturnRemainder("variable", function); } + return keyRemainder(remainder); + } + + /** + * The remainder after a name such as header must start the key, so ${headerfoo} is not the header foo (and a + * custom function named such as headerCount can be called). + */ + private static String keyRemainder(String remainder) { + if (remainder != null && !remainder.isEmpty()) { + char c = remainder.charAt(0); + if (c != '.' && c != ':' && c != '?' && c != '[') { + return null; + } + } return remainder; } + + /** + * The index of the parenthesis that closes the argument list, where the text is what comes after the opening + * parenthesis. Parentheses inside nested functions and quotes are skipped, so the type in + * ${convertTo(${header.foo.trim()},Integer)} is found. Returns -1 if there is no closing parenthesis. + */ + public static int indexOfClosingParenthesis(String text) { + int depth = 0; + boolean single = false; + boolean dubble = false; + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (c == '\'' && !dubble) { + single = !single; + } else if (c == '"' && !single) { + dubble = !dubble; + } else if (!single && !dubble) { + if (c == '(') { + depth++; + } else if (c == ')') { + if (depth == 0) { + return i; + } + depth--; + } + } + } + return -1; + } } diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/BeanFunctionFactory.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/BeanFunctionFactory.java index 0ed4f8e09f43..92e15b06e3ac 100644 --- a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/BeanFunctionFactory.java +++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/BeanFunctionFactory.java @@ -91,6 +91,17 @@ public final class BeanFunctionFactory implements SimpleLanguageFunctionFactory if (doubleColonIndex > 0 && (!remainder.contains("(") || doubleColonIndex < beginOfParameterDeclaration)) { ref = remainder.substring(0, doubleColonIndex); method = remainder.substring(doubleColonIndex + 2); + } else if (remainder.startsWith("type:")) { + // type:com.foo.MyClass.myMethod: the class name has dots, so the method is the last part + // when it starts with a lower case letter, as Java method names do + String beforeParams = beginOfParameterDeclaration > 0 + ? remainder.substring(0, beginOfParameterDeclaration) : remainder; + int idx = beforeParams.lastIndexOf('.'); + if (idx > 0 && idx + 1 < beforeParams.length() + && Character.isLowerCase(beforeParams.charAt(idx + 1))) { + ref = remainder.substring(0, idx); + method = remainder.substring(idx + 1); + } } else { int idx = remainder.indexOf('.'); if (idx > 0) { diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/ExchangeFunctionFactory.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/ExchangeFunctionFactory.java index 6b2cdd98bdcd..324cb1424b16 100644 --- a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/ExchangeFunctionFactory.java +++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/ExchangeFunctionFactory.java @@ -23,6 +23,7 @@ import org.apache.camel.language.simple.types.SimpleParserException; import org.apache.camel.spi.SimpleLanguageFunctionFactory; import org.apache.camel.support.builder.ExpressionBuilder; import org.apache.camel.util.OgnlHelper; +import org.apache.camel.util.StringHelper; import static org.apache.camel.language.simple.SimpleFunctionHelper.ifStartsWithReturnRemainder; @@ -38,7 +39,7 @@ public final class ExchangeFunctionFactory implements SimpleLanguageFunctionFact public Expression createFunction(CamelContext camelContext, String function, int index) { // camelContext OGNL String remainder = ifStartsWithReturnRemainder("camelContext", function); - if (remainder != null) { + if (remainder != null && startsOgnl(remainder)) { boolean invalid = OgnlHelper.isInvalidValidOgnlExpression(remainder); if (invalid) { throw new SimpleParserException("Valid syntax: ${camelContext.OGNL} was: " + function, index); @@ -48,7 +49,7 @@ public final class ExchangeFunctionFactory implements SimpleLanguageFunctionFact // Exception OGNL — exchangeProperty/exchange checked separately, no prefix clash here remainder = ifStartsWithReturnRemainder("exception", function); - if (remainder != null) { + if (remainder != null && startsOgnl(remainder)) { boolean invalid = OgnlHelper.isInvalidValidOgnlExpression(remainder); if (invalid) { throw new SimpleParserException("Valid syntax: ${exception.OGNL} was: " + function, index); @@ -58,7 +59,7 @@ public final class ExchangeFunctionFactory implements SimpleLanguageFunctionFact // exchangeProperty must be checked before exchange to avoid prefix clash remainder = ifStartsWithReturnRemainder("exchangeProperty", function); - if (remainder != null) { + if (remainder != null && (startsOgnl(remainder) || remainder.startsWith(":"))) { // remove leading character (dot, colon or ?) if (remainder.startsWith(".") || remainder.startsWith(":") || remainder.startsWith("?")) { remainder = remainder.substring(1); @@ -66,6 +67,11 @@ public final class ExchangeFunctionFactory implements SimpleLanguageFunctionFact // remove starting and ending brackets if (remainder.startsWith("[") && remainder.endsWith("]")) { remainder = remainder.substring(1, remainder.length() - 1); + String unquoted = StringHelper.removeLeadingAndEndingQuotes(remainder); + if (!unquoted.equals(remainder)) { + // a quoted key such as ['a.b'] is the name, not an OGNL expression + return ExpressionBuilder.exchangePropertyExpression(unquoted); + } } boolean invalid = OgnlHelper.isInvalidValidOgnlExpression(remainder); @@ -82,7 +88,7 @@ public final class ExchangeFunctionFactory implements SimpleLanguageFunctionFact // exchange OGNL remainder = ifStartsWithReturnRemainder("exchange", function); - if (remainder != null && (remainder.startsWith(".") || remainder.startsWith("?") || remainder.startsWith("["))) { + if (remainder != null && startsOgnl(remainder)) { // only ${exchange.OGNL}: ${exchangeCounter} is not an exchange OGNL but an unknown function boolean invalid = OgnlHelper.isInvalidValidOgnlExpression(remainder); if (invalid) { @@ -93,4 +99,12 @@ public final class ExchangeFunctionFactory implements SimpleLanguageFunctionFact return null; } + + /** + * Whether the remainder after the function name starts an OGNL expression, so ${exchangeCounter} or + * ${exceptionInfo} are not taken as ${exchange} or ${exception} with some OGNL glued on. + */ + private static boolean startsOgnl(String remainder) { + return remainder.startsWith(".") || remainder.startsWith("?") || remainder.startsWith("["); + } } diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/HeaderFunctionFactory.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/HeaderFunctionFactory.java index 05b7f0ce5be1..2882dea4df8d 100644 --- a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/HeaderFunctionFactory.java +++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/HeaderFunctionFactory.java @@ -19,6 +19,7 @@ package org.apache.camel.language.simple.functions; import org.apache.camel.CamelContext; import org.apache.camel.Expression; import org.apache.camel.language.simple.OgnlExpressionBuilder; +import org.apache.camel.language.simple.SimpleFunctionHelper; import org.apache.camel.language.simple.types.SimpleParserException; import org.apache.camel.spi.SimpleLanguageFunctionFactory; import org.apache.camel.support.builder.ExpressionBuilder; @@ -40,13 +41,14 @@ public final class HeaderFunctionFactory implements SimpleLanguageFunctionFactor // headerAs String remainder = ifStartsWithReturnRemainder("headerAs(", function); if (remainder != null) { - String keyAndType = StringHelper.before(remainder, ")"); + int end = SimpleFunctionHelper.indexOfClosingParenthesis(remainder); + String keyAndType = end >= 0 ? remainder.substring(0, end) : null; if (keyAndType == null) { throw new SimpleParserException("Valid syntax: ${headerAs(key, type)} was: " + function, index); } String key = StringHelper.before(keyAndType, ","); String type = StringHelper.after(keyAndType, ","); - remainder = StringHelper.after(remainder, ")"); + remainder = remainder.substring(end + 1); if (ObjectHelper.isEmpty(key) || ObjectHelper.isEmpty(type) || ObjectHelper.isNotEmpty(remainder)) { throw new SimpleParserException("Valid syntax: ${headerAs(key, type)} was: " + function, index); } @@ -71,6 +73,11 @@ public final class HeaderFunctionFactory implements SimpleLanguageFunctionFactor } if (remainder.startsWith("[") && remainder.endsWith("]")) { remainder = remainder.substring(1, remainder.length() - 1); + String unquoted = StringHelper.removeLeadingAndEndingQuotes(remainder); + if (!unquoted.equals(remainder)) { + // a quoted key such as ['a.b'] is the name, not an OGNL expression + return ExpressionBuilder.headerExpression(unquoted); + } } String key = StringHelper.removeLeadingAndEndingQuotes(remainder); 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 175b624c2b97..ff4c848cfa31 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 @@ -40,19 +40,19 @@ public final class MathFunctionFactory implements SimpleLanguageFunctionFactory if (remainder != null) { String value = StringHelper.beforeLast(remainder, ")"); return MathExpressionBuilder.absExpression( - ObjectHelper.isNotEmpty(value) ? StringHelper.removeQuotes(value) : null); + ObjectHelper.isNotEmpty(value) ? StringHelper.removeLeadingAndEndingQuotes(value) : null); } remainder = ifStartsWithReturnRemainder("floor(", function); if (remainder != null) { String value = StringHelper.beforeLast(remainder, ")"); return MathExpressionBuilder.floorExpression( - ObjectHelper.isNotEmpty(value) ? StringHelper.removeQuotes(value) : null); + ObjectHelper.isNotEmpty(value) ? StringHelper.removeLeadingAndEndingQuotes(value) : null); } remainder = ifStartsWithReturnRemainder("ceil(", function); if (remainder != null) { String value = StringHelper.beforeLast(remainder, ")"); return MathExpressionBuilder.ceilExpression( - ObjectHelper.isNotEmpty(value) ? StringHelper.removeQuotes(value) : null); + ObjectHelper.isNotEmpty(value) ? StringHelper.removeLeadingAndEndingQuotes(value) : null); } remainder = ifStartsWithReturnRemainder("sum(", function); if (remainder != null) { 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 05708f1528df..bfb7f8ec1f4b 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 @@ -19,6 +19,7 @@ package org.apache.camel.language.simple.functions; import org.apache.camel.CamelContext; import org.apache.camel.Expression; import org.apache.camel.language.simple.MiscExpressionBuilder; +import org.apache.camel.language.simple.SimpleFunctionHelper; import org.apache.camel.language.simple.types.SimpleParserException; import org.apache.camel.spi.SimpleLanguageFunctionFactory; import org.apache.camel.util.ObjectHelper; @@ -47,7 +48,7 @@ public final class MiscFunctionFactory implements SimpleLanguageFunctionFactory String exp = null; String value = StringHelper.beforeLast(remainder, ")"); if (ObjectHelper.isNotEmpty(value)) { - exp = StringHelper.removeQuotes(value); + exp = StringHelper.removeLeadingAndEndingQuotes(value); } return MiscExpressionBuilder.isEmptyExpression(exp); } @@ -57,7 +58,7 @@ public final class MiscFunctionFactory implements SimpleLanguageFunctionFactory String exp = null; String value = StringHelper.beforeLast(remainder, ")"); if (ObjectHelper.isNotEmpty(value)) { - exp = StringHelper.removeQuotes(value); + exp = StringHelper.removeLeadingAndEndingQuotes(value); } return MiscExpressionBuilder.isAlphaExpression(exp); } @@ -67,7 +68,7 @@ public final class MiscFunctionFactory implements SimpleLanguageFunctionFactory String exp = null; String value = StringHelper.beforeLast(remainder, ")"); if (ObjectHelper.isNotEmpty(value)) { - exp = StringHelper.removeQuotes(value); + exp = StringHelper.removeLeadingAndEndingQuotes(value); } return MiscExpressionBuilder.isAlphaNumericExpression(exp); } @@ -77,7 +78,7 @@ public final class MiscFunctionFactory implements SimpleLanguageFunctionFactory String exp = null; String value = StringHelper.beforeLast(remainder, ")"); if (ObjectHelper.isNotEmpty(value)) { - exp = StringHelper.removeQuotes(value); + exp = StringHelper.removeLeadingAndEndingQuotes(value); } return MiscExpressionBuilder.isNumericExpression(exp); } @@ -97,7 +98,7 @@ public final class MiscFunctionFactory implements SimpleLanguageFunctionFactory String exp = null; String value = StringHelper.beforeLast(remainder, ")"); if (ObjectHelper.isNotEmpty(value)) { - exp = StringHelper.removeQuotes(value); + exp = StringHelper.removeLeadingAndEndingQuotes(value); } return MiscExpressionBuilder.kindOfTypeExpression(exp); } @@ -109,18 +110,17 @@ public final class MiscFunctionFactory implements SimpleLanguageFunctionFactory String values = StringHelper.beforeLast(remainder, ")"); if (values == null || ObjectHelper.isEmpty(values)) { throw new SimpleParserException( - "Valid syntax: ${throwException(msg)} or ${throwException(type,msg)} was: " + function, index); + "Valid syntax: ${throwException(msg)} or ${throwException(msg,type)} was: " + function, index); } - if (values.contains(",")) { - String[] tokens = StringQuoteHelper.splitSafeQuote(values, ',', true, true); - if (tokens.length > 2) { - throw new SimpleParserException( - "Valid syntax: ${throwException(msg)} or ${throwException(type,msg)} was: " + function, index); - } - msg = StringHelper.removeQuotes(tokens[0]); - type = StringHelper.removeQuotes(tokens[1]); - } else { - msg = StringHelper.removeQuotes(values.trim()); + String[] tokens = StringQuoteHelper.splitSafeQuote(values, ',', true, true); + if (tokens.length > 2) { + 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) { + type = StringHelper.removeLeadingAndEndingQuotes(tokens[1]); } return MiscExpressionBuilder.throwExceptionExpression(msg, type); } @@ -135,14 +135,15 @@ public final class MiscFunctionFactory implements SimpleLanguageFunctionFactory if (tokens.length != 2) { throw new SimpleParserException("Valid syntax: ${assert(exp,msg)} was: " + function, index); } - return MiscExpressionBuilder.assertExpression(tokens[0], StringHelper.removeQuotes(tokens[1])); + return MiscExpressionBuilder.assertExpression(tokens[0], StringHelper.removeLeadingAndEndingQuotes(tokens[1])); } remainder = ifStartsWithReturnRemainder("convertTo(", function); if (remainder != null) { String exp = "${body}"; String type; - String values = StringHelper.before(remainder, ")"); + int end = SimpleFunctionHelper.indexOfClosingParenthesis(remainder); + String values = end >= 0 ? remainder.substring(0, end) : null; if (values == null || ObjectHelper.isEmpty(values)) { throw new SimpleParserException( "Valid syntax: ${convertTo(type)} or ${convertTo(exp,type)} was: " + function, index); @@ -153,12 +154,12 @@ public final class MiscFunctionFactory implements SimpleLanguageFunctionFactory throw new SimpleParserException( "Valid syntax: ${convertTo(type)} or ${convertTo(exp,type)} was: " + function, index); } - exp = StringHelper.removeQuotes(tokens[0]); - type = StringHelper.removeQuotes(tokens[1]); + exp = StringHelper.removeLeadingAndEndingQuotes(tokens[0]); + type = StringHelper.removeLeadingAndEndingQuotes(tokens[1]); } else { - type = StringHelper.removeQuotes(values.trim()); + type = StringHelper.removeLeadingAndEndingQuotes(values.trim()); } - remainder = StringHelper.after(remainder, ")"); + remainder = remainder.substring(end + 1); if (ObjectHelper.isNotEmpty(remainder)) { boolean invalid = OgnlHelper.isInvalidValidOgnlExpression(remainder); if (invalid) { @@ -172,7 +173,7 @@ public final class MiscFunctionFactory implements SimpleLanguageFunctionFactory } remainder = ifStartsWithReturnRemainder("messageHistory", function); - if (remainder != null) { + if (remainder != null && remainder.startsWith("(")) { boolean detailed; String values = StringHelper.between(remainder, "(", ")"); if (values == null || ObjectHelper.isEmpty(values)) { @@ -186,7 +187,8 @@ public final class MiscFunctionFactory implements SimpleLanguageFunctionFactory } remainder = ifStartsWithReturnRemainder("uuid", function); - if (remainder != null) { + // ${uuid(kind)}, but ${uuidv7} is not the uuid function + if (remainder != null && remainder.startsWith("(")) { String values = StringHelper.between(remainder, "(", ")"); return MiscExpressionBuilder.uuidExpression(values); } else if (ObjectHelper.equal(function, "uuid")) { @@ -200,15 +202,17 @@ public final class MiscFunctionFactory implements SimpleLanguageFunctionFactory throw new SimpleParserException( "Valid syntax: ${hash(value,algorithm)} or ${hash(value)} was: " + function, index); } - if (values.contains(",")) { - String[] tokens = values.split(",", 2); - if (tokens.length > 2) { - throw new SimpleParserException( - "Valid syntax: ${hash(value,algorithm)} or ${hash(value)} was: " + function, index); - } - return MiscExpressionBuilder.hashExpression(tokens[0].trim(), tokens[1].trim()); + // a comma inside quotes is part of the value + String[] tokens = StringQuoteHelper.splitSafeQuote(values, ',', true, true); + 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())); } else { - return MiscExpressionBuilder.hashExpression(values.trim(), "SHA-256"); + return MiscExpressionBuilder.hashExpression(value, "SHA-256"); } } @@ -238,7 +242,7 @@ public final class MiscFunctionFactory implements SimpleLanguageFunctionFactory "Valid syntax: ${iif(predicate,trueExpression,falseExpression)} was: " + function, index); } String[] tokens = StringQuoteHelper.splitSafeQuote(values, ',', true, true); - if (tokens.length > 3) { + if (tokens.length != 3) { throw new SimpleParserException( "Valid syntax: ${iif(predicate,trueExpression,falseExpression)} was: " + function, index); } @@ -251,7 +255,7 @@ public final class MiscFunctionFactory implements SimpleLanguageFunctionFactory if (ObjectHelper.isEmpty(value)) { throw new SimpleParserException("Valid syntax: ${load(name)} but was: " + function, index); } - return MiscExpressionBuilder.loadExpression(StringHelper.removeQuotes(value)); + return MiscExpressionBuilder.loadExpression(StringHelper.removeLeadingAndEndingQuotes(value)); } return null; 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 b19fdad17973..e32ed2227139 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 @@ -50,9 +50,9 @@ public final class StringFunctionFactory implements SimpleLanguageFunctionFactor "Valid syntax: ${replace(from,to)} or ${replace(from,to,expression)} was: " + function, index); } String[] tokens = StringQuoteHelper.splitSafeQuote(values, ',', false); - if (tokens.length > 3) { + if (tokens.length < 2 || tokens.length > 3) { throw new SimpleParserException( - "Valid syntax: ${replace(from,to,expression)} was: " + function, index); + "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]); @@ -196,7 +196,7 @@ public final class StringFunctionFactory implements SimpleLanguageFunctionFactor String exp = null; String value = StringHelper.beforeLast(remainder, ")"); if (ObjectHelper.isNotEmpty(value)) { - exp = StringHelper.removeQuotes(value); + exp = StringHelper.removeLeadingAndEndingQuotes(value); } return StringExpressionBuilder.trimExpression(exp); } @@ -216,7 +216,7 @@ public final class StringFunctionFactory implements SimpleLanguageFunctionFactor String exp = null; String value = StringHelper.beforeLast(remainder, ")"); if (ObjectHelper.isNotEmpty(value)) { - exp = StringHelper.removeQuotes(value); + exp = StringHelper.removeLeadingAndEndingQuotes(value); } return StringExpressionBuilder.capitalizeExpression(exp); } @@ -237,10 +237,10 @@ public final class StringFunctionFactory implements SimpleLanguageFunctionFactor throw new SimpleParserException( "Valid syntax: ${pad(exp,len)} or ${pad(exp,len,separator)} was: " + function, index); } - exp = StringHelper.removeQuotes(tokens[0]); - len = StringHelper.removeQuotes(tokens[1]); + exp = StringHelper.removeLeadingAndEndingQuotes(tokens[0]); + len = StringHelper.removeLeadingAndEndingQuotes(tokens[1]); if (tokens.length == 3) { - separator = StringHelper.removeQuotes(tokens[2]); + separator = StringHelper.removeLeadingAndEndingQuotes(tokens[2]); } return StringExpressionBuilder.padExpression(exp, len, separator); } @@ -257,21 +257,22 @@ public final class StringFunctionFactory implements SimpleLanguageFunctionFactor + function, index); } - if (values.contains(",")) { - String[] tokens = StringQuoteHelper.splitSafeQuote(values, ',', true, true); - if (tokens.length > 3) { - throw new SimpleParserException( - "Valid syntax: ${concat(exp)} or ${concat(exp,exp)} or ${concat(exp,exp,separator)} was: " - + function, - index); - } - exp1 = StringHelper.removeQuotes(tokens[0]); - exp2 = StringHelper.removeQuotes(tokens[1]); + // a comma inside quotes is part of the value, such as ${concat('Hello, ')} + String[] tokens = StringQuoteHelper.splitSafeQuote(values, ',', true, true); + if (tokens.length > 3) { + 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]); + exp2 = StringHelper.removeLeadingAndEndingQuotes(tokens[1]); if (tokens.length == 3) { - separator = StringHelper.removeQuotes(tokens[2]); + separator = StringHelper.removeLeadingAndEndingQuotes(tokens[2]); } } else { - exp2 = StringHelper.removeQuotes(values.trim()); + exp2 = StringHelper.removeLeadingAndEndingQuotes(tokens[0]); } return StringExpressionBuilder.concatExpression(exp1, exp2, separator); } @@ -281,7 +282,7 @@ public final class StringFunctionFactory implements SimpleLanguageFunctionFactor String exp = null; String value = StringHelper.beforeLast(remainder, ")"); if (ObjectHelper.isNotEmpty(value)) { - exp = StringHelper.removeQuotes(value); + exp = StringHelper.removeLeadingAndEndingQuotes(value); } return StringExpressionBuilder.quoteExpression(exp); } @@ -291,7 +292,7 @@ public final class StringFunctionFactory implements SimpleLanguageFunctionFactor String exp = null; String value = StringHelper.beforeLast(remainder, ")"); if (ObjectHelper.isNotEmpty(value)) { - exp = StringHelper.removeQuotes(value); + exp = StringHelper.removeLeadingAndEndingQuotes(value); } return StringExpressionBuilder.safeQuoteExpression(exp); } @@ -301,7 +302,7 @@ public final class StringFunctionFactory implements SimpleLanguageFunctionFactor String exp = null; String value = StringHelper.beforeLast(remainder, ")"); if (ObjectHelper.isNotEmpty(value)) { - exp = StringHelper.removeQuotes(value); + exp = StringHelper.removeLeadingAndEndingQuotes(value); } return StringExpressionBuilder.unquoteExpression(exp); } @@ -311,7 +312,7 @@ public final class StringFunctionFactory implements SimpleLanguageFunctionFactor String exp = null; String value = StringHelper.beforeLast(remainder, ")"); if (ObjectHelper.isNotEmpty(value)) { - exp = StringHelper.removeQuotes(value); + exp = StringHelper.removeLeadingAndEndingQuotes(value); } return StringExpressionBuilder.uppercaseExpression(exp); } @@ -321,7 +322,7 @@ public final class StringFunctionFactory implements SimpleLanguageFunctionFactor String exp = null; String value = StringHelper.beforeLast(remainder, ")"); if (ObjectHelper.isNotEmpty(value)) { - exp = StringHelper.removeQuotes(value); + exp = StringHelper.removeLeadingAndEndingQuotes(value); } return StringExpressionBuilder.lowercaseExpression(exp); } @@ -331,7 +332,7 @@ public final class StringFunctionFactory implements SimpleLanguageFunctionFactor String exp = null; String value = StringHelper.beforeLast(remainder, ")"); if (ObjectHelper.isNotEmpty(value)) { - exp = StringHelper.removeQuotes(value); + exp = StringHelper.removeLeadingAndEndingQuotes(value); } return StringExpressionBuilder.lengthExpression(exp); } @@ -341,7 +342,7 @@ public final class StringFunctionFactory implements SimpleLanguageFunctionFactor String exp = null; String value = StringHelper.beforeLast(remainder, ")"); if (ObjectHelper.isNotEmpty(value)) { - exp = StringHelper.removeQuotes(value); + exp = StringHelper.removeLeadingAndEndingQuotes(value); } return StringExpressionBuilder.sizeExpression(exp); } @@ -364,7 +365,7 @@ public final class StringFunctionFactory implements SimpleLanguageFunctionFactor index); } if (ObjectHelper.isNotEmpty(exp)) { - exp = StringHelper.removeQuotes(exp.trim()); + exp = StringHelper.removeLeadingAndEndingQuotes(exp.trim()); } else { exp = null; } @@ -376,7 +377,7 @@ public final class StringFunctionFactory implements SimpleLanguageFunctionFactor String exp = null; String value = StringHelper.beforeLast(remainder, ")"); if (ObjectHelper.isNotEmpty(value)) { - exp = StringHelper.removeQuotes(value); + exp = StringHelper.removeLeadingAndEndingQuotes(value); } return StringExpressionBuilder.normalizeWhitespaceExpression(exp); } diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/VariableFunctionFactory.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/VariableFunctionFactory.java index 4b5b7c2f20e2..4d9b8671e277 100644 --- a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/VariableFunctionFactory.java +++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/VariableFunctionFactory.java @@ -19,6 +19,7 @@ package org.apache.camel.language.simple.functions; import org.apache.camel.CamelContext; import org.apache.camel.Expression; import org.apache.camel.language.simple.OgnlExpressionBuilder; +import org.apache.camel.language.simple.SimpleFunctionHelper; import org.apache.camel.language.simple.types.SimpleParserException; import org.apache.camel.spi.SimpleLanguageFunctionFactory; import org.apache.camel.support.builder.ExpressionBuilder; @@ -40,13 +41,14 @@ public final class VariableFunctionFactory implements SimpleLanguageFunctionFact // variableAs String remainder = ifStartsWithReturnRemainder("variableAs(", function); if (remainder != null) { - String keyAndType = StringHelper.before(remainder, ")"); + int end = SimpleFunctionHelper.indexOfClosingParenthesis(remainder); + String keyAndType = end >= 0 ? remainder.substring(0, end) : null; if (keyAndType == null) { throw new SimpleParserException("Valid syntax: ${variableAs(key, type)} was: " + function, index); } String key = StringHelper.before(keyAndType, ","); String type = StringHelper.after(keyAndType, ","); - remainder = StringHelper.after(remainder, ")"); + remainder = remainder.substring(end + 1); if (ObjectHelper.isEmpty(key) || ObjectHelper.isEmpty(type) || ObjectHelper.isNotEmpty(remainder)) { throw new SimpleParserException("Valid syntax: ${variableAs(key, type)} was: " + function, index); } @@ -71,6 +73,11 @@ public final class VariableFunctionFactory implements SimpleLanguageFunctionFact } if (remainder.startsWith("[") && remainder.endsWith("]")) { remainder = remainder.substring(1, remainder.length() - 1); + String unquoted = StringHelper.removeLeadingAndEndingQuotes(remainder); + if (!unquoted.equals(remainder)) { + // a quoted key such as ['a.b'] is the name, not an OGNL expression + return ExpressionBuilder.variableExpression(unquoted); + } } String key = StringHelper.removeLeadingAndEndingQuotes(remainder); diff --git a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleFunctionArgumentsTest.java b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleFunctionArgumentsTest.java new file mode 100644 index 000000000000..c14b50aa6a8a --- /dev/null +++ b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleFunctionArgumentsTest.java @@ -0,0 +1,117 @@ +/* + * 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; + +import org.apache.camel.LanguageTestSupport; +import org.apache.camel.language.simple.types.SimpleIllegalSyntaxException; +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; + +/** + * CAMEL-24967: how the function names are matched and the arguments are split. + */ +public class SimpleFunctionArgumentsTest extends LanguageTestSupport { + + @Override + protected String getLanguageName() { + return "simple"; + } + + @Test + public void testQuotesInsideANestedFunctionAreKept() { + exchange.getMessage().setBody("a,b,c"); + assertExpression("${size(${body.split(',')})}", 3); + } + + @Test + public void testConvertToWithParenthesesInTheExpression() { + exchange.getMessage().setHeader("foo", " 42 "); + assertExpression("${convertTo(${header.foo.trim()},Integer)}", 42); + } + + @Test + public void testCommaInsideQuotes() { + exchange.getMessage().setBody("World"); + // concat(exp) appends to the message body + assertExpression("${concat('Hello, ')}", "WorldHello, "); + Exception e = assertThrows(Exception.class, + () -> context.resolveLanguage("simple").createExpression("${throwException('Order failed, retry later')}") + .evaluate(exchange, Object.class)); + assertEquals("Order failed, retry later", e.getMessage()); + String hash = context.resolveLanguage("simple").createExpression("${hash('a,b')}").evaluate(exchange, String.class); + exchange.getMessage().setBody("a,b"); + assertEquals(context.resolveLanguage("simple").createExpression("${hash(${body})}").evaluate(exchange, String.class), + hash); + } + + @Test + public void testTooFewArguments() { + exchange.getMessage().setHeader("foo", 1); + // with a nested function the arguments are parsed when evaluated + Exception e = assertThrows(Exception.class, + () -> context.resolveLanguage("simple").createExpression("${iif(${header.foo} > 0,'yes')}") + .evaluate(exchange, Object.class)); + assertTrue(e.getMessage().contains("Valid syntax: ${iif(predicate,trueExpression,falseExpression)}"), e.getMessage()); + e = assertThrows(SimpleIllegalSyntaxException.class, + () -> context.resolveLanguage("simple").createExpression("${replace(a)}")); + assertTrue(e.getMessage().contains("Valid syntax: ${replace(from,to)}"), e.getMessage()); + } + + @Test + public void testBeanTypeWithPackage() { + assertExpression("${bean:type:java.lang.System.lineSeparator}", System.lineSeparator()); + assertExpression("${bean:type:org.apache.camel.language.simple.SimpleFunctionArgumentsTest$MyStatic.hello}", "Hi"); + } + + @Test + public void testQuotedKeyIsNotOgnl() { + exchange.getMessage().setHeader("a", "xyz"); + exchange.getMessage().setHeader("a.b", "yes"); + assertExpression("${header['a.b']}", "yes"); + exchange.getMessage().removeHeader("a.b"); + assertExpression("${header['a.b']}", null); + exchange.setVariable("x.y", "var"); + assertExpression("${variable['x.y']}", "var"); + exchange.setProperty("p.q", "prop"); + assertExpression("${exchangeProperty['p.q']}", "prop"); + } + + @Test + public void testNamesMustNotBeGluedToAPrefix() { + exchange.getMessage().setHeader("foo", "bar"); + assertThrows(SimpleIllegalSyntaxException.class, + () -> context.resolveLanguage("simple").createExpression("${headerfoo}")); + assertThrows(SimpleIllegalSyntaxException.class, + () -> context.resolveLanguage("simple").createExpression("${uuidv7}")); + assertThrows(SimpleIllegalSyntaxException.class, + () -> context.resolveLanguage("simple").createExpression("${exceptionInfo}")); + // the proper forms still work + assertExpression("${header.foo}", "bar"); + assertExpression("${header:foo}", "bar"); + assertExpression("${header[foo]}", "bar"); + assertExpression("${headers.foo}", "bar"); + } + + public static class MyStatic { + public static String hello() { + return "Hi"; + } + } +} 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 e693c5f6719b..67f3b96d87de 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 @@ -864,7 +864,7 @@ public class SimpleTest extends LanguageTestSupport { // exchange scoped assertExpression("${variableAs('cheese', 'String')}", "gauda"); assertExpression("${variableAs('foo', 'int')}", null); - assertExpression("${variableAA('bar', 'int')}", null); + assertExpression("${variableAs('bar', 'int')}", null); // global scoped assertExpression("${variableAs('global:cheese', 'String')}", "gorgonzola");
