This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch sf in repository https://gitbox.apache.org/repos/asf/camel.git
commit 001bbcabe26abc2d44aaa4cad0385d6c485575e9 Author: Claus Ibsen <[email protected]> AuthorDate: Sun Feb 1 15:24:49 2026 +0100 camel-core - Simple language chain operator supports calling functions from functions --- .../org/apache/camel/catalog/languages/simple.json | 13 ++- .../language/csimple/joor/OriginalSimpleTest.java | 21 ++++ .../apache/camel/spi/SimpleFunctionRegistry.java | 7 ++ .../impl/engine/DefaultSimpleFunctionRegistry.java | 6 + .../org/apache/camel/language/simple/simple.json | 13 ++- .../modules/languages/pages/simple-language.adoc | 31 ++++- .../camel/language/csimple/CSimpleHelper.java | 13 +++ .../camel/language/simple/SimpleConstants.java | 4 + .../language/simple/SimpleExpressionBuilder.java | 43 ++++++- .../camel/language/simple/ast/ChainExpression.java | 92 +++++++++------ .../simple/ast/SimpleFunctionExpression.java | 53 +++++++++ .../language/simple/SimpleInitBlockChainTest.java | 71 ----------- .../simple/SimpleInitBlockFunctionTest.java | 130 +++++++++++++++++++++ .../apache/camel/language/simple/SimpleTest.java | 21 ++++ 14 files changed, 395 insertions(+), 123 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/simple.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/simple.json index aef6018b1262..4c7afb123c23 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/simple.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/simple.json @@ -128,11 +128,12 @@ "trim(exp)": { "index": 101, "kind": "function", "displayName": "Trim", "group": "function", "label": "function", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The trim function trims the message body (or expression) by removing all leading and trailing white spaces.", "ognl": false, "suffix": "}" }, "type:name.field": { "index": 102, "kind": "function", "displayName": "Java Field Value", "group": "function", "label": "function", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "To refer to a type or field by its classname. To refer to a field, you can append .FIELD_NAME. For example, you can refer to the constant field from Exchange as: `org.apache.camel.Exchange.FILE_NAME`", [...] "kindOfType(exp)": { "index": 103, "kind": "function", "displayName": "Kind of Type", "group": "function", "label": "function", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "What kind of type is the value (null,number,string,boolean,array,object)", "ognl": false, "suffix": "}" }, - "uppercase(exp)": { "index": 104, "kind": "function", "displayName": "Uppercase", "group": "function", "label": "function", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Uppercases the message body (or expression)", "ognl": false, "suffix": "}" }, - "uuid(type)": { "index": 105, "kind": "function", "displayName": "Generate UUID", "group": "function", "label": "function", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a UUID using the Camel `UuidGenerator`. You can choose between `default`, `classic`, `short` and `simple` as the type. If no type is given, the default is used. It is also possible to use a custom `Uuid [...] - "variable.name": { "index": 106, "kind": "function", "displayName": "Variable", "group": "function", "label": "function", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The variable with the given name", "ognl": true, "suffix": "}" }, - "variableAs(key,type)": { "index": 107, "kind": "function", "displayName": "Variable As", "group": "function", "label": "function", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the variable to the given type (classname).", "ognl": false, "suffix": "}" }, - "variables": { "index": 108, "kind": "function", "displayName": "Variables", "group": "function", "label": "function", "required": false, "javaType": "java.util.Map", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns all the variables from the current Exchange in a Map", "ognl": false, "suffix": "}" }, - "xpath(input,exp)": { "index": 109, "kind": "function", "displayName": "XPath", "group": "function", "label": "function", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "When working with XML data, then this allows using the XPath language, for example, to extract data from the message body (in XML format). This requires having camel-xpath JAR on the classpath. For input (optiona [...] + "unquote(exp)": { "index": 104, "kind": "function", "displayName": "Unquote", "group": "function", "label": "function", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the message body (or expression) with any leading\/ending quotes removed", "ognl": false, "suffix": "}" }, + "uppercase(exp)": { "index": 105, "kind": "function", "displayName": "Uppercase", "group": "function", "label": "function", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Uppercases the message body (or expression)", "ognl": false, "suffix": "}" }, + "uuid(type)": { "index": 106, "kind": "function", "displayName": "Generate UUID", "group": "function", "label": "function", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a UUID using the Camel `UuidGenerator`. You can choose between `default`, `classic`, `short` and `simple` as the type. If no type is given, the default is used. It is also possible to use a custom `Uuid [...] + "variable.name": { "index": 107, "kind": "function", "displayName": "Variable", "group": "function", "label": "function", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The variable with the given name", "ognl": true, "suffix": "}" }, + "variableAs(key,type)": { "index": 108, "kind": "function", "displayName": "Variable As", "group": "function", "label": "function", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the variable to the given type (classname).", "ognl": false, "suffix": "}" }, + "variables": { "index": 109, "kind": "function", "displayName": "Variables", "group": "function", "label": "function", "required": false, "javaType": "java.util.Map", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns all the variables from the current Exchange in a Map", "ognl": false, "suffix": "}" }, + "xpath(input,exp)": { "index": 110, "kind": "function", "displayName": "XPath", "group": "function", "label": "function", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "When working with XML data, then this allows using the XPath language, for example, to extract data from the message body (in XML format). This requires having camel-xpath JAR on the classpath. For input (optiona [...] } } diff --git a/components/camel-csimple-joor/src/test/java/org/apache/camel/language/csimple/joor/OriginalSimpleTest.java b/components/camel-csimple-joor/src/test/java/org/apache/camel/language/csimple/joor/OriginalSimpleTest.java index 6e7d6a2ebd12..180913cc4b57 100644 --- a/components/camel-csimple-joor/src/test/java/org/apache/camel/language/csimple/joor/OriginalSimpleTest.java +++ b/components/camel-csimple-joor/src/test/java/org/apache/camel/language/csimple/joor/OriginalSimpleTest.java @@ -2682,6 +2682,27 @@ public class OriginalSimpleTest extends LanguageTestSupport { assertEquals("\"{A=1, B=2}\"", s); } + @Test + public void testUnquote() { + exchange.getMessage().setBody("\"Hello World\""); + + Expression expression = context.resolveLanguage("csimple").createExpression("${unquote()}"); + String s = expression.evaluate(exchange, String.class); + assertEquals("Hello World", s); + + expression = context.resolveLanguage("csimple").createExpression("${unquote(${body})}"); + s = expression.evaluate(exchange, String.class); + assertEquals("Hello World", s); + + expression = context.resolveLanguage("csimple").createExpression("${unquote('\"Hi\"')}"); + s = expression.evaluate(exchange, String.class); + assertEquals("Hi", s); + + expression = context.resolveLanguage("csimple").createExpression("${unquote('Hi')}"); + s = expression.evaluate(exchange, String.class); + assertEquals("Hi", s); + } + @Test public void testTrim() { exchange.getMessage().setBody(" Hello World "); diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/SimpleFunctionRegistry.java b/core/camel-api/src/main/java/org/apache/camel/spi/SimpleFunctionRegistry.java index d0bcc1d061db..1eb3f9fa66af 100644 --- a/core/camel-api/src/main/java/org/apache/camel/spi/SimpleFunctionRegistry.java +++ b/core/camel-api/src/main/java/org/apache/camel/spi/SimpleFunctionRegistry.java @@ -16,6 +16,8 @@ */ package org.apache.camel.spi; +import java.util.Set; + import org.apache.camel.Expression; /** @@ -46,6 +48,11 @@ public interface SimpleFunctionRegistry { */ Expression getFunction(String name); + /** + * Returns a set with all the function names + */ + Set<String> getFunctionNames(); + /** * Number of custom functions */ diff --git a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultSimpleFunctionRegistry.java b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultSimpleFunctionRegistry.java index 33030f5471de..d7d414081325 100644 --- a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultSimpleFunctionRegistry.java +++ b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultSimpleFunctionRegistry.java @@ -17,6 +17,7 @@ package org.apache.camel.impl.engine; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.apache.camel.CamelContext; @@ -53,6 +54,11 @@ public class DefaultSimpleFunctionRegistry extends ServiceSupport implements Sim return functions.get(name); } + @Override + public Set<String> getFunctionNames() { + return functions.keySet(); + } + @Override protected void doStop() throws Exception { super.doShutdown(); diff --git a/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json b/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json index aef6018b1262..4c7afb123c23 100644 --- a/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json +++ b/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json @@ -128,11 +128,12 @@ "trim(exp)": { "index": 101, "kind": "function", "displayName": "Trim", "group": "function", "label": "function", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The trim function trims the message body (or expression) by removing all leading and trailing white spaces.", "ognl": false, "suffix": "}" }, "type:name.field": { "index": 102, "kind": "function", "displayName": "Java Field Value", "group": "function", "label": "function", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "To refer to a type or field by its classname. To refer to a field, you can append .FIELD_NAME. For example, you can refer to the constant field from Exchange as: `org.apache.camel.Exchange.FILE_NAME`", [...] "kindOfType(exp)": { "index": 103, "kind": "function", "displayName": "Kind of Type", "group": "function", "label": "function", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "What kind of type is the value (null,number,string,boolean,array,object)", "ognl": false, "suffix": "}" }, - "uppercase(exp)": { "index": 104, "kind": "function", "displayName": "Uppercase", "group": "function", "label": "function", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Uppercases the message body (or expression)", "ognl": false, "suffix": "}" }, - "uuid(type)": { "index": 105, "kind": "function", "displayName": "Generate UUID", "group": "function", "label": "function", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a UUID using the Camel `UuidGenerator`. You can choose between `default`, `classic`, `short` and `simple` as the type. If no type is given, the default is used. It is also possible to use a custom `Uuid [...] - "variable.name": { "index": 106, "kind": "function", "displayName": "Variable", "group": "function", "label": "function", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The variable with the given name", "ognl": true, "suffix": "}" }, - "variableAs(key,type)": { "index": 107, "kind": "function", "displayName": "Variable As", "group": "function", "label": "function", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the variable to the given type (classname).", "ognl": false, "suffix": "}" }, - "variables": { "index": 108, "kind": "function", "displayName": "Variables", "group": "function", "label": "function", "required": false, "javaType": "java.util.Map", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns all the variables from the current Exchange in a Map", "ognl": false, "suffix": "}" }, - "xpath(input,exp)": { "index": 109, "kind": "function", "displayName": "XPath", "group": "function", "label": "function", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "When working with XML data, then this allows using the XPath language, for example, to extract data from the message body (in XML format). This requires having camel-xpath JAR on the classpath. For input (optiona [...] + "unquote(exp)": { "index": 104, "kind": "function", "displayName": "Unquote", "group": "function", "label": "function", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns the message body (or expression) with any leading\/ending quotes removed", "ognl": false, "suffix": "}" }, + "uppercase(exp)": { "index": 105, "kind": "function", "displayName": "Uppercase", "group": "function", "label": "function", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Uppercases the message body (or expression)", "ognl": false, "suffix": "}" }, + "uuid(type)": { "index": 106, "kind": "function", "displayName": "Generate UUID", "group": "function", "label": "function", "required": false, "javaType": "String", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns a UUID using the Camel `UuidGenerator`. You can choose between `default`, `classic`, `short` and `simple` as the type. If no type is given, the default is used. It is also possible to use a custom `Uuid [...] + "variable.name": { "index": 107, "kind": "function", "displayName": "Variable", "group": "function", "label": "function", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The variable with the given name", "ognl": true, "suffix": "}" }, + "variableAs(key,type)": { "index": 108, "kind": "function", "displayName": "Variable As", "group": "function", "label": "function", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the variable to the given type (classname).", "ognl": false, "suffix": "}" }, + "variables": { "index": 109, "kind": "function", "displayName": "Variables", "group": "function", "label": "function", "required": false, "javaType": "java.util.Map", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns all the variables from the current Exchange in a Map", "ognl": false, "suffix": "}" }, + "xpath(input,exp)": { "index": 110, "kind": "function", "displayName": "XPath", "group": "function", "label": "function", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "When working with XML data, then this allows using the XPath language, for example, to extract data from the message body (in XML format). This requires having camel-xpath JAR on the classpath. For input (optiona [...] } } diff --git a/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-language.adoc b/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-language.adoc index 49f4f33d2e5f..4a37b75a3a00 100644 --- a/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-language.adoc +++ b/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-language.adoc @@ -473,6 +473,8 @@ The generator supports different kinds `default`, `classic`, `short`, `simple` a |`substringBetween(exp,after,before` | `String` | Returns a substring of the expression that are between before and after. Returns `null` if nothing comes between. |`trim()` | `String` | The trim function trims the message body by removing all leading and trailing white spaces. |`trim(exp)` | `String` | The trim function trims the expression by removing all leading and trailing white spaces. +|`unquote()` | `String` | Returns the message body with any leading/ending quotes removed +|`unquote(exp)` | `String` | Returns the expression with any leading/ending quotes removed |`quote()` | `String` | Returns the message body as a double-quoted string |`quote(exp)` | `String` | Returns the expression as a double-quoted string |`uppercase()` | `String` | Uppercases the message body @@ -1299,8 +1301,6 @@ $init{ }init$ ---- -NOTE: You cannot declare custom functions that call other function functions. This is currently not supported. - The function will by default use the message body as the input, such that the following: [source,java] @@ -1319,6 +1319,33 @@ simple("Incoming message: ${cleanUp(' Please clean this text for me ')} Here the parameter is a fixed `String` but you can also pass in other type of value such as a number, boolean, or even a nested function. +==== Calling custom functions from custom functions + +It is also possible to reuse custom functions from other functions using `${function(name)}`, such shown: + +[source,text] +---- +$init{ + $cleanUp ~:= ${trim()} ~> ${normalizeWhitespace()} ~> ${uppercase()} + $count ~:= ${function(cleanUp)} ~> ${split(' ')} ~> ${size()} +}init$ +---- + +Here the `$count` is declared as a function which calls the cleanUp function and then counts the words via split and size functions. + +Instead of using `${function(name)}` you can also same syntax as the built-in functions, as follows: + +[source,text] +---- +$init{ + $cleanUp ~:= ${trim()} ~> ${normalizeWhitespace()} ~> ${uppercase()} + $count ~:= ${cleanUp()} ~> ${split(' ')} ~> ${size()} +}init$ +---- + +Notice how `${cleanUp()}` despite being a custom function is using the same syntax as all the built-in functions that are being used, for example: + `${trim()}`. + == OGNL Expression Support diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/csimple/CSimpleHelper.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/csimple/CSimpleHelper.java index b156f359575e..3c38b5eeb3c8 100644 --- a/core/camel-core-languages/src/main/java/org/apache/camel/language/csimple/CSimpleHelper.java +++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/csimple/CSimpleHelper.java @@ -1075,6 +1075,19 @@ public final class CSimpleHelper { return value; } + public static String unquote(Exchange exchange, Object value) { + String body; + if (value != null) { + body = exchange.getContext().getTypeConverter().tryConvertTo(String.class, exchange, value); + } else { + body = exchange.getMessage().getBody(String.class); + } + if (body != null) { + body = StringHelper.removeLeadingAndEndingQuotes(body); + } + return body; + } + public static String trim(Exchange exchange, Object value) { String body; if (value != null) { diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleConstants.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleConstants.java index f41ae793f8a0..0f53f09b3f22 100644 --- a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleConstants.java +++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleConstants.java @@ -421,6 +421,10 @@ public final class SimpleConstants { label = "function", javaType = "Object", displayName = "Kind of Type") public static final String KIND_OF_TYPE = "kindOfType(exp)"; + @Metadata(description = "Returns the message body (or expression) with any leading/ending quotes removed", + label = "function", javaType = "String") + public static final String UNQUOTE = "unquote(exp)"; + @Metadata(description = "Uppercases the message body (or expression)", label = "function", javaType = "String", displayName = "Uppercase") public static final String UPPERCASE = "uppercase(exp)"; diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleExpressionBuilder.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleExpressionBuilder.java index 9d3cb2eff36a..3279a159d18f 100644 --- a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleExpressionBuilder.java +++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleExpressionBuilder.java @@ -686,14 +686,13 @@ public final class SimpleExpressionBuilder { } else if (value instanceof CharSequence) { return "string"; } else if (ObjectHelper.isPrimitiveArrayType(type) || value instanceof Collection - || value instanceof Map<?, ?>) { + || value instanceof Map<?, ?>) { return "array"; } else { return "object"; } } - @Override public String toString() { if (expression != null) { @@ -746,6 +745,46 @@ public final class SimpleExpressionBuilder { }; } + /** + * Un quotes the given expressions (uses message body if expression is null) + */ + public static Expression unquoteExpression(final String expression) { + return new ExpressionAdapter() { + private Expression exp; + + @Override + public void init(CamelContext context) { + if (expression != null) { + exp = context.resolveLanguage("simple").createExpression(expression); + exp.init(context); + } + } + + @Override + public Object evaluate(Exchange exchange) { + String value; + if (exp != null) { + value = exp.evaluate(exchange, String.class); + } else { + value = exchange.getMessage().getBody(String.class); + } + if (value != null) { + value = StringHelper.removeLeadingAndEndingQuotes(value); + } + return value; + } + + @Override + public String toString() { + if (expression != null) { + return "unquote(" + expression + ")"; + } else { + return "unquote()"; + } + } + }; + } + /** * Trims the given expressions (uses message body if expression is null) */ diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/ChainExpression.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/ChainExpression.java index fcbcb64d6977..01e752c063a5 100644 --- a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/ChainExpression.java +++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/ChainExpression.java @@ -19,6 +19,7 @@ package org.apache.camel.language.simple.ast; import java.util.ArrayList; import java.util.List; import java.util.StringJoiner; +import java.util.concurrent.atomic.AtomicBoolean; import org.apache.camel.CamelContext; import org.apache.camel.Exchange; @@ -28,6 +29,7 @@ import org.apache.camel.language.simple.types.ChainOperatorType; import org.apache.camel.language.simple.types.SimpleParserException; import org.apache.camel.language.simple.types.SimpleToken; import org.apache.camel.support.ExpressionAdapter; +import org.apache.camel.support.PluginHelper; import org.apache.camel.util.ObjectHelper; import org.apache.camel.util.StringHelper; @@ -83,47 +85,22 @@ public class ChainExpression extends BaseSimpleNode { ObjectHelper.notNull(left, "left node", this); ObjectHelper.notNull(right, "right node", this); - // the expression parser does not parse literal text into single/double quote tokens - // so we need to manually remove leading quotes from the literal text when using the other operators + // prepare nodes as the chain may refer to other functions + AtomicBoolean param = new AtomicBoolean(); + left = prepareChainNode(camelContext, left, param); final Expression leftExp = left.createExpression(camelContext, expression); final List<Expression> rightExp = new ArrayList<>(); for (SimpleNode rn : right) { - boolean param = false; - if (rn instanceof LiteralExpression le) { - String text = le.getText(); - String changed = StringHelper.removeLeadingAndEndingQuotes(text); - if (changed.contains("$param")) { - changed = changed.replace("$param", "${variable." + CHAIN_VARIABLE + "}"); - param = true; - } - if (!changed.equals(text)) { - le.replaceText(changed); - } - } else if (rn instanceof SimpleFunctionStart sf) { - for (var child : sf.getBlock().getChildren()) { - if (child instanceof LiteralExpression le) { - String text = le.getText(); - String changed = StringHelper.removeLeadingAndEndingQuotes(text); - if (changed.contains("$param")) { - changed = changed.replace("$param", "${variable." + CHAIN_VARIABLE + "}"); - param = true; - } - if (!changed.equals(text)) { - le.replaceText(changed); - } - } - } - } - final Expression exp = rn.createExpression(camelContext, expression); - Expression target = exp; - if (param) { - target = new ExpressionAdapter() { + rn = prepareChainNode(camelContext, rn, param); + final Expression right = rn.createExpression(camelContext, expression); + if (param.get()) { + rightExp.add(new ExpressionAdapter() { @Override public Object evaluate(Exchange exchange) { exchange.setVariable(CHAIN_VARIABLE, exchange.getMessage().getBody()); try { - return exp.evaluate(exchange, Object.class); + return right.evaluate(exchange, Object.class); } finally { exchange.removeVariable(CHAIN_VARIABLE); } @@ -131,11 +108,12 @@ public class ChainExpression extends BaseSimpleNode { @Override public String toString() { - return exp.toString(); + return right.toString(); } - }; + }); + } else { + rightExp.add(right); } - rightExp.add(target); } if (operator == ChainOperatorType.CHAIN || operator == ChainOperatorType.CHAIN_NULL_SAFE) { @@ -146,6 +124,48 @@ public class ChainExpression extends BaseSimpleNode { throw new SimpleParserException("Unknown chain operator " + operator, token.getIndex()); } + private SimpleNode prepareChainNode(CamelContext camelContext, SimpleNode node, AtomicBoolean param) { + param.set(false); + if (node instanceof LiteralExpression le) { + String text = le.getText(); + if (text.startsWith("$")) { + // this may be a function + String key = text.substring(1); + key = StringHelper.before(key, "(", key); + if (PluginHelper.getSimpleFunctionRegistry(camelContext).getFunctionNames().contains(key)) { + String changed = text.replace("$" + key + "()", "function(" + key + ")"); + if (changed.equals(text)) { + changed = text.replace("$" + key + "(", "function(" + key); + } + le.replaceText(changed); + + SimpleFunctionStart sfs = new SimpleFunctionStart(le.getToken(), null, true); + sfs.getBlock().addChild(le); + node = sfs; + } + } + text = le.getText(); + if (text.contains("$param")) { + text = text.replace("$param", "${variable." + CHAIN_VARIABLE + "}"); + param.set(true); + le.replaceText(text); + } + return node; + } else if (node instanceof SimpleFunctionStart sf) { + for (var child : sf.getBlock().getChildren()) { + if (child instanceof LiteralExpression le) { + String text = le.getText(); + if (text.contains("$param")) { + text = text.replace("$param", "${variable." + CHAIN_VARIABLE + "}"); + param.set(true); + le.replaceText(text); + } + } + } + } + return node; + } + private Expression createChainExpression( final CamelContext camelContext, final Expression leftExp, final List<Expression> rightExp, boolean nullSafe) { return new Expression() { diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/SimpleFunctionExpression.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/SimpleFunctionExpression.java index 52f2cbf77279..b53b4a9f6899 100644 --- a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/SimpleFunctionExpression.java +++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/SimpleFunctionExpression.java @@ -35,6 +35,7 @@ import org.apache.camel.language.simple.types.SimpleParserException; import org.apache.camel.language.simple.types.SimpleToken; import org.apache.camel.spi.Language; import org.apache.camel.spi.SimpleLanguageFunctionFactory; +import org.apache.camel.support.PluginHelper; import org.apache.camel.support.ResolverHelper; import org.apache.camel.support.builder.ExpressionBuilder; import org.apache.camel.util.ObjectHelper; @@ -370,6 +371,21 @@ public class SimpleFunctionExpression extends LiteralExpression { } } + // it may be a custom function + String name = StringHelper.before(function, "(", function); + if (PluginHelper.getSimpleFunctionRegistry(camelContext).getFunctionNames().contains(name)) { + String after = StringHelper.after(function, "("); + if (after.equals(")")) { + function = "function(" + name + ")"; + } else { + function = "function(" + name + "," + after; + } + Expression exp = createSimpleCustomFunction(camelContext, function, strict); + if (exp != null) { + return exp; + } + } + if (strict) { throw new SimpleParserException("Unknown function: " + function, token.getIndex()); } else { @@ -1214,6 +1230,16 @@ public class SimpleFunctionExpression extends LiteralExpression { } return SimpleExpressionBuilder.safeQuoteExpression(exp); } + // unquote function + remainder = ifStartsWithReturnRemainder("unquote(", function); + if (remainder != null) { + String exp = null; + String value = StringHelper.beforeLast(remainder, ")"); + if (ObjectHelper.isNotEmpty(value)) { + exp = StringHelper.removeQuotes(value); + } + return SimpleExpressionBuilder.unquoteExpression(exp); + } // trim function remainder = ifStartsWithReturnRemainder("trim(", function); @@ -3011,6 +3037,33 @@ public class SimpleFunctionExpression extends LiteralExpression { } return "Object o = " + exp + ";\n return safeQuote(exchange, o);"; } + // unquote function + remainder = ifStartsWithReturnRemainder("unquote(", function); + if (remainder != null) { + String exp = null; + String values = StringHelper.beforeLast(remainder, ")"); + if (ObjectHelper.isNotEmpty(values)) { + String[] tokens = codeSplitSafe(values, ',', true, true); + if (tokens.length != 1) { + throw new SimpleParserException( + "Valid syntax: ${unquote(exp)} was: " + function, token.getIndex()); + } + // single quotes should be double quotes + String s = tokens[0]; + if (StringHelper.isSingleQuoted(s)) { + s = StringHelper.removeLeadingAndEndingQuotes(s); + // need to escape double quotes + s = s.replace("\"", "\\\""); + // and enclose in a string + s = StringQuoteHelper.doubleQuote(s); + } + exp = s; + } + if (ObjectHelper.isEmpty(exp)) { + exp = "null"; + } + return "Object o = " + exp + ";\n return unquote(exchange, o);"; + } // trim function remainder = ifStartsWithReturnRemainder("trim(", function); diff --git a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleInitBlockChainTest.java b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleInitBlockChainTest.java deleted file mode 100644 index 48011db0e916..000000000000 --- a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleInitBlockChainTest.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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.junit.jupiter.api.Test; - -public class SimpleInitBlockChainTest extends LanguageTestSupport { - - private static final String INIT = """ - $init{ - $clean ~:= ${trim()} ~> ${normalizeWhitespace()} ~> ${uppercase()} - }init$ - You said: $clean() - """; - - private static final String INIT2 = """ - $init{ - $clean ~:= ${trim()} ~> ${normalizeWhitespace()} ~> ${uppercase()} - $count ~:= ${trim()} ~> ${normalizeWhitespace()} ~> ${uppercase()} ~> ${split(' ')} ~> ${size()} - }init$ - You said: $clean() in $count() words - """; - - private static final String INIT3 = """ - $init{ - $clean ~:= ${trim()} ~> ${normalizeWhitespace()} ~> ${uppercase()} - }init$ - You said: ${clean(' Clean this text please ... ')} and then do something else - """; - - @Test - public void testInitBlockChain() throws Exception { - exchange.getMessage().setBody(" Hello big World "); - - assertExpression(exchange, INIT, "You said: HELLO BIG WORLD\n"); - } - - @Test - public void testInitBlockChain2() throws Exception { - exchange.getMessage().setBody(" Hello big World "); - - assertExpression(exchange, INIT2, "You said: HELLO BIG WORLD in 3 words\n"); - } - - @Test - public void testInitBlockChain3() throws Exception { - exchange.getMessage().setBody("Hello World"); - - assertExpression(exchange, INIT3, "You said: CLEAN THIS TEXT PLEASE ... and then do something else\n"); - } - - @Override - protected String getLanguageName() { - return "simple"; - } -} diff --git a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleInitBlockFunctionTest.java b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleInitBlockFunctionTest.java new file mode 100644 index 000000000000..2194182e2354 --- /dev/null +++ b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleInitBlockFunctionTest.java @@ -0,0 +1,130 @@ +/* + * 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.junit.jupiter.api.Test; + +public class SimpleInitBlockFunctionTest extends LanguageTestSupport { + + private static final String INIT1 = """ + $init{ + $clean ~:= ${trim()} ~> ${normalizeWhitespace()} ~> ${uppercase()} + }init$ + You said: $clean() + """; + + private static final String INIT2 = """ + $init{ + $clean ~:= ${trim()} ~> ${normalizeWhitespace()} ~> ${uppercase()} + $count ~:= ${function(clean)} ~> ${split(' ')} ~> ${size()} + }init$ + You said: $clean() in $count() words + """; + + private static final String INIT3 = """ + $init{ + $clean ~:= ${trim()} ~> ${normalizeWhitespace()} ~> ${uppercase()} + }init$ + You said: ${clean(' Clean this text please ... ')} and then do something else + """; + + private static final String INIT4 = """ + $init{ + $clean ~:= ${trim()} ~> ${normalizeWhitespace()} ~> ${uppercase()} + $count ~:= ${quote()} ~> ${function(clean)} ~> ${unquote()} ~> ${trim()} ~> ${split(' ')} ~> ${size()} + }init$ + You said: $clean() in $count() words + """; + + private static final String INIT5 = """ + $init{ + $clean ~:= ${trim()} ~> ${normalizeWhitespace()} ~> ${uppercase()} + $count ~:= $clean() ~> ${split(' ')} ~> ${size()} + }init$ + You said: $clean() in $count() words + """; + + private static final String INIT6 = """ + $init{ + $clean ~:= ${trim()} ~> ${normalizeWhitespace()} ~> ${uppercase()} + $count ~:= ${quote()} ~> ${clean()} ~> ${unquote()} ~> ${trim()} ~> ${split(' ')} ~> ${size()} + }init$ + You said: $clean() in $count() words + """; + + private static final String INIT7 = """ + $init{ + $clean ~:= ${trim()} ~> ${normalizeWhitespace()} ~> ${uppercase()} + }init$ + You said: ${clean()} + """; + + @Test + public void testInitBlockChain1() throws Exception { + exchange.getMessage().setBody(" Hello big World "); + + assertExpression(exchange, INIT1, "You said: HELLO BIG WORLD\n"); + } + + @Test + public void testInitBlockChain2() throws Exception { + exchange.getMessage().setBody(" Hello big World "); + + assertExpression(exchange, INIT2, "You said: HELLO BIG WORLD in 3 words\n"); + } + + @Test + public void testInitBlockChain3() throws Exception { + exchange.getMessage().setBody("Hello World"); + + assertExpression(exchange, INIT3, "You said: CLEAN THIS TEXT PLEASE ... and then do something else\n"); + } + + @Test + public void testInitBlockChain4() throws Exception { + exchange.getMessage().setBody(" Hello big World "); + + assertExpression(exchange, INIT4, "You said: HELLO BIG WORLD in 3 words\n"); + } + + @Test + public void testInitBlockChain5() throws Exception { + exchange.getMessage().setBody(" Hello big World "); + + assertExpression(exchange, INIT5, "You said: HELLO BIG WORLD in 3 words\n"); + } + + @Test + public void testInitBlockChain6() throws Exception { + exchange.getMessage().setBody(" Hello big World "); + + assertExpression(exchange, INIT6, "You said: HELLO BIG WORLD in 3 words\n"); + } + + @Test + public void testInitBlockChain7() throws Exception { + exchange.getMessage().setBody(" Hello big World "); + + assertExpression(exchange, INIT7, "You said: HELLO BIG WORLD\n"); + } + + @Override + protected String getLanguageName() { + return "simple"; + } +} 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 0e5e5d8fdd5a..ea9446f48d5e 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 @@ -3007,6 +3007,27 @@ public class SimpleTest extends LanguageTestSupport { assertEquals("\"{A=1, B=2}\"", s); } + @Test + public void testUnquote() { + exchange.getMessage().setBody("\"Hello World\""); + + Expression expression = context.resolveLanguage("simple").createExpression("${unquote()}"); + String s = expression.evaluate(exchange, String.class); + assertEquals("Hello World", s); + + expression = context.resolveLanguage("simple").createExpression("${unquote(${body})}"); + s = expression.evaluate(exchange, String.class); + assertEquals("Hello World", s); + + expression = context.resolveLanguage("simple").createExpression("${unquote('\"Hi\"')}"); + s = expression.evaluate(exchange, String.class); + assertEquals("Hi", s); + + expression = context.resolveLanguage("simple").createExpression("${unquote('Hi')}"); + s = expression.evaluate(exchange, String.class); + assertEquals("Hi", s); + } + @Test public void testTrim() { exchange.getMessage().setBody(" Hello World ");
