001 /**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017 package org.apache.camel.language.simple;
018
019 import java.util.ArrayList;
020 import java.util.Iterator;
021 import java.util.List;
022 import java.util.Stack;
023 import java.util.concurrent.atomic.AtomicBoolean;
024
025 import org.apache.camel.Expression;
026 import org.apache.camel.Predicate;
027 import org.apache.camel.builder.PredicateBuilder;
028 import org.apache.camel.language.simple.ast.BinaryExpression;
029 import org.apache.camel.language.simple.ast.DoubleQuoteEnd;
030 import org.apache.camel.language.simple.ast.DoubleQuoteStart;
031 import org.apache.camel.language.simple.ast.LiteralExpression;
032 import org.apache.camel.language.simple.ast.LiteralNode;
033 import org.apache.camel.language.simple.ast.LogicalExpression;
034 import org.apache.camel.language.simple.ast.NullExpression;
035 import org.apache.camel.language.simple.ast.SimpleFunctionEnd;
036 import org.apache.camel.language.simple.ast.SimpleFunctionStart;
037 import org.apache.camel.language.simple.ast.SimpleNode;
038 import org.apache.camel.language.simple.ast.SingleQuoteEnd;
039 import org.apache.camel.language.simple.ast.SingleQuoteStart;
040 import org.apache.camel.language.simple.ast.UnaryExpression;
041 import org.apache.camel.language.simple.types.BinaryOperatorType;
042 import org.apache.camel.language.simple.types.LogicalOperatorType;
043 import org.apache.camel.language.simple.types.SimpleIllegalSyntaxException;
044 import org.apache.camel.language.simple.types.SimpleParserException;
045 import org.apache.camel.language.simple.types.SimpleToken;
046 import org.apache.camel.language.simple.types.TokenType;
047 import org.apache.camel.util.ExpressionToPredicateAdapter;
048
049 /**
050 * A parser to parse simple language as a Camel {@link Predicate}
051 */
052 public class SimplePredicateParser extends BaseSimpleParser {
053
054 @Deprecated
055 public SimplePredicateParser(String expression) {
056 super(expression, true);
057 }
058
059 public SimplePredicateParser(String expression, boolean allowEscape) {
060 super(expression, allowEscape);
061 }
062
063 public Predicate parsePredicate() {
064 clear();
065 try {
066 return doParsePredicate();
067 } catch (SimpleParserException e) {
068 // catch parser exception and turn that into a syntax exceptions
069 throw new SimpleIllegalSyntaxException(expression, e.getIndex(), e.getMessage(), e);
070 } catch (Exception e) {
071 // include exception in rethrown exception
072 throw new SimpleIllegalSyntaxException(expression, -1, e.getMessage(), e);
073 }
074 }
075
076 protected Predicate doParsePredicate() {
077
078 // parse using the following grammar
079 nextToken();
080 while (!token.getType().isEol()) {
081 // predicate supports quotes, functions, operators and whitespaces
082 //CHECKSTYLE:OFF
083 if (!singleQuotedLiteralWithFunctionsText()
084 && !doubleQuotedLiteralWithFunctionsText()
085 && !functionText()
086 && !unaryOperator()
087 && !binaryOperator()
088 && !logicalOperator()
089 && !token.getType().isWhitespace()
090 && !token.getType().isEol()) {
091 // okay the symbol was not one of the above, so its not supported
092 // use the previous index as that is where the problem is
093 throw new SimpleParserException("Unexpected token " + token, previousIndex);
094 }
095 //CHECKSTYLE:ON
096 // take the next token
097 nextToken();
098 }
099
100 // now after parsing we need a bit of work to do, to make it easier to turn the tokens
101 // into and ast, and then from the ast, to Camel predicate(s).
102 // hence why there is a number of tasks going on below to accomplish this
103
104 // remove any ignorable white space tokens
105 removeIgnorableWhiteSpaceTokens();
106 // turn the tokens into the ast model
107 parseTokensAndCreateNodes();
108 // compact and stack blocks (eg function start/end, quotes start/end, etc.)
109 prepareBlocks();
110 // compact and stack unary expressions
111 prepareUnaryExpressions();
112 // compact and stack binary expressions
113 prepareBinaryExpressions();
114 // compact and stack logical expressions
115 prepareLogicalExpressions();
116
117 // create and return as a Camel predicate
118 List<Predicate> predicates = createPredicates();
119 if (predicates.isEmpty()) {
120 // return a false predicate as response as there was nothing to parse
121 return PredicateBuilder.constant(false);
122 } else if (predicates.size() == 1) {
123 return predicates.get(0);
124 } else {
125 return PredicateBuilder.and(predicates);
126 }
127 }
128
129 /**
130 * Parses the tokens and crates the AST nodes.
131 * <p/>
132 * After the initial parsing of the input (input -> tokens) then we
133 * parse again (tokens -> ast).
134 * <p/>
135 * In this parsing the balance of the blocks is checked, so that each block has a matching
136 * start and end token. For example a single quote block, or a function block etc.
137 */
138 protected void parseTokensAndCreateNodes() {
139 // we loop the tokens and create a sequence of ast nodes
140
141 // we need to keep a bit of state for keeping track of single and double quotes
142 // which need to be balanced and have matching start/end pairs
143 SimpleNode lastSingle = null;
144 SimpleNode lastDouble = null;
145 SimpleNode lastFunction = null;
146 AtomicBoolean startSingle = new AtomicBoolean(false);
147 AtomicBoolean startDouble = new AtomicBoolean(false);
148 AtomicBoolean startFunction = new AtomicBoolean(false);
149
150 LiteralNode imageToken = null;
151 for (SimpleToken token : tokens) {
152 // break if eol
153 if (token.getType().isEol()) {
154 break;
155 }
156
157 // create a node from the token
158 SimpleNode node = createNode(token, startSingle, startDouble, startFunction);
159 if (node != null) {
160 // keep state of last single/double
161 if (node instanceof SingleQuoteStart) {
162 lastSingle = node;
163 } else if (node instanceof DoubleQuoteStart) {
164 lastDouble = node;
165 } else if (node instanceof SimpleFunctionStart) {
166 lastFunction = node;
167 }
168
169 // a new token was created so the current image token need to be added first
170 if (imageToken != null) {
171 nodes.add(imageToken);
172 imageToken = null;
173 }
174 // and then add the created node
175 nodes.add(node);
176 // continue to next
177 continue;
178 }
179
180 // if no token was created then its a character/whitespace/escaped symbol
181 // which we need to add together in the same image
182 if (imageToken == null) {
183 imageToken = new LiteralExpression(token);
184 }
185 imageToken.addText(token.getText());
186 }
187
188 // append any leftover image tokens (when we reached eol)
189 if (imageToken != null) {
190 nodes.add(imageToken);
191 }
192
193 // validate the single, double quote pairs and functions is in balance
194 if (startSingle.get()) {
195 int index = lastSingle != null ? lastSingle.getToken().getIndex() : 0;
196 throw new SimpleParserException("single quote has no ending quote", index);
197 }
198 if (startDouble.get()) {
199 int index = lastDouble != null ? lastDouble.getToken().getIndex() : 0;
200 throw new SimpleParserException("double quote has no ending quote", index);
201 }
202 if (startFunction.get()) {
203 // we have a start function, but no ending function
204 int index = lastFunction != null ? lastFunction.getToken().getIndex() : 0;
205 throw new SimpleParserException("function has no ending token", index);
206 }
207 }
208
209
210 /**
211 * Creates a node from the given token
212 *
213 * @param token the token
214 * @param startSingle state of single quoted blocks
215 * @param startDouble state of double quoted blocks
216 * @param startFunction state of function blocks
217 * @return the created node, or <tt>null</tt> to let a default node be created instead.
218 */
219 private SimpleNode createNode(SimpleToken token, AtomicBoolean startSingle, AtomicBoolean startDouble,
220 AtomicBoolean startFunction) {
221 if (token.getType().isFunctionStart()) {
222 startFunction.set(true);
223 return new SimpleFunctionStart(token);
224 } else if (token.getType().isFunctionEnd()) {
225 startFunction.set(false);
226 return new SimpleFunctionEnd(token);
227 }
228
229 // if we are inside a function, then we do not support any other kind of tokens
230 // as we want all the tokens to be literal instead
231 if (startFunction.get()) {
232 return null;
233 }
234
235 // okay so far we also want to support quotes
236 if (token.getType().isSingleQuote()) {
237 SimpleNode answer;
238 boolean start = startSingle.get();
239 if (!start) {
240 answer = new SingleQuoteStart(token);
241 } else {
242 answer = new SingleQuoteEnd(token);
243 }
244 // flip state on start/end flag
245 startSingle.set(!start);
246 return answer;
247 } else if (token.getType().isDoubleQuote()) {
248 SimpleNode answer;
249 boolean start = startDouble.get();
250 if (!start) {
251 answer = new DoubleQuoteStart(token);
252 } else {
253 answer = new DoubleQuoteEnd(token);
254 }
255 // flip state on start/end flag
256 startDouble.set(!start);
257 return answer;
258 }
259
260 // if we are inside a quote, then we do not support any further kind of tokens
261 // as we want to only support embedded functions and all other kinds to be literal tokens
262 if (startSingle.get() || startDouble.get()) {
263 return null;
264 }
265
266 // okay we are not inside a function or quote, so we want to support operators
267 // and the special null value as well
268 if (token.getType().isUnary()) {
269 return new UnaryExpression(token);
270 } else if (token.getType().isBinary()) {
271 return new BinaryExpression(token);
272 } else if (token.getType().isLogical()) {
273 return new LogicalExpression(token);
274 } else if (token.getType().isNullValue()) {
275 return new NullExpression(token);
276 }
277
278 // by returning null, we will let the parser determine what to do
279 return null;
280 }
281
282 /**
283 * Removes any ignorable whitespace tokens.
284 * <p/>
285 * During the initial parsing (input -> tokens), then there may
286 * be excessive whitespace tokens, which can safely be removed,
287 * which makes the succeeding parsing easier.
288 */
289 private void removeIgnorableWhiteSpaceTokens() {
290 // white space can be removed if its not part of a quoted text
291 boolean quote = false;
292
293 Iterator<SimpleToken> it = tokens.iterator();
294 while (it.hasNext()) {
295 SimpleToken token = it.next();
296 if (token.getType().isSingleQuote()) {
297 quote = !quote;
298 } else if (token.getType().isWhitespace() && !quote) {
299 it.remove();
300 }
301 }
302 }
303
304 /**
305 * Prepares binary expressions.
306 * <p/>
307 * This process prepares the binary expressions in the AST. This is done
308 * by linking the binary operator with both the right and left hand side
309 * nodes, to have the AST graph updated and prepared properly.
310 * <p/>
311 * So when the AST node is later used to create the {@link Predicate}s
312 * to be used by Camel then the AST graph has a linked and prepared
313 * graph of nodes which represent the input expression.
314 */
315 private void prepareBinaryExpressions() {
316 Stack<SimpleNode> stack = new Stack<SimpleNode>();
317
318 SimpleNode left = null;
319 for (int i = 0; i < nodes.size(); i++) {
320 if (left == null) {
321 left = i > 0 ? nodes.get(i - 1) : null;
322 }
323 SimpleNode token = nodes.get(i);
324 SimpleNode right = i < nodes.size() - 1 ? nodes.get(i + 1) : null;
325
326 if (token instanceof BinaryExpression) {
327 BinaryExpression binary = (BinaryExpression) token;
328
329 // remember the binary operator
330 String operator = binary.getOperator().toString();
331
332 if (left == null) {
333 throw new SimpleParserException("Binary operator " + operator + " has no left hand side token", token.getToken().getIndex());
334 }
335 if (!binary.acceptLeftNode(left)) {
336 throw new SimpleParserException("Binary operator " + operator + " does not support left hand side token " + left.getToken(), token.getToken().getIndex());
337 }
338 if (right == null) {
339 throw new SimpleParserException("Binary operator " + operator + " has no right hand side token", token.getToken().getIndex());
340 }
341 if (!binary.acceptRightNode(right)) {
342 throw new SimpleParserException("Binary operator " + operator + " does not support right hand side token " + right.getToken(), token.getToken().getIndex());
343 }
344
345 // pop previous as we need to replace it with this binary operator
346 stack.pop();
347 stack.push(token);
348 // advantage after the right hand side
349 i++;
350 // this token is now the left for the next loop
351 left = token;
352 } else {
353 // clear left
354 left = null;
355 stack.push(token);
356 }
357 }
358
359 nodes.clear();
360 nodes.addAll(stack);
361 }
362
363 /**
364 * Prepares logical expressions.
365 * <p/>
366 * This process prepares the logical expressions in the AST. This is done
367 * by linking the logical operator with both the right and left hand side
368 * nodes, to have the AST graph updated and prepared properly.
369 * <p/>
370 * So when the AST node is later used to create the {@link Predicate}s
371 * to be used by Camel then the AST graph has a linked and prepared
372 * graph of nodes which represent the input expression.
373 */
374 private void prepareLogicalExpressions() {
375 Stack<SimpleNode> stack = new Stack<SimpleNode>();
376
377 SimpleNode left = null;
378 for (int i = 0; i < nodes.size(); i++) {
379 if (left == null) {
380 left = i > 0 ? nodes.get(i - 1) : null;
381 }
382 SimpleNode token = nodes.get(i);
383 SimpleNode right = i < nodes.size() - 1 ? nodes.get(i + 1) : null;
384
385 if (token instanceof LogicalExpression) {
386 LogicalExpression logical = (LogicalExpression) token;
387
388 // remember the logical operator
389 String operator = logical.getOperator().toString();
390
391 if (left == null) {
392 throw new SimpleParserException("Logical operator " + operator + " has no left hand side token", token.getToken().getIndex());
393 }
394 if (!logical.acceptLeftNode(left)) {
395 throw new SimpleParserException("Logical operator " + operator + " does not support left hand side token " + left.getToken(), token.getToken().getIndex());
396 }
397 if (right == null) {
398 throw new SimpleParserException("Logical operator " + operator + " has no right hand side token", token.getToken().getIndex());
399 }
400 if (!logical.acceptRightNode(right)) {
401 throw new SimpleParserException("Logical operator " + operator + " does not support right hand side token " + left.getToken(), token.getToken().getIndex());
402 }
403
404 // pop previous as we need to replace it with this binary operator
405 stack.pop();
406 stack.push(token);
407 // advantage after the right hand side
408 i++;
409 // this token is now the left for the next loop
410 left = token;
411 } else {
412 // clear left
413 left = null;
414 stack.push(token);
415 }
416 }
417
418 nodes.clear();
419 nodes.addAll(stack);
420 }
421
422 /**
423 * Creates the {@link Predicate}s from the AST nodes.
424 *
425 * @return the created {@link Predicate}s, is never <tt>null</tt>.
426 */
427 private List<Predicate> createPredicates() {
428 List<Predicate> answer = new ArrayList<Predicate>();
429 for (SimpleNode node : nodes) {
430 Expression exp = node.createExpression(expression);
431 if (exp != null) {
432 Predicate predicate = ExpressionToPredicateAdapter.toPredicate(exp);
433 answer.add(predicate);
434 }
435 }
436 return answer;
437 }
438
439 // --------------------------------------------------------------
440 // grammar
441 // --------------------------------------------------------------
442
443 // the predicate parser understands a lot more than the expression parser
444 // - single quoted = block of nodes enclosed by single quotes
445 // - double quoted = block of nodes enclosed by double quotes
446 // - single quoted with functions = block of nodes enclosed by single quotes allowing embedded functions
447 // - double quoted with functions = block of nodes enclosed by double quotes allowing embedded functions
448 // - function = simple functions such as ${body} etc
449 // - numeric = numeric value
450 // - boolean = boolean value
451 // - null = null value
452 // - unary operator = operator attached to the left hand side node
453 // - binary operator = operator attached to both the left and right hand side nodes
454 // - logical operator = operator attached to both the left and right hand side nodes
455
456 protected boolean singleQuotedLiteralWithFunctionsText() {
457 if (accept(TokenType.singleQuote)) {
458 nextToken(TokenType.singleQuote, TokenType.eol, TokenType.functionStart, TokenType.functionEnd);
459 while (!token.getType().isSingleQuote() && !token.getType().isEol()) {
460 // we need to loop until we find the ending single quote, or the eol
461 nextToken(TokenType.singleQuote, TokenType.eol, TokenType.functionStart, TokenType.functionEnd);
462 }
463 expect(TokenType.singleQuote);
464 return true;
465 }
466 return false;
467 }
468
469 protected boolean singleQuotedLiteralText() {
470 if (accept(TokenType.singleQuote)) {
471 nextToken(TokenType.singleQuote, TokenType.eol);
472 while (!token.getType().isSingleQuote() && !token.getType().isEol()) {
473 // we need to loop until we find the ending single quote, or the eol
474 nextToken(TokenType.singleQuote, TokenType.eol);
475 }
476 expect(TokenType.singleQuote);
477 return true;
478 }
479 return false;
480 }
481
482 protected boolean doubleQuotedLiteralWithFunctionsText() {
483 if (accept(TokenType.doubleQuote)) {
484 nextToken(TokenType.doubleQuote, TokenType.eol, TokenType.functionStart, TokenType.functionEnd);
485 while (!token.getType().isDoubleQuote() && !token.getType().isEol()) {
486 // we need to loop until we find the ending double quote, or the eol
487 nextToken(TokenType.doubleQuote, TokenType.eol, TokenType.functionStart, TokenType.functionEnd);
488 }
489 expect(TokenType.doubleQuote);
490 return true;
491 }
492 return false;
493 }
494
495 protected boolean doubleQuotedLiteralText() {
496 if (accept(TokenType.doubleQuote)) {
497 nextToken(TokenType.doubleQuote, TokenType.eol);
498 while (!token.getType().isDoubleQuote() && !token.getType().isEol()) {
499 // we need to loop until we find the ending double quote, or the eol
500 nextToken(TokenType.doubleQuote, TokenType.eol);
501 }
502 expect(TokenType.doubleQuote);
503 return true;
504 }
505 return false;
506 }
507
508 protected boolean functionText() {
509 if (accept(TokenType.functionStart)) {
510 nextToken();
511 while (!token.getType().isFunctionEnd() && !token.getType().isEol()) {
512 if (token.getType().isFunctionStart()) {
513 // embedded function
514 functionText();
515 }
516 // we need to loop until we find the ending function quote, an embedded function, or the eol
517 nextToken();
518 }
519 // if its not an embedded function then we expect the end token
520 if (!token.getType().isFunctionStart()) {
521 expect(TokenType.functionEnd);
522 }
523 return true;
524 }
525 return false;
526 }
527
528 protected boolean unaryOperator() {
529 if (accept(TokenType.unaryOperator)) {
530 nextToken();
531 // there should be a whitespace after the operator
532 expect(TokenType.whiteSpace);
533 return true;
534 }
535 return false;
536 }
537
538 protected boolean binaryOperator() {
539 if (accept(TokenType.binaryOperator)) {
540 // remember the binary operator
541 BinaryOperatorType operatorType = BinaryOperatorType.asOperator(token.getText());
542
543 nextToken();
544 // there should be at least one whitespace after the operator
545 expectAndAcceptMore(TokenType.whiteSpace);
546
547 // okay a binary operator may not support all kind if preceding parameters, so we need to limit this
548 BinaryOperatorType.ParameterType[] types = BinaryOperatorType.supportedParameterTypes(operatorType);
549
550 // based on the parameter types the binary operator support, we need to set this state into
551 // the following booleans so we know how to proceed in the grammar
552 boolean literalWithFunctionsSupported = false;
553 boolean literalSupported = false;
554 boolean functionSupported = false;
555 boolean numericSupported = false;
556 boolean booleanSupported = false;
557 boolean nullSupported = false;
558 if (types == null || types.length == 0) {
559 literalWithFunctionsSupported = true;
560 // favor literal with functions over literals without functions
561 literalSupported = false;
562 functionSupported = true;
563 numericSupported = true;
564 booleanSupported = true;
565 nullSupported = true;
566 } else {
567 for (BinaryOperatorType.ParameterType parameterType : types) {
568 literalSupported |= parameterType.isLiteralSupported();
569 literalWithFunctionsSupported |= parameterType.isLiteralWithFunctionSupport();
570 functionSupported |= parameterType.isFunctionSupport();
571 nullSupported |= parameterType.isNumericValueSupported();
572 booleanSupported |= parameterType.isBooleanValueSupported();
573 nullSupported |= parameterType.isNullValueSupported();
574 }
575 }
576
577 // then we proceed in the grammar according to the parameter types supported by the given binary operator
578 //CHECKSTYLE:OFF
579 if ((literalWithFunctionsSupported && singleQuotedLiteralWithFunctionsText())
580 || (literalWithFunctionsSupported && doubleQuotedLiteralWithFunctionsText())
581 || (literalSupported && singleQuotedLiteralText())
582 || (literalSupported && doubleQuotedLiteralText())
583 || (functionSupported && functionText())
584 || (numericSupported && numericValue())
585 || (booleanSupported && booleanValue())
586 || (nullSupported && nullValue())) {
587 // then after the right hand side value, there should be a whitespace if there is more tokens
588 nextToken();
589 if (!token.getType().isEol()) {
590 expect(TokenType.whiteSpace);
591 }
592 } else {
593 throw new SimpleParserException("Binary operator " + operatorType + " does not support token " + token, token.getIndex());
594 }
595 //CHECKSTYLE:ON
596 return true;
597 }
598 return false;
599 }
600
601 protected boolean logicalOperator() {
602 if (accept(TokenType.logicalOperator)) {
603 // remember the logical operator
604 LogicalOperatorType operatorType = LogicalOperatorType.asOperator(token.getText());
605
606 nextToken();
607 // there should be at least one whitespace after the operator
608 expectAndAcceptMore(TokenType.whiteSpace);
609
610 // then we expect either some quoted text, another function, or a numeric, boolean or null value
611 if (singleQuotedLiteralWithFunctionsText()
612 || doubleQuotedLiteralWithFunctionsText()
613 || functionText()
614 || numericValue()
615 || booleanValue()
616 || nullValue()) {
617 // then after the right hand side value, there should be a whitespace if there is more tokens
618 nextToken();
619 if (!token.getType().isEol()) {
620 expect(TokenType.whiteSpace);
621 }
622 } else {
623 throw new SimpleParserException("Logical operator " + operatorType + " does not support token " + token, token.getIndex());
624 }
625 return true;
626 }
627 return false;
628 }
629
630 protected boolean numericValue() {
631 return accept(TokenType.numericValue);
632 // no other tokens to check so do not use nextToken
633 }
634
635 protected boolean booleanValue() {
636 return accept(TokenType.booleanValue);
637 // no other tokens to check so do not use nextToken
638 }
639
640 protected boolean nullValue() {
641 return accept(TokenType.nullValue);
642 // no other tokens to check so do not use nextToken
643 }
644
645 }