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.ArrayList;
020import java.util.List;
021import java.util.Map;
022import java.util.concurrent.atomic.AtomicInteger;
023
024import org.apache.camel.Expression;
025import org.apache.camel.builder.ExpressionBuilder;
026import org.apache.camel.language.simple.ast.LiteralExpression;
027import org.apache.camel.language.simple.ast.LiteralNode;
028import org.apache.camel.language.simple.ast.SimpleFunctionEnd;
029import org.apache.camel.language.simple.ast.SimpleFunctionStart;
030import org.apache.camel.language.simple.ast.SimpleNode;
031import org.apache.camel.language.simple.ast.UnaryExpression;
032import org.apache.camel.language.simple.types.SimpleIllegalSyntaxException;
033import org.apache.camel.language.simple.types.SimpleParserException;
034import org.apache.camel.language.simple.types.SimpleToken;
035import org.apache.camel.language.simple.types.TokenType;
036
037/**
038 * A parser to parse simple language as a Camel {@link Expression}
039 */
040public class SimpleExpressionParser extends BaseSimpleParser {
041
042    // use caches to avoid re-parsing the same expressions over and over again
043    private Map<String, Expression> cacheExpression;
044
045    @Deprecated
046    public SimpleExpressionParser(String expression) {
047        super(expression, true);
048    }
049
050    @Deprecated
051    public SimpleExpressionParser(String expression, boolean allowEscape) {
052        super(expression, allowEscape);
053    }
054
055    public SimpleExpressionParser(String expression, boolean allowEscape,
056                                  Map<String, Expression> cacheExpression) {
057        super(expression, allowEscape);
058        this.cacheExpression = cacheExpression;
059    }
060
061    public Expression parseExpression() {
062        clear();
063        try {
064            return doParseExpression();
065        } catch (SimpleParserException e) {
066            // catch parser exception and turn that into a syntax exceptions
067            throw new SimpleIllegalSyntaxException(expression, e.getIndex(), e.getMessage(), e);
068        } catch (Exception e) {
069            // include exception in rethrown exception
070            throw new SimpleIllegalSyntaxException(expression, -1, e.getMessage(), e);
071        }
072    }
073
074    protected Expression doParseExpression() {
075        // parse the expression using the following grammar
076        nextToken();
077        while (!token.getType().isEol()) {
078            // an expression supports just template (eg text), functions, or unary operator
079            templateText();
080            functionText();
081            unaryOperator();
082            nextToken();
083        }
084
085        // now after parsing we need a bit of work to do, to make it easier to turn the tokens
086        // into an ast, and then from the ast, to Camel expression(s).
087        // hence why there is a number of tasks going on below to accomplish this
088
089        // turn the tokens into the ast model
090        parseAndCreateAstModel();
091        // compact and stack blocks (eg function start/end)
092        prepareBlocks();
093        // compact and stack unary operators
094        prepareUnaryExpressions();
095
096        // create and return as a Camel expression
097        List<Expression> expressions = createExpressions();
098        if (expressions.isEmpty()) {
099            // return an empty string as response as there was nothing to parse
100            return ExpressionBuilder.constantExpression("");
101        } else if (expressions.size() == 1) {
102            return expressions.get(0);
103        } else {
104            // concat expressions as evaluating an expression is like a template language
105            return ExpressionBuilder.concatExpression(expressions, expression);
106        }
107    }
108
109    protected void parseAndCreateAstModel() {
110        // we loop the tokens and create a sequence of ast nodes
111
112        // counter to keep track of number of functions in the tokens
113        AtomicInteger functions = new AtomicInteger();
114
115        LiteralNode imageToken = null;
116        for (SimpleToken token : tokens) {
117            // break if eol
118            if (token.getType().isEol()) {
119                break;
120            }
121
122            // create a node from the token
123            SimpleNode node = createNode(token, functions);
124            if (node != null) {
125                // a new token was created so the current image token need to be added first
126                if (imageToken != null) {
127                    nodes.add(imageToken);
128                    imageToken = null;
129                }
130                // and then add the created node
131                nodes.add(node);
132                // continue to next
133                continue;
134            }
135
136            // if no token was created then its a character/whitespace/escaped symbol
137            // which we need to add together in the same image
138            if (imageToken == null) {
139                imageToken = new LiteralExpression(token);
140            }
141            imageToken.addText(token.getText());
142        }
143
144        // append any leftover image tokens (when we reached eol)
145        if (imageToken != null) {
146            nodes.add(imageToken);
147        }
148    }
149
150    private SimpleNode createNode(SimpleToken token, AtomicInteger functions) {
151        // expression only support functions and unary operators
152        if (token.getType().isFunctionStart()) {
153            // starting a new function
154            functions.incrementAndGet();
155            return new SimpleFunctionStart(token, cacheExpression);
156        } else if (functions.get() > 0 && token.getType().isFunctionEnd()) {
157            // there must be a start function already, to let this be a end function
158            functions.decrementAndGet();
159            return new SimpleFunctionEnd(token);
160        } else if (token.getType().isUnary()) {
161            // there must be a end function as previous, to let this be a unary function
162            if (!nodes.isEmpty() && nodes.get(nodes.size() - 1) instanceof SimpleFunctionEnd) {
163                return new UnaryExpression(token);
164            }
165        }
166
167        // by returning null, we will let the parser determine what to do
168        return null;
169    }
170
171    private List<Expression> createExpressions() {
172        List<Expression> answer = new ArrayList<>();
173        for (SimpleNode token : nodes) {
174            Expression exp = token.createExpression(expression);
175            if (exp != null) {
176                answer.add(exp);
177            }
178        }
179        return answer;
180    }
181
182    // --------------------------------------------------------------
183    // grammar
184    // --------------------------------------------------------------
185
186    // the expression parser only understands
187    // - template = literal texts with can contain embedded functions
188    // - function = simple functions such as ${body} etc
189    // - unary operator = operator attached to the left hand side node
190
191    protected void templateText() {
192        // for template we accept anything but functions
193        while (!token.getType().isFunctionStart() && !token.getType().isFunctionEnd() && !token.getType().isEol()) {
194            nextToken();
195        }
196    }
197
198    protected boolean functionText() {
199        if (accept(TokenType.functionStart)) {
200            nextToken();
201            while (!token.getType().isFunctionEnd() && !token.getType().isEol()) {
202                if (token.getType().isFunctionStart()) {
203                    // embedded function
204                    functionText();
205                }
206                // we need to loop until we find the ending function quote, an embedded function, or the eol
207                nextToken();
208            }
209            // if its not an embedded function then we expect the end token
210            if (!token.getType().isFunctionStart()) {
211                expect(TokenType.functionEnd);
212            }
213            return true;
214        }
215        return false;
216    }
217
218    protected boolean unaryOperator() {
219        if (accept(TokenType.unaryOperator)) {
220            nextToken();
221            // there should be a whitespace after the operator
222            expect(TokenType.whiteSpace);
223            return true;
224        }
225        return false;
226    }
227}