You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/01/30 12:11:41 UTC
(camel) 02/05: CAMEL-22899: camel-core - Simple language - Add custom function in init block via chains
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 <cl...@gmail.com>
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