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 }