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

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


The following commit(s) were added to refs/heads/main by this push:
     new 6ed7fef6e6b1 CAMEL-22899: camel-core - Add chain operator to simple 
language (#21107)
6ed7fef6e6b1 is described below

commit 6ed7fef6e6b12e40adb27e82d216f74f7a29a54d
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Jan 28 18:55:08 2026 +0100

    CAMEL-22899: camel-core - Add chain operator to simple language (#21107)
    
    * CAMEL-22899: camel-core - Add chain operator to simple language
---
 .../modules/languages/pages/simple-language.adoc   |  59 ++++++++
 .../camel/language/simple/BaseSimpleParser.java    |  64 ++++++++-
 .../language/simple/SimpleExpressionParser.java    |  43 +++++-
 .../language/simple/SimpleInitBlockParser.java     |   3 +-
 .../language/simple/SimplePredicateParser.java     |  37 +++++
 .../camel/language/simple/SimpleTokenizer.java     |  20 +--
 .../camel/language/simple/ast/ChainExpression.java | 155 +++++++++++++++++++++
 ...herOperatorType.java => ChainOperatorType.java} |  23 +--
 .../language/simple/types/OtherOperatorType.java   |  14 +-
 .../language/simple/types/SimpleTokenType.java     |   7 +
 .../camel/language/simple/types/TokenType.java     |   1 +
 .../camel/language/simple/SimpleOperatorTest.java  |  57 ++++++++
 12 files changed, 459 insertions(+), 24 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 aea31cef6a0d..6f1912d6c81d 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
@@ -772,6 +772,28 @@ The following numeric operators can be used:
 |`--` | To decrement a number by one. The left-hand side must be a function, 
otherwise parsed as literal.
 |====
 
+These operators are unary and requires to be attached directly next to a 
function:
+
+[source,text]
+----
+${function}OP
+----
+
+For example to increment the result of the function:
+
+[source,java]
+----
+simple("${header.myNumber}++ > 10");
+----
+
+And the same for decrement:
+
+[source,java]
+----
+simple("${header.myNumber}-- < 10");
+----
+
+
 === Other Operators
 
 The following other operators can be used:
@@ -781,8 +803,12 @@ The following other operators can be used:
 |Operator |Description
 |`? :` | The ternary operator evaluates a condition and returns a value based 
on the result. If the condition is true, the first value (after `?`) is 
returned; otherwise, the second value (after `:`) is returned. There must be 
spaces around both `?` and `:` operators. This is similar to the ternary 
operator in Java.
 |`?:` | The elvis operator returns the left-hand side if it has an effective 
Boolean value of true, otherwise it returns the right-hand side. This is useful 
for providing fallback values when an expression may evaluate to a value with 
an effective Boolean value of false (such as `null`, `false`, `0`, empty/blank 
string).
+|`~>` | The chain operator is used in the situations where multiple nested 
functions need to be applied to a value, while making it easy to read. The 
value on the left-hand-side is evaluated, and set as the new message body 
before evaluating the right-hand-side function. This concept is similar to the 
xref:eips:pipeline-eip.adoc[Pipeline EIP].
+|`?~>` | The null-safe chain operator, where the chain will not continue when 
a function returned `null`.
 |====
 
+==== Ternary Operator
+
 The syntax for the ternary operator is:
 
 [source,text]
@@ -804,6 +830,8 @@ Ternary operators can also be nested to handle multiple 
conditions:
 simple("${header.score >= 90 ? 'A' : ${header.score >= 80 ? 'B' : 'C'}}");
 ----
 
+==== Elvis Operator
+
 For example the following elvis operator will return the username header 
unless its null or empty, which
 then the default value of `Guest` is returned.
 
@@ -812,6 +840,37 @@ then the default value of `Guest` is returned.
 simple("${header.username} ?: 'Guest'");
 ----
 
+==== Chain Operator
+
+IMPORTANT: The chain operator only supports passing results via the message 
body. This may change in the future to allow more flexible syntax to
+specify which parameter to use as input in the next fuction.
+
+The syntax for the chain operator is:
+
+[source,text]
+----
+${leftValue} ~> ${rightValue}
+----
+
+And there can be as many chains:
+
+[source,text]
+----
+${leftValue} ~> ${midValue} ~> ${midValue} -> ${rightValue}
+----
+
+For example if the message body contains `Hello World` then the follow would 
return `WORLD`:
+
+[source,java]
+----
+simple("${substringAfter('Hello')} ~> ${trim()} ~> ${uppercase()}");
+----
+
+The _null safe_ variant (`?~>`) can be used to avoid `NullPointerException` if 
it's accepted to not continue the chain when any function returned `null`:
+
+However, many of the simple functions have NPE protection built-in, so this 
variant is only needed in special situations.
+
+
 === Boolean Operators
 
 And the following boolean operators can be used to group expressions:
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/BaseSimpleParser.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/BaseSimpleParser.java
index 2696696b1820..e821b04ec370 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/BaseSimpleParser.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/BaseSimpleParser.java
@@ -27,6 +27,7 @@ import org.apache.camel.Predicate;
 import org.apache.camel.language.simple.ast.Block;
 import org.apache.camel.language.simple.ast.BlockEnd;
 import org.apache.camel.language.simple.ast.BlockStart;
+import org.apache.camel.language.simple.ast.ChainExpression;
 import org.apache.camel.language.simple.ast.OtherExpression;
 import org.apache.camel.language.simple.ast.SimpleNode;
 import org.apache.camel.language.simple.ast.TernaryExpression;
@@ -217,7 +218,7 @@ public abstract class BaseSimpleParser {
     }
 
     /**
-     * Prepares other expressions.
+     * Prepares chain expressions.
      * <p/>
      * This process prepares the other expressions in the AST. This is done by 
linking the other operator with both the
      * right and left hand side nodes, to have the AST graph updated and 
prepared properly.
@@ -225,6 +226,67 @@ public abstract class BaseSimpleParser {
      * So when the AST node is later used to create the {@link Predicate}s to 
be used by Camel then the AST graph has a
      * linked and prepared graph of nodes which represent the input expression.
      */
+    protected void prepareChainExpression() {
+        Deque<SimpleNode> stack = new ArrayDeque<>();
+
+        SimpleNode left = null;
+        for (int i = 0; i < nodes.size(); i++) {
+            if (left == null) {
+                left = i > 0 ? nodes.get(i - 1) : null;
+            }
+            SimpleNode token = nodes.get(i);
+            SimpleNode right = i < nodes.size() - 1 ? nodes.get(i + 1) : null;
+
+            if (token instanceof ChainExpression chain) {
+                // remember the chain operator
+                String operator = chain.getOperator().toString();
+
+                if (left == null) {
+                    throw new SimpleParserException(
+                            "Chain operator " + operator + " has no left hand 
side token", token.getToken().getIndex());
+                }
+                if (right == null) {
+                    throw new SimpleParserException(
+                            "Chain operator " + operator + " has no right hand 
side token", token.getToken().getIndex());
+                }
+
+                if (left instanceof ChainExpression chainLeft) {
+                    // append to existing chain on right hand side
+                    chainLeft.acceptRightNode(right);
+                } else {
+                    if (!chain.acceptLeftNode(left)) {
+                        throw new SimpleParserException(
+                                "Chain operator " + operator + " does not 
support left hand side token " + left.getToken(),
+                                token.getToken().getIndex());
+                    }
+                    if (!chain.acceptRightNode(right)) {
+                        throw new SimpleParserException(
+                                "Chain operator " + operator + " does not 
support right hand side token " + right.getToken(),
+                                token.getToken().getIndex());
+                    }
+
+                    // pop previous as we need to replace it with this other 
operator
+                    stack.pop();
+                    stack.push(token);
+                    // this token is now the left for the next loop
+                    left = token;
+                }
+
+                // advantage after the right hand side
+                i++;
+            } else {
+                // clear left
+                left = null;
+                stack.push(token);
+            }
+        }
+
+        nodes.clear();
+        nodes.addAll(stack);
+        // must reverse as it was added from a stack that is reverse
+        Collections.reverse(nodes);
+    }
+
     protected void prepareOtherExpressions() {
         Deque<SimpleNode> stack = new ArrayDeque<>();
 
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 c46c556c86f0..d9c4ab3a7a9d 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
@@ -23,6 +23,7 @@ import java.util.concurrent.atomic.AtomicInteger;
 
 import org.apache.camel.CamelContext;
 import org.apache.camel.Expression;
+import org.apache.camel.language.simple.ast.ChainExpression;
 import org.apache.camel.language.simple.ast.InitBlockExpression;
 import org.apache.camel.language.simple.ast.LiteralExpression;
 import org.apache.camel.language.simple.ast.LiteralNode;
@@ -32,6 +33,7 @@ import 
org.apache.camel.language.simple.ast.SimpleFunctionStart;
 import org.apache.camel.language.simple.ast.SimpleNode;
 import org.apache.camel.language.simple.ast.TernaryExpression;
 import org.apache.camel.language.simple.ast.UnaryExpression;
+import org.apache.camel.language.simple.types.ChainOperatorType;
 import org.apache.camel.language.simple.types.OtherOperatorType;
 import org.apache.camel.language.simple.types.SimpleIllegalSyntaxException;
 import org.apache.camel.language.simple.types.SimpleParserException;
@@ -141,6 +143,7 @@ public class SimpleExpressionParser extends 
BaseSimpleParser {
             functionText();
             unaryOperator();
             ternaryOperator();
+            chainOperator();
             otherOperator();
             nextToken();
         }
@@ -159,6 +162,8 @@ public class SimpleExpressionParser extends 
BaseSimpleParser {
         prepareUnaryExpressions();
         // compact and stack ternary expressions
         prepareTernaryExpressions();
+        // compact and stack chain expressions
+        prepareChainExpression();
         // compact and stack other expressions
         prepareOtherExpressions();
 
@@ -207,13 +212,13 @@ public class SimpleExpressionParser extends 
BaseSimpleParser {
         // remove all ignored
         tokens.removeIf(t -> t.getType().isIgnore());
 
-        // white space should be removed before and after the other operator
+        // white space should be removed before and after the chain/other 
operator
         List<SimpleToken> toRemove = new ArrayList<>();
         for (int i = 1; i < tokens.size() - 1; i++) {
             SimpleToken prev = tokens.get(i - 1);
             SimpleToken cur = tokens.get(i);
             SimpleToken next = tokens.get(i + 1);
-            if (cur.getType().isOther() || cur.getType().isInit()) {
+            if (cur.getType().isOther() || cur.getType().isChain() || 
cur.getType().isInit()) {
                 if (prev.getType().isWhitespace()) {
                     toRemove.add(prev);
                 }
@@ -298,6 +303,8 @@ public class SimpleExpressionParser extends 
BaseSimpleParser {
             // there must be a start ternary already, to let this be an end 
ternary
             ternary.decrementAndGet();
             return new TernaryExpression(token);
+        } else if (token.getType().isChain()) {
+            return new ChainExpression(token);
         } else if (token.getType().isOther()) {
             return new OtherExpression(token);
         } else if (token.getType().isInit()) {
@@ -384,7 +391,7 @@ public class SimpleExpressionParser extends 
BaseSimpleParser {
     protected void templateText() {
         // for template, we accept anything but functions / ternary operator / 
other operator
         while (!token.getType().isFunctionStart() && 
!token.getType().isFunctionEnd() && !token.getType().isEol()
-                && !token.getType().isTernary() && !token.getType().isOther()) 
{
+                && !token.getType().isTernary() && !token.getType().isOther() 
&& !token.getType().isChain()) {
             nextToken();
         }
     }
@@ -439,6 +446,36 @@ public class SimpleExpressionParser extends 
BaseSimpleParser {
         return false;
     }
 
+    protected boolean chainOperator() {
+        if (accept(TokenType.chainOperator)) {
+            // remember the chain operator
+            ChainOperatorType operatorType = 
ChainOperatorType.asOperator(token.getText());
+
+            nextToken();
+            // there should be at least one whitespace after the operator
+            expectAndAcceptMore(TokenType.whiteSpace);
+
+            // then we expect either some quoted text, another function, or a 
numeric, boolean or null value
+            if (singleQuotedLiteralWithFunctionsText()
+                    || doubleQuotedLiteralWithFunctionsText()
+                    || functionText()
+                    || numericValue()
+                    || booleanValue()
+                    || nullValue()) {
+                // then after the right hand side value, there should be a 
whitespace if there is more tokens
+                nextToken();
+                if (!token.getType().isEol()) {
+                    expectAndAcceptMore(TokenType.whiteSpace);
+                }
+            } else {
+                throw new SimpleParserException(
+                        "Chain operator " + operatorType + " does not support 
token " + token, token.getIndex());
+            }
+            return true;
+        }
+        return false;
+    }
+
     protected boolean ternaryOperator() {
         if (accept(TokenType.ternaryOperator)) {
             nextToken();
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 2f3ca8caa03d..6e34eeb0b181 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
@@ -125,7 +125,8 @@ class SimpleInitBlockParser extends SimpleExpressionParser {
         getTokenizer().setAcceptInitTokens(true);
         while (!token.getType().isInitVariable() && !token.getType().isEol()) {
             // skip until we find init variable/function (this skips code 
comments)
-            nextToken(TokenType.functionStart, TokenType.unaryOperator, 
TokenType.otherOperator, TokenType.initVariable,
+            nextToken(TokenType.functionStart, TokenType.unaryOperator, 
TokenType.chainOperator, TokenType.otherOperator,
+                    TokenType.initVariable,
                     TokenType.eol);
         }
         if (accept(TokenType.initVariable)) {
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimplePredicateParser.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimplePredicateParser.java
index 2f6ba0ce7c77..0766704ed241 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimplePredicateParser.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimplePredicateParser.java
@@ -30,6 +30,7 @@ import org.apache.camel.Expression;
 import org.apache.camel.Predicate;
 import org.apache.camel.language.simple.ast.BinaryExpression;
 import org.apache.camel.language.simple.ast.BooleanExpression;
+import org.apache.camel.language.simple.ast.ChainExpression;
 import org.apache.camel.language.simple.ast.DoubleQuoteEnd;
 import org.apache.camel.language.simple.ast.DoubleQuoteStart;
 import org.apache.camel.language.simple.ast.LiteralExpression;
@@ -46,6 +47,7 @@ import org.apache.camel.language.simple.ast.SingleQuoteStart;
 import org.apache.camel.language.simple.ast.TernaryExpression;
 import org.apache.camel.language.simple.ast.UnaryExpression;
 import org.apache.camel.language.simple.types.BinaryOperatorType;
+import org.apache.camel.language.simple.types.ChainOperatorType;
 import org.apache.camel.language.simple.types.LogicalOperatorType;
 import org.apache.camel.language.simple.types.OtherOperatorType;
 import org.apache.camel.language.simple.types.SimpleIllegalSyntaxException;
@@ -150,6 +152,7 @@ public class SimplePredicateParser extends BaseSimpleParser 
{
                     && !unaryOperator()
                     && !binaryOperator()
                     && !ternaryOperator()
+                    && !chainOperator()
                     && !otherOperator()
                     && !logicalOperator()
                     && !isBooleanValue()
@@ -175,6 +178,8 @@ public class SimplePredicateParser extends BaseSimpleParser 
{
         prepareBlocks();
         // compact and stack unary expressions
         prepareUnaryExpressions();
+        // compact and stack chain expressions
+        prepareChainExpression();
         // compact and stack binary expressions
         prepareBinaryExpressions();
         // compact and stack ternary expressions
@@ -374,6 +379,8 @@ public class SimplePredicateParser extends BaseSimpleParser 
{
             return new TernaryExpression(token);
         } else if (token.getType().isOther()) {
             return new OtherExpression(token);
+        } else if (token.getType().isChain()) {
+            return new ChainExpression(token);
         } else if (token.getType().isLogical()) {
             return new LogicalExpression(token);
         } else if (token.getType().isNullValue()) {
@@ -824,6 +831,36 @@ public class SimplePredicateParser extends 
BaseSimpleParser {
         return false;
     }
 
+    protected boolean chainOperator() {
+        if (accept(TokenType.chainOperator)) {
+            // remember the chain operator
+            ChainOperatorType operatorType = 
ChainOperatorType.asOperator(token.getText());
+
+            nextToken();
+            // there should be at least one whitespace after the operator
+            expectAndAcceptMore(TokenType.whiteSpace);
+
+            // then we expect either some quoted text, another function, or a 
numeric, boolean or null value
+            if (singleQuotedLiteralWithFunctionsText()
+                    || doubleQuotedLiteralWithFunctionsText()
+                    || functionText()
+                    || numericValue()
+                    || booleanValue()
+                    || nullValue()) {
+                // then after the right hand side value, there should be a 
whitespace if there is more tokens
+                nextToken();
+                if (!token.getType().isEol()) {
+                    expectAndAcceptMore(TokenType.whiteSpace);
+                }
+            } else {
+                throw new SimpleParserException(
+                        "Chain operator " + operatorType + " does not support 
token " + token, token.getIndex());
+            }
+            return true;
+        }
+        return false;
+    }
+
     protected boolean logicalOperator() {
         if (accept(TokenType.logicalOperator)) {
             // remember the logical operator
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleTokenizer.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleTokenizer.java
index 6a55dfafe83c..0335a87a5464 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleTokenizer.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleTokenizer.java
@@ -27,7 +27,7 @@ import org.apache.camel.util.ObjectHelper;
 public class SimpleTokenizer {
 
     // keep this number in sync with tokens list
-    private static final int NUMBER_OF_TOKENS = 52;
+    private static final int NUMBER_OF_TOKENS = 54;
 
     private static final SimpleTokenType[] KNOWN_TOKENS = new 
SimpleTokenType[NUMBER_OF_TOKENS];
 
@@ -88,22 +88,26 @@ public class SimpleTokenizer {
         // other operators
         KNOWN_TOKENS[44] = new SimpleTokenType(TokenType.otherOperator, "?:");
 
+        // chain operators
+        KNOWN_TOKENS[45] = new SimpleTokenType(TokenType.chainOperator, "~>");
+        KNOWN_TOKENS[46] = new SimpleTokenType(TokenType.chainOperator, "?~>");
+
         // unary operators
-        KNOWN_TOKENS[45] = new SimpleTokenType(TokenType.unaryOperator, "++");
-        KNOWN_TOKENS[46] = new SimpleTokenType(TokenType.unaryOperator, "--");
+        KNOWN_TOKENS[47] = new SimpleTokenType(TokenType.unaryOperator, "++");
+        KNOWN_TOKENS[48] = new SimpleTokenType(TokenType.unaryOperator, "--");
 
         // logical operators
-        KNOWN_TOKENS[47] = new SimpleTokenType(TokenType.logicalOperator, 
"&&");
-        KNOWN_TOKENS[48] = new SimpleTokenType(TokenType.logicalOperator, 
"||");
+        KNOWN_TOKENS[49] = new SimpleTokenType(TokenType.logicalOperator, 
"&&");
+        KNOWN_TOKENS[50] = new SimpleTokenType(TokenType.logicalOperator, 
"||");
 
         // ternary operators
-        KNOWN_TOKENS[49] = new SimpleTokenType(TokenType.ternaryOperator, "?");
-        KNOWN_TOKENS[50] = new SimpleTokenType(TokenType.ternaryOperator, ":");
+        KNOWN_TOKENS[51] = new SimpleTokenType(TokenType.ternaryOperator, "?");
+        KNOWN_TOKENS[52] = new SimpleTokenType(TokenType.ternaryOperator, ":");
 
         //binary operator
         // it is added as the last item because unary -- has the priority
         // if unary not found it is highly possible - operator is run into.
-        KNOWN_TOKENS[51] = new SimpleTokenType(TokenType.minusValue, "-");
+        KNOWN_TOKENS[53] = new SimpleTokenType(TokenType.minusValue, "-");
     }
 
     /**
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
new file mode 100644
index 000000000000..955843346854
--- /dev/null
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/ChainExpression.java
@@ -0,0 +1,155 @@
+/*
+ * 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.ast;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.StringJoiner;
+
+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.types.ChainOperatorType;
+import org.apache.camel.language.simple.types.SimpleParserException;
+import org.apache.camel.language.simple.types.SimpleToken;
+import org.apache.camel.util.ObjectHelper;
+import org.apache.camel.util.StringHelper;
+
+/**
+ * Represents chain operator expression in the AST.
+ */
+public class ChainExpression extends BaseSimpleNode {
+
+    private final ChainOperatorType operator;
+    private SimpleNode left;
+    private final List<SimpleNode> right = new ArrayList<>();
+
+    public ChainExpression(SimpleToken token) {
+        super(token);
+        operator = ChainOperatorType.asOperator(token.getText());
+    }
+
+    @Override
+    public String toString() {
+        StringJoiner sj = new StringJoiner(" " + token.getText() + " ");
+        for (SimpleNode rn : right) {
+            sj.add(rn.toString());
+        }
+        return left + " " + token.getText() + " " + sj;
+    }
+
+    public boolean acceptLeftNode(SimpleNode lef) {
+        this.left = lef;
+        return true;
+    }
+
+    public boolean acceptRightNode(SimpleNode right) {
+        this.right.add(right);
+        return true;
+    }
+
+    public ChainOperatorType getOperator() {
+        return operator;
+    }
+
+    public SimpleNode getLeft() {
+        return left;
+    }
+
+    public List<SimpleNode> getRight() {
+        return right;
+    }
+
+    @Override
+    public Expression createExpression(CamelContext camelContext, String 
expression) {
+        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
+        final Expression leftExp = left.createExpression(camelContext, 
expression);
+
+        final List<Expression> rightExp = new ArrayList<>();
+        for (SimpleNode rn : right) {
+            if (rn instanceof LiteralExpression le) {
+                String text = le.getText();
+                String changed = 
StringHelper.removeLeadingAndEndingQuotes(text);
+                if (!changed.equals(text)) {
+                    le.replaceText(changed);
+                }
+            }
+            Expression exp = rn.createExpression(camelContext, expression);
+            rightExp.add(exp);
+        }
+
+        if (operator == ChainOperatorType.CHAIN || operator == 
ChainOperatorType.CHAIN_NULL_SAFE) {
+            boolean nullSafe = operator == ChainOperatorType.CHAIN_NULL_SAFE;
+            return createChainExpression(camelContext, leftExp, rightExp, 
nullSafe);
+        }
+
+        throw new SimpleParserException("Unknown chain operator " + operator, 
token.getIndex());
+    }
+
+    private Expression createChainExpression(
+            final CamelContext camelContext, final Expression leftExp, final 
List<Expression> rightExp, boolean nullSafe) {
+        return new Expression() {
+            @Override
+            public <T> T evaluate(Exchange exchange, Class<T> type) {
+                // left is input to right
+                Object value = leftExp.evaluate(exchange, Object.class);
+                if (value == null && nullSafe) {
+                    return null; // break out
+                }
+
+                Object originalBody = exchange.getMessage().getBody();
+                try {
+                    exchange.getMessage().setBody(value);
+                    for (Expression exp : rightExp) {
+                        value = exp.evaluate(exchange, Object.class);
+                        if (value == null && nullSafe) {
+                            return null; // break out
+                        }
+                        exchange.getMessage().setBody(value);
+                    }
+                    if (value == null) {
+                        return null;
+                    }
+                    return camelContext.getTypeConverter().convertTo(type, 
exchange, value);
+                } finally {
+                    // restore original body to avoid side effects
+                    exchange.getMessage().setBody(originalBody);
+                }
+            }
+
+            @Override
+            public String toString() {
+                return left + " " + token.getText() + " " + right;
+            }
+        };
+    }
+
+    @Override
+    public String createCode(CamelContext camelContext, String expression) 
throws SimpleParserException {
+        return BaseSimpleParser.CODE_START + doCreateCode(camelContext, 
expression) + BaseSimpleParser.CODE_END;
+    }
+
+    private String doCreateCode(CamelContext camelContext, String expression) 
throws SimpleParserException {
+        throw new SimpleParserException("Chain operator " + operator + " not 
supported in csimple", token.getIndex());
+    }
+
+}
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/types/OtherOperatorType.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/types/ChainOperatorType.java
similarity index 68%
copy from 
core/camel-core-languages/src/main/java/org/apache/camel/language/simple/types/OtherOperatorType.java
copy to 
core/camel-core-languages/src/main/java/org/apache/camel/language/simple/types/ChainOperatorType.java
index 7ae09a14339d..65f25f379e5c 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/types/OtherOperatorType.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/types/ChainOperatorType.java
@@ -17,22 +17,27 @@
 package org.apache.camel.language.simple.types;
 
 /**
- * Types of other operators supported
+ * Types of chain operators supported
  */
-public enum OtherOperatorType {
+public enum ChainOperatorType {
 
-    ELVIS;
+    CHAIN,
+    CHAIN_NULL_SAFE;
 
-    public static OtherOperatorType asOperator(String text) {
-        if ("?:".equals(text)) {
-            return ELVIS;
+    public static ChainOperatorType asOperator(String text) {
+        if ("~>".equals(text)) {
+            return CHAIN;
+        } else if ("?~>".equals(text)) {
+            return CHAIN_NULL_SAFE;
         }
         throw new IllegalArgumentException("Operator not supported: " + text);
     }
 
-    public static String getOperatorText(OtherOperatorType operator) {
-        if (operator == ELVIS) {
-            return "?:";
+    public static String getOperatorText(ChainOperatorType operator) {
+        if (operator == CHAIN) {
+            return "~>";
+        } else if (operator == CHAIN_NULL_SAFE) {
+            return "?~>";
         }
         return "";
     }
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/types/OtherOperatorType.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/types/OtherOperatorType.java
index 7ae09a14339d..8440d3a6a797 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/types/OtherOperatorType.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/types/OtherOperatorType.java
@@ -21,17 +21,27 @@ package org.apache.camel.language.simple.types;
  */
 public enum OtherOperatorType {
 
+    CHAIN,
+    CHAIN_NULL_SAFE,
     ELVIS;
 
     public static OtherOperatorType asOperator(String text) {
-        if ("?:".equals(text)) {
+        if ("~>".equals(text)) {
+            return CHAIN;
+        } else if ("?~>".equals(text)) {
+            return CHAIN_NULL_SAFE;
+        } else if ("?:".equals(text)) {
             return ELVIS;
         }
         throw new IllegalArgumentException("Operator not supported: " + text);
     }
 
     public static String getOperatorText(OtherOperatorType operator) {
-        if (operator == ELVIS) {
+        if (operator == CHAIN) {
+            return "~>";
+        } else if (operator == CHAIN_NULL_SAFE) {
+            return "?~>";
+        } else if (operator == ELVIS) {
             return "?:";
         }
         return "";
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/types/SimpleTokenType.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/types/SimpleTokenType.java
index 6a773d8816f6..f4f6ff780954 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/types/SimpleTokenType.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/types/SimpleTokenType.java
@@ -194,6 +194,13 @@ public final class SimpleTokenType {
         return type == TokenType.ternaryOperator && ":".equals(value);
     }
 
+    /**
+     * Whether the type is chain operator
+     */
+    public boolean isChain() {
+        return type == TokenType.chainOperator;
+    }
+
     @Override
     public String toString() {
         return value;
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/types/TokenType.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/types/TokenType.java
index 0cebb226f734..99bc5dbba43b 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/types/TokenType.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/types/TokenType.java
@@ -40,6 +40,7 @@ public enum TokenType {
     initOperator,
     initVariable,
     ternaryOperator,
+    chainOperator,
     eol
 
 }
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleOperatorTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleOperatorTest.java
index 2fe58b9b5b8c..5effdce5234d 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleOperatorTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleOperatorTest.java
@@ -18,12 +18,15 @@ package org.apache.camel.language.simple;
 
 import org.apache.camel.Exchange;
 import org.apache.camel.LanguageTestSupport;
+import org.apache.camel.Predicate;
 import org.apache.camel.language.simple.types.SimpleIllegalSyntaxException;
 import org.apache.camel.spi.Registry;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 public class SimpleOperatorTest extends LanguageTestSupport {
 
@@ -894,6 +897,60 @@ public class SimpleOperatorTest extends 
LanguageTestSupport {
                 ">>> Message received from WebSocket Client : Hello World");
     }
 
+    @Test
+    public void testChain() {
+        exchange.getIn().setBody(null);
+        assertExpression("${substringAfter('Hello')} ~> ${trim()} ~> 
${uppercase()}", null);
+        exchange.getIn().setBody("   Hello    World   ");
+        assertExpression("${substringAfter('Hello')} ~> ${trim()} ~> 
${uppercase()}", "WORLD");
+        // run 2nd time give same result
+        assertExpression("${substringAfter('Hello')} ~> ${trim()} ~> 
${uppercase()}", "WORLD");
+
+        exchange.getIn().setBody("  Hello    World   ");
+        Predicate predicate = context.resolveLanguage("simple")
+                .createPredicate("${substringAfter('Hello')} ~> ${trim()} ~> 
${uppercase()} == 'WORLD'");
+        boolean matches = predicate.matches(exchange);
+        assertTrue(matches);
+        // run 2nd time give same result
+        matches = predicate.matches(exchange);
+        assertTrue(matches);
+
+        exchange.getIn().setBody("  Hello    Camel   ");
+        predicate = context.resolveLanguage("simple")
+                .createPredicate("${substringAfter('Hello')} ~> ${trim()} ~> 
${uppercase()} == 'WORLD'");
+        matches = predicate.matches(exchange);
+        assertFalse(matches);
+    }
+
+    @Test
+    public void testChainNullSafe() {
+        exchange.getIn().setBody(null);
+        assertExpression("${substringAfter('Hello')} ?~> ${collate(2)} ~> 
${uppercase()}", null);
+        // run 2nd time give same result
+        assertExpression("${substringAfter('Hello')} ?~> ${collate(2)} ~> 
${uppercase()}", null);
+
+        Predicate predicate = context.resolveLanguage("simple")
+                .createPredicate("${substringAfter('Hello')} ?~> ${collate(2)} 
~> ${uppercase()}");
+        boolean matches = predicate.matches(exchange);
+        assertFalse(matches);
+        // run 2nd time give same result
+        matches = predicate.matches(exchange);
+        assertFalse(matches);
+
+        exchange.getIn().setBody("Hello World,Hello Camel");
+        assertExpression("${substringAfter('Hello')} ?~> ${collate(2)} ~> 
${kindOfType()}", "object");
+        // run 2nd time give same result
+        assertExpression("${substringAfter('Hello')} ?~> ${collate(2)} ~> 
${kindOfType()}", "object");
+
+        predicate = context.resolveLanguage("simple")
+                .createPredicate("${substringAfter('Hello')} ?~> ${collate(2)} 
~> ${kindOfType()} == 'object'");
+        matches = predicate.matches(exchange);
+        assertTrue(matches);
+        // run 2nd time give same result
+        matches = predicate.matches(exchange);
+        assertTrue(matches);
+    }
+
     @Override
     protected String getLanguageName() {
         return "simple";


Reply via email to