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