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.concurrent.atomic.AtomicBoolean;
026
027import org.apache.camel.Expression;
028import org.apache.camel.Predicate;
029import org.apache.camel.builder.PredicateBuilder;
030import org.apache.camel.language.simple.ast.BinaryExpression;
031import org.apache.camel.language.simple.ast.DoubleQuoteEnd;
032import org.apache.camel.language.simple.ast.DoubleQuoteStart;
033import org.apache.camel.language.simple.ast.LiteralExpression;
034import org.apache.camel.language.simple.ast.LiteralNode;
035import org.apache.camel.language.simple.ast.LogicalExpression;
036import org.apache.camel.language.simple.ast.NullExpression;
037import org.apache.camel.language.simple.ast.SimpleFunctionEnd;
038import org.apache.camel.language.simple.ast.SimpleFunctionStart;
039import org.apache.camel.language.simple.ast.SimpleNode;
040import org.apache.camel.language.simple.ast.SingleQuoteEnd;
041import org.apache.camel.language.simple.ast.SingleQuoteStart;
042import org.apache.camel.language.simple.ast.UnaryExpression;
043import org.apache.camel.language.simple.types.BinaryOperatorType;
044import org.apache.camel.language.simple.types.LogicalOperatorType;
045import org.apache.camel.language.simple.types.SimpleIllegalSyntaxException;
046import org.apache.camel.language.simple.types.SimpleParserException;
047import org.apache.camel.language.simple.types.SimpleToken;
048import org.apache.camel.language.simple.types.TokenType;
049import org.apache.camel.util.ExpressionToPredicateAdapter;
050import org.apache.camel.util.LRUCache;
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 LRUCache<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, LRUCache<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<Predicate>();
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}