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 ");


Reply via email to