Skip to content

Commit 2624a15

Browse files
authored
CAMEL-22870: camel-core - Add elvis ?: operator to simple language (#20863)
1 parent c3c71b8 commit 2624a15

File tree

14 files changed

+556
-10
lines changed

14 files changed

+556
-10
lines changed

components/camel-csimple-joor/src/test/java/org/apache/camel/language/csimple/joor/OriginalSimpleOperatorTest.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public class OriginalSimpleOperatorTest extends LanguageTestSupport {
3434
public void testValueWithSpace() {
3535
exchange.getIn().setBody("Hello Big World");
3636
assertPredicate("${in.body} == 'Hello Big World'", true);
37+
assertPredicate("${in.body} == ${body}", true);
3738
}
3839

3940
@Test
@@ -805,6 +806,38 @@ public void testNotEndsWith() {
805806
assertPredicate("${in.body} !endsWith 'Hi'", true);
806807
}
807808

809+
@Test
810+
public void testElvis() {
811+
exchange.getIn().setBody(false);
812+
assertPredicate("${body} ?: 'true'", true);
813+
assertPredicate("${body} ?: 'false'", false);
814+
exchange.getIn().setBody("Hello");
815+
assertPredicate("${body} ?: 'false'", true);
816+
exchange.getIn().setBody(0);
817+
assertPredicate("${body} ?: 'true'", true);
818+
assertPredicate("${body} ?: 'false'", false);
819+
exchange.getIn().setBody(1);
820+
assertPredicate("${body} ?: 'true'", true);
821+
assertPredicate("${body} ?: 'false'", true);
822+
823+
exchange.getIn().setBody(null);
824+
assertExpression("${body} ?: 'World'", "World");
825+
exchange.getIn().setBody("");
826+
assertExpression("${body} ?: 'World'", "World");
827+
exchange.getIn().setBody("Hello");
828+
assertExpression("${body} ?: 'World'", "Hello");
829+
exchange.getIn().setBody(false);
830+
assertExpression("${body} ?: 'World'", "World");
831+
exchange.getIn().setBody(true);
832+
assertExpression("${body} ?: 'World'", true);
833+
exchange.getIn().setHeader("myHeader", "Camel");
834+
assertExpression("${header.myHeader} ?: 'World'", "Camel");
835+
exchange.getIn().setBody(0);
836+
assertExpression("${body} ?: 'World'", "World");
837+
exchange.getIn().setBody(1);
838+
assertExpression("${body} ?: 'World'", 1);
839+
}
840+
808841
@Override
809842
protected String getLanguageName() {
810843
return "csimple";

core/camel-core-languages/src/main/docs/modules/languages/pages/simple-language.adoc

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,22 @@ And the following unary operators can be used:
336336
|`--` | To decrement a number by one. The left-hand side must be a function, otherwise parsed as literal.
337337
|====
338338

339+
And the following other operators can be used:
340+
341+
[width="100%",cols="50%,50%",options="header",]
342+
|====
343+
|Operator |Description
344+
|`?:` | 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).
345+
|====
346+
347+
For example the following elvis operator will return the username header unless its null or empty, which
348+
then the default value of `Guest` is returned.
349+
350+
[source,java]
351+
----
352+
simple("${header.username} ?: 'Guest'");
353+
----
354+
339355
And the following special symbols:
340356

341357
[width="100%",cols="50%,50%",options="header",]

core/camel-core-languages/src/main/java/org/apache/camel/language/csimple/CSimpleHelper.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -979,4 +979,12 @@ public static int size(Exchange exchange, Object value) {
979979
return 0;
980980
}
981981

982+
public static Object elvis(Exchange exchange, Object left, Object right) {
983+
if (left == null || Boolean.FALSE == left || ObjectHelper.isEmpty(left) || ObjectHelper.equal(0, left)) {
984+
return right;
985+
} else {
986+
return left;
987+
}
988+
}
989+
982990
}

core/camel-core-languages/src/main/java/org/apache/camel/language/simple/BaseSimpleParser.java

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@
2323
import java.util.List;
2424

2525
import org.apache.camel.CamelContext;
26+
import org.apache.camel.Predicate;
2627
import org.apache.camel.language.simple.ast.Block;
2728
import org.apache.camel.language.simple.ast.BlockEnd;
2829
import org.apache.camel.language.simple.ast.BlockStart;
30+
import org.apache.camel.language.simple.ast.OtherExpression;
2931
import org.apache.camel.language.simple.ast.SimpleNode;
3032
import org.apache.camel.language.simple.ast.UnaryExpression;
3133
import org.apache.camel.language.simple.types.SimpleParserException;
@@ -205,6 +207,69 @@ protected void prepareUnaryExpressions() {
205207
Collections.reverse(nodes);
206208
}
207209

210+
/**
211+
* Prepares other expressions.
212+
* <p/>
213+
* This process prepares the other expressions in the AST. This is done by linking the other operator with both the
214+
* right and left hand side nodes, to have the AST graph updated and prepared properly.
215+
* <p/>
216+
* 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
217+
* linked and prepared graph of nodes which represent the input expression.
218+
*/
219+
protected void prepareOtherExpressions() {
220+
Deque<SimpleNode> stack = new ArrayDeque<>();
221+
222+
SimpleNode left = null;
223+
for (int i = 0; i < nodes.size(); i++) {
224+
if (left == null) {
225+
left = i > 0 ? nodes.get(i - 1) : null;
226+
}
227+
SimpleNode token = nodes.get(i);
228+
SimpleNode right = i < nodes.size() - 1 ? nodes.get(i + 1) : null;
229+
230+
if (token instanceof OtherExpression other) {
231+
// remember the other operator
232+
String operator = other.getOperator().toString();
233+
234+
if (left == null) {
235+
throw new SimpleParserException(
236+
"Other operator " + operator + " has no left hand side token", token.getToken().getIndex());
237+
}
238+
if (!other.acceptLeftNode(left)) {
239+
throw new SimpleParserException(
240+
"Other operator " + operator + " does not support left hand side token " + left.getToken(),
241+
token.getToken().getIndex());
242+
}
243+
if (right == null) {
244+
throw new SimpleParserException(
245+
"Other operator " + operator + " has no right hand side token", token.getToken().getIndex());
246+
}
247+
if (!other.acceptRightNode(right)) {
248+
throw new SimpleParserException(
249+
"Other operator " + operator + " does not support right hand side token " + right.getToken(),
250+
token.getToken().getIndex());
251+
}
252+
253+
// pop previous as we need to replace it with this other operator
254+
stack.pop();
255+
stack.push(token);
256+
// advantage after the right hand side
257+
i++;
258+
// this token is now the left for the next loop
259+
left = token;
260+
} else {
261+
// clear left
262+
left = null;
263+
stack.push(token);
264+
}
265+
}
266+
267+
nodes.clear();
268+
nodes.addAll(stack);
269+
// must reverse as it was added from a stack that is reverse
270+
Collections.reverse(nodes);
271+
}
272+
208273
// --------------------------------------------------------------
209274
// grammar
210275
// --------------------------------------------------------------

core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleExpressionParser.java

Lines changed: 115 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@
2525
import org.apache.camel.Expression;
2626
import org.apache.camel.language.simple.ast.LiteralExpression;
2727
import org.apache.camel.language.simple.ast.LiteralNode;
28+
import org.apache.camel.language.simple.ast.OtherExpression;
2829
import org.apache.camel.language.simple.ast.SimpleFunctionEnd;
2930
import org.apache.camel.language.simple.ast.SimpleFunctionStart;
3031
import org.apache.camel.language.simple.ast.SimpleNode;
3132
import org.apache.camel.language.simple.ast.UnaryExpression;
33+
import org.apache.camel.language.simple.types.OtherOperatorType;
3234
import org.apache.camel.language.simple.types.SimpleIllegalSyntaxException;
3335
import org.apache.camel.language.simple.types.SimpleParserException;
3436
import org.apache.camel.language.simple.types.SimpleToken;
@@ -97,23 +99,28 @@ protected List<SimpleNode> parseTokens() {
9799
// parse the expression using the following grammar
98100
nextToken();
99101
while (!token.getType().isEol()) {
100-
// an expression supports just template (eg text), functions, or unary operator
102+
// an expression supports just template (eg text), functions, unary, or other operator
101103
templateText();
102104
functionText();
103105
unaryOperator();
106+
otherOperator();
104107
nextToken();
105108
}
106109

107110
// now after parsing, we need a bit of work to do, to make it easier to turn the tokens
108111
// into an ast, and then from the ast, to Camel expression(s).
109112
// hence why there are a number of tasks going on below to accomplish this
110113

114+
// remove any ignorable white space tokens
115+
removeIgnorableWhiteSpaceTokens();
111116
// turn the tokens into the ast model
112117
parseAndCreateAstModel();
113118
// compact and stack blocks (eg function start/end)
114119
prepareBlocks();
115120
// compact and stack unary operators
116121
prepareUnaryExpressions();
122+
// compact and stack other expressions
123+
prepareOtherExpressions();
117124

118125
return nodes;
119126
}
@@ -135,6 +142,34 @@ protected Expression doParseExpression() {
135142
}
136143
}
137144

145+
/**
146+
* Removes any ignorable whitespace tokens before and after other operators.
147+
* <p/>
148+
* During the initial parsing (input -> tokens), then there may be excessive whitespace tokens, which can safely be
149+
* removed, which makes the succeeding parsing easier.
150+
*/
151+
private void removeIgnorableWhiteSpaceTokens() {
152+
// white space should be removed before and after the other operator
153+
List<SimpleToken> toRemove = new ArrayList<>();
154+
for (int i = 1; i < tokens.size() - 1; i++) {
155+
SimpleToken prev = tokens.get(i - 1);
156+
SimpleToken cur = tokens.get(i);
157+
SimpleToken next = tokens.get(i + 1);
158+
if (cur.getType().isOther()) {
159+
if (prev.getType().isWhitespace()) {
160+
toRemove.add(prev);
161+
}
162+
if (next.getType().isWhitespace()) {
163+
toRemove.add(next);
164+
}
165+
}
166+
}
167+
168+
if (!toRemove.isEmpty()) {
169+
tokens.removeAll(toRemove);
170+
}
171+
}
172+
138173
protected void parseAndCreateAstModel() {
139174
// we loop the tokens and create a sequence of ast nodes
140175

@@ -187,10 +222,12 @@ private SimpleNode createNode(SimpleToken token, AtomicInteger functions) {
187222
functions.decrementAndGet();
188223
return new SimpleFunctionEnd(token);
189224
} else if (token.getType().isUnary()) {
190-
// there must be a end function as previous, to let this be a unary function
225+
// there must be an end function as previous, to let this be a unary function
191226
if (!nodes.isEmpty() && nodes.get(nodes.size() - 1) instanceof SimpleFunctionEnd) {
192227
return new UnaryExpression(token);
193228
}
229+
} else if (token.getType().isOther()) {
230+
return new OtherExpression(token);
194231
}
195232

196233
// by returning null, we will let the parser determine what to do
@@ -268,10 +305,12 @@ static void parseLiteralNode(StringBuilder sb, SimpleNode node, String exp) {
268305
// - template = literal texts with can contain embedded functions
269306
// - function = simple functions such as ${body} etc.
270307
// - unary operator = operator attached to the left-hand side node
308+
// - other operator = operator attached to both the left and right hand side nodes
271309

272310
protected void templateText() {
273-
// for template, we accept anything but functions
274-
while (!token.getType().isFunctionStart() && !token.getType().isFunctionEnd() && !token.getType().isEol()) {
311+
// for template, we accept anything but functions / other operator
312+
while (!token.getType().isFunctionStart() && !token.getType().isFunctionEnd() && !token.getType().isEol()
313+
&& !token.getType().isOther()) {
275314
nextToken();
276315
}
277316
}
@@ -296,6 +335,36 @@ protected boolean functionText() {
296335
return false;
297336
}
298337

338+
protected boolean otherOperator() {
339+
if (accept(TokenType.otherOperator)) {
340+
// remember the other operator
341+
OtherOperatorType operatorType = OtherOperatorType.asOperator(token.getText());
342+
343+
nextToken();
344+
// there should be at least one whitespace after the operator
345+
expectAndAcceptMore(TokenType.whiteSpace);
346+
347+
// then we expect either some quoted text, another function, or a numeric, boolean or null value
348+
if (singleQuotedLiteralWithFunctionsText()
349+
|| doubleQuotedLiteralWithFunctionsText()
350+
|| functionText()
351+
|| numericValue()
352+
|| booleanValue()
353+
|| nullValue()) {
354+
// then after the right hand side value, there should be a whitespace if there is more tokens
355+
nextToken();
356+
if (!token.getType().isEol()) {
357+
expect(TokenType.whiteSpace);
358+
}
359+
} else {
360+
throw new SimpleParserException(
361+
"Other operator " + operatorType + " does not support token " + token, token.getIndex());
362+
}
363+
return true;
364+
}
365+
return false;
366+
}
367+
299368
protected boolean unaryOperator() {
300369
if (accept(TokenType.unaryOperator)) {
301370
nextToken();
@@ -305,4 +374,46 @@ protected boolean unaryOperator() {
305374
}
306375
return false;
307376
}
377+
378+
protected boolean singleQuotedLiteralWithFunctionsText() {
379+
if (accept(TokenType.singleQuote)) {
380+
nextToken(TokenType.singleQuote, TokenType.eol, TokenType.functionStart, TokenType.functionEnd);
381+
while (!token.getType().isSingleQuote() && !token.getType().isEol()) {
382+
// we need to loop until we find the ending single quote, or the eol
383+
nextToken(TokenType.singleQuote, TokenType.eol, TokenType.functionStart, TokenType.functionEnd);
384+
}
385+
expect(TokenType.singleQuote);
386+
return true;
387+
}
388+
return false;
389+
}
390+
391+
protected boolean doubleQuotedLiteralWithFunctionsText() {
392+
if (accept(TokenType.doubleQuote)) {
393+
nextToken(TokenType.doubleQuote, TokenType.eol, TokenType.functionStart, TokenType.functionEnd);
394+
while (!token.getType().isDoubleQuote() && !token.getType().isEol()) {
395+
// we need to loop until we find the ending double quote, or the eol
396+
nextToken(TokenType.doubleQuote, TokenType.eol, TokenType.functionStart, TokenType.functionEnd);
397+
}
398+
expect(TokenType.doubleQuote);
399+
return true;
400+
}
401+
return false;
402+
}
403+
404+
protected boolean numericValue() {
405+
return accept(TokenType.numericValue);
406+
// no other tokens to check so do not use nextToken
407+
}
408+
409+
protected boolean booleanValue() {
410+
return accept(TokenType.booleanValue);
411+
// no other tokens to check so do not use nextToken
412+
}
413+
414+
protected boolean nullValue() {
415+
return accept(TokenType.nullValue);
416+
// no other tokens to check so do not use nextToken
417+
}
418+
308419
}

0 commit comments

Comments
 (0)