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    }