This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch ic
in repository https://gitbox.apache.org/repos/asf/camel.git

commit c8a094284588aa2b23711c01f51b3b8e915334c8
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Jan 30 11:01:03 2026 +0100

    CAMEL-22899: camel-core - Simple language - Add custom function in init 
block via chains
---
 .../modules/languages/pages/simple-language.adoc   | 28 ++++++++++++++
 .../simple/DefaultSimpleFunctionRegistry.java}     | 39 ++++++++++++--------
 .../language/simple/SimpleExpressionParser.java    | 24 ++++++------
 .../language/simple/SimpleFunctionRegistry.java}   | 43 +++++++++++++---------
 .../language/simple/SimpleInitBlockParser.java     | 13 ++++++-
 .../camel/language/simple/SimpleLanguage.java      |  9 +++++
 .../language/simple/ast/InitBlockExpression.java   | 24 ++++++++----
 .../simple/ast/SimpleFunctionExpression.java       | 24 ++++++++++++
 .../language/simple/SimpleInitBlockChainTest.java  | 17 ++++++++-
 9 files changed, 167 insertions(+), 54 deletions(-)

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 6cbc590598de..827e990751dd 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
@@ -1256,6 +1256,34 @@ Notice how we can refer to the variable (`$level`) in 
the log statement using th
 TIP: Instead of inlining the simple script in the route, you can externalize 
this to a source file such as `mymapping.txt`
 and then refer to the file such as `resource:classpath:mymapping.txt` where 
the file is located in the root classpath (can also be located in sub packages).
 
+=== Init Blocks with custom functions
+
+You can also declare custom functions using
+
+Inside the init block, it is a lso possible to define custom functions in the 
syntax `$nane ~:= ...` where you can then use simple language to declare
+the structure of the function. Then you can later use these custom functions 
in your simple language expressions.
+
+For example to create a function that can cleanup a `String` value:
+
+[source,text]
+----
+$init{
+  $cleanUp ~:= ${trim()} ~> ${normalizeWhitespace()} ~> ${uppercase()}
+}init$
+----
+
+The function will by default use the message body as the input, such that the 
following:
+
+[source,java]
+----
+simple("Incoming message: $cleanUp()");
+----
+
+Would then call the _clean_ function with the message body as the input, which 
will then be used for trim, normalize and upper-casing.
+
+NOTE: You cannot declare custom functions that call other function functions. 
This is currently not supported.
+
+
 == OGNL Expression Support
 
 The xref:simple-language.adoc[Simple] and xref:simple-language.adoc[Bean] 
languages support a _OGNL like_ notation for invoking methods (using 
reflection) in a fluent builder like style.
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleInitBlockChainTest.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/DefaultSimpleFunctionRegistry.java
similarity index 51%
copy from 
core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleInitBlockChainTest.java
copy to 
core/camel-core-languages/src/main/java/org/apache/camel/language/simple/DefaultSimpleFunctionRegistry.java
index a6a1c974ec25..f7227c936d85 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleInitBlockChainTest.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/DefaultSimpleFunctionRegistry.java
@@ -16,27 +16,36 @@
  */
 package org.apache.camel.language.simple;
 
-import org.apache.camel.LanguageTestSupport;
-import org.junit.jupiter.api.Test;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 
-public class SimpleInitBlockChainTest extends LanguageTestSupport {
+import org.apache.camel.Expression;
+import org.apache.camel.NonManagedService;
+import org.apache.camel.support.service.ServiceSupport;
 
-    private static final String INIT = """
-            $init{
-              $clean ~:= ${trim()} ~> ${normalizeWhitespace()} ~> 
${uppercase()}
-            }init$
-            You said: $clean()
-            """;
+public class DefaultSimpleFunctionRegistry extends ServiceSupport implements 
SimpleFunctionRegistry, NonManagedService {
 
-    @Test
-    public void testInitBlockChain() throws Exception {
-        exchange.getMessage().setBody("   Hello  big   World      ");
+    private final Map<String, Expression> functions = new 
ConcurrentHashMap<>();
 
-        assertExpression(exchange, INIT, "You said: HELLO BIG WORLD");
+    @Override
+    public void addFunction(String name, Expression expression) {
+        functions.put(name, expression);
+    }
+
+    @Override
+    public void removeFunction(String name) {
+        functions.remove(name);
     }
 
     @Override
-    protected String getLanguageName() {
-        return "simple";
+    public Expression getFunction(String name) {
+        return functions.get(name);
     }
+
+    @Override
+    protected void doStop() throws Exception {
+        super.doShutdown();
+        functions.clear();
+    }
+
 }
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleExpressionParser.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleExpressionParser.java
index d9c4ab3a7a9d..347ddb58f63a 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleExpressionParser.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleExpressionParser.java
@@ -83,17 +83,19 @@ public class SimpleExpressionParser extends 
BaseSimpleParser {
                         = new SimpleInitBlockParser(camelContext, expression, 
allowEscape, skipFileFunctions, cacheExpression);
                 // the init block should be parsed in predicate mode as that 
is needed to fully parse with all the operators and functions
                 init = initParser.parseExpression();
-                if (init != null) {
-                    String part = StringHelper.after(expression, 
SimpleInitBlockTokenizer.INIT_END);
-                    if (part.startsWith("\n")) {
-                        // skip newline after ending init block
-                        part = part.substring(1);
-                    }
-                    this.expression = part;
-                    // use $$key as local variable in the expression afterwards
-                    for (String key : initParser.getInitKeys()) {
-                        this.expression = this.expression.replace("$" + key, 
"${variable." + key + "}");
-                    }
+                String part = StringHelper.after(expression, 
SimpleInitBlockTokenizer.INIT_END);
+                if (part.startsWith("\n")) {
+                    // skip newline after ending init block
+                    part = part.substring(1);
+                }
+                this.expression = part;
+                // use $$key as local variable in the expression afterwards
+                for (String key : initParser.getInitKeys()) {
+                    this.expression = this.expression.replace("$" + key, 
"${variable." + key + "}");
+                }
+                // use $$key() as local function in the expression afterwards
+                for (String key : initParser.getInitFunctions()) {
+                    this.expression = this.expression.replace("$" + key + 
"()", "${function." + key + "}");
                 }
             }
 
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleInitBlockChainTest.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleFunctionRegistry.java
similarity index 56%
copy from 
core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleInitBlockChainTest.java
copy to 
core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleFunctionRegistry.java
index a6a1c974ec25..06c0f17644ff 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleInitBlockChainTest.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleFunctionRegistry.java
@@ -16,27 +16,34 @@
  */
 package org.apache.camel.language.simple;
 
-import org.apache.camel.LanguageTestSupport;
-import org.junit.jupiter.api.Test;
+import org.apache.camel.Expression;
 
-public class SimpleInitBlockChainTest extends LanguageTestSupport {
+/**
+ * Registry for custom simple functions.
+ */
+public interface SimpleFunctionRegistry {
 
-    private static final String INIT = """
-            $init{
-              $clean ~:= ${trim()} ~> ${normalizeWhitespace()} ~> 
${uppercase()}
-            }init$
-            You said: $clean()
-            """;
+    /**
+     * Add a function
+     *
+     * @param name       name of function
+     * @param expression the expression to use as the function
+     */
+    void addFunction(String name, Expression expression);
 
-    @Test
-    public void testInitBlockChain() throws Exception {
-        exchange.getMessage().setBody("   Hello  big   World      ");
+    /**
+     * Remove a function
+     *
+     * @param name name of function
+     */
+    void removeFunction(String name);
 
-        assertExpression(exchange, INIT, "You said: HELLO BIG WORLD");
-    }
+    /**
+     * Gets the function
+     *
+     * @param  name name of function
+     * @return      the function, or <tt>null</tt> if no function exists
+     */
+    Expression getFunction(String name);
 
-    @Override
-    protected String getLanguageName() {
-        return "simple";
-    }
 }
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleInitBlockParser.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleInitBlockParser.java
index ec686f0e5dd9..a627321d29d0 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleInitBlockParser.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleInitBlockParser.java
@@ -27,12 +27,14 @@ import org.apache.camel.Expression;
 import org.apache.camel.language.simple.ast.InitBlockExpression;
 import org.apache.camel.language.simple.ast.LiteralNode;
 import org.apache.camel.language.simple.ast.SimpleNode;
+import org.apache.camel.language.simple.types.InitOperatorType;
 import org.apache.camel.language.simple.types.TokenType;
 import org.apache.camel.util.StringHelper;
 
 class SimpleInitBlockParser extends SimpleExpressionParser {
 
     private final Set<String> initKeys = new LinkedHashSet<>();
+    private final Set<String> initFunctions = new LinkedHashSet<>();
 
     public SimpleInitBlockParser(CamelContext camelContext, String expression, 
boolean allowEscape, boolean skipFileFunctions,
                                  Map<String, Expression> cacheExpression) {
@@ -45,6 +47,10 @@ class SimpleInitBlockParser extends SimpleExpressionParser {
         return initKeys;
     }
 
+    public Set<String> getInitFunctions() {
+        return initFunctions;
+    }
+
     protected SimpleInitBlockTokenizer getTokenizer() {
         return (SimpleInitBlockTokenizer) tokenizer;
     }
@@ -83,6 +89,7 @@ class SimpleInitBlockParser extends SimpleExpressionParser {
     protected List<SimpleNode> parseInitTokens() {
         clear();
         initKeys.clear();
+        initFunctions.clear();
 
         // parse the expression using the following grammar
         // init statements are variables assigned to functions/operators
@@ -171,7 +178,11 @@ class SimpleInitBlockParser extends SimpleExpressionParser 
{
                     String key = StringHelper.after(ln.getText(), "$");
                     if (key != null) {
                         key = key.trim();
-                        initKeys.add(key);
+                        if 
(ie.getOperator().equals(InitOperatorType.CHAIN_ASSIGNMENT)) {
+                            initFunctions.add(key);
+                        } else {
+                            initKeys.add(key);
+                        }
                     }
                 }
             }
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleLanguage.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleLanguage.java
index b5bfc7e23cf8..88e8305b9f26 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleLanguage.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleLanguage.java
@@ -30,6 +30,7 @@ import org.apache.camel.support.LanguageSupport;
 import org.apache.camel.support.PredicateToExpressionAdapter;
 import org.apache.camel.support.ScriptHelper;
 import org.apache.camel.support.builder.ExpressionBuilder;
+import org.apache.camel.support.service.ServiceHelper;
 import org.apache.camel.util.ObjectHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -48,6 +49,8 @@ public class SimpleLanguage extends LanguageSupport 
implements StaticService {
     // a special prefix to avoid cache clash
     private static final String CACHE_KEY_PREFIX = "@SIMPLE@";
 
+    private SimpleFunctionRegistry registry;
+
     boolean allowEscape = true;
     boolean skipFileFunctions;
 
@@ -81,6 +84,10 @@ public class SimpleLanguage extends LanguageSupport 
implements StaticService {
                 LOG.debug("Simple language disabled predicate/expression 
cache");
             }
         }
+        registry = new DefaultSimpleFunctionRegistry();
+        ServiceHelper.initService(registry);
+        // register so we can obtain it during parsing
+        
getCamelContext().getCamelContextExtension().addContextPlugin(SimpleFunctionRegistry.class,
 registry);
     }
 
     @Override
@@ -88,6 +95,7 @@ public class SimpleLanguage extends LanguageSupport 
implements StaticService {
         if (getCamelContext() != null) {
             SIMPLE.setCamelContext(getCamelContext());
         }
+        ServiceHelper.startService(registry);
     }
 
     @Override
@@ -106,6 +114,7 @@ public class SimpleLanguage extends LanguageSupport 
implements StaticService {
             }
             cacheExpression.clear();
         }
+        ServiceHelper.stopService(registry);
     }
 
     @Override
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/InitBlockExpression.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/InitBlockExpression.java
index 684e78f50c85..a54b14a1bfb0 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/InitBlockExpression.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/InitBlockExpression.java
@@ -20,6 +20,7 @@ import org.apache.camel.CamelContext;
 import org.apache.camel.Exchange;
 import org.apache.camel.Expression;
 import org.apache.camel.language.simple.BaseSimpleParser;
+import org.apache.camel.language.simple.SimpleFunctionRegistry;
 import org.apache.camel.language.simple.types.InitOperatorType;
 import org.apache.camel.language.simple.types.SimpleParserException;
 import org.apache.camel.language.simple.types.SimpleToken;
@@ -72,9 +73,16 @@ public class InitBlockExpression extends BaseSimpleNode {
         ObjectHelper.notNull(left, "left node", this);
         ObjectHelper.notNull(right, "right node", this);
 
+        String key = null;
+        if (left instanceof LiteralExpression le) {
+            key = le.getText();
+            key = key.trim();
+            key = StringHelper.after(key, "$", key);
+        }
+        ObjectHelper.notNull(key, "left node should be a literal text 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
-        final Expression leftExp = left.createExpression(camelContext, 
expression);
         if (right instanceof LiteralExpression le) {
             String text = le.getText();
             String changed = StringHelper.removeLeadingAndEndingQuotes(text);
@@ -85,24 +93,24 @@ public class InitBlockExpression extends BaseSimpleNode {
         final Expression rightExp = right.createExpression(camelContext, 
expression);
 
         if (operator == InitOperatorType.ASSIGNMENT) {
-            return createAssignmentExpression(camelContext, leftExp, rightExp);
+            return createAssignmentExpression(camelContext, key, rightExp);
         } else if (operator == InitOperatorType.CHAIN_ASSIGNMENT) {
-            throw new UnsupportedOperationException("TODO: Implement ~:=");
+            SimpleFunctionRegistry registry
+                    = 
camelContext.getCamelContextExtension().getContextPlugin(SimpleFunctionRegistry.class);
+            registry.addFunction(key, rightExp);
+            return null;
         }
 
         throw new SimpleParserException("Unknown init operator " + operator, 
token.getIndex());
     }
 
     private Expression createAssignmentExpression(
-            final CamelContext camelContext, final Expression leftExp, final 
Expression rightExp) {
+            final CamelContext camelContext, final String key, final 
Expression rightExp) {
         return new Expression() {
             @Override
             public <T> T evaluate(Exchange exchange, Class<T> type) {
-                String name = leftExp.evaluate(exchange, String.class);
-                name = name.trim();
-                name = StringHelper.after(name, "$", name);
                 Object value = rightExp.evaluate(exchange, Object.class);
-                exchange.setVariable(name, value);
+                exchange.setVariable(key, value);
                 return null;
             }
 
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 1ee3138a0b15..d0ea95103ac3 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
@@ -30,6 +30,7 @@ import org.apache.camel.Expression;
 import org.apache.camel.RuntimeCamelException;
 import org.apache.camel.language.simple.BaseSimpleParser;
 import org.apache.camel.language.simple.SimpleExpressionBuilder;
+import org.apache.camel.language.simple.SimpleFunctionRegistry;
 import org.apache.camel.language.simple.SimplePredicateParser;
 import org.apache.camel.language.simple.types.SimpleParserException;
 import org.apache.camel.language.simple.types.SimpleToken;
@@ -103,6 +104,12 @@ public class SimpleFunctionExpression extends 
LiteralExpression {
         if (answer != null) {
             return answer;
         }
+        // custom functions
+        answer = createSimpleCustomFunction(camelContext, function, strict);
+        if (answer != null) {
+            return answer;
+        }
+
         // custom languages
         answer = createSimpleCustomLanguage(function, strict);
         if (answer != null) {
@@ -599,6 +606,23 @@ public class SimpleFunctionExpression extends 
LiteralExpression {
         return null;
     }
 
+    private Expression createSimpleCustomFunction(CamelContext camelContext, 
String function, boolean strict) {
+        String remainder = ifStartsWithReturnRemainder("function.", function);
+        if (remainder != null) {
+            String key = StringHelper.removeLeadingAndEndingQuotes(remainder);
+            key = key.trim();
+            SimpleFunctionRegistry registry
+                    = 
camelContext.getCamelContextExtension().getContextPlugin(SimpleFunctionRegistry.class);
+            Expression answer = registry.getFunction(key);
+            if (answer == null) {
+                throw new IllegalArgumentException("No custom simple function 
with name: " + key);
+            }
+            return answer;
+        }
+
+        return null;
+    }
+
     private Expression createSimpleCustomLanguage(String function, boolean 
strict) {
         // jq
         String remainder = ifStartsWithReturnRemainder("jq(", 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
index a6a1c974ec25..0eb4f44c59f0 100644
--- 
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
@@ -28,11 +28,26 @@ public class SimpleInitBlockChainTest extends 
LanguageTestSupport {
             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
+            """;
+
     @Test
     public void testInitBlockChain() throws Exception {
         exchange.getMessage().setBody("   Hello  big   World      ");
 
-        assertExpression(exchange, INIT, "You said: 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");
     }
 
     @Override

Reply via email to