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
