This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch elvis in repository https://gitbox.apache.org/repos/asf/camel.git
commit d082d6aa08ae0578fb1464819dc29bbafe837116 Author: Claus Ibsen <[email protected]> AuthorDate: Sun Jan 18 13:13:05 2026 +0100 CAMEL-22870: camel-core - Add elvis ?: operator to simple language --- .../csimple/joor/OriginalSimpleOperatorTest.java | 33 +++++ .../modules/languages/pages/simple-language.adoc | 16 +++ .../camel/language/csimple/CSimpleHelper.java | 8 ++ .../camel/language/simple/BaseSimpleParser.java | 65 +++++++++ .../language/simple/SimpleExpressionParser.java | 119 ++++++++++++++++- .../language/simple/SimplePredicateParser.java | 38 ++++++ .../camel/language/simple/SimpleTokenizer.java | 29 ++++- .../language/simple/ast/LiteralExpression.java | 5 + .../camel/language/simple/ast/OtherExpression.java | 145 +++++++++++++++++++++ .../{TokenType.java => OtherOperatorType.java} | 39 +++--- .../language/simple/types/SimpleTokenType.java | 7 + .../camel/language/simple/types/TokenType.java | 1 + .../camel/language/simple/SimpleOperatorTest.java | 33 +++++ .../simple/SimplePredicateParserNodesTest.java | 22 ++++ 14 files changed, 533 insertions(+), 27 deletions(-) diff --git a/components/camel-csimple-joor/src/test/java/org/apache/camel/language/csimple/joor/OriginalSimpleOperatorTest.java b/components/camel-csimple-joor/src/test/java/org/apache/camel/language/csimple/joor/OriginalSimpleOperatorTest.java index 9e11a8ed20fd..bc0e81d3efd0 100644 --- a/components/camel-csimple-joor/src/test/java/org/apache/camel/language/csimple/joor/OriginalSimpleOperatorTest.java +++ b/components/camel-csimple-joor/src/test/java/org/apache/camel/language/csimple/joor/OriginalSimpleOperatorTest.java @@ -34,6 +34,7 @@ public class OriginalSimpleOperatorTest extends LanguageTestSupport { public void testValueWithSpace() { exchange.getIn().setBody("Hello Big World"); assertPredicate("${in.body} == 'Hello Big World'", true); + assertPredicate("${in.body} == ${body}", true); } @Test @@ -805,6 +806,38 @@ public class OriginalSimpleOperatorTest extends LanguageTestSupport { assertPredicate("${in.body} !endsWith 'Hi'", true); } + @Test + public void testElvis() { + exchange.getIn().setBody(false); + assertPredicate("${body} ?: 'true'", true); + assertPredicate("${body} ?: 'false'", false); + exchange.getIn().setBody("Hello"); + assertPredicate("${body} ?: 'false'", true); + exchange.getIn().setBody(0); + assertPredicate("${body} ?: 'true'", true); + assertPredicate("${body} ?: 'false'", false); + exchange.getIn().setBody(1); + assertPredicate("${body} ?: 'true'", true); + assertPredicate("${body} ?: 'false'", true); + + exchange.getIn().setBody(null); + assertExpression("${body} ?: 'World'", "World"); + exchange.getIn().setBody(""); + assertExpression("${body} ?: 'World'", "World"); + exchange.getIn().setBody("Hello"); + assertExpression("${body} ?: 'World'", "Hello"); + exchange.getIn().setBody(false); + assertExpression("${body} ?: 'World'", "World"); + exchange.getIn().setBody(true); + assertExpression("${body} ?: 'World'", true); + exchange.getIn().setHeader("myHeader", "Camel"); + assertExpression("${header.myHeader} ?: 'World'", "Camel"); + exchange.getIn().setBody(0); + assertExpression("${body} ?: 'World'", "World"); + exchange.getIn().setBody(1); + assertExpression("${body} ?: 'World'", 1); + } + @Override protected String getLanguageName() { return "csimple"; 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 85767faa8282..c50f5303b6a9 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 @@ -336,6 +336,22 @@ And the following unary operators can be used: |`--` | To decrement a number by one. The left-hand side must be a function, otherwise parsed as literal. |==== +And the following other operators can be used: + +[width="100%",cols="50%,50%",options="header",] +|==== +|Operator |Description +|`?:` | 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). +|==== + +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. + +[source,java] +---- +simple("${header.username} ?: 'Guest'"); +---- + And the following special symbols: [width="100%",cols="50%,50%",options="header",] diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/csimple/CSimpleHelper.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/csimple/CSimpleHelper.java index efb47b7c3cd1..2d5fe8a2178d 100644 --- a/core/camel-core-languages/src/main/java/org/apache/camel/language/csimple/CSimpleHelper.java +++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/csimple/CSimpleHelper.java @@ -979,4 +979,12 @@ public final class CSimpleHelper { return 0; } + public static Object elvis(Exchange exchange, Object left, Object right) { + if (left == null || Boolean.FALSE == left || ObjectHelper.isEmpty(left) || ObjectHelper.equal(0, left)) { + return right; + } else { + return left; + } + } + } 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 863714d5eb45..359f7a979ccb 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 @@ -23,9 +23,11 @@ import java.util.Deque; import java.util.List; import org.apache.camel.CamelContext; +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.OtherExpression; import org.apache.camel.language.simple.ast.SimpleNode; import org.apache.camel.language.simple.ast.UnaryExpression; import org.apache.camel.language.simple.types.SimpleParserException; @@ -205,6 +207,69 @@ public abstract class BaseSimpleParser { Collections.reverse(nodes); } + /** + * Prepares other 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. + * <p/> + * 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 prepareOtherExpressions() { + 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 OtherExpression other) { + // remember the other operator + String operator = other.getOperator().toString(); + + if (left == null) { + throw new SimpleParserException( + "Other operator " + operator + " has no left hand side token", token.getToken().getIndex()); + } + if (!other.acceptLeftNode(left)) { + throw new SimpleParserException( + "Other operator " + operator + " does not support left hand side token " + left.getToken(), + token.getToken().getIndex()); + } + if (right == null) { + throw new SimpleParserException( + "Other operator " + operator + " has no right hand side token", token.getToken().getIndex()); + } + if (!other.acceptRightNode(right)) { + throw new SimpleParserException( + "Other 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); + // advantage after the right hand side + i++; + // this token is now the left for the next loop + left = token; + } 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); + } + // -------------------------------------------------------------- // grammar // -------------------------------------------------------------- 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 33efc4c65c99..7ef8b8271dc7 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 @@ -25,10 +25,12 @@ import org.apache.camel.CamelContext; import org.apache.camel.Expression; import org.apache.camel.language.simple.ast.LiteralExpression; import org.apache.camel.language.simple.ast.LiteralNode; +import org.apache.camel.language.simple.ast.OtherExpression; import org.apache.camel.language.simple.ast.SimpleFunctionEnd; import org.apache.camel.language.simple.ast.SimpleFunctionStart; import org.apache.camel.language.simple.ast.SimpleNode; import org.apache.camel.language.simple.ast.UnaryExpression; +import org.apache.camel.language.simple.types.OtherOperatorType; import org.apache.camel.language.simple.types.SimpleIllegalSyntaxException; import org.apache.camel.language.simple.types.SimpleParserException; import org.apache.camel.language.simple.types.SimpleToken; @@ -97,10 +99,11 @@ public class SimpleExpressionParser extends BaseSimpleParser { // parse the expression using the following grammar nextToken(); while (!token.getType().isEol()) { - // an expression supports just template (eg text), functions, or unary operator + // an expression supports just template (eg text), functions, unary, or other operator templateText(); functionText(); unaryOperator(); + otherOperator(); nextToken(); } @@ -108,12 +111,16 @@ public class SimpleExpressionParser extends BaseSimpleParser { // into an ast, and then from the ast, to Camel expression(s). // hence why there are a number of tasks going on below to accomplish this + // remove any ignorable white space tokens + removeIgnorableWhiteSpaceTokens(); // turn the tokens into the ast model parseAndCreateAstModel(); // compact and stack blocks (eg function start/end) prepareBlocks(); // compact and stack unary operators prepareUnaryExpressions(); + // compact and stack other expressions + prepareOtherExpressions(); return nodes; } @@ -135,6 +142,34 @@ public class SimpleExpressionParser extends BaseSimpleParser { } } + /** + * Removes any ignorable whitespace tokens before and after other operators. + * <p/> + * During the initial parsing (input -> tokens), then there may be excessive whitespace tokens, which can safely be + * removed, which makes the succeeding parsing easier. + */ + private void removeIgnorableWhiteSpaceTokens() { + // white space should be removed before and after the 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()) { + if (prev.getType().isWhitespace()) { + toRemove.add(prev); + } + if (next.getType().isWhitespace()) { + toRemove.add(next); + } + } + } + + if (!toRemove.isEmpty()) { + tokens.removeAll(toRemove); + } + } + protected void parseAndCreateAstModel() { // we loop the tokens and create a sequence of ast nodes @@ -187,10 +222,12 @@ public class SimpleExpressionParser extends BaseSimpleParser { functions.decrementAndGet(); return new SimpleFunctionEnd(token); } else if (token.getType().isUnary()) { - // there must be a end function as previous, to let this be a unary function + // there must be an end function as previous, to let this be a unary function if (!nodes.isEmpty() && nodes.get(nodes.size() - 1) instanceof SimpleFunctionEnd) { return new UnaryExpression(token); } + } else if (token.getType().isOther()) { + return new OtherExpression(token); } // by returning null, we will let the parser determine what to do @@ -268,10 +305,12 @@ public class SimpleExpressionParser extends BaseSimpleParser { // - template = literal texts with can contain embedded functions // - function = simple functions such as ${body} etc. // - unary operator = operator attached to the left-hand side node + // - other operator = operator attached to both the left and right hand side nodes protected void templateText() { - // for template, we accept anything but functions - while (!token.getType().isFunctionStart() && !token.getType().isFunctionEnd() && !token.getType().isEol()) { + // for template, we accept anything but functions / other operator + while (!token.getType().isFunctionStart() && !token.getType().isFunctionEnd() && !token.getType().isEol() + && !token.getType().isOther()) { nextToken(); } } @@ -296,6 +335,36 @@ public class SimpleExpressionParser extends BaseSimpleParser { return false; } + protected boolean otherOperator() { + if (accept(TokenType.otherOperator)) { + // remember the other operator + OtherOperatorType operatorType = OtherOperatorType.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()) { + expect(TokenType.whiteSpace); + } + } else { + throw new SimpleParserException( + "Other operator " + operatorType + " does not support token " + token, token.getIndex()); + } + return true; + } + return false; + } + protected boolean unaryOperator() { if (accept(TokenType.unaryOperator)) { nextToken(); @@ -305,4 +374,46 @@ public class SimpleExpressionParser extends BaseSimpleParser { } return false; } + + protected boolean singleQuotedLiteralWithFunctionsText() { + if (accept(TokenType.singleQuote)) { + nextToken(TokenType.singleQuote, TokenType.eol, TokenType.functionStart, TokenType.functionEnd); + while (!token.getType().isSingleQuote() && !token.getType().isEol()) { + // we need to loop until we find the ending single quote, or the eol + nextToken(TokenType.singleQuote, TokenType.eol, TokenType.functionStart, TokenType.functionEnd); + } + expect(TokenType.singleQuote); + return true; + } + return false; + } + + protected boolean doubleQuotedLiteralWithFunctionsText() { + if (accept(TokenType.doubleQuote)) { + nextToken(TokenType.doubleQuote, TokenType.eol, TokenType.functionStart, TokenType.functionEnd); + while (!token.getType().isDoubleQuote() && !token.getType().isEol()) { + // we need to loop until we find the ending double quote, or the eol + nextToken(TokenType.doubleQuote, TokenType.eol, TokenType.functionStart, TokenType.functionEnd); + } + expect(TokenType.doubleQuote); + return true; + } + return false; + } + + protected boolean numericValue() { + return accept(TokenType.numericValue); + // no other tokens to check so do not use nextToken + } + + protected boolean booleanValue() { + return accept(TokenType.booleanValue); + // no other tokens to check so do not use nextToken + } + + protected boolean nullValue() { + return accept(TokenType.nullValue); + // no other tokens to check so do not use nextToken + } + } 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 c4204c01cf94..a0f674adaa26 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 @@ -37,6 +37,7 @@ import org.apache.camel.language.simple.ast.LiteralNode; import org.apache.camel.language.simple.ast.LogicalExpression; import org.apache.camel.language.simple.ast.NullExpression; import org.apache.camel.language.simple.ast.NumericExpression; +import org.apache.camel.language.simple.ast.OtherExpression; import org.apache.camel.language.simple.ast.SimpleFunctionEnd; import org.apache.camel.language.simple.ast.SimpleFunctionStart; import org.apache.camel.language.simple.ast.SimpleNode; @@ -45,6 +46,7 @@ import org.apache.camel.language.simple.ast.SingleQuoteStart; import org.apache.camel.language.simple.ast.UnaryExpression; import org.apache.camel.language.simple.types.BinaryOperatorType; import org.apache.camel.language.simple.types.LogicalOperatorType; +import org.apache.camel.language.simple.types.OtherOperatorType; import org.apache.camel.language.simple.types.SimpleIllegalSyntaxException; import org.apache.camel.language.simple.types.SimpleParserException; import org.apache.camel.language.simple.types.SimpleToken; @@ -120,6 +122,7 @@ public class SimplePredicateParser extends BaseSimpleParser { && !functionText() && !unaryOperator() && !binaryOperator() + && !otherOperator() && !logicalOperator() && !isBooleanValue() && !token.getType().isWhitespace() @@ -146,6 +149,8 @@ public class SimplePredicateParser extends BaseSimpleParser { prepareUnaryExpressions(); // compact and stack binary expressions prepareBinaryExpressions(); + // compact and stack other expressions + prepareOtherExpressions(); // compact and stack logical expressions prepareLogicalExpressions(); @@ -335,6 +340,8 @@ public class SimplePredicateParser extends BaseSimpleParser { return new UnaryExpression(token); } else if (token.getType().isBinary()) { return new BinaryExpression(token); + } else if (token.getType().isOther()) { + return new OtherExpression(token); } else if (token.getType().isLogical()) { return new LogicalExpression(token); } else if (token.getType().isNullValue()) { @@ -560,6 +567,7 @@ public class SimplePredicateParser extends BaseSimpleParser { // - null = null value // - unary operator = operator attached to the left hand side node // - binary operator = operator attached to both the left and right hand side nodes + // - other operator = operator attached to both the left and right hand side nodes // - logical operator = operator attached to both the left and right hand side nodes protected boolean isBooleanValue() { @@ -724,6 +732,36 @@ public class SimplePredicateParser extends BaseSimpleParser { return false; } + protected boolean otherOperator() { + if (accept(TokenType.otherOperator)) { + // remember the other operator + OtherOperatorType operatorType = OtherOperatorType.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()) { + expect(TokenType.whiteSpace); + } + } else { + throw new SimpleParserException( + "Other 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 bbaf198ab832..0258ce18b8ff 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 final class SimpleTokenizer { // keep this number in sync with tokens list - private static final int NUMBER_OF_TOKENS = 49; + private static final int NUMBER_OF_TOKENS = 50; private static final SimpleTokenType[] KNOWN_TOKENS = new SimpleTokenType[NUMBER_OF_TOKENS]; @@ -85,18 +85,21 @@ public final class SimpleTokenizer { KNOWN_TOKENS[42] = new SimpleTokenType(TokenType.binaryOperator, "ends with"); KNOWN_TOKENS[43] = new SimpleTokenType(TokenType.binaryOperator, "!endsWith"); + // other operators + KNOWN_TOKENS[44] = new SimpleTokenType(TokenType.otherOperator, "?:"); + // unary operators - KNOWN_TOKENS[44] = new SimpleTokenType(TokenType.unaryOperator, "++"); - KNOWN_TOKENS[45] = new SimpleTokenType(TokenType.unaryOperator, "--"); + KNOWN_TOKENS[45] = new SimpleTokenType(TokenType.unaryOperator, "++"); + KNOWN_TOKENS[46] = new SimpleTokenType(TokenType.unaryOperator, "--"); // logical operators - KNOWN_TOKENS[46] = new SimpleTokenType(TokenType.logicalOperator, "&&"); - KNOWN_TOKENS[47] = new SimpleTokenType(TokenType.logicalOperator, "||"); + KNOWN_TOKENS[47] = new SimpleTokenType(TokenType.logicalOperator, "&&"); + KNOWN_TOKENS[48] = new SimpleTokenType(TokenType.logicalOperator, "||"); //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[48] = new SimpleTokenType(TokenType.minusValue, "-"); + KNOWN_TOKENS[49] = new SimpleTokenType(TokenType.minusValue, "-"); } private SimpleTokenizer() { @@ -261,6 +264,9 @@ public final class SimpleTokenizer { if (token.isBinary()) { return evalBinary(token, text, expression, index); } + if (token.isOther()) { + return evalOther(token, text, expression, index); + } return text.startsWith(token.getValue()); } @@ -276,6 +282,17 @@ public final class SimpleTokenizer { return " ".equals(previousOne) && " ".equals(afterOne) && text.substring(0, len).equals(token.getValue()); } + private static boolean evalOther(SimpleTokenType token, String text, String expression, int index) { + int len = token.getValue().length(); + // The other operator must be used in the format of "exp1 op exp2" + if (index < 2 || len >= text.length() - 1) { + return false; + } + String previousOne = expression.substring(index - 1, index); + String afterOne = text.substring(len, len + 1); + return " ".equals(previousOne) && " ".equals(afterOne) && text.substring(0, len).equals(token.getValue()); + } + private static boolean evalUnary(SimpleTokenType token, String text, String expression, int index) { int endLen = 1; diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/LiteralExpression.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/LiteralExpression.java index 5c59c101e1ad..626ab79c5626 100644 --- a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/LiteralExpression.java +++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/LiteralExpression.java @@ -43,6 +43,11 @@ public class LiteralExpression extends BaseSimpleNode implements LiteralNode { this.text.append(text); } + void replaceText(String text) { + this.text.setLength(0); + addText(text); + } + @Override public String getText() { return text.toString(); diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/OtherExpression.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/OtherExpression.java new file mode 100644 index 000000000000..09902ffd6fe0 --- /dev/null +++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/OtherExpression.java @@ -0,0 +1,145 @@ +/* + * 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 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.OtherOperatorType; +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; +import org.apache.camel.util.StringQuoteHelper; + +/** + * Represents other operator expression in the AST. + */ +public class OtherExpression extends BaseSimpleNode { + + private final OtherOperatorType operator; + private SimpleNode left; + private SimpleNode right; + + public OtherExpression(SimpleToken token) { + super(token); + operator = OtherOperatorType.asOperator(token.getText()); + } + + @Override + public String toString() { + return left + " " + token.getText() + " " + right; + } + + public boolean acceptLeftNode(SimpleNode lef) { + this.left = lef; + return true; + } + + public boolean acceptRightNode(SimpleNode right) { + this.right = right; + return true; + } + + public OtherOperatorType getOperator() { + return operator; + } + + public SimpleNode getLeft() { + return left; + } + + public SimpleNode getRight() { + return right; + } + + @Override + public Expression createExpression(CamelContext camelContext, String expression) { + org.apache.camel.util.ObjectHelper.notNull(left, "left node", this); + org.apache.camel.util.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); + if (right instanceof LiteralExpression le) { + String text = le.getText(); + String changed = StringHelper.removeLeadingAndEndingQuotes(text); + if (!changed.equals(text)) { + le.replaceText(changed); + } + } + final Expression rightExp = right.createExpression(camelContext, expression); + + if (operator == OtherOperatorType.ELVIS) { + return createElvisExpression(camelContext, leftExp, rightExp); + } + + throw new SimpleParserException("Unknown other operator " + operator, token.getIndex()); + } + + private Expression createElvisExpression( + final CamelContext camelContext, final Expression leftExp, final Expression rightExp) { + return new Expression() { + @Override + public <T> T evaluate(Exchange exchange, Class<T> type) { + Object value = leftExp.evaluate(exchange, Object.class); + if (value == null || Boolean.FALSE == value || ObjectHelper.isEmpty(value) || ObjectHelper.equal(0, value)) { + return rightExp.evaluate(exchange, type); + } else { + return leftExp.evaluate(exchange, type); + } + } + + @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 { + org.apache.camel.util.ObjectHelper.notNull(left, "left node", this); + org.apache.camel.util.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 String leftExp = left.createCode(camelContext, expression); + if (right instanceof LiteralExpression le) { + String text = le.getText(); + // must be in double quotes to be a String type + String changed = StringHelper.removeLeadingAndEndingQuotes(text); + changed = StringQuoteHelper.doubleQuote(changed); + if (!changed.equals(text)) { + le.replaceText(changed); + } + } + final String rightExp = right.createCode(camelContext, expression); + + if (operator == OtherOperatorType.ELVIS) { + return "elvis(exchange, " + leftExp + ", " + rightExp + ")"; + } + + throw new SimpleParserException("Unknown other operator " + operator, token.getIndex()); + } + +} 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/OtherOperatorType.java similarity index 60% copy from core/camel-core-languages/src/main/java/org/apache/camel/language/simple/types/TokenType.java copy to core/camel-core-languages/src/main/java/org/apache/camel/language/simple/types/OtherOperatorType.java index a7a6e6923d68..7ae09a14339d 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/OtherOperatorType.java @@ -17,24 +17,29 @@ package org.apache.camel.language.simple.types; /** - * Classifications of known token types. + * Types of other operators supported */ -public enum TokenType { +public enum OtherOperatorType { - whiteSpace, - character, - booleanValue, - numericValue, - nullValue, - singleQuote, - doubleQuote, - minusValue, - escape, - functionStart, - functionEnd, - binaryOperator, - unaryOperator, - logicalOperator, - eol + ELVIS; + + public static OtherOperatorType asOperator(String text) { + if ("?:".equals(text)) { + return ELVIS; + } + throw new IllegalArgumentException("Operator not supported: " + text); + } + + public static String getOperatorText(OtherOperatorType operator) { + if (operator == ELVIS) { + return "?:"; + } + return ""; + } + + @Override + public String toString() { + return getOperatorText(this); + } } 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 558db07ed7da..7663e1d21cd3 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 @@ -103,6 +103,13 @@ public final class SimpleTokenType { return type == TokenType.binaryOperator; } + /** + * Whether the type is other operator + */ + public boolean isOther() { + return type == TokenType.otherOperator; + } + /** * Whether the type is unary operator */ 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 a7a6e6923d68..e737ab809b09 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 @@ -33,6 +33,7 @@ public enum TokenType { functionStart, functionEnd, binaryOperator, + otherOperator, unaryOperator, logicalOperator, 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 8360f656447e..36a893be856e 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 @@ -38,6 +38,7 @@ public class SimpleOperatorTest extends LanguageTestSupport { public void testValueWithSpace() { exchange.getIn().setBody("Hello Big World"); assertPredicate("${in.body} == 'Hello Big World'", true); + assertPredicate("${in.body} == ${body}", true); } @Test @@ -827,6 +828,38 @@ public class SimpleOperatorTest extends LanguageTestSupport { assertPredicate("${in.body} !endsWith 'Hi'", true); } + @Test + public void testElvis() { + exchange.getIn().setBody(false); + assertPredicate("${body} ?: 'true'", true); + assertPredicate("${body} ?: 'false'", false); + exchange.getIn().setBody("Hello"); + assertPredicate("${body} ?: 'false'", true); + exchange.getIn().setBody(0); + assertPredicate("${body} ?: 'true'", true); + assertPredicate("${body} ?: 'false'", false); + exchange.getIn().setBody(1); + assertPredicate("${body} ?: 'true'", true); + assertPredicate("${body} ?: 'false'", true); + + exchange.getIn().setBody(null); + assertExpression("${body} ?: 'World'", "World"); + exchange.getIn().setBody(""); + assertExpression("${body} ?: 'World'", "World"); + exchange.getIn().setBody("Hello"); + assertExpression("${body} ?: 'World'", "Hello"); + exchange.getIn().setBody(false); + assertExpression("${body} ?: 'World'", "World"); + exchange.getIn().setBody(true); + assertExpression("${body} ?: 'World'", true); + exchange.getIn().setHeader("myHeader", "Camel"); + assertExpression("${header.myHeader} ?: 'World'", "Camel"); + exchange.getIn().setBody(0); + assertExpression("${body} ?: 'World'", "World"); + exchange.getIn().setBody(1); + assertExpression("${body} ?: 'World'", 1); + } + @Override protected String getLanguageName() { return "simple"; diff --git a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimplePredicateParserNodesTest.java b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimplePredicateParserNodesTest.java index 51c2b87b5908..4b4c879d4250 100644 --- a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimplePredicateParserNodesTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimplePredicateParserNodesTest.java @@ -21,10 +21,12 @@ import java.util.List; import org.apache.camel.ExchangeTestSupport; import org.apache.camel.language.simple.ast.BinaryExpression; import org.apache.camel.language.simple.ast.LiteralNode; +import org.apache.camel.language.simple.ast.OtherExpression; import org.apache.camel.language.simple.ast.SimpleFunctionStart; import org.apache.camel.language.simple.ast.SimpleNode; import org.apache.camel.language.simple.ast.SingleQuoteStart; import org.apache.camel.language.simple.types.BinaryOperatorType; +import org.apache.camel.language.simple.types.OtherOperatorType; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -70,4 +72,24 @@ public class SimplePredicateParserNodesTest extends ExchangeTestSupport { Assertions.assertEquals("header.bar", ln.toString()); } + @Test + public void testParserNodesElvis() { + exchange.getIn().setBody("Hello"); + + SimplePredicateParser parser = new SimplePredicateParser(null, "${body} ?: 'Bye'", true, null); + List<SimpleNode> nodes = parser.parseTokens(); + Assertions.assertEquals(1, nodes.size()); + OtherExpression be = (OtherExpression) nodes.get(0); + + Assertions.assertEquals(OtherOperatorType.ELVIS, be.getOperator()); + + SingleQuoteStart qe = (SingleQuoteStart) be.getRight(); + LiteralNode ln = (LiteralNode) qe.getBlock().getChildren().get(0); + Assertions.assertEquals("Bye", ln.getText()); + + SimpleFunctionStart fe = (SimpleFunctionStart) be.getLeft(); + ln = (LiteralNode) fe.getBlock().getChildren().get(0); + Assertions.assertEquals("body", ln.toString()); + } + }
